@exadev/semantic-release-workspace 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ One orchestrator run, five stages:
22
22
 
23
23
  When a package releases version `V`, every *not-yet-released-this-run* workspace package that depends on it gets its dependency range updated — and the timing of that update is the least obvious part of the whole design, because getting it wrong is exactly the bug class the documents.js ecosystem's old cross-repo automation hit (the `sibling-dependency-update` heal-job downgrade race: repository state and published manifests disagreeing, then automation "healing" in the wrong direction; see [documents.js#664](https://github.com/ExaDev/documents.js/issues/664)).
24
24
 
25
- The orchestrator's rule: **the moment a package's release completes, each dependent's manifest is rewritten on disk, committed, and pushed — before anything else happens.** A bump commit looks like:
25
+ The orchestrator's rule: **the moment a package's release completes, each dependent's manifest is rewritten on disk, `pnpm-lock.yaml` is regenerated to match, and both are committed together and pushed — before anything else happens.** A bump commit looks like:
26
26
 
27
27
  ```
28
28
  chore(deps): bump @fixture/a to ^1.1.0 in @fixture/b [skip ci]
@@ -31,6 +31,7 @@ chore(deps): bump @fixture/a to ^1.1.0 in @fixture/b [skip ci]
31
31
  Why commit immediately, rather than the alternatives:
32
32
 
33
33
  - **Why commit at all (not just edit the working tree)?** A dependent's semantic-release run analyses *git history*, not the working tree. An uncommitted manifest edit is invisible to its commit analysis, so the dependent could be judged "no changes" and skip a release — leaving a manifest that names a version the registry has, but which the dependent never published, stranded uncommitted in one developer's checkout.
34
+ - **Why regenerate `pnpm-lock.yaml` in the same commit?** `pnpm install --frozen-lockfile` (what CI runs) rejects a tree where the lockfile's recorded specifier for a workspace dependency disagrees with the manifest. Committing the manifest bump without regenerating the lockfile leaves exactly that disagreement, breaking `--frozen-lockfile` installs on the dependent's directory until someone runs `pnpm install` by hand and commits the result — so the lockfile is regenerated and committed alongside the manifest, never as a separate step.
34
35
  - **Why before the dependent's own run (not after)?** The dependent's release commit and its published artifact must carry the new range. Bumping after would publish the dependent with a stale range, then mutate the repository afterwards — the repository/published-artifact disagreement this tool exists to prevent.
35
36
  - **Why push immediately?** The same crash-consistency discipline semantic-release applies to its own release commits: if the orchestrator dies halfway through the run, everything pushed so far (releases, tags, bump commits) is a consistent prefix, and the next run picks up cleanly from the tags. `[skip ci]` on the bump message stops the push from triggering a *second*, racing release run. The bump commit also carries a machine-parseable trailer alongside its human-readable subject, recording the dependency, its released version, and the new range -- so a fresh run's per-package analysis recognises a bump commit that already exists in history (whether from earlier in the same run or left over from a run that stopped right after pushing it) and still forces the dependent's patch release, rather than depending on a record that only ever existed in the process that made the commit.
36
37
 
@@ -42,7 +43,7 @@ Dependency-range handling, in full:
42
43
 
43
44
  | Range in the dependent's manifest | What happens |
44
45
  | --- | --- |
45
- | `^1.0.0`, `~1.0.0`, `>=1.0.0`, `=1.0.0`, `1.0.0`, `workspace:^1.0.0` | Rewritten in place, preserving the comparator (`^1.0.0` → `^1.1.0`), committed and pushed; dependent gets at least a patch release |
46
+ | `^1.0.0`, `~1.0.0`, `>=1.0.0`, `=1.0.0`, `1.0.0`, `workspace:^1.0.0` | Rewritten in place, preserving the comparator (`^1.0.0` → `^1.1.0`); `pnpm-lock.yaml` is regenerated to match, and both are committed and pushed together; dependent gets at least a patch release |
46
47
  | `workspace:*`, `workspace:^`, `workspace:~` | No manifest edit (pnpm resolves these at pack time), but the published range still changes, so the dependent still gets a patch release |
47
48
  | `*`, `x`, `latest` | Nothing to update and the published range is unaffected — no bump, no forced release |
48
49
  | Compound ranges (`>=1.0.0 <2.0.0`), unions (`1.x \|\| 2.x`), `<`/`<=` bounds, `catalog:`, `npm:` aliases, git/tarball URLs | The run stops with `UnsupportedDependencyRangeError` — rewriting any of these wrongly, or leaving them silently stale, both produce a published manifest that disagrees with the repository, so neither is attempted |
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import validateNpmPackageName from "validate-npm-package-name";
13
13
  import { glob } from "tinyglobby";
14
14
  import { parse } from "yaml";
15
15
  //#region package.json
16
- var version = "1.0.1";
16
+ var version = "1.0.3";
17
17
  //#endregion
18
18
  //#region src/errors.ts
19
19
  /**
@@ -52,6 +52,12 @@ var GitCommandError = class extends WorkspaceReleaseError {
52
52
  };
53
53
  /** The workspace's git state does not support the release operation -- for example a detached HEAD, which names no branch that dependency-bump commits could be pushed to. */
54
54
  var WorkspaceStateError = class extends WorkspaceReleaseError {};
55
+ /** `pnpm install --lockfile-only` failed while regenerating the lockfile for a dependency-range bump. Left unhandled, a dependent's manifest would be committed with `pnpm-lock.yaml` still naming the old range -- the exact drift this step exists to prevent -- so the bump aborts instead of committing a manifest and lockfile that disagree. */
56
+ var PnpmCommandError = class extends WorkspaceReleaseError {
57
+ constructor(cwd, detail) {
58
+ super(`pnpm install --lockfile-only failed in ${cwd}: ${detail}`);
59
+ }
60
+ };
55
61
  //#endregion
56
62
  //#region src/json.ts
57
63
  function isJsonObject(value) {
@@ -125,7 +131,7 @@ function matchTrailerLine(message, key) {
125
131
  }
126
132
  //#endregion
127
133
  //#region src/git.ts
128
- const execFileAsync = promisify(execFile);
134
+ const execFileAsync$1 = promisify(execFile);
129
135
  /** `git log --name-only` over everything since a package's last release tag can legitimately produce tens of megabytes of path output on a long-lived monorepo, well past execFile's default buffer, failing on exactly the big workspaces this tool exists for. */
130
136
  const GIT_MAX_BUFFER_BYTES = 104857600;
131
137
  /** Separates one commit's record in `git log --format` output. Chosen from the C0 control range so it can never appear in a hash or a file path. */
@@ -137,7 +143,7 @@ const BOT_IDENTITY = {
137
143
  };
138
144
  async function git(args, options) {
139
145
  try {
140
- const { stdout } = await execFileAsync("git", [...args], {
146
+ const { stdout } = await execFileAsync$1("git", [...args], {
141
147
  cwd: options.cwd,
142
148
  maxBuffer: GIT_MAX_BUFFER_BYTES
143
149
  });
@@ -261,7 +267,11 @@ function createScopedPlugins(scope) {
261
267
  let cached;
262
268
  async function commitsForPackage(context) {
263
269
  const from = context.lastRelease?.gitHead ?? void 0;
264
- if (cached?.from !== from) cached = {
270
+ if (cached === void 0) cached = {
271
+ from,
272
+ paths: changedPathsSince(from, { cwd: context.cwd })
273
+ };
274
+ else if (cached.from !== from) cached = {
265
275
  from,
266
276
  paths: changedPathsSince(from, { cwd: context.cwd })
267
277
  };
@@ -560,6 +570,23 @@ function firstUnplacedDependency(name, graph, unplaced) {
560
570
  return (graph.dependencies.get(name) ?? []).map((edge) => edge.dependency).filter((dependency) => unplaced.has(dependency)).sort()[0];
561
571
  }
562
572
  //#endregion
573
+ //#region src/pnpm.ts
574
+ const execFileAsync = promisify(execFile);
575
+ /**
576
+ * Regenerates `pnpm-lock.yaml` for the whole workspace from the manifests currently on disk, without touching `node_modules` or installing anything -- the same lockfile-refresh step a contributor runs by hand after editing a `package.json` dependency range.
577
+ *
578
+ * Every dependency-range bump this tool writes to a manifest must be followed by this before the bump is committed: `pnpm install --frozen-lockfile` (what CI runs) rejects a tree where the lockfile's recorded specifier for a workspace dependency disagrees with the manifest, so a manifest bump committed without a matching lockfile update leaves every subsequent CI run broken until someone runs `pnpm install` by hand and commits the result.
579
+ */
580
+ async function regenerateLockfile(options) {
581
+ try {
582
+ await execFileAsync("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
583
+ } catch (cause) {
584
+ const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
585
+ const detail = stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause);
586
+ throw new PnpmCommandError(options.cwd, detail);
587
+ }
588
+ }
589
+ //#endregion
563
590
  //#region src/version-range.ts
564
591
  const WORKSPACE_PROTOCOL = "workspace:";
565
592
  const CATALOG_PROTOCOL = "catalog:";
@@ -723,10 +750,12 @@ async function runPackageRelease(pkg, options) {
723
750
  throw new WorkspaceReleaseError(`Release of ${pkg.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
724
751
  }
