@exadev/semantic-release-workspace 1.3.5 → 1.3.7
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 +8 -6
- package/dist/cli.js +165 -67
- package/dist/index.cjs +163 -65
- package/dist/index.d.cts +27 -18
- package/dist/index.d.ts +27 -18
- package/dist/index.js +163 -65
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -37,20 +37,22 @@ Why commit immediately, rather than the alternatives:
|
|
|
37
37
|
|
|
38
38
|
Because every dependent sits downstream in topological order, its own run always sees the bump commit: the commit touches only the dependent's directory, so it passes that dependent's path filter and participates in its analysis.
|
|
39
39
|
|
|
40
|
-
**A package whose only change is dependency bumps still gets a patch release — deliberately.**
|
|
40
|
+
**A package whose only change is dependency bumps still gets a patch release — deliberately.** A range rewritten on disk (`^1.0.0` → `^1.1.0`) changes the dependent's *published* dependency range, so the dependent must be republished for the change to reach consumers. A range that names no version, such as a bare `workspace:^` in a private package, has nothing to rewrite, but the dependent still releases, so a package always follows a sibling it depends on. This is not left to chance: the wrapped `analyzeCommits` returns `patch` whenever the standard analyzer found nothing but this run bumped one of the package's dependency ranges, so the behaviour does not depend on how the workspace's own analyzer config happens to classify `chore(deps)` commits (many presets release nothing for `chore`). Release notes gain a `### Dependencies` section listing the bumps, so the release is self-explaining rather than empty.
|
|
41
41
|
|
|
42
42
|
Dependency-range handling, in full:
|
|
43
43
|
|
|
44
44
|
| Range in the dependent's manifest | What happens |
|
|
45
45
|
| --- | --- |
|
|
46
|
-
| `^1.0.0`, `~1.0.0`, `>=1.0.0`, `=1.0.0`, `1.0.0
|
|
47
|
-
| `workspace
|
|
46
|
+
| `^1.0.0`, `~1.0.0`, `>=1.0.0`, `=1.0.0`, `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 |
|
|
47
|
+
| `workspace:^1.0.0` and the other anchored `workspace:` forms, in `devDependencies` or a private package | Rewritten in place with the `workspace:` prefix kept (`workspace:^1.0.0` → `workspace:^1.1.0`), committed and pushed as above; dependent gets at least a patch release |
|
|
48
|
+
| `workspace:*`, `workspace:^`, `workspace:~`, in `devDependencies` or a private package | No manifest edit, since the range names no version; the dependent still gets a patch release |
|
|
49
|
+
| Any `workspace:`, `catalog:`, `link:`, or `file:` specifier in the `dependencies`, `peerDependencies`, or `optionalDependencies` of a package that is not private, whichever package it names | The run stops with `UnsupportedDependencyRangeError`: `npm publish` ships the specifier unchanged, so the published package could not be installed |
|
|
48
50
|
| `*`, `x`, `latest` | Nothing to update and the published range is unaffected — no bump, no forced release |
|
|
49
51
|
| 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 |
|
|
50
52
|
|
|
51
|
-
Every
|
|
53
|
+
Every range above is validated before the release loop starts, not just when the dependency it names happens to release: both the shape of each workspace dependency edge and the specifiers in each publishable package's installed dependency fields are static properties of the manifests, knowable at discovery time, so `UnsupportedDependencyRangeError` stops the run before the first package publishes, rather than after some upstream sibling has already been published, tagged, committed, and pushed. Every offending entry is listed in one error. `resume` checks the manifests again before it publishes anything.
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
Publishing goes through `@semantic-release/npm`, which is plain `npm publish`: it ships a manifest's specifiers unchanged and never substitutes pnpm's protocols the way `pnpm publish` does. A publishable package therefore declares a concrete version range for each workspace sibling, which the orchestrator keeps up to date for you, and keeps `linkWorkspacePackages: true` in `pnpm-workspace.yaml` so pnpm still links the sibling locally instead of fetching it from the registry. `workspace:` ranges remain usable in private packages, which never publish, and in `devDependencies`, which no consumer installs.
|
|
54
56
|
|
|
55
57
|
## Commit strategies
|
|
56
58
|
|
|
@@ -149,7 +151,7 @@ The core technique is the same one multi-semantic-release proved in production:
|
|
|
149
151
|
- **In-process delegation, not CLI wrapping.** semantic-release is invoked through its programmatic API with inline plugin functions, so the wrappers delegate to the real @semantic-release/commit-analyzer and @semantic-release/release-notes-generator running in the same process. (The analysed plugins are ESM named exports here, resolved as peers of this package — no plugin re-implementation anywhere.)
|
|
150
152
|
- **Bump-only dependents always release.** multi-semantic-release rewrites dependency ranges in the working tree without committing them, so a dependent whose only change is a dependency update can go unreleased until some other commit triggers it. Here the bump is committed before the dependent's turn and a patch release is forced deterministically (see the timing section above).
|
|
151
153
|
- **Loud failures by design.** A dependency cycle, an unsupported dependency range, a publish pipeline without @semantic-release/git (which would leave released manifests uncommitted), an unresolvable plugin, a duplicate package name — each stops the run with a specific error rather than degrading silently. There is deliberately no "skip this package and carry on" path: a partially-consistent set of publishes is worse than none.
|
|
152
|
-
- **
|
|
154
|
+
- **Only installable specifiers reach the registry.** `workspace:`, `catalog:`, `link:`, and `file:` specifiers in a publishable package's installed dependency fields are rejected instead of being published as written. In private packages and `devDependencies`, `workspace:*`/`workspace:^`/`workspace:~` are understood as naming no version (bump the release, not the manifest text), and `catalog:` and `npm:` aliases on a workspace sibling are rejected with an explanation instead of being mangled.
|
|
153
155
|
- **Workspace-agnostic discovery.** Everything comes from `pnpm-workspace.yaml` and the manifests its globs match; pointing the orchestrator at any pnpm workspace is the entire configuration.
|
|
154
156
|
|
|
155
157
|
Out of scope, on purpose: parallelising independent branches of the dependency graph (packages release sequentially in topological order for correctness first — a real future optimisation, not attempted here), and any Changesets-style explicit-changeset mode, which is a different paradigm rather than a missing feature.
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,6 @@ import { Command, InvalidArgumentError } from "commander";
|
|
|
6
6
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { detachRelease, resumeRelease } from "@exadev/release-gate";
|
|
8
8
|
import { execFile } from "node:child_process";
|
|
9
|
-
import { promisify } from "node:util";
|
|
10
9
|
import validateNpmPackageName from "validate-npm-package-name";
|
|
11
10
|
import { glob } from "tinyglobby";
|
|
12
11
|
import { parse } from "yaml";
|
|
@@ -15,7 +14,7 @@ import { generateNotes } from "@semantic-release/release-notes-generator";
|
|
|
15
14
|
import semanticRelease from "semantic-release";
|
|
16
15
|
import { pathToFileURL } from "node:url";
|
|
17
16
|
//#region package.json
|
|
18
|
-
var version = "1.3.
|
|
17
|
+
var version = "1.3.7";
|
|
19
18
|
//#endregion
|
|
20
19
|
//#region src/errors.ts
|
|
21
20
|
/**
|
|
@@ -40,7 +39,7 @@ var DependencyCycleError = class extends WorkspaceReleaseError {
|
|
|
40
39
|
this.cycle = cycle;
|
|
41
40
|
}
|
|
42
41
|
};
|
|
43
|
-
/** A dependency
|
|
42
|
+
/** A dependency range this tool cannot release correctly: either a range on a workspace sibling that it cannot rewrite with confidence (rewriting it wrongly, or leaving it silently stale, produces a published manifest that disagrees with the repository), or a `workspace:`, `catalog:`, `link:`, or `file:` specifier in a publishable package's published dependencies, which `npm publish` ships verbatim and no consumer can install. The run stops instead. */
|
|
44
43
|
var UnsupportedDependencyRangeError = class extends WorkspaceReleaseError {};
|
|
45
44
|
/** The semantic-release options handed to the orchestrator cannot be scoped to a single package -- typically a publish plugin list that would leave a release commit or a cross-package manifest bump uncommitted. */
|
|
46
45
|
var ReleaseConfigurationError = class extends WorkspaceReleaseError {};
|
|
@@ -48,7 +47,7 @@ var ReleaseConfigurationError = class extends WorkspaceReleaseError {};
|
|
|
48
47
|
var GitCommandError = class extends WorkspaceReleaseError {
|
|
49
48
|
exitCode;
|
|
50
49
|
constructor(args, cwd, exitCode, detail) {
|
|
51
|
-
super(`git ${args.join(" ")} failed in ${cwd}${exitCode === void 0 ? "" : ` (exit ${exitCode})`}: ${detail}`);
|
|
50
|
+
super(`git ${args.join(" ")} failed in ${cwd}${exitCode === void 0 ? "" : ` (exit ${String(exitCode)})`}: ${detail}`);
|
|
52
51
|
this.exitCode = exitCode;
|
|
53
52
|
}
|
|
54
53
|
};
|
|
@@ -61,10 +60,30 @@ var PnpmCommandError = class extends WorkspaceReleaseError {
|
|
|
61
60
|
}
|
|
62
61
|
};
|
|
63
62
|
//#endregion
|
|
63
|
+
//#region src/exec-file.ts
|
|
64
|
+
/**
|
|
65
|
+
* A hand-written promise wrapper around `child_process.execFile`, in place of `util.promisify(execFile)`: `execFile` synchronously returns a `ChildProcess` in addition to invoking its callback, which trips `@typescript-eslint/strict-void-return` when the whole function is handed to `promisify` (a value-returning function used where a void-returning one is contextually expected there) -- exactly the void-return contravariance leniency that rule exists to catch, even though `tsc` itself accepts the pattern. Calling `execFile` directly with our own callback, whose own return type really is `void`, sidesteps the mismatch instead of suppressing it.
|
|
66
|
+
*/
|
|
67
|
+
async function execFile$1(command, args, options) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
execFile(command, [...args], {
|
|
70
|
+
cwd: options.cwd,
|
|
71
|
+
env: options.env,
|
|
72
|
+
maxBuffer: options.maxBuffer
|
|
73
|
+
}, (error, stdout, stderr) => {
|
|
74
|
+
if (error) {
|
|
75
|
+
reject(error instanceof Error ? error : new Error(error.message));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
resolve({
|
|
79
|
+
stdout,
|
|
80
|
+
stderr
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
64
86
|
//#region src/git.ts
|
|
65
|
-
const execFileAsync$1 = promisify(execFile);
|
|
66
|
-
/** `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. */
|
|
67
|
-
const GIT_MAX_BUFFER_BYTES = 104857600;
|
|
68
87
|
/** 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. */
|
|
69
88
|
const COMMIT_RECORD_SEPARATOR = "";
|
|
70
89
|
/** The identity semantic-release's own core writes release commits under in CI when nothing else is configured (its COMMIT_NAME/COMMIT_EMAIL constants); dependency-bump commits use the same fallback so every commit a release run produces has a consistent author when the repository declares none. */
|
|
@@ -92,18 +111,22 @@ const GIT_REPOSITORY_DISCOVERY_ENV_KEYS = [
|
|
|
92
111
|
*/
|
|
93
112
|
function sanitizeGitEnv(env) {
|
|
94
113
|
const sanitized = { ...env };
|
|
95
|
-
for (const key of GIT_REPOSITORY_DISCOVERY_ENV_KEYS)
|
|
114
|
+
for (const key of GIT_REPOSITORY_DISCOVERY_ENV_KEYS) Reflect.deleteProperty(sanitized, key);
|
|
96
115
|
return sanitized;
|
|
97
116
|
}
|
|
98
117
|
const SANITIZED_PROCESS_GIT_ENV = sanitizeGitEnv(process.env);
|
|
118
|
+
/** `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 -- hence the generous 100 MiB default, rather than execFile's own. */
|
|
119
|
+
async function execGit(args, cwd, maxBuffer = 104857600) {
|
|
120
|
+
const { stdout } = await execFile$1("git", args, {
|
|
121
|
+
cwd,
|
|
122
|
+
maxBuffer,
|
|
123
|
+
env: SANITIZED_PROCESS_GIT_ENV
|
|
124
|
+
});
|
|
125
|
+
return stdout;
|
|
126
|
+
}
|
|
99
127
|
async function git(args, options) {
|
|
100
128
|
try {
|
|
101
|
-
|
|
102
|
-
cwd: options.cwd,
|
|
103
|
-
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
104
|
-
env: SANITIZED_PROCESS_GIT_ENV
|
|
105
|
-
});
|
|
106
|
-
return stdout;
|
|
129
|
+
return await execGit(args, options.cwd);
|
|
107
130
|
} catch (cause) {
|
|
108
131
|
throw toGitCommandError(args, options.cwd, cause);
|
|
109
132
|
}
|
|
@@ -217,9 +240,11 @@ async function workingTreeChanges(options) {
|
|
|
217
240
|
const paths = [];
|
|
218
241
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
219
242
|
const entry = tokens[index];
|
|
220
|
-
if (entry === void 0
|
|
243
|
+
if (entry === void 0) continue;
|
|
221
244
|
const statusCode = entry.slice(0, 2);
|
|
222
|
-
|
|
245
|
+
const path = entry.slice(3);
|
|
246
|
+
if (path === "") continue;
|
|
247
|
+
paths.push(path);
|
|
223
248
|
if (statusCode.includes("R") || statusCode.includes("C")) index += 1;
|
|
224
249
|
}
|
|
225
250
|
return paths;
|
|
@@ -283,6 +308,12 @@ const DEPENDENCY_FIELDS = [
|
|
|
283
308
|
"peerDependencies",
|
|
284
309
|
"optionalDependencies"
|
|
285
310
|
];
|
|
311
|
+
/** The fields whose entries a consumer's package manager resolves when it installs the published package. `devDependencies` is absent: it is published, but nothing ever installs it for a consumer. */
|
|
312
|
+
const INSTALLED_DEPENDENCY_FIELDS = [
|
|
313
|
+
"dependencies",
|
|
314
|
+
"peerDependencies",
|
|
315
|
+
"optionalDependencies"
|
|
316
|
+
];
|
|
286
317
|
async function readManifest(path) {
|
|
287
318
|
const text = await readFile(path, "utf8");
|
|
288
319
|
const parsed = JSON.parse(text);
|
|
@@ -290,7 +321,7 @@ async function readManifest(path) {
|
|
|
290
321
|
const { name, version } = parsed;
|
|
291
322
|
if (typeof name !== "string" || name.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "name". Every workspace package needs a name: releases are ordered, tagged, and matched to dependents by it.`);
|
|
292
323
|
const validity = validateNpmPackageName(name);
|
|
293
|
-
if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${
|
|
324
|
+
if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${validity.errors.join("; ")}`);
|
|
294
325
|
if (typeof version !== "string" || version.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "version".`);
|
|
295
326
|
const dependencies = /* @__PURE__ */ new Map();
|
|
296
327
|
for (const field of DEPENDENCY_FIELDS) {
|
|
@@ -302,6 +333,7 @@ async function readManifest(path) {
|
|
|
302
333
|
return {
|
|
303
334
|
name,
|
|
304
335
|
version,
|
|
336
|
+
private: parsed.private === true,
|
|
305
337
|
dependencies
|
|
306
338
|
};
|
|
307
339
|
}
|
|
@@ -324,7 +356,14 @@ async function writeDependencyRange(path, field, dependency, range) {
|
|
|
324
356
|
const WORKSPACE_PROTOCOL = "workspace:";
|
|
325
357
|
const CATALOG_PROTOCOL = "catalog:";
|
|
326
358
|
const NPM_ALIAS_PROTOCOL = "npm:";
|
|
327
|
-
/**
|
|
359
|
+
/** Specifier protocols that only a workspace-aware package manager resolves locally. `npm publish` copies them into the registry as written, so a published package declaring one in an installed dependency field cannot be installed. */
|
|
360
|
+
const UNPUBLISHABLE_SPECIFIER_PROTOCOLS = [
|
|
361
|
+
WORKSPACE_PROTOCOL,
|
|
362
|
+
CATALOG_PROTOCOL,
|
|
363
|
+
"link:",
|
|
364
|
+
"file:"
|
|
365
|
+
];
|
|
366
|
+
/** The `workspace:` suffixes that name no version, and so leave nothing in the manifest to rewrite. */
|
|
328
367
|
const PUBLISH_RESOLVED_WORKSPACE_SUFFIXES = [
|
|
329
368
|
"*",
|
|
330
369
|
"^",
|
|
@@ -339,6 +378,13 @@ const WILDCARD_RANGES = [
|
|
|
339
378
|
"latest"
|
|
340
379
|
];
|
|
341
380
|
/**
|
|
381
|
+
* The protocol of a specifier that `npm publish` would ship verbatim while no consumer's package manager can resolve it (`workspace:`, `catalog:`, `link:`, `file:`), or `undefined` for a specifier a consumer can install.
|
|
382
|
+
*/
|
|
383
|
+
function unpublishableSpecifierProtocol(specifier) {
|
|
384
|
+
const trimmed = specifier.trim();
|
|
385
|
+
return UNPUBLISHABLE_SPECIFIER_PROTOCOLS.find((protocol) => trimmed.startsWith(protocol));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
342
388
|
* A single comparator whose version can be replaced in place without changing the comparator's intent. `<` and `<=` are deliberately absent: rewriting `<2.0.0` to `<1.4.0` narrows an upper bound to the very version being released, which is never what the author meant, so such a range is rejected rather than mangled.
|
|
343
389
|
*/
|
|
344
390
|
const REWRITABLE_COMPARATOR = /^(\^|~|>=|=)?(\d+\.\d+\.\d+(?:-[\dA-Za-z.-]+)?(?:\+[\dA-Za-z.-]+)?)$/;
|
|
@@ -360,7 +406,7 @@ function classifyDependencyRange(current) {
|
|
|
360
406
|
comparator: inner.comparator
|
|
361
407
|
};
|
|
362
408
|
}
|
|
363
|
-
if (range.startsWith(CATALOG_PROTOCOL)) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": the version of a "catalog:" dependency lives in pnpm-workspace.yaml, not in the package manifest, so bumping it here would leave the catalog entry stale.
|
|
409
|
+
if (range.startsWith(CATALOG_PROTOCOL)) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": the version of a "catalog:" dependency lives in pnpm-workspace.yaml, not in the package manifest, so bumping it here would leave the catalog entry stale. Declare the sibling's version range directly (for example "^1.0.0") instead.`);
|
|
364
410
|
if (range.startsWith(NPM_ALIAS_PROTOCOL)) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": an "npm:" alias points at a differently-named package, so the version released in this workspace is not necessarily the version this range refers to.`);
|
|
365
411
|
if (WILDCARD_RANGES.includes(range)) return { kind: "wildcard" };
|
|
366
412
|
const match = REWRITABLE_COMPARATOR.exec(range);
|
|
@@ -384,6 +430,27 @@ function updateDependencyRange(current, version) {
|
|
|
384
430
|
};
|
|
385
431
|
}
|
|
386
432
|
//#endregion
|
|
433
|
+
//#region src/publishable-dependencies.ts
|
|
434
|
+
/**
|
|
435
|
+
* Throws `UnsupportedDependencyRangeError` unless every publishable package's installed dependencies (`dependencies`, `peerDependencies`, `optionalDependencies`) are ranges a consumer's package manager can resolve.
|
|
436
|
+
*
|
|
437
|
+
* Publishing goes through `@semantic-release/npm`, that is plain `npm publish`, which copies the manifest's specifiers into the registry unchanged. A `workspace:`, `catalog:`, `link:`, or `file:` specifier therefore reaches consumers as written and makes the package uninstallable, whichever sibling or external package it names and whether or not the tool would have rewritten it. `pnpm publish` would substitute some of them at pack time, but this tool never packs with pnpm, so accepting them would mean publishing a broken package with no warning.
|
|
438
|
+
*
|
|
439
|
+
* A `private` package never publishes and is exempt, and so is `devDependencies`, which no consumer installs. Every offending entry across every package is reported in one error, so a workspace with several of them is fixed in one pass rather than one failed run per entry.
|
|
440
|
+
*/
|
|
441
|
+
function assertPublishableDependencies(packages) {
|
|
442
|
+
const offences = [];
|
|
443
|
+
for (const pkg of packages) {
|
|
444
|
+
if (pkg.private) continue;
|
|
445
|
+
for (const field of INSTALLED_DEPENDENCY_FIELDS) {
|
|
446
|
+
const declared = pkg.dependencies.get(field);
|
|
447
|
+
if (declared === void 0) continue;
|
|
448
|
+
for (const [dependency, specifier] of declared) if (unpublishableSpecifierProtocol(specifier) !== void 0) offences.push(` ${pkg.name}: "${dependency}" in ${field} is declared as "${specifier}"`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (offences.length > 0) throw new UnsupportedDependencyRangeError(`Refusing to release: npm publish ships these dependency specifiers unchanged, so the published package could not be installed.\n${offences.join("\n")}\nDeclare a plain version range instead (for example "^1.2.3"). For a sibling in this workspace, keep "linkWorkspacePackages: true" in pnpm-workspace.yaml so pnpm still links it locally; this tool rewrites the range whenever the sibling releases. Packages marked "private" are never published and are exempt.`);
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
387
454
|
//#region src/workspace.ts
|
|
388
455
|
/** The one filename pnpm recognises as a workspace definition. */
|
|
389
456
|
const WORKSPACE_MANIFEST = "pnpm-workspace.yaml";
|
|
@@ -420,6 +487,7 @@ async function discoverWorkspace(root) {
|
|
|
420
487
|
packages.push({
|
|
421
488
|
name: manifest.name,
|
|
422
489
|
version: manifest.version,
|
|
490
|
+
private: manifest.private,
|
|
423
491
|
directory,
|
|
424
492
|
relativeDirectory,
|
|
425
493
|
repoRelativeDirectory,
|
|
@@ -539,11 +607,15 @@ function firstUnplacedDependency(name, graph, unplaced) {
|
|
|
539
607
|
return (graph.dependencies.get(name) ?? []).map((edge) => edge.dependency).filter((dependency) => unplaced.has(dependency)).sort()[0];
|
|
540
608
|
}
|
|
541
609
|
/**
|
|
542
|
-
*
|
|
610
|
+
* Validates every dependency range in the workspace before anything releases, so a run stops before the first publish rather than after some sibling has already been published, tagged, committed, and pushed. Two independent checks, both static properties of the manifests:
|
|
611
|
+
*
|
|
612
|
+
* - Every publishable package's installed dependencies must be specifiers a consumer can install (see `assertPublishableDependencies`), whichever package they name.
|
|
613
|
+
* - Every workspace dependency edge's range must have a shape this tool can maintain (see `classifyDependencyRange`, which depends only on the range text and never on which version a sibling ends up releasing).
|
|
543
614
|
*
|
|
544
|
-
* Shared by
|
|
615
|
+
* Both throw `UnsupportedDependencyRangeError`. Shared by every release path (`release.ts`'s per-package loop, `single-commit-release.ts`'s analysis phase, and `gate-publish.ts`'s detach), which is why it lives alongside the graph it validates rather than inside any one of them.
|
|
545
616
|
*/
|
|
546
|
-
function
|
|
617
|
+
function validateDependencyRanges(graph) {
|
|
618
|
+
assertPublishableDependencies([...graph.packages.values()]);
|
|
547
619
|
for (const edges of graph.dependencies.values()) for (const edge of edges) classifyDependencyRange(edge.range);
|
|
548
620
|
}
|
|
549
621
|
/**
|
|
@@ -596,6 +668,10 @@ function matchTrailerLine(message, key) {
|
|
|
596
668
|
}
|
|
597
669
|
//#endregion
|
|
598
670
|
//#region src/plugins.ts
|
|
671
|
+
/** semantic-release's own `getLastRelease` returns `{}` for a package with no prior tag -- not `undefined`, and not a fully-populated `LastRelease` -- contradicting the `gitHead: string` its own type declares. Narrows structurally rather than trusting that declared type, so a first-release context's `lastRelease` (correctly, at runtime) never claims a `gitHead` it does not have. */
|
|
672
|
+
function hasGitHead(lastRelease) {
|
|
673
|
+
return typeof lastRelease === "object" && lastRelease !== null && "gitHead" in lastRelease && typeof lastRelease.gitHead === "string";
|
|
674
|
+
}
|
|
599
675
|
/** The standard publish pipeline this orchestrator coordinates when a workspace configures none of its own. Every entry reuses the corresponding official plugin -- the orchestrator scopes and sequences them per package, it does not reimplement npm publishing, GitHub release creation, or changelog writing. */
|
|
600
676
|
const DEFAULT_PUBLISH_PLUGINS = [
|
|
601
677
|
"@semantic-release/changelog",
|
|
@@ -606,7 +682,7 @@ const DEFAULT_PUBLISH_PLUGINS = [
|
|
|
606
682
|
message: "chore(release): ${nextRelease.gitTag} [skip ci]"
|
|
607
683
|
}]
|
|
608
684
|
];
|
|
609
|
-
/** The standard publish pipeline for `commitStrategy: 'single'`: the same as `DEFAULT_PUBLISH_PLUGINS` minus
|
|
685
|
+
/** The standard publish pipeline for `commitStrategy: 'single'`: the same as `DEFAULT_PUBLISH_PLUGINS` minus `@semantic-release/git`, which that mode never runs -- see `resolvePublishPlugins`'s `forbidGitPlugin` option for why it is rejected outright rather than merely unused. Single-commit mode does its own committing (one combined commit for every released package), so a `prepare`-step git plugin here would create the very per-package commits that mode exists to avoid. */
|
|
610
686
|
const SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS = [
|
|
611
687
|
"@semantic-release/changelog",
|
|
612
688
|
"@semantic-release/npm",
|
|
@@ -616,14 +692,14 @@ const STEP_PLUGINS_THE_ORCHESTRATOR_OWNS = /* @__PURE__ */ new Set(["@semantic-r
|
|
|
616
692
|
/**
|
|
617
693
|
* Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
|
|
618
694
|
*
|
|
619
|
-
* Both apply the same path scoping before delegating to the real
|
|
695
|
+
* Both apply the same path scoping before delegating to the real `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator`: the commit list semantic-release already fetched for the release range is filtered down to commits whose `git log --name-only` file list intersects the package's own directory, and only the filtered list reaches the standard plugin. Conventional-commit parsing and changelog formatting stay entirely inside the standard plugins.
|
|
620
696
|
*
|
|
621
697
|
* The `analyzeCommits` wrapper carries one addition beyond filtering: when the standard analyzer finds no releasable commits but a workspace dependency range of the package's has changed, it returns 'patch' anyway. A dependent whose only change is a dependency bump still needs a release for that range to reach the registry. "Has changed" is read from two sources, merged: bumps recorded in memory earlier in the current run (`scope.bumps`), and bumps recorded in the package's own filtered commit history via the trailer `dependency-bump-commit.ts` writes and reads -- the latter is what lets a run that starts after a previous run already committed and pushed the bump (a crash recovery, or simply a later run) reach the same decision, rather than depending on state that existed only inside the process that made the commit.
|
|
622
698
|
*/
|
|
623
699
|
function createScopedPlugins(scope) {
|
|
624
700
|
let cached;
|
|
625
701
|
async function commitsForPackage(context) {
|
|
626
|
-
const from = context.lastRelease
|
|
702
|
+
const from = hasGitHead(context.lastRelease) ? context.lastRelease.gitHead : void 0;
|
|
627
703
|
if (cached === void 0) cached = {
|
|
628
704
|
from,
|
|
629
705
|
paths: changedPathsSince(from, { cwd: context.cwd })
|
|
@@ -643,10 +719,10 @@ function createScopedPlugins(scope) {
|
|
|
643
719
|
...context,
|
|
644
720
|
commits
|
|
645
721
|
});
|
|
646
|
-
if (type) return type;
|
|
722
|
+
if (typeof type === "string") return type;
|
|
647
723
|
const bumps = mergeDependencyBumps(scope.bumps.bumpsFor(scope.pkg.name), commits);
|
|
648
724
|
if (bumps.length === 0) return false;
|
|
649
|
-
context.logger.log(`No releasable commits under ${scope.pkg.relativeDirectory}, but ${bumps.length === 1 ? "a workspace dependency range changed" : `${bumps.length} workspace dependency ranges changed`}; forcing a patch release.`);
|
|
725
|
+
context.logger.log(`No releasable commits under ${scope.pkg.relativeDirectory}, but ${bumps.length === 1 ? "a workspace dependency range changed" : `${String(bumps.length)} workspace dependency ranges changed`}; forcing a patch release.`);
|
|
650
726
|
return "patch";
|
|
651
727
|
},
|
|
652
728
|
async generateNotes(_pluginConfig, context) {
|
|
@@ -662,7 +738,7 @@ function createScopedPlugins(scope) {
|
|
|
662
738
|
"",
|
|
663
739
|
...bumps.map((bump) => describeDependencyBump(bump))
|
|
664
740
|
].join("\n");
|
|
665
|
-
return notes ? `${notes}\n\n${section}` : section;
|
|
741
|
+
return typeof notes === "string" ? `${notes}\n\n${section}` : section;
|
|
666
742
|
}
|
|
667
743
|
};
|
|
668
744
|
}
|
|
@@ -734,7 +810,6 @@ function parsePublishPluginSpec(spec) {
|
|
|
734
810
|
}
|
|
735
811
|
//#endregion
|
|
736
812
|
//#region src/pnpm.ts
|
|
737
|
-
const execFileAsync = promisify(execFile);
|
|
738
813
|
/**
|
|
739
814
|
* 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.
|
|
740
815
|
*
|
|
@@ -742,7 +817,7 @@ const execFileAsync = promisify(execFile);
|
|
|
742
817
|
*/
|
|
743
818
|
async function regenerateLockfile(options) {
|
|
744
819
|
try {
|
|
745
|
-
await
|
|
820
|
+
await execFile$1("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
|
|
746
821
|
} catch (cause) {
|
|
747
822
|
const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
|
|
748
823
|
const detail = stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause);
|
|
@@ -757,8 +832,8 @@ async function regenerateLockfile(options) {
|
|
|
757
832
|
* Five phases, all inside one `releaseWorkspaceSingleCommit` call:
|
|
758
833
|
*
|
|
759
834
|
* 1. **Analyse** (this file's `analysePackage`): for every package, in topological order, run semantic-release with `dryRun: true` forced (regardless of the caller's own `dryRun` option) using the same path-scoped `analyzeCommits`/`generateNotes` wrapper `commitStrategy: 'per-package'` uses -- computing each package's next version and notes without writing, committing, tagging, or publishing anything. Cross-package dependency bumps are tracked purely in memory during this phase (`pendingBumps`), exactly as the per-package strategy tracks them for the span of one run; nothing is committed yet for a later run to recover from, because this strategy never leaves a partial commit for a crash to recover from in the first place -- either the whole combined commit lands, or nothing does.
|
|
760
|
-
* 2. **Verify** every released package's configured publish plugins' `verifyConditions` step (npm registry auth, GitHub token/repo access), before any file is written -- the same fail-fast-before-anything-releases discipline `
|
|
761
|
-
* 3. **Prepare**: for every released package, in topological order, apply any dependency-range bump its own manifest received (writing `package.json` directly, the same `writeDependencyRange` the per-package strategy uses), then run every configured publish plugin's own `prepare` step generically (whichever it defines --
|
|
835
|
+
* 2. **Verify** every released package's configured publish plugins' `verifyConditions` step (npm registry auth, GitHub token/repo access), before any file is written -- the same fail-fast-before-anything-releases discipline `validateDependencyRanges` already applies to dependency ranges.
|
|
836
|
+
* 3. **Prepare**: for every released package, in topological order, apply any dependency-range bump its own manifest received (writing `package.json` directly, the same `writeDependencyRange` the per-package strategy uses), then run every configured publish plugin's own `prepare` step generically (whichever it defines -- `@semantic-release/npm` bumps `package.json`'s version, `@semantic-release/changelog` writes `CHANGELOG.md`). `@semantic-release/git` is rejected outright from this mode's plugin list (see `resolvePublishPlugins`'s `forbidGitPlugin`), since its own `prepare` step would create exactly the per-package commit this mode exists to avoid. The lockfile is regenerated once at the end, not once per bump, since `pnpm install --lockfile-only` recomputes it from whatever is on disk regardless of how many manifests changed.
|
|
762
837
|
* 4. **Commit**: discover every file phase 3 touched via `git status` (rather than predicting filenames per plugin), make one commit, tag it once per released package (`name@version`, lightweight, matching semantic-release's own tag form), and push the commit and every tag together.
|
|
763
838
|
* 5. **Publish**: for every released package, in topological order, call each configured plugin's own `publish` step directly (not through semantic-release's top-level orchestrator -- see the note below), then `success`.
|
|
764
839
|
*
|
|
@@ -773,9 +848,9 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
773
848
|
const repoRoot = (await git(["rev-parse", "--show-toplevel"], { cwd: workspace.root })).trim();
|
|
774
849
|
await assertCleanWorkingTree({ cwd: repoRoot });
|
|
775
850
|
const graph = buildDependencyGraph(workspace.packages);
|
|
776
|
-
|
|
851
|
+
validateDependencyRanges(graph);
|
|
777
852
|
const order = topologicalOrder(graph);
|
|
778
|
-
log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
853
|
+
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
779
854
|
const resolvedPlugins = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
|
|
780
855
|
requireGitPlugin: false,
|
|
781
856
|
forbidGitPlugin: true
|
|
@@ -801,19 +876,26 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
801
876
|
bumpsForThisPackage,
|
|
802
877
|
env,
|
|
803
878
|
branches: options.branches,
|
|
804
|
-
onCommitsResolved: (commits) =>
|
|
879
|
+
onCommitsResolved: (commits) => {
|
|
880
|
+
capturedCommits.set(name, commits);
|
|
881
|
+
},
|
|
805
882
|
onContextCaptured: (context) => {
|
|
806
883
|
captured.branch = context.branch;
|
|
807
884
|
captured.repositoryUrl = context.repositoryUrl;
|
|
808
885
|
}
|
|
809
886
|
});
|
|
810
|
-
outcomes.push({
|
|
887
|
+
outcomes.push(nextRelease === void 0 ? {
|
|
888
|
+
name,
|
|
889
|
+
directory: pkg.directory,
|
|
890
|
+
released: false,
|
|
891
|
+
dependencyBumps: bumpsForThisPackage
|
|
892
|
+
} : {
|
|
811
893
|
name,
|
|
812
894
|
directory: pkg.directory,
|
|
813
|
-
released:
|
|
814
|
-
version: nextRelease
|
|
815
|
-
gitTag: nextRelease
|
|
816
|
-
type: nextRelease
|
|
895
|
+
released: true,
|
|
896
|
+
version: nextRelease.version,
|
|
897
|
+
gitTag: nextRelease.gitTag,
|
|
898
|
+
type: nextRelease.type,
|
|
817
899
|
dependencyBumps: bumpsForThisPackage
|
|
818
900
|
});
|
|
819
901
|
if (nextRelease === void 0) {
|
|
@@ -841,7 +923,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
841
923
|
};
|
|
842
924
|
const branch = captured.branch;
|
|
843
925
|
const repositoryUrl = captured.repositoryUrl;
|
|
844
|
-
if (branch === void 0 || repositoryUrl === void 0) throw new ReleaseConfigurationError(`Internal error: ${packageName} analysed ${planned.length} package release(s) but never captured a branch/repositoryUrl from semantic-release's own context. This should be impossible when at least one package releases.`);
|
|
926
|
+
if (branch === void 0 || repositoryUrl === void 0) throw new ReleaseConfigurationError(`Internal error: ${packageName} analysed ${String(planned.length)} package release(s) but never captured a branch/repositoryUrl from semantic-release's own context. This should be impossible when at least one package releases.`);
|
|
845
927
|
const shared = {
|
|
846
928
|
env,
|
|
847
929
|
branch,
|
|
@@ -867,7 +949,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
867
949
|
}
|
|
868
950
|
if (anyRangeRewritten) await regenerateLockfile({ cwd: workspace.root });
|
|
869
951
|
const touchedPaths = await workingTreeChanges({ cwd: repoRoot });
|
|
870
|
-
if (touchedPaths.length === 0) throw new ReleaseConfigurationError(`${packageName}: analysis planned ${planned.length} release(s), but no files changed while preparing them. Every configured publish plugin's own "prepare" step (bumping package.json, writing CHANGELOG.md) produced nothing to commit -- check the plugin list includes something that writes the version, e.g. @semantic-release/npm.`);
|
|
952
|
+
if (touchedPaths.length === 0) throw new ReleaseConfigurationError(`${packageName}: analysis planned ${String(planned.length)} release(s), but no files changed while preparing them. Every configured publish plugin's own "prepare" step (bumping package.json, writing CHANGELOG.md) produced nothing to commit -- check the plugin list includes something that writes the version, e.g. @semantic-release/npm.`);
|
|
871
953
|
const identity = await resolveCommitIdentity({ cwd: repoRoot });
|
|
872
954
|
await commitFiles(touchedPaths, describeCombinedCommit(planned), {
|
|
873
955
|
cwd: repoRoot,
|
|
@@ -877,7 +959,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
877
959
|
const tagNames = planned.map((release) => release.gitTag);
|
|
878
960
|
for (const tagName of tagNames) await createTag(tagName, commitSha, { cwd: repoRoot });
|
|
879
961
|
await pushHeadAndTags(tagNames, { cwd: repoRoot });
|
|
880
|
-
log(`${packageName}: committed ${commitSha} and pushed ${tagNames.length} tag(s): ${tagNames.join(", ")}`);
|
|
962
|
+
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
881
963
|
for (const release of planned) {
|
|
882
964
|
const releases = [];
|
|
883
965
|
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
@@ -969,10 +1051,18 @@ function describeCombinedCommit(planned) {
|
|
|
969
1051
|
}
|
|
970
1052
|
function buildPluginContext(release, shared, capturedCommits, releases) {
|
|
971
1053
|
const logger = {
|
|
972
|
-
log: (...args) =>
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1054
|
+
log: (...args) => {
|
|
1055
|
+
shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
|
|
1056
|
+
},
|
|
1057
|
+
warn: (...args) => {
|
|
1058
|
+
shared.log(`[${release.pkg.name}] warn: ${args.map(String).join(" ")}`);
|
|
1059
|
+
},
|
|
1060
|
+
error: (...args) => {
|
|
1061
|
+
shared.log(`[${release.pkg.name}] error: ${args.map(String).join(" ")}`);
|
|
1062
|
+
},
|
|
1063
|
+
success: (...args) => {
|
|
1064
|
+
shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
|
|
1065
|
+
}
|
|
976
1066
|
};
|
|
977
1067
|
return {
|
|
978
1068
|
cwd: release.pkg.directory,
|
|
@@ -1037,9 +1127,9 @@ async function releaseWorkspace(options = {}) {
|
|
|
1037
1127
|
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
1038
1128
|
const workspace = await discoverWorkspace(root);
|
|
1039
1129
|
const graph = buildDependencyGraph(workspace.packages);
|
|
1040
|
-
|
|
1130
|
+
validateDependencyRanges(graph);
|
|
1041
1131
|
const order = topologicalOrder(graph);
|
|
1042
|
-
log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")}`);
|
|
1132
|
+
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1043
1133
|
const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
|
|
1044
1134
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1045
1135
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
@@ -1063,13 +1153,18 @@ async function releaseWorkspace(options = {}) {
|
|
|
1063
1153
|
};
|
|
1064
1154
|
})).map((entry) => {
|
|
1065
1155
|
const nextRelease = entry.result === false ? void 0 : entry.result.nextRelease;
|
|
1066
|
-
return {
|
|
1156
|
+
return nextRelease === void 0 ? {
|
|
1067
1157
|
name: entry.name,
|
|
1068
1158
|
directory: entry.directory,
|
|
1069
|
-
released:
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1159
|
+
released: false,
|
|
1160
|
+
dependencyBumps: entry.dependencyBumps
|
|
1161
|
+
} : {
|
|
1162
|
+
name: entry.name,
|
|
1163
|
+
directory: entry.directory,
|
|
1164
|
+
released: true,
|
|
1165
|
+
version: nextRelease.version,
|
|
1166
|
+
gitTag: nextRelease.gitTag,
|
|
1167
|
+
type: nextRelease.type,
|
|
1073
1168
|
dependencyBumps: entry.dependencyBumps
|
|
1074
1169
|
};
|
|
1075
1170
|
})
|
|
@@ -1195,9 +1290,9 @@ async function detachWorkspaceRelease(options) {
|
|
|
1195
1290
|
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
1196
1291
|
const workspace = await discoverWorkspace(root);
|
|
1197
1292
|
const graph = buildDependencyGraph(workspace.packages);
|
|
1198
|
-
|
|
1293
|
+
validateDependencyRanges(graph);
|
|
1199
1294
|
const order = topologicalOrder(graph);
|
|
1200
|
-
log(`${packageName}: ${order.length} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1295
|
+
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1201
1296
|
const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
|
|
1202
1297
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1203
1298
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
@@ -1219,15 +1314,20 @@ async function detachWorkspaceRelease(options) {
|
|
|
1219
1314
|
});
|
|
1220
1315
|
return {
|
|
1221
1316
|
order,
|
|
1222
|
-
packages: entries.map((entry) =>
|
|
1317
|
+
packages: entries.map((entry) => entry.result === null ? {
|
|
1318
|
+
name: entry.name,
|
|
1319
|
+
directory: entry.directory,
|
|
1320
|
+
released: false,
|
|
1321
|
+
dependencyBumps: entry.dependencyBumps
|
|
1322
|
+
} : {
|
|
1223
1323
|
name: entry.name,
|
|
1224
1324
|
directory: entry.directory,
|
|
1225
|
-
released:
|
|
1226
|
-
version: entry.result
|
|
1227
|
-
gitTag: entry.result
|
|
1228
|
-
type: entry.result
|
|
1325
|
+
released: true,
|
|
1326
|
+
version: entry.result.nextRelease.version,
|
|
1327
|
+
gitTag: entry.result.nextRelease.gitTag,
|
|
1328
|
+
type: entry.result.nextRelease.type,
|
|
1229
1329
|
dependencyBumps: entry.dependencyBumps
|
|
1230
|
-
})
|
|
1330
|
+
}),
|
|
1231
1331
|
detached: entries.map((entry) => ({
|
|
1232
1332
|
name: entry.name,
|
|
1233
1333
|
relativeDirectory: entry.relativeDirectory,
|
|
@@ -1279,6 +1379,7 @@ async function resumeWorkspaceRelease(options) {
|
|
|
1279
1379
|
const root = resolve(options.root ?? process.cwd());
|
|
1280
1380
|
const log = options.log ?? console.log;
|
|
1281
1381
|
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
1382
|
+
assertPublishableDependencies(await Promise.all(options.detached.filter((entry) => entry.state !== null).map(async (entry) => readManifest(resolve(root, entry.relativeDirectory, "package.json")))));
|
|
1282
1383
|
const order = [];
|
|
1283
1384
|
const packages = [];
|
|
1284
1385
|
for (const entry of options.detached) {
|
|
@@ -1289,9 +1390,6 @@ async function resumeWorkspaceRelease(options) {
|
|
|
1289
1390
|
name: entry.name,
|
|
1290
1391
|
directory: resolve(root, entry.relativeDirectory),
|
|
1291
1392
|
released: false,
|
|
1292
|
-
version: void 0,
|
|
1293
|
-
gitTag: void 0,
|
|
1294
|
-
type: void 0,
|
|
1295
1393
|
dependencyBumps: entry.dependencyBumps
|
|
1296
1394
|
});
|
|
1297
1395
|
continue;
|
|
@@ -1307,7 +1405,7 @@ async function resumeWorkspaceRelease(options) {
|
|
|
1307
1405
|
} catch (cause) {
|
|
1308
1406
|
throw new WorkspaceReleaseError(`Resuming ${entry.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
1309
1407
|
}
|
|
1310
|
-
log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${releases.length} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
|
|
1408
|
+
log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${String(releases.length)} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
|
|
1311
1409
|
packages.push({
|
|
1312
1410
|
name: entry.name,
|
|
1313
1411
|
directory,
|
|
@@ -1396,7 +1494,7 @@ async function runRelease(flags) {
|
|
|
1396
1494
|
if (gatePublish) {
|
|
1397
1495
|
if (flags.gateStateFile === void 0) throw new WorkspaceReleaseError("gatePublish was true but no --gate-state-file was resolved -- this should be unreachable.");
|
|
1398
1496
|
await writeFile(flags.gateStateFile, JSON.stringify(outcome.detached ?? [], null, 2));
|
|
1399
|
-
console.log(`${packageName}: wrote gate state for ${(outcome.detached ?? []).length} package(s) to ${flags.gateStateFile}`);
|
|
1497
|
+
console.log(`${packageName}: wrote gate state for ${String((outcome.detached ?? []).length)} package(s) to ${flags.gateStateFile}`);
|
|
1400
1498
|
}
|
|
1401
1499
|
}
|
|
1402
1500
|
async function runResume(flags) {
|