@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 +3 -2
- package/dist/cli.js +32 -6
- package/dist/index.cjs +31 -5
- package/dist/index.js +31 -5
- package/package.json +1 -1
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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