725
752
  }
753
+ /** The one filename pnpm recognises as its lockfile, always sitting beside `pnpm-workspace.yaml` at the workspace root regardless of which dependent's manifest a bump rewrites. */
754
+ const LOCKFILE_FILENAME = "pnpm-lock.yaml";
726
755
  /**
727
756
  * Rewrites the released package's range in every dependent's manifest, immediately after the release and before any dependent's own turn.
728
757
  *
729
- * The bump is committed (and pushed) right away rather than staged, because a dependent's semantic-release run analyses git history, not the working tree: an uncommitted bump would be invisible to its commit analysis, and would be stranded uncommitted if the dependent then released nothing -- a manifest that names versions the registry has never seen. Committing immediately means the dependent's own run sees the bump commit (it touches only the dependent's directory, so it passes that dependent's path filter), the forced-patch logic in the scoped analyzer covers the case where that bump is the dependent's only change, and a run interrupted partway leaves the remote describing exactly what was published. Pushing immediately mirrors what semantic-release itself does with release commits.
758
+ * The bump is committed (and pushed) right away rather than staged, because a dependent's semantic-release run analyses git history, not the working tree: an uncommitted bump would be invisible to its commit analysis, and would be stranded uncommitted if the dependent then released nothing -- a manifest that names versions the registry has never seen. Committing immediately means the dependent's own run sees the bump commit (it touches only the dependent's directory, so it passes that dependent's path filter), the forced-patch logic in the scoped analyzer covers the case where that bump is the dependent's only change, and a run interrupted partway leaves the remote describing exactly what was published. Pushing immediately mirrors what semantic-release itself does with release commits. The lockfile is regenerated (`pnpm install --lockfile-only`) and committed alongside the manifest for the same reason: a manifest bump committed without it leaves `pnpm-lock.yaml` naming the old range, which `pnpm install --frozen-lockfile` (what CI runs) then rejects.
730
759
  */
