@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 +3 -2
- package/dist/cli.js +37 -7
- package/dist/index.cjs +36 -6
- package/dist/index.js +36 -6
- 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
|
});
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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