@exadev/semantic-release-workspace 1.0.2 → 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.2";
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
  });
@@ -564,6 +570,23 @@ function firstUnplacedDependency(name, graph, unplaced) {
564
570
  return (graph.dependencies.get(name) ?? []).map((edge) => edge.dependency).filter((dependency) => unplaced.has(dependency)).sort()[0];
565
571
  }
566
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
567
590
  //#region src/version-range.ts
568
591
  const WORKSPACE_PROTOCOL = "workspace:";
569
592
  const CATALOG_PROTOCOL = "catalog:";
@@ -727,10 +750,12 @@ async function runPackageRelease(pkg, options) {
727
750
  throw new WorkspaceReleaseError(`Release of ${pkg.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
728
751
  }
729
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";
730
755
  /**
731
756
  * Rewrites the released package's range in every dependent's manifest, immediately after the release and before any dependent's own turn.
732
757
  *
733
- * 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.
734
759
  */
735
760
  async function bumpDependents(released, version, graph, options) {
736
761
  const applied = [];
@@ -743,18 +768,19 @@ async function bumpDependents(released, version, graph, options) {
743
768
  if (update.kind === "rewritten") {
744
769
  if (!options.dryRun) {
745
770
  await writeDependencyRange(dependent.manifestPath, edge.field, released.name, update.range);
771
+ await regenerateLockfile({ cwd: options.workspace.root });
746
772
  const message = formatDependencyBumpMessage({
747
773
  dependency: released.name,
748
774
  version,
749
775
  range: update.range,
750
776
  dependent: edge.dependent
751
777
  });
752
- await commitFiles([`${dependent.relativeDirectory}/package.json`], message, {
778
+ await commitFiles([`${dependent.relativeDirectory}/package.json`, LOCKFILE_FILENAME], message, {
753
779
  cwd: options.workspace.root,
754
780
  identity: options.identity
755
781
  });
756
782
  await pushHead({ cwd: options.workspace.root });
757
- 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`);
758
784
  } else options.log(`Would bump ${released.name} to ${update.range} in ${edge.dependent} (${edge.field})`);
759
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`);
760
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
  });
@@ -641,6 +647,23 @@ function parsePublishPluginSpec(spec) {
641
647
  return [name, config ?? {}];
642
648
  }
643
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
644
667
  //#region src/release.ts
645
668
  /**
646
669
  * Releases every package in a pnpm workspace with independent versions, in dependency order.
@@ -740,10 +763,12 @@ async function runPackageRelease(pkg, options) {
740
763
  throw new WorkspaceReleaseError(`Release of ${pkg.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
741
764
  }
742
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";
743
768
  /**
744
769
  * Rewrites the released package's range in every dependent's manifest, immediately after the release and before any dependent's own turn.
745
770
  *
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.
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.
747
772
  */
748
773
  async function bumpDependents(released, version, graph, options) {
749
774
  const applied = [];
@@ -756,18 +781,19 @@ async function bumpDependents(released, version, graph, options) {
756
781
  if (update.kind === "rewritten") {
757
782
  if (!options.dryRun) {
758
783
  await writeDependencyRange(dependent.manifestPath, edge.field, released.name, update.range);
784
+ await regenerateLockfile({ cwd: options.workspace.root });
759
785
  const message = formatDependencyBumpMessage({
760
786
  dependency: released.name,
761
787
  version,
762
788
  range: update.range,
763
789
  dependent: edge.dependent
764
790
  });
765
- await commitFiles([`${dependent.relativeDirectory}/package.json`], message, {
791
+ await commitFiles([`${dependent.relativeDirectory}/package.json`, LOCKFILE_FILENAME], message, {
766
792
  cwd: options.workspace.root,
767
793
  identity: options.identity
768
794
  });
769
795
  await pushHead({ cwd: options.workspace.root });
770
- 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`);
771
797
  } else options.log(`Would bump ${released.name} to ${update.range} in ${edge.dependent} (${edge.field})`);
772
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`);
773
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
  });
@@ -616,6 +622,23 @@ function parsePublishPluginSpec(spec) {
616
622
  return [name, config ?? {}];
617
623
  }
618
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
619
642
  //#region src/release.ts
620
643
  /**
621
644
  * Releases every package in a pnpm workspace with independent versions, in dependency order.
@@ -715,10 +738,12 @@ async function runPackageRelease(pkg, options) {
715
738
  throw new WorkspaceReleaseError(`Release of ${pkg.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
716
739
  }
717
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";
718
743
  /**
719
744
  * Rewrites the released package's range in every dependent's manifest, immediately after the release and before any dependent's own turn.
720
745
  *
721
- * 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.
722
747
  */
723
748
  async function bumpDependents(released, version, graph, options) {
724
749
  const applied = [];
@@ -731,18 +756,19 @@ async function bumpDependents(released, version, graph, options) {
731
756
  if (update.kind === "rewritten") {
732
757
  if (!options.dryRun) {
733
758
  await writeDependencyRange(dependent.manifestPath, edge.field, released.name, update.range);
759
+ await regenerateLockfile({ cwd: options.workspace.root });
734
760
  const message = formatDependencyBumpMessage({
735
761
  dependency: released.name,
736
762
  version,
737
763
  range: update.range,
738
764
  dependent: edge.dependent
739
765
  });
740
- await commitFiles([`${dependent.relativeDirectory}/package.json`], message, {
766
+ await commitFiles([`${dependent.relativeDirectory}/package.json`, LOCKFILE_FILENAME], message, {
741
767
  cwd: options.workspace.root,
742
768
  identity: options.identity
743
769
  });
744
770
  await pushHead({ cwd: options.workspace.root });
745
- 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`);
746
772
  } else options.log(`Would bump ${released.name} to ${update.range} in ${edge.dependent} (${edge.field})`);
747
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`);
748
774
  applied.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exadev/semantic-release-workspace",
3
- "version": "1.0.2",
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": {