731
760
  async function bumpDependents(released, version, graph, options) {
732
761
  const applied = [];
@@ -739,18 +768,19 @@ async function bumpDependents(released, version, graph, options) {
739
768
  if (update.kind === "rewritten") {
740
769
  if (!options.dryRun) {
741
770
  await writeDependencyRange(dependent.manifestPath, edge.field, released.name, update.range);
771
+ await regenerateLockfile({ cwd: options.workspace.root });
742
772
  const message = formatDependencyBumpMessage({
743
773
  dependency: released.name,
744
774
  version,
745
775
  range: update.range,
746
776
  dependent: edge.dependent
747
777
  });
748
- await commitFiles([`${dependent.relativeDirectory}/package.json`], message, {
778
+ await commitFiles([`${dependent.relativeDirectory}/package.json`, LOCKFILE_FILENAME], message, {
749
779
  cwd: options.workspace.root,
750
780
  identity: options.identity
751
781
  });
752
782
  await pushHead({ cwd: options.workspace.root });
753
- options.log(`Bumped ${released.name} to ${update.range} in ${edge.dependent}, committed and pushed`);
783
+ options.log(`Bumped ${released.name} to ${update.range} in ${edge.dependent}, regenerated the lockfile, committed and pushed`);
754
784
  } else options.log(`Would bump ${released.name} to ${update.range} in ${edge.dependent} (${edge.field})`);
755
785
  } else options.log(`${edge.dependent} declares ${released.name} as ${edge.range}; the manifest needs no edit, pnpm re-resolves it to ${version} at publish time`);
756
786
  applied.push({
package/dist/index.cjs CHANGED
@@ -74,9 +74,15 @@ var GitCommandError = class extends WorkspaceReleaseError {
74
74
  };
75
75
  /** The workspace's git state does not support the release operation -- for example a detached HEAD, which names no branch that dependency-bump commits could be pushed to. */
76
76
  var WorkspaceStateError = class extends WorkspaceReleaseError {};
77
+ /** `pnpm install --lockfile-only` failed while regenerating the lockfile for a dependency-range bump. Left unhandled, a dependent's manifest would be committed with `pnpm-lock.yaml` still naming the old range -- the exact drift this step exists to prevent -- so the bump aborts instead of committing a manifest and lockfile that disagree. */
78
+ var PnpmCommandError = class extends WorkspaceReleaseError {
79
+ constructor(cwd, detail) {
80
+ super(`pnpm install --lockfile-only failed in ${cwd}: ${detail}`);
81
+ }
82
+ };
77
83
  //#endregion
78
84
  //#region src/git.ts
79
- const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
85
+ const execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
80
86
  /** `git log --name-only` over everything since a package's last release tag can legitimately produce tens of megabytes of path output on a long-lived monorepo, well past execFile's default buffer, failing on exactly the big workspaces this tool exists for. */
81
87
  const GIT_MAX_BUFFER_BYTES = 104857600;
82
88
  /** Separates one commit's record in `git log --format` output. Chosen from the C0 control range so it can never appear in a hash or a file path. */
@@ -88,7 +94,7 @@ const BOT_IDENTITY = {
88
94
  };
89
95
  async function git(args, options) {
90
96
  try {
91
- const { stdout } = await execFileAsync("git", [...args], {
97
+ const { stdout } = await execFileAsync$1("git", [...args], {
92
98
  cwd: options.cwd,
93
99
  maxBuffer: GIT_MAX_BUFFER_BYTES
94
100
  });
@@ -537,7 +543,11 @@ function createScopedPlugins(scope) {
537
543
  let cached;
538
544
  async function commitsForPackage(context) {
539
545
  const from = context.lastRelease?.gitHead ?? void 0;
540
- if (cached?.from !== from) cached = {
546
+ if (cached === void 0) cached = {
547
+ from,
548
+ paths: changedPathsSince(from, { cwd: context.cwd })
549
+ };
550
+ else if (cached.from !== from) cached = {
541
551
  from,
542
552
  paths: changedPathsSince(from, { cwd: context.cwd })
543
553
  };
@@ -637,6 +647,23 @@ function parsePublishPluginSpec(spec) {
637
647
  return [name, config ?? {}];
638
648
  }
639
649
  //#endregion
650
+ //#region src/pnpm.ts
651
+ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
652
+ /**
653
+ * Regenerates `pnpm-lock.yaml` for the whole workspace from the manifests currently on disk, without touching `node_modules` or installing anything -- the same lockfile-refresh step a contributor runs by hand after editing a `package.json` dependency range.
654
+ *
655
+ * Every dependency-range bump this tool writes to a manifest must be followed by this before the bump is committed: `pnpm install --frozen-lockfile` (what CI runs) rejects a tree where the lockfile's recorded specifier for a workspace dependency disagrees with the manifest, so a manifest bump committed without a matching lockfile update leaves every subsequent CI run broken until someone runs `pnpm install` by hand and commits the result.
656
+ */
657
+ async function regenerateLockfile(options) {
658
+ try {
659
+ await execFileAsync("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
660
+ } catch (cause) {
661
+ const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
662
+ const detail = stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause);
663
+ throw new PnpmCommandError(options.cwd, detail);
664
+ }
665
+ }
666
+ //#endregion
640
667
  //#region src/release.ts
641
668
  /**
642
669
  * Releases every package in a pnpm workspace with independent versions, in dependency order.
@@ -736,10 +763,12 @@ async function runPackageRelease(pkg, options) {
736
763
  throw new WorkspaceReleaseError(`Release of ${pkg.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
737
764
  }
738
765
  }
766
+ /** The one filename pnpm recognises as its lockfile, always sitting beside `pnpm-workspace.yaml` at the workspace root regardless of which dependent's manifest a bump rewrites. */
767
+ const LOCKFILE_FILENAME = "pnpm-lock.yaml";
739
768
  /**
740
769
  * Rewrites the released package's range in every dependent's manifest, immediately after the release and before any dependent's own turn.
741
770
  *
742
- * The bump is committed (and pushed) right away rather than staged, because a dependent's semantic-release run analyses git history, not the working tree: an uncommitted bump would be invisible to its commit analysis, and would be stranded uncommitted if the dependent then released nothing -- a manifest that names versions the registry has never seen. Committing immediately means the dependent's own run sees the bump commit (it touches only the dependent's directory, so it passes that dependent's path filter), the forced-patch logic in the scoped analyzer covers the case where that bump is the dependent's only change, and a run interrupted partway leaves the remote describing exactly what was published. Pushing immediately mirrors what semantic-release itself does with release commits.
771
+ * The bump is committed (and pushed) right away rather than staged, because a dependent's semantic-release run analyses git history, not the working tree: an uncommitted bump would be invisible to its commit analysis, and would be stranded uncommitted if the dependent then released nothing -- a manifest that names versions the registry has never seen. Committing immediately means the dependent's own run sees the bump commit (it touches only the dependent's directory, so it passes that dependent's path filter), the forced-patch logic in the scoped analyzer covers the case where that bump is the dependent's only change, and a run interrupted partway leaves the remote describing exactly what was published. Pushing immediately mirrors what semantic-release itself does with release commits. The lockfile is regenerated (`pnpm install --lockfile-only`) and committed alongside the manifest for the same reason: a manifest bump committed without it leaves `pnpm-lock.yaml` naming the old range, which `pnpm install --frozen-lockfile` (what CI runs) then rejects.
743
772
  */
744
773
  async function bumpDependents(released, version, graph, options) {
745
774
  const applied = [];
@@ -752,18 +781,19 @@ async function bumpDependents(released, version, graph, options) {
752
781
  if (update.kind === "rewritten") {
753
782
  if (!options.dryRun) {
754
783
  await writeDependencyRange(dependent.manifestPath, edge.field, released.name, update.range);
784
+ await regenerateLockfile({ cwd: options.workspace.root });
755
785
  const message = formatDependencyBumpMessage({
756
786
  dependency: released.name,
757
787
  version,
758
788
  range: update.range,
759
789
  dependent: edge.dependent
760
790
  });
761
- await commitFiles([`${dependent.relativeDirectory}/package.json`], message, {
791
+ await commitFiles([`${dependent.relativeDirectory}/package.json`, LOCKFILE_FILENAME], message, {
762
792
  cwd: options.workspace.root,
763
793
  identity: options.identity
764
794
  });
765
795
  await pushHead({ cwd: options.workspace.root });
766
- options.log(`Bumped ${released.name} to ${update.range} in ${edge.dependent}, committed and pushed`);
796
+ options.log(`Bumped ${released.name} to ${update.range} in ${edge.dependent}, regenerated the lockfile, committed and pushed`);
767
797
  } else options.log(`Would bump ${released.name} to ${update.range} in ${edge.dependent} (${edge.field})`);
768
798
  } else options.log(`${edge.dependent} declares ${released.name} as ${edge.range}; the manifest needs no edit, pnpm re-resolves it to ${version} at publish time`);
769
799
  applied.push({
package/dist/index.js CHANGED
@@ -49,9 +49,15 @@ var GitCommandError = class extends WorkspaceReleaseError {
49
49
  };
50
50
  /** The workspace's git state does not support the release operation -- for example a detached HEAD, which names no branch that dependency-bump commits could be pushed to. */
51
51
  var WorkspaceStateError = class extends WorkspaceReleaseError {};
52
+ /** `pnpm install --lockfile-only` failed while regenerating the lockfile for a dependency-range bump. Left unhandled, a dependent's manifest would be committed with `pnpm-lock.yaml` still naming the old range -- the exact drift this step exists to prevent -- so the bump aborts instead of committing a manifest and lockfile that disagree. */
53
+ var PnpmCommandError = class extends WorkspaceReleaseError {
54
+ constructor(cwd, detail) {
55
+ super(`pnpm install --lockfile-only failed in ${cwd}: ${detail}`);
56
+ }
57
+ };
52
58
  //#endregion
53
59
  //#region src/git.ts
54
- const execFileAsync = promisify(execFile);
60
+ const execFileAsync$1 = promisify(execFile);
55
61
  /** `git log --name-only` over everything since a package's last release tag can legitimately produce tens of megabytes of path output on a long-lived monorepo, well past execFile's default buffer, failing on exactly the big workspaces this tool exists for. */
56
62
  const GIT_MAX_BUFFER_BYTES = 104857600;
57
63
  /** Separates one commit's record in `git log --format` output. Chosen from the C0 control range so it can never appear in a hash or a file path. */
@@ -63,7 +69,7 @@ const BOT_IDENTITY = {
63
69
  };
64
70
  async function git(args, options) {
65
71
  try {
66
- const { stdout } = await execFileAsync("git", [...args], {
72
+ const { stdout } = await execFileAsync$1("git", [...args], {
67
73
  cwd: options.cwd,
68
74
  maxBuffer: GIT_MAX_BUFFER_BYTES
69
75
  });
@@ -512,7 +518,11 @@ function createScopedPlugins(scope) {
512
518
  let cached;
513
519
  async function commitsForPackage(context) {
514
520
  const from = context.lastRelease?.gitHead ?? void 0;
515
- if (cached?.from !== from) cached = {
521
+ if (cached === void 0) cached = {
522
+ from,
523
+ paths: changedPathsSince(from, { cwd: context.cwd })
524
+ };
525
+ else if (cached.from !== from) cached = {
516
526
  from,
517
527
  paths: changedPathsSince(from, { cwd: context.cwd })
518
528
  };
@@ -612,6 +622,23 @@ function parsePublishPluginSpec(spec) {
612
622
  return [name, config ?? {}];
613
623
  }
614
624
  //#endregion
625
+ //#region src/pnpm.ts
626
+ const execFileAsync = promisify(execFile);
627
+ /**
628
+ * Regenerates `pnpm-lock.yaml` for the whole workspace from the manifests currently on disk, without touching `node_modules` or installing anything -- the same lockfile-refresh step a contributor runs by hand after editing a `package.json` dependency range.
629
+ *
630
+ * Every dependency-range bump this tool writes to a manifest must be followed by this before the bump is committed: `pnpm install --frozen-lockfile` (what CI runs) rejects a tree where the lockfile's recorded specifier for a workspace dependency disagrees with the manifest, so a manifest bump committed without a matching lockfile update leaves every subsequent CI run broken until someone runs `pnpm install` by hand and commits the result.
631
+ */
632
+ async function regenerateLockfile(options) {
633
+ try {
634
+ await execFileAsync("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
635
+ } catch (cause) {
636
+ const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
637
+ const detail = stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause);
638
+ throw new PnpmCommandError(options.cwd, detail);
639
+ }
640
+ }
641
+ //#endregion
615
642
  //#region src/release.ts
616
643
  /**
617
644
  * Releases every package in a pnpm workspace with independent versions, in dependency order.
@@ -711,10 +738,12 @@ async function runPackageRelease(pkg, options) {
711
738
  throw new WorkspaceReleaseError(`Release of ${pkg.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
712
739
  }
713
740
  }
741
+ /** The one filename pnpm recognises as its lockfile, always sitting beside `pnpm-workspace.yaml` at the workspace root regardless of which dependent's manifest a bump rewrites. */
742
+ const LOCKFILE_FILENAME = "pnpm-lock.yaml";
714
743
  /**
715
744
  * Rewrites the released package's range in every dependent's manifest, immediately after the release and before any dependent's own turn.
716
745
  *
717
- * The bump is committed (and pushed) right away rather than staged, because a dependent's semantic-release run analyses git history, not the working tree: an uncommitted bump would be invisible to its commit analysis, and would be stranded uncommitted if the dependent then released nothing -- a manifest that names versions the registry has never seen. Committing immediately means the dependent's own run sees the bump commit (it touches only the dependent's directory, so it passes that dependent's path filter), the forced-patch logic in the scoped analyzer covers the case where that bump is the dependent's only change, and a run interrupted partway leaves the remote describing exactly what was published. Pushing immediately mirrors what semantic-release itself does with release commits.
746
+ * The bump is committed (and pushed) right away rather than staged, because a dependent's semantic-release run analyses git history, not the working tree: an uncommitted bump would be invisible to its commit analysis, and would be stranded uncommitted if the dependent then released nothing -- a manifest that names versions the registry has never seen. Committing immediately means the dependent's own run sees the bump commit (it touches only the dependent's directory, so it passes that dependent's path filter), the forced-patch logic in the scoped analyzer covers the case where that bump is the dependent's only change, and a run interrupted partway leaves the remote describing exactly what was published. Pushing immediately mirrors what semantic-release itself does with release commits. The lockfile is regenerated (`pnpm install --lockfile-only`) and committed alongside the manifest for the same reason: a manifest bump committed without it leaves `pnpm-lock.yaml` naming the old range, which `pnpm install --frozen-lockfile` (what CI runs) then rejects.
718
747
  */
719
748
  async function bumpDependents(released, version, graph, options) {
720
749
  const applied = [];
@@ -727,18 +756,19 @@ async function bumpDependents(released, version, graph, options) {
727
756
  if (update.kind === "rewritten") {
728
757
  if (!options.dryRun) {
729
758
  await writeDependencyRange(dependent.manifestPath, edge.field, released.name, update.range);
759
+ await regenerateLockfile({ cwd: options.workspace.root });
730
760
  const message = formatDependencyBumpMessage({
731
761
  dependency: released.name,
732
762
  version,
733
763
  range: update.range,
734
764
  dependent: edge.dependent
735
765
  });
736
- await commitFiles([`${dependent.relativeDirectory}/package.json`], message, {
766
+ await commitFiles([`${dependent.relativeDirectory}/package.json`, LOCKFILE_FILENAME], message, {
737
767
  cwd: options.workspace.root,
738
768
  identity: options.identity
739
769
  });
740
770
  await pushHead({ cwd: options.workspace.root });
741
- options.log(`Bumped ${released.name} to ${update.range} in ${edge.dependent}, committed and pushed`);
771
+ options.log(`Bumped ${released.name} to ${update.range} in ${edge.dependent}, regenerated the lockfile, committed and pushed`);
742
772
  } else options.log(`Would bump ${released.name} to ${update.range} in ${edge.dependent} (${edge.field})`);
743
773
  } else options.log(`${edge.dependent} declares ${released.name} as ${edge.range}; the manifest needs no edit, pnpm re-resolves it to ${version} at publish time`);
744
774
  applied.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exadev/semantic-release-workspace",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Independent per-package semantic-release orchestration for pnpm workspaces, without lockstep versioning.",
5
5
  "type": "module",
6
6
  "repository": {