@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/dist/index.cjs
CHANGED
|
@@ -26,7 +26,6 @@ let node_path = require("node:path");
|
|
|
26
26
|
let tinyglobby = require("tinyglobby");
|
|
27
27
|
let yaml = require("yaml");
|
|
28
28
|
let node_child_process = require("node:child_process");
|
|
29
|
-
let node_util = require("node:util");
|
|
30
29
|
let validate_npm_package_name = require("validate-npm-package-name");
|
|
31
30
|
validate_npm_package_name = __toESM(validate_npm_package_name, 1);
|
|
32
31
|
let node_module = require("node:module");
|
|
@@ -62,7 +61,7 @@ var DependencyCycleError = class extends WorkspaceReleaseError {
|
|
|
62
61
|
this.cycle = cycle;
|
|
63
62
|
}
|
|
64
63
|
};
|
|
65
|
-
/** A dependency
|
|
64
|
+
/** 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. */
|
|
66
65
|
var UnsupportedDependencyRangeError = class extends WorkspaceReleaseError {};
|
|
67
66
|
/** 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. */
|
|
68
67
|
var ReleaseConfigurationError = class extends WorkspaceReleaseError {};
|
|
@@ -70,7 +69,7 @@ var ReleaseConfigurationError = class extends WorkspaceReleaseError {};
|
|
|
70
69
|
var GitCommandError = class extends WorkspaceReleaseError {
|
|
71
70
|
exitCode;
|
|
72
71
|
constructor(args, cwd, exitCode, detail) {
|
|
73
|
-
super(`git ${args.join(" ")} failed in ${cwd}${exitCode === void 0 ? "" : ` (exit ${exitCode})`}: ${detail}`);
|
|
72
|
+
super(`git ${args.join(" ")} failed in ${cwd}${exitCode === void 0 ? "" : ` (exit ${String(exitCode)})`}: ${detail}`);
|
|
74
73
|
this.exitCode = exitCode;
|
|
75
74
|
}
|
|
76
75
|
};
|
|
@@ -83,10 +82,30 @@ var PnpmCommandError = class extends WorkspaceReleaseError {
|
|
|
83
82
|
}
|
|
84
83
|
};
|
|
85
84
|
//#endregion
|
|
85
|
+
//#region src/exec-file.ts
|
|
86
|
+
/**
|
|
87
|
+
* 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.
|
|
88
|
+
*/
|
|
89
|
+
async function execFile(command, args, options) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
(0, node_child_process.execFile)(command, [...args], {
|
|
92
|
+
cwd: options.cwd,
|
|
93
|
+
env: options.env,
|
|
94
|
+
maxBuffer: options.maxBuffer
|
|
95
|
+
}, (error, stdout, stderr) => {
|
|
96
|
+
if (error) {
|
|
97
|
+
reject(error instanceof Error ? error : new Error(error.message));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
resolve({
|
|
101
|
+
stdout,
|
|
102
|
+
stderr
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
86
108
|
//#region src/git.ts
|
|
87
|
-
const execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
|
|
88
|
-
/** `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. */
|
|
89
|
-
const GIT_MAX_BUFFER_BYTES = 104857600;
|
|
90
109
|
/** 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. */
|
|
91
110
|
const COMMIT_RECORD_SEPARATOR = "";
|
|
92
111
|
/** 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. */
|
|
@@ -114,18 +133,22 @@ const GIT_REPOSITORY_DISCOVERY_ENV_KEYS = [
|
|
|
114
133
|
*/
|
|
115
134
|
function sanitizeGitEnv(env) {
|
|
116
135
|
const sanitized = { ...env };
|
|
117
|
-
for (const key of GIT_REPOSITORY_DISCOVERY_ENV_KEYS)
|
|
136
|
+
for (const key of GIT_REPOSITORY_DISCOVERY_ENV_KEYS) Reflect.deleteProperty(sanitized, key);
|
|
118
137
|
return sanitized;
|
|
119
138
|
}
|
|
120
139
|
const SANITIZED_PROCESS_GIT_ENV = sanitizeGitEnv(process.env);
|
|
140
|
+
/** `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. */
|
|
141
|
+
async function execGit(args, cwd, maxBuffer = 104857600) {
|
|
142
|
+
const { stdout } = await execFile("git", args, {
|
|
143
|
+
cwd,
|
|
144
|
+
maxBuffer,
|
|
145
|
+
env: SANITIZED_PROCESS_GIT_ENV
|
|
146
|
+
});
|
|
147
|
+
return stdout;
|
|
148
|
+
}
|
|
121
149
|
async function git(args, options) {
|
|
122
150
|
try {
|
|
123
|
-
|
|
124
|
-
cwd: options.cwd,
|
|
125
|
-
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
126
|
-
env: SANITIZED_PROCESS_GIT_ENV
|
|
127
|
-
});
|
|
128
|
-
return stdout;
|
|
151
|
+
return await execGit(args, options.cwd);
|
|
129
152
|
} catch (cause) {
|
|
130
153
|
throw toGitCommandError(args, options.cwd, cause);
|
|
131
154
|
}
|
|
@@ -239,9 +262,11 @@ async function workingTreeChanges(options) {
|
|
|
239
262
|
const paths = [];
|
|
240
263
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
241
264
|
const entry = tokens[index];
|
|
242
|
-
if (entry === void 0
|
|
265
|
+
if (entry === void 0) continue;
|
|
243
266
|
const statusCode = entry.slice(0, 2);
|
|
244
|
-
|
|
267
|
+
const path = entry.slice(3);
|
|
268
|
+
if (path === "") continue;
|
|
269
|
+
paths.push(path);
|
|
245
270
|
if (statusCode.includes("R") || statusCode.includes("C")) index += 1;
|
|
246
271
|
}
|
|
247
272
|
return paths;
|
|
@@ -299,6 +324,12 @@ const DEPENDENCY_FIELDS = [
|
|
|
299
324
|
"peerDependencies",
|
|
300
325
|
"optionalDependencies"
|
|
301
326
|
];
|
|
327
|
+
/** 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. */
|
|
328
|
+
const INSTALLED_DEPENDENCY_FIELDS = [
|
|
329
|
+
"dependencies",
|
|
330
|
+
"peerDependencies",
|
|
331
|
+
"optionalDependencies"
|
|
332
|
+
];
|
|
302
333
|
async function readManifest(path) {
|
|
303
334
|
const text = await (0, node_fs_promises.readFile)(path, "utf8");
|
|
304
335
|
const parsed = JSON.parse(text);
|
|
@@ -306,7 +337,7 @@ async function readManifest(path) {
|
|
|
306
337
|
const { name, version } = parsed;
|
|
307
338
|
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.`);
|
|
308
339
|
const validity = (0, validate_npm_package_name.default)(name);
|
|
309
|
-
if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${
|
|
340
|
+
if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${validity.errors.join("; ")}`);
|
|
310
341
|
if (typeof version !== "string" || version.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "version".`);
|
|
311
342
|
const dependencies = /* @__PURE__ */ new Map();
|
|
312
343
|
for (const field of DEPENDENCY_FIELDS) {
|
|
@@ -318,6 +349,7 @@ async function readManifest(path) {
|
|
|
318
349
|
return {
|
|
319
350
|
name,
|
|
320
351
|
version,
|
|
352
|
+
private: parsed.private === true,
|
|
321
353
|
dependencies
|
|
322
354
|
};
|
|
323
355
|
}
|
|
@@ -372,6 +404,7 @@ async function discoverWorkspace(root) {
|
|
|
372
404
|
packages.push({
|
|
373
405
|
name: manifest.name,
|
|
374
406
|
version: manifest.version,
|
|
407
|
+
private: manifest.private,
|
|
375
408
|
directory,
|
|
376
409
|
relativeDirectory,
|
|
377
410
|
repoRelativeDirectory,
|
|
@@ -420,7 +453,14 @@ function toPosix(path) {
|
|
|
420
453
|
const WORKSPACE_PROTOCOL = "workspace:";
|
|
421
454
|
const CATALOG_PROTOCOL = "catalog:";
|
|
422
455
|
const NPM_ALIAS_PROTOCOL = "npm:";
|
|
423
|
-
/**
|
|
456
|
+
/** 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. */
|
|
457
|
+
const UNPUBLISHABLE_SPECIFIER_PROTOCOLS = [
|
|
458
|
+
WORKSPACE_PROTOCOL,
|
|
459
|
+
CATALOG_PROTOCOL,
|
|
460
|
+
"link:",
|
|
461
|
+
"file:"
|
|
462
|
+
];
|
|
463
|
+
/** The `workspace:` suffixes that name no version, and so leave nothing in the manifest to rewrite. */
|
|
424
464
|
const PUBLISH_RESOLVED_WORKSPACE_SUFFIXES = [
|
|
425
465
|
"*",
|
|
426
466
|
"^",
|
|
@@ -435,6 +475,13 @@ const WILDCARD_RANGES = [
|
|
|
435
475
|
"latest"
|
|
436
476
|
];
|
|
437
477
|
/**
|
|
478
|
+
* 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.
|
|
479
|
+
*/
|
|
480
|
+
function unpublishableSpecifierProtocol(specifier) {
|
|
481
|
+
const trimmed = specifier.trim();
|
|
482
|
+
return UNPUBLISHABLE_SPECIFIER_PROTOCOLS.find((protocol) => trimmed.startsWith(protocol));
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
438
485
|
* 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.
|
|
439
486
|
*/
|
|
440
487
|
const REWRITABLE_COMPARATOR = /^(\^|~|>=|=)?(\d+\.\d+\.\d+(?:-[\dA-Za-z.-]+)?(?:\+[\dA-Za-z.-]+)?)$/;
|
|
@@ -456,7 +503,7 @@ function classifyDependencyRange(current) {
|
|
|
456
503
|
comparator: inner.comparator
|
|
457
504
|
};
|
|
458
505
|
}
|
|
459
|
-
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.
|
|
506
|
+
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.`);
|
|
460
507
|
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.`);
|
|
461
508
|
if (WILDCARD_RANGES.includes(range)) return { kind: "wildcard" };
|
|
462
509
|
const match = REWRITABLE_COMPARATOR.exec(range);
|
|
@@ -480,6 +527,27 @@ function updateDependencyRange(current, version) {
|
|
|
480
527
|
};
|
|
481
528
|
}
|
|
482
529
|
//#endregion
|
|
530
|
+
//#region src/publishable-dependencies.ts
|
|
531
|
+
/**
|
|
532
|
+
* Throws `UnsupportedDependencyRangeError` unless every publishable package's installed dependencies (`dependencies`, `peerDependencies`, `optionalDependencies`) are ranges a consumer's package manager can resolve.
|
|
533
|
+
*
|
|
534
|
+
* 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.
|
|
535
|
+
*
|
|
536
|
+
* 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.
|
|
537
|
+
*/
|
|
538
|
+
function assertPublishableDependencies(packages) {
|
|
539
|
+
const offences = [];
|
|
540
|
+
for (const pkg of packages) {
|
|
541
|
+
if (pkg.private) continue;
|
|
542
|
+
for (const field of INSTALLED_DEPENDENCY_FIELDS) {
|
|
543
|
+
const declared = pkg.dependencies.get(field);
|
|
544
|
+
if (declared === void 0) continue;
|
|
545
|
+
for (const [dependency, specifier] of declared) if (unpublishableSpecifierProtocol(specifier) !== void 0) offences.push(` ${pkg.name}: "${dependency}" in ${field} is declared as "${specifier}"`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
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.`);
|
|
549
|
+
}
|
|
550
|
+
//#endregion
|
|
483
551
|
//#region src/graph.ts
|
|
484
552
|
/**
|
|
485
553
|
* Builds the inter-package dependency graph from the manifests alone.
|
|
@@ -555,11 +623,15 @@ function firstUnplacedDependency(name, graph, unplaced) {
|
|
|
555
623
|
return (graph.dependencies.get(name) ?? []).map((edge) => edge.dependency).filter((dependency) => unplaced.has(dependency)).sort()[0];
|
|
556
624
|
}
|
|
557
625
|
/**
|
|
558
|
-
*
|
|
626
|
+
* 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:
|
|
627
|
+
*
|
|
628
|
+
* - Every publishable package's installed dependencies must be specifiers a consumer can install (see `assertPublishableDependencies`), whichever package they name.
|
|
629
|
+
* - 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).
|
|
559
630
|
*
|
|
560
|
-
* Shared by
|
|
631
|
+
* 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.
|
|
561
632
|
*/
|
|
562
|
-
function
|
|
633
|
+
function validateDependencyRanges(graph) {
|
|
634
|
+
assertPublishableDependencies([...graph.packages.values()]);
|
|
563
635
|
for (const edges of graph.dependencies.values()) for (const edge of edges) classifyDependencyRange(edge.range);
|
|
564
636
|
}
|
|
565
637
|
/**
|
|
@@ -609,6 +681,10 @@ function matchTrailerLine(message, key) {
|
|
|
609
681
|
}
|
|
610
682
|
//#endregion
|
|
611
683
|
//#region src/plugins.ts
|
|
684
|
+
/** 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. */
|
|
685
|
+
function hasGitHead(lastRelease) {
|
|
686
|
+
return typeof lastRelease === "object" && lastRelease !== null && "gitHead" in lastRelease && typeof lastRelease.gitHead === "string";
|
|
687
|
+
}
|
|
612
688
|
/** 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. */
|
|
613
689
|
const DEFAULT_PUBLISH_PLUGINS = [
|
|
614
690
|
"@semantic-release/changelog",
|
|
@@ -619,7 +695,7 @@ const DEFAULT_PUBLISH_PLUGINS = [
|
|
|
619
695
|
message: "chore(release): ${nextRelease.gitTag} [skip ci]"
|
|
620
696
|
}]
|
|
621
697
|
];
|
|
622
|
-
/** The standard publish pipeline for `commitStrategy: 'single'`: the same as `DEFAULT_PUBLISH_PLUGINS` minus
|
|
698
|
+
/** 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. */
|
|
623
699
|
const SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS = [
|
|
624
700
|
"@semantic-release/changelog",
|
|
625
701
|
"@semantic-release/npm",
|
|
@@ -629,14 +705,14 @@ const STEP_PLUGINS_THE_ORCHESTRATOR_OWNS = /* @__PURE__ */ new Set(["@semantic-r
|
|
|
629
705
|
/**
|
|
630
706
|
* Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
|
|
631
707
|
*
|
|
632
|
-
* Both apply the same path scoping before delegating to the real
|
|
708
|
+
* 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.
|
|
633
709
|
*
|
|
634
710
|
* 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.
|
|
635
711
|
*/
|
|
636
712
|
function createScopedPlugins(scope) {
|
|
637
713
|
let cached;
|
|
638
714
|
async function commitsForPackage(context) {
|
|
639
|
-
const from = context.lastRelease
|
|
715
|
+
const from = hasGitHead(context.lastRelease) ? context.lastRelease.gitHead : void 0;
|
|
640
716
|
if (cached === void 0) cached = {
|
|
641
717
|
from,
|
|
642
718
|
paths: changedPathsSince(from, { cwd: context.cwd })
|
|
@@ -656,10 +732,10 @@ function createScopedPlugins(scope) {
|
|
|
656
732
|
...context,
|
|
657
733
|
commits
|
|
658
734
|
});
|
|
659
|
-
if (type) return type;
|
|
735
|
+
if (typeof type === "string") return type;
|
|
660
736
|
const bumps = mergeDependencyBumps(scope.bumps.bumpsFor(scope.pkg.name), commits);
|
|
661
737
|
if (bumps.length === 0) return false;
|
|
662
|
-
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.`);
|
|
738
|
+
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.`);
|
|
663
739
|
return "patch";
|
|
664
740
|
},
|
|
665
741
|
async generateNotes(_pluginConfig, context) {
|
|
@@ -675,7 +751,7 @@ function createScopedPlugins(scope) {
|
|
|
675
751
|
"",
|
|
676
752
|
...bumps.map((bump) => describeDependencyBump(bump))
|
|
677
753
|
].join("\n");
|
|
678
|
-
return notes ? `${notes}\n\n${section}` : section;
|
|
754
|
+
return typeof notes === "string" ? `${notes}\n\n${section}` : section;
|
|
679
755
|
}
|
|
680
756
|
};
|
|
681
757
|
}
|
|
@@ -747,7 +823,6 @@ function parsePublishPluginSpec(spec) {
|
|
|
747
823
|
}
|
|
748
824
|
//#endregion
|
|
749
825
|
//#region src/pnpm.ts
|
|
750
|
-
const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
751
826
|
/**
|
|
752
827
|
* 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.
|
|
753
828
|
*
|
|
@@ -755,7 +830,7 @@ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
|
755
830
|
*/
|
|
756
831
|
async function regenerateLockfile(options) {
|
|
757
832
|
try {
|
|
758
|
-
await
|
|
833
|
+
await execFile("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
|
|
759
834
|
} catch (cause) {
|
|
760
835
|
const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
|
|
761
836
|
const detail = stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause);
|
|
@@ -770,8 +845,8 @@ async function regenerateLockfile(options) {
|
|
|
770
845
|
* Five phases, all inside one `releaseWorkspaceSingleCommit` call:
|
|
771
846
|
*
|
|
772
847
|
* 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.
|
|
773
|
-
* 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 `
|
|
774
|
-
* 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 --
|
|
848
|
+
* 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.
|
|
849
|
+
* 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.
|
|
775
850
|
* 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.
|
|
776
851
|
* 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`.
|
|
777
852
|
*
|
|
@@ -786,9 +861,9 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
786
861
|
const repoRoot = (await git(["rev-parse", "--show-toplevel"], { cwd: workspace.root })).trim();
|
|
787
862
|
await assertCleanWorkingTree({ cwd: repoRoot });
|
|
788
863
|
const graph = buildDependencyGraph(workspace.packages);
|
|
789
|
-
|
|
864
|
+
validateDependencyRanges(graph);
|
|
790
865
|
const order = topologicalOrder(graph);
|
|
791
|
-
log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
866
|
+
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
792
867
|
const resolvedPlugins = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
|
|
793
868
|
requireGitPlugin: false,
|
|
794
869
|
forbidGitPlugin: true
|
|
@@ -814,19 +889,26 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
814
889
|
bumpsForThisPackage,
|
|
815
890
|
env,
|
|
816
891
|
branches: options.branches,
|
|
817
|
-
onCommitsResolved: (commits) =>
|
|
892
|
+
onCommitsResolved: (commits) => {
|
|
893
|
+
capturedCommits.set(name, commits);
|
|
894
|
+
},
|
|
818
895
|
onContextCaptured: (context) => {
|
|
819
896
|
captured.branch = context.branch;
|
|
820
897
|
captured.repositoryUrl = context.repositoryUrl;
|
|
821
898
|
}
|
|
822
899
|
});
|
|
823
|
-
outcomes.push({
|
|
900
|
+
outcomes.push(nextRelease === void 0 ? {
|
|
901
|
+
name,
|
|
902
|
+
directory: pkg.directory,
|
|
903
|
+
released: false,
|
|
904
|
+
dependencyBumps: bumpsForThisPackage
|
|
905
|
+
} : {
|
|
824
906
|
name,
|
|
825
907
|
directory: pkg.directory,
|
|
826
|
-
released:
|
|
827
|
-
version: nextRelease
|
|
828
|
-
gitTag: nextRelease
|
|
829
|
-
type: nextRelease
|
|
908
|
+
released: true,
|
|
909
|
+
version: nextRelease.version,
|
|
910
|
+
gitTag: nextRelease.gitTag,
|
|
911
|
+
type: nextRelease.type,
|
|
830
912
|
dependencyBumps: bumpsForThisPackage
|
|
831
913
|
});
|
|
832
914
|
if (nextRelease === void 0) {
|
|
@@ -854,7 +936,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
854
936
|
};
|
|
855
937
|
const branch = captured.branch;
|
|
856
938
|
const repositoryUrl = captured.repositoryUrl;
|
|
857
|
-
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.`);
|
|
939
|
+
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.`);
|
|
858
940
|
const shared = {
|
|
859
941
|
env,
|
|
860
942
|
branch,
|
|
@@ -880,7 +962,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
880
962
|
}
|
|
881
963
|
if (anyRangeRewritten) await regenerateLockfile({ cwd: workspace.root });
|
|
882
964
|
const touchedPaths = await workingTreeChanges({ cwd: repoRoot });
|
|
883
|
-
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.`);
|
|
965
|
+
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.`);
|
|
884
966
|
const identity = await resolveCommitIdentity({ cwd: repoRoot });
|
|
885
967
|
await commitFiles(touchedPaths, describeCombinedCommit(planned), {
|
|
886
968
|
cwd: repoRoot,
|
|
@@ -890,7 +972,7 @@ async function releaseWorkspaceSingleCommit(options) {
|
|
|
890
972
|
const tagNames = planned.map((release) => release.gitTag);
|
|
891
973
|
for (const tagName of tagNames) await createTag(tagName, commitSha, { cwd: repoRoot });
|
|
892
974
|
await pushHeadAndTags(tagNames, { cwd: repoRoot });
|
|
893
|
-
log(`${packageName}: committed ${commitSha} and pushed ${tagNames.length} tag(s): ${tagNames.join(", ")}`);
|
|
975
|
+
log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
|
|
894
976
|
for (const release of planned) {
|
|
895
977
|
const releases = [];
|
|
896
978
|
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
@@ -982,10 +1064,18 @@ function describeCombinedCommit(planned) {
|
|
|
982
1064
|
}
|
|
983
1065
|
function buildPluginContext(release, shared, capturedCommits, releases) {
|
|
984
1066
|
const logger = {
|
|
985
|
-
log: (...args) =>
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1067
|
+
log: (...args) => {
|
|
1068
|
+
shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
|
|
1069
|
+
},
|
|
1070
|
+
warn: (...args) => {
|
|
1071
|
+
shared.log(`[${release.pkg.name}] warn: ${args.map(String).join(" ")}`);
|
|
1072
|
+
},
|
|
1073
|
+
error: (...args) => {
|
|
1074
|
+
shared.log(`[${release.pkg.name}] error: ${args.map(String).join(" ")}`);
|
|
1075
|
+
},
|
|
1076
|
+
success: (...args) => {
|
|
1077
|
+
shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
|
|
1078
|
+
}
|
|
989
1079
|
};
|
|
990
1080
|
return {
|
|
991
1081
|
cwd: release.pkg.directory,
|
|
@@ -1043,9 +1133,9 @@ async function detachWorkspaceRelease(options) {
|
|
|
1043
1133
|
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
1044
1134
|
const workspace = await discoverWorkspace(root);
|
|
1045
1135
|
const graph = buildDependencyGraph(workspace.packages);
|
|
1046
|
-
|
|
1136
|
+
validateDependencyRanges(graph);
|
|
1047
1137
|
const order = topologicalOrder(graph);
|
|
1048
|
-
log(`${packageName}: ${order.length} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1138
|
+
log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
|
|
1049
1139
|
const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
|
|
1050
1140
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1051
1141
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
@@ -1067,15 +1157,20 @@ async function detachWorkspaceRelease(options) {
|
|
|
1067
1157
|
});
|
|
1068
1158
|
return {
|
|
1069
1159
|
order,
|
|
1070
|
-
packages: entries.map((entry) =>
|
|
1160
|
+
packages: entries.map((entry) => entry.result === null ? {
|
|
1071
1161
|
name: entry.name,
|
|
1072
1162
|
directory: entry.directory,
|
|
1073
|
-
released:
|
|
1074
|
-
version: entry.result?.nextRelease.version,
|
|
1075
|
-
gitTag: entry.result?.nextRelease.gitTag,
|
|
1076
|
-
type: entry.result?.nextRelease.type,
|
|
1163
|
+
released: false,
|
|
1077
1164
|
dependencyBumps: entry.dependencyBumps
|
|
1078
|
-
}
|
|
1165
|
+
} : {
|
|
1166
|
+
name: entry.name,
|
|
1167
|
+
directory: entry.directory,
|
|
1168
|
+
released: true,
|
|
1169
|
+
version: entry.result.nextRelease.version,
|
|
1170
|
+
gitTag: entry.result.nextRelease.gitTag,
|
|
1171
|
+
type: entry.result.nextRelease.type,
|
|
1172
|
+
dependencyBumps: entry.dependencyBumps
|
|
1173
|
+
}),
|
|
1079
1174
|
detached: entries.map((entry) => ({
|
|
1080
1175
|
name: entry.name,
|
|
1081
1176
|
relativeDirectory: entry.relativeDirectory,
|
|
@@ -1115,6 +1210,7 @@ async function resumeWorkspaceRelease(options) {
|
|
|
1115
1210
|
const root = (0, node_path.resolve)(options.root ?? process.cwd());
|
|
1116
1211
|
const log = options.log ?? console.log;
|
|
1117
1212
|
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
1213
|
+
assertPublishableDependencies(await Promise.all(options.detached.filter((entry) => entry.state !== null).map(async (entry) => readManifest((0, node_path.resolve)(root, entry.relativeDirectory, "package.json")))));
|
|
1118
1214
|
const order = [];
|
|
1119
1215
|
const packages = [];
|
|
1120
1216
|
for (const entry of options.detached) {
|
|
@@ -1125,9 +1221,6 @@ async function resumeWorkspaceRelease(options) {
|
|
|
1125
1221
|
name: entry.name,
|
|
1126
1222
|
directory: (0, node_path.resolve)(root, entry.relativeDirectory),
|
|
1127
1223
|
released: false,
|
|
1128
|
-
version: void 0,
|
|
1129
|
-
gitTag: void 0,
|
|
1130
|
-
type: void 0,
|
|
1131
1224
|
dependencyBumps: entry.dependencyBumps
|
|
1132
1225
|
});
|
|
1133
1226
|
continue;
|
|
@@ -1143,7 +1236,7 @@ async function resumeWorkspaceRelease(options) {
|
|
|
1143
1236
|
} catch (cause) {
|
|
1144
1237
|
throw new WorkspaceReleaseError(`Resuming ${entry.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
1145
1238
|
}
|
|
1146
|
-
log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${releases.length} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
|
|
1239
|
+
log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${String(releases.length)} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
|
|
1147
1240
|
packages.push({
|
|
1148
1241
|
name: entry.name,
|
|
1149
1242
|
directory,
|
|
@@ -1178,9 +1271,9 @@ async function releaseWorkspace(options = {}) {
|
|
|
1178
1271
|
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
1179
1272
|
const workspace = await discoverWorkspace(root);
|
|
1180
1273
|
const graph = buildDependencyGraph(workspace.packages);
|
|
1181
|
-
|
|
1274
|
+
validateDependencyRanges(graph);
|
|
1182
1275
|
const order = topologicalOrder(graph);
|
|
1183
|
-
log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")}`);
|
|
1276
|
+
log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
|
|
1184
1277
|
const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
|
|
1185
1278
|
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
1186
1279
|
const generateNotesConfig = options.generateNotes ?? {};
|
|
@@ -1204,13 +1297,18 @@ async function releaseWorkspace(options = {}) {
|
|
|
1204
1297
|
};
|
|
1205
1298
|
})).map((entry) => {
|
|
1206
1299
|
const nextRelease = entry.result === false ? void 0 : entry.result.nextRelease;
|
|
1207
|
-
return {
|
|
1300
|
+
return nextRelease === void 0 ? {
|
|
1208
1301
|
name: entry.name,
|
|
1209
1302
|
directory: entry.directory,
|
|
1210
|
-
released:
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1303
|
+
released: false,
|
|
1304
|
+
dependencyBumps: entry.dependencyBumps
|
|
1305
|
+
} : {
|
|
1306
|
+
name: entry.name,
|
|
1307
|
+
directory: entry.directory,
|
|
1308
|
+
released: true,
|
|
1309
|
+
version: nextRelease.version,
|
|
1310
|
+
gitTag: nextRelease.gitTag,
|
|
1311
|
+
type: nextRelease.type,
|
|
1214
1312
|
dependencyBumps: entry.dependencyBumps
|
|
1215
1313
|
};
|
|
1216
1314
|
})
|