@exadev/semantic-release-workspace 1.1.7 → 1.2.1
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 +29 -2
- package/dist/cli.js +444 -74
- package/dist/index.cjs +429 -72
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +429 -73
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ let _semantic_release_commit_analyzer = require("@semantic-release/commit-analyz
|
|
|
34
34
|
let _semantic_release_release_notes_generator = require("@semantic-release/release-notes-generator");
|
|
35
35
|
let semantic_release = require("semantic-release");
|
|
36
36
|
semantic_release = __toESM(semantic_release, 1);
|
|
37
|
+
let node_url = require("node:url");
|
|
37
38
|
//#region src/package-name.ts
|
|
38
39
|
const packageName = "@exadev/semantic-release-workspace";
|
|
39
40
|
//#endregion
|
|
@@ -92,11 +93,36 @@ const BOT_IDENTITY = {
|
|
|
92
93
|
name: "semantic-release-bot",
|
|
93
94
|
email: "semantic-release-bot@martynus.net"
|
|
94
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* The environment variables git itself sets when it invokes a hook (`pre-push`, `pre-commit`, and friends), which then leak into every child process a hook spawns unless a child explicitly clears them: `GIT_DIR` (and, in a worktree checkout, its value points at that worktree's own git-dir under the main repository's `.git/worktrees/<name>`), `GIT_WORK_TREE`, `GIT_INDEX_FILE`, `GIT_PREFIX`, `GIT_OBJECT_DIRECTORY`, `GIT_ALTERNATE_OBJECT_DIRECTORIES`, `GIT_COMMON_DIR`, and `GIT_NAMESPACE`. Every one of these overrides git's normal cwd-based repository discovery, so if any of them survive into a `git()` call this package makes with an explicit `cwd` (most obviously `git init` on a throwaway temporary directory in the test suite's own fixtures), the command silently targets whatever repository the *outer* invocation was hooked from instead of the directory this package actually asked for -- observed directly as `git init` in a fixture's temp directory failing to lock the real repository's own `.git/config`, because `GIT_DIR` inherited from this package's own `pre-push` hook pointed straight back at it. `GIT_EXEC_PATH`, `GIT_EDITOR`, and networking/auth-related `GIT_*` variables (SSH, proxy, credential helpers) are deliberately left alone: they do not affect which repository a command targets, only how it behaves once it has found one.
|
|
98
|
+
*/
|
|
99
|
+
const GIT_REPOSITORY_DISCOVERY_ENV_KEYS = [
|
|
100
|
+
"GIT_DIR",
|
|
101
|
+
"GIT_WORK_TREE",
|
|
102
|
+
"GIT_INDEX_FILE",
|
|
103
|
+
"GIT_PREFIX",
|
|
104
|
+
"GIT_OBJECT_DIRECTORY",
|
|
105
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
106
|
+
"GIT_COMMON_DIR",
|
|
107
|
+
"GIT_NAMESPACE"
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* A copy of the given environment with every repository-discovery-affecting `GIT_*` key removed -- see `GIT_REPOSITORY_DISCOVERY_ENV_KEYS`.
|
|
111
|
+
*
|
|
112
|
+
* Exported (not just used internally by `git()`) because this package hands an environment to two different kinds of subprocess: its own `git()` calls (sanitized unconditionally below, from `process.env`, since those never take a caller-supplied environment) and semantic-release's own programmatic API, which spawns its *own* internal git subprocesses (tag, push, verifyAuth) using whatever `env` this package passes it -- release.ts and single-commit-release.ts both call this on the environment they construct for that, so a `GIT_DIR` inherited from an outer hook cannot leak into semantic-release's internal git calls either, not just into this package's own.
|
|
113
|
+
*/
|
|
114
|
+
function sanitizeGitEnv(env) {
|
|
115
|
+
const sanitized = { ...env };
|
|
116
|
+
for (const key of GIT_REPOSITORY_DISCOVERY_ENV_KEYS) delete sanitized[key];
|
|
117
|
+
return sanitized;
|
|
118
|
+
}
|
|
119
|
+
const SANITIZED_PROCESS_GIT_ENV = sanitizeGitEnv(process.env);
|
|
95
120
|
async function git(args, options) {
|
|
96
121
|
try {
|
|
97
122
|
const { stdout } = await execFileAsync$1("git", [...args], {
|
|
98
123
|
cwd: options.cwd,
|
|
99
|
-
maxBuffer: GIT_MAX_BUFFER_BYTES
|
|
124
|
+
maxBuffer: GIT_MAX_BUFFER_BYTES,
|
|
125
|
+
env: SANITIZED_PROCESS_GIT_ENV
|
|
100
126
|
});
|
|
101
127
|
return stdout;
|
|
102
128
|
} catch (cause) {
|
|
@@ -189,6 +215,50 @@ async function pushHead(options) {
|
|
|
189
215
|
`HEAD:${await currentBranch(options)}`
|
|
190
216
|
], options);
|
|
191
217
|
}
|
|
218
|
+
/** Creates a lightweight tag (`git tag <name> <ref>`, no annotation) at the given commit -- the same form semantic-release's own core creates its release tags with (see its `lib/git.js`), so a tag this tool creates directly is indistinguishable from one semantic-release would have made itself. */
|
|
219
|
+
async function createTag(name, ref, options) {
|
|
220
|
+
await git([
|
|
221
|
+
"tag",
|
|
222
|
+
name,
|
|
223
|
+
ref
|
|
224
|
+
], options);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Lists every path with a working-tree or index change (modified, added, deleted, untracked), repository-root-relative, via `git status --porcelain=v1 -z`. `commitStrategy: 'single'` uses this rather than predicting which files each configured prepare plugin touched (a version bump, a changelog write, a dependency-range rewrite, a regenerated lockfile) by name: asking git what actually changed is correct regardless of which prepare plugins are configured or how they name their own output files.
|
|
228
|
+
*
|
|
229
|
+
* `-z` NUL-terminates every field so a path containing a space or newline cannot be misread as two paths; a rename or copy (status codes `R`/`C`) carries two NUL-terminated fields (the new path first, then the origin path), so it consumes two tokens instead of one.
|
|
230
|
+
*/
|
|
231
|
+
async function workingTreeChanges(options) {
|
|
232
|
+
const tokens = (await git([
|
|
233
|
+
"status",
|
|
234
|
+
"--porcelain=v1",
|
|
235
|
+
"--untracked-files=all",
|
|
236
|
+
"-z"
|
|
237
|
+
], options)).split("\0").filter((token) => token !== "");
|
|
238
|
+
const paths = [];
|
|
239
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
240
|
+
const entry = tokens[index];
|
|
241
|
+
if (entry === void 0 || entry.length < 4) continue;
|
|
242
|
+
const statusCode = entry.slice(0, 2);
|
|
243
|
+
paths.push(entry.slice(3));
|
|
244
|
+
if (statusCode.includes("R") || statusCode.includes("C")) index += 1;
|
|
245
|
+
}
|
|
246
|
+
return paths;
|
|
247
|
+
}
|
|
248
|
+
/** Fails loudly if the working tree is not clean, rather than silently folding pre-existing, unrelated dirty state into a release commit. `commitStrategy: 'single'` calls this before it starts, since it relies on `workingTreeChanges` to discover exactly what its own run touches. */
|
|
249
|
+
async function assertCleanWorkingTree(options) {
|
|
250
|
+
const changes = await workingTreeChanges(options);
|
|
251
|
+
if (changes.length > 0) throw new WorkspaceStateError(`The working tree in ${options.cwd} is not clean: ${changes.join(", ")}. commitStrategy "single" discovers what it touched via "git status", so it requires a clean tree to start from; commit, stash elsewhere, or discard these changes first.`);
|
|
252
|
+
}
|
|
253
|
+
/** Pushes the current branch's head and a set of tags to origin in one push, so a combined release commit and every tag pointing at it land on the remote as a single atomic-looking update rather than as separate pushes an interrupted run could split across. */
|
|
254
|
+
async function pushHeadAndTags(tagNames, options) {
|
|
255
|
+
await git([
|
|
256
|
+
"push",
|
|
257
|
+
"origin",
|
|
258
|
+
`HEAD:${await currentBranch(options)}`,
|
|
259
|
+
...tagNames
|
|
260
|
+
], options);
|
|
261
|
+
}
|
|
192
262
|
function toGitCommandError(args, cwd, cause) {
|
|
193
263
|
const exitCode = cause instanceof Error && "code" in cause && typeof cause.code === "number" ? cause.code : void 0;
|
|
194
264
|
const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
|
|
@@ -344,6 +414,70 @@ function toPosix(path) {
|
|
|
344
414
|
return node_path.sep === "/" ? path : path.split(node_path.sep).join("/");
|
|
345
415
|
}
|
|
346
416
|
//#endregion
|
|
417
|
+
//#region src/version-range.ts
|
|
418
|
+
const WORKSPACE_PROTOCOL = "workspace:";
|
|
419
|
+
const CATALOG_PROTOCOL = "catalog:";
|
|
420
|
+
const NPM_ALIAS_PROTOCOL = "npm:";
|
|
421
|
+
/** The `workspace:` suffixes pnpm resolves against the sibling's version at pack time rather than against anything written in the manifest. */
|
|
422
|
+
const PUBLISH_RESOLVED_WORKSPACE_SUFFIXES = [
|
|
423
|
+
"*",
|
|
424
|
+
"^",
|
|
425
|
+
"~"
|
|
426
|
+
];
|
|
427
|
+
/** Ranges that pin nothing, so a sibling's new version cannot change what they mean. */
|
|
428
|
+
const WILDCARD_RANGES = [
|
|
429
|
+
"",
|
|
430
|
+
"*",
|
|
431
|
+
"x",
|
|
432
|
+
"X",
|
|
433
|
+
"latest"
|
|
434
|
+
];
|
|
435
|
+
/**
|
|
436
|
+
* 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.
|
|
437
|
+
*/
|
|
438
|
+
const REWRITABLE_COMPARATOR = /^(\^|~|>=|=)?(\d+\.\d+\.\d+(?:-[\dA-Za-z.-]+)?(?:\+[\dA-Za-z.-]+)?)$/;
|
|
439
|
+
/**
|
|
440
|
+
* Classifies a dependency range's shape, throwing `UnsupportedDependencyRangeError` for anything this tool cannot rewrite with confidence: a compound range (`>=1.0.0 <2.0.0`), a union (`1.x || 2.x`), a `catalog:` reference whose real version lives in `pnpm-workspace.yaml`, an `npm:` alias, a git or tarball URL. Guessing at those would either corrupt the range or silently leave it pointing at a version that no longer exists in the workspace, and a stale published range is exactly the divergence this tool exists to prevent.
|
|
441
|
+
*
|
|
442
|
+
* This never needs the version a sibling is releasing: every case above depends only on the shape of `current` itself, which is what lets `releaseWorkspace` validate every workspace dependency edge up front, before any package has published anything, rather than discovering an unsupported range only when the first dependency it names happens to release.
|
|
443
|
+
*/
|
|
444
|
+
function classifyDependencyRange(current) {
|
|
445
|
+
const range = current.trim();
|
|
446
|
+
if (range.startsWith(WORKSPACE_PROTOCOL)) {
|
|
447
|
+
const suffix = range.slice(10);
|
|
448
|
+
if (PUBLISH_RESOLVED_WORKSPACE_SUFFIXES.includes(suffix)) return { kind: "resolved-at-publish" };
|
|
449
|
+
const inner = classifyDependencyRange(suffix);
|
|
450
|
+
if (inner.kind !== "rewritable") throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": only "workspace:*", "workspace:^", "workspace:~", and "workspace:" followed by a single concrete version range are supported.`);
|
|
451
|
+
return {
|
|
452
|
+
kind: "rewritable",
|
|
453
|
+
workspacePrefixed: true,
|
|
454
|
+
comparator: inner.comparator
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
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. Depend on the sibling directly (for example "workspace:^") instead.`);
|
|
458
|
+
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.`);
|
|
459
|
+
if (WILDCARD_RANGES.includes(range)) return { kind: "wildcard" };
|
|
460
|
+
const match = REWRITABLE_COMPARATOR.exec(range);
|
|
461
|
+
if (match === null) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": only a single "^", "~", ">=", "=", or bare version comparator can be rewritten in place.`);
|
|
462
|
+
return {
|
|
463
|
+
kind: "rewritable",
|
|
464
|
+
workspacePrefixed: false,
|
|
465
|
+
comparator: match[1] ?? ""
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Computes what a dependency range on a workspace sibling becomes once that sibling releases `version`, by classifying the range's shape and then, for a rewritable shape, substituting `version` in place of the version it currently names.
|
|
470
|
+
*/
|
|
471
|
+
function updateDependencyRange(current, version) {
|
|
472
|
+
const shape = classifyDependencyRange(current);
|
|
473
|
+
if (shape.kind !== "rewritable") return shape;
|
|
474
|
+
const rewritten = `${shape.comparator}${version}`;
|
|
475
|
+
return {
|
|
476
|
+
kind: "rewritten",
|
|
477
|
+
range: shape.workspacePrefixed ? `${WORKSPACE_PROTOCOL}${rewritten}` : rewritten
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
347
481
|
//#region src/graph.ts
|
|
348
482
|
/**
|
|
349
483
|
* Builds the inter-package dependency graph from the manifests alone.
|
|
@@ -418,69 +552,21 @@ function firstUnplacedDependency(name, graph, unplaced) {
|
|
|
418
552
|
if (name === void 0) return [...unplaced].sort()[0];
|
|
419
553
|
return (graph.dependencies.get(name) ?? []).map((edge) => edge.dependency).filter((dependency) => unplaced.has(dependency)).sort()[0];
|
|
420
554
|
}
|
|
421
|
-
//#endregion
|
|
422
|
-
//#region src/version-range.ts
|
|
423
|
-
const WORKSPACE_PROTOCOL = "workspace:";
|
|
424
|
-
const CATALOG_PROTOCOL = "catalog:";
|
|
425
|
-
const NPM_ALIAS_PROTOCOL = "npm:";
|
|
426
|
-
/** The `workspace:` suffixes pnpm resolves against the sibling's version at pack time rather than against anything written in the manifest. */
|
|
427
|
-
const PUBLISH_RESOLVED_WORKSPACE_SUFFIXES = [
|
|
428
|
-
"*",
|
|
429
|
-
"^",
|
|
430
|
-
"~"
|
|
431
|
-
];
|
|
432
|
-
/** Ranges that pin nothing, so a sibling's new version cannot change what they mean. */
|
|
433
|
-
const WILDCARD_RANGES = [
|
|
434
|
-
"",
|
|
435
|
-
"*",
|
|
436
|
-
"x",
|
|
437
|
-
"X",
|
|
438
|
-
"latest"
|
|
439
|
-
];
|
|
440
555
|
/**
|
|
441
|
-
*
|
|
442
|
-
*/
|
|
443
|
-
const REWRITABLE_COMPARATOR = /^(\^|~|>=|=)?(\d+\.\d+\.\d+(?:-[\dA-Za-z.-]+)?(?:\+[\dA-Za-z.-]+)?)$/;
|
|
444
|
-
/**
|
|
445
|
-
* Classifies a dependency range's shape, throwing `UnsupportedDependencyRangeError` for anything this tool cannot rewrite with confidence: a compound range (`>=1.0.0 <2.0.0`), a union (`1.x || 2.x`), a `catalog:` reference whose real version lives in `pnpm-workspace.yaml`, an `npm:` alias, a git or tarball URL. Guessing at those would either corrupt the range or silently leave it pointing at a version that no longer exists in the workspace, and a stale published range is exactly the divergence this tool exists to prevent.
|
|
556
|
+
* Checks every workspace dependency edge's range shape before anything releases, so an `UnsupportedDependencyRangeError` stops a run before the first publish rather than after some sibling has already been published, tagged, committed, and pushed. The shape a range supports depends only on the range text itself (see `classifyDependencyRange`), never on which version a sibling ends up releasing, so this can run once up front for the whole graph instead of only being discovered edge by edge as each dependency happens to release.
|
|
446
557
|
*
|
|
447
|
-
*
|
|
558
|
+
* Shared by both commit strategies (`release.ts`'s per-package loop and `single-commit-release.ts`'s analysis phase), which is why it lives alongside the graph it validates rather than inside either strategy's own module.
|
|
448
559
|
*/
|
|
449
|
-
function
|
|
450
|
-
const
|
|
451
|
-
if (range.startsWith(WORKSPACE_PROTOCOL)) {
|
|
452
|
-
const suffix = range.slice(10);
|
|
453
|
-
if (PUBLISH_RESOLVED_WORKSPACE_SUFFIXES.includes(suffix)) return { kind: "resolved-at-publish" };
|
|
454
|
-
const inner = classifyDependencyRange(suffix);
|
|
455
|
-
if (inner.kind !== "rewritable") throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": only "workspace:*", "workspace:^", "workspace:~", and "workspace:" followed by a single concrete version range are supported.`);
|
|
456
|
-
return {
|
|
457
|
-
kind: "rewritable",
|
|
458
|
-
workspacePrefixed: true,
|
|
459
|
-
comparator: inner.comparator
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
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. Depend on the sibling directly (for example "workspace:^") instead.`);
|
|
463
|
-
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.`);
|
|
464
|
-
if (WILDCARD_RANGES.includes(range)) return { kind: "wildcard" };
|
|
465
|
-
const match = REWRITABLE_COMPARATOR.exec(range);
|
|
466
|
-
if (match === null) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": only a single "^", "~", ">=", "=", or bare version comparator can be rewritten in place.`);
|
|
467
|
-
return {
|
|
468
|
-
kind: "rewritable",
|
|
469
|
-
workspacePrefixed: false,
|
|
470
|
-
comparator: match[1] ?? ""
|
|
471
|
-
};
|
|
560
|
+
function validateDependencyRangeShapes(graph) {
|
|
561
|
+
for (const edges of graph.dependencies.values()) for (const edge of edges) classifyDependencyRange(edge.range);
|
|
472
562
|
}
|
|
473
563
|
/**
|
|
474
|
-
*
|
|
564
|
+
* Looks up a value the caller already knows must be present -- a package or edge the topological order or graph just produced -- throwing rather than returning `undefined` if it somehow is not. An internal-consistency guard, not a user-facing validation, so both commit strategies share it rather than each carrying its own copy.
|
|
475
565
|
*/
|
|
476
|
-
function
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
return {
|
|
481
|
-
kind: "rewritten",
|
|
482
|
-
range: shape.workspacePrefixed ? `${WORKSPACE_PROTOCOL}${rewritten}` : rewritten
|
|
483
|
-
};
|
|
566
|
+
function mustGet(map, key, what) {
|
|
567
|
+
const value = map.get(key);
|
|
568
|
+
if (value === void 0) throw new WorkspaceReleaseError(`Internal error: ${what} "${key}" disappeared from the dependency graph mid-run.`);
|
|
569
|
+
return value;
|
|
484
570
|
}
|
|
485
571
|
//#endregion
|
|
486
572
|
//#region src/dependency-bump-commit.ts
|
|
@@ -531,6 +617,12 @@ const DEFAULT_PUBLISH_PLUGINS = [
|
|
|
531
617
|
message: "chore(release): ${nextRelease.gitTag} [skip ci]"
|
|
532
618
|
}]
|
|
533
619
|
];
|
|
620
|
+
/** 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. */
|
|
621
|
+
const SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS = [
|
|
622
|
+
"@semantic-release/changelog",
|
|
623
|
+
"@semantic-release/npm",
|
|
624
|
+
"@semantic-release/github"
|
|
625
|
+
];
|
|
534
626
|
const STEP_PLUGINS_THE_ORCHESTRATOR_OWNS = /* @__PURE__ */ new Set(["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator"]);
|
|
535
627
|
/**
|
|
536
628
|
* Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
|
|
@@ -551,7 +643,9 @@ function createScopedPlugins(scope) {
|
|
|
551
643
|
from,
|
|
552
644
|
paths: changedPathsSince(from, { cwd: context.cwd })
|
|
553
645
|
};
|
|
554
|
-
|
|
646
|
+
const commits = filterCommitsToDirectory(context.commits, await cached.paths, scope.pkg.repoRelativeDirectory);
|
|
647
|
+
scope.onCommitsResolved?.(commits);
|
|
648
|
+
return commits;
|
|
555
649
|
}
|
|
556
650
|
return {
|
|
557
651
|
async analyzeCommits(_pluginConfig, context) {
|
|
@@ -622,7 +716,10 @@ function resolvePublishPlugins(specs, workspaceRoot, options) {
|
|
|
622
716
|
for (const spec of specs) {
|
|
623
717
|
const [name, config] = parsePublishPluginSpec(spec);
|
|
624
718
|
if (STEP_PLUGINS_THE_ORCHESTRATOR_OWNS.has(name)) throw new ReleaseConfigurationError(`"${name}" is listed as a publish plugin, but ${packageName} always provides the ${name === "@semantic-release/commit-analyzer" ? "analyzeCommits" : "generateNotes"} step itself, wrapped around that plugin. Passing it here would make its configuration a silent no-op; set that configuration on the orchestrator's analyzeCommits/generateNotes options instead.`);
|
|
625
|
-
if (name === "@semantic-release/git")
|
|
719
|
+
if (name === "@semantic-release/git") {
|
|
720
|
+
hasGitPlugin = true;
|
|
721
|
+
if (options.forbidGitPlugin === true) throw new ReleaseConfigurationError(`"@semantic-release/git" is listed as a publish plugin, but commitStrategy "single" does its own committing -- one combined commit for every released package, tagged once every package has been analysed -- rather than letting each package's own release commit itself. Remove @semantic-release/git from the plugin list; its version bump and changelog write still happen (via its sibling prepare plugins), just folded into the combined commit instead of made on their own.`);
|
|
722
|
+
}
|
|
626
723
|
const entry = [resolvePluginModule(name, requireFromTool, requireFromWorkspace), config];
|
|
627
724
|
resolved.push(entry);
|
|
628
725
|
}
|
|
@@ -664,6 +761,275 @@ async function regenerateLockfile(options) {
|
|
|
664
761
|
}
|
|
665
762
|
}
|
|
666
763
|
//#endregion
|
|
764
|
+
//#region src/single-commit-release.ts
|
|
765
|
+
/**
|
|
766
|
+
* `commitStrategy: 'single'`: one combined commit for the whole run instead of one commit per package release plus one per dependency bump.
|
|
767
|
+
*
|
|
768
|
+
* Five phases, all inside one `releaseWorkspaceSingleCommit` call:
|
|
769
|
+
*
|
|
770
|
+
* 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.
|
|
771
|
+
* 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 `validateDependencyRangeShapes` already applies to dependency ranges.
|
|
772
|
+
* 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.
|
|
773
|
+
* 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.
|
|
774
|
+
* 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`.
|
|
775
|
+
*
|
|
776
|
+
* Why publish is not just another semantic-release() call: semantic-release's own `run()` unconditionally derives `lastRelease` from the newest tag already on the branch matching `tagFormat`, and this mode has, by the time phase 5 runs, already created and pushed that exact tag itself. A second real `semanticRelease()` call would see its own just-created tag as the already-published release and compute the wrong next version from it. Phase 5 instead calls each resolved plugin module's own exported `verifyConditions`/`publish`/`success` functions directly, with a hand-built context -- the same public per-plugin API surface semantic-release's own core calls internally, just invoked without going through the parts of `run()` that assume a not-yet-tagged repository.
|
|
777
|
+
*/
|
|
778
|
+
async function releaseWorkspaceSingleCommit(options) {
|
|
779
|
+
const root = (0, node_path.resolve)(options.root ?? process.cwd());
|
|
780
|
+
const log = options.log ?? console.log;
|
|
781
|
+
const dryRun = options.dryRun === true;
|
|
782
|
+
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
783
|
+
const workspace = await discoverWorkspace(root);
|
|
784
|
+
const repoRoot = (await git(["rev-parse", "--show-toplevel"], { cwd: workspace.root })).trim();
|
|
785
|
+
await assertCleanWorkingTree({ cwd: repoRoot });
|
|
786
|
+
const graph = buildDependencyGraph(workspace.packages);
|
|
787
|
+
validateDependencyRangeShapes(graph);
|
|
788
|
+
const order = topologicalOrder(graph);
|
|
789
|
+
log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
|
|
790
|
+
const resolvedPlugins = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
|
|
791
|
+
requireGitPlugin: false,
|
|
792
|
+
forbidGitPlugin: true
|
|
793
|
+
});
|
|
794
|
+
const analyzeCommitsConfig = options.analyzeCommits ?? {};
|
|
795
|
+
const generateNotesConfig = options.generateNotes ?? {};
|
|
796
|
+
const capturedCommits = /* @__PURE__ */ new Map();
|
|
797
|
+
const captured = {
|
|
798
|
+
branch: void 0,
|
|
799
|
+
repositoryUrl: void 0
|
|
800
|
+
};
|
|
801
|
+
const pendingBumps = /* @__PURE__ */ new Map();
|
|
802
|
+
const outcomes = [];
|
|
803
|
+
const planned = [];
|
|
804
|
+
for (const name of order) {
|
|
805
|
+
const pkg = mustGet(graph.packages, name, "package");
|
|
806
|
+
const bumpsForThisPackage = pendingBumps.get(name) ?? [];
|
|
807
|
+
pendingBumps.delete(name);
|
|
808
|
+
const nextRelease = await analysePackage(pkg, {
|
|
809
|
+
resolvedPlugins,
|
|
810
|
+
analyzeCommitsConfig,
|
|
811
|
+
generateNotesConfig,
|
|
812
|
+
bumpsForThisPackage,
|
|
813
|
+
env,
|
|
814
|
+
branches: options.branches,
|
|
815
|
+
onCommitsResolved: (commits) => capturedCommits.set(name, commits),
|
|
816
|
+
onContextCaptured: (context) => {
|
|
817
|
+
captured.branch = context.branch;
|
|
818
|
+
captured.repositoryUrl = context.repositoryUrl;
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
outcomes.push({
|
|
822
|
+
name,
|
|
823
|
+
directory: pkg.directory,
|
|
824
|
+
released: nextRelease !== void 0,
|
|
825
|
+
version: nextRelease?.version,
|
|
826
|
+
gitTag: nextRelease?.gitTag,
|
|
827
|
+
type: nextRelease?.type,
|
|
828
|
+
dependencyBumps: bumpsForThisPackage
|
|
829
|
+
});
|
|
830
|
+
if (nextRelease === void 0) {
|
|
831
|
+
log(`${name}: no release`);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
log(`${name}: would release ${nextRelease.gitTag}`);
|
|
835
|
+
planned.push({
|
|
836
|
+
pkg,
|
|
837
|
+
type: nextRelease.type,
|
|
838
|
+
version: nextRelease.version,
|
|
839
|
+
gitTag: nextRelease.gitTag,
|
|
840
|
+
notes: nextRelease.notes,
|
|
841
|
+
bumps: bumpsForThisPackage
|
|
842
|
+
});
|
|
843
|
+
for (const bump of planDependentBumps(pkg, nextRelease.version, graph)) {
|
|
844
|
+
const forDependent = pendingBumps.get(bump.dependent) ?? [];
|
|
845
|
+
forDependent.push(bump);
|
|
846
|
+
pendingBumps.set(bump.dependent, forDependent);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (dryRun || planned.length === 0) return {
|
|
850
|
+
order,
|
|
851
|
+
packages: outcomes
|
|
852
|
+
};
|
|
853
|
+
const branch = captured.branch;
|
|
854
|
+
const repositoryUrl = captured.repositoryUrl;
|
|
855
|
+
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.`);
|
|
856
|
+
const shared = {
|
|
857
|
+
env,
|
|
858
|
+
branch,
|
|
859
|
+
repositoryUrl,
|
|
860
|
+
log
|
|
861
|
+
};
|
|
862
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
863
|
+
for (const release of planned) for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
864
|
+
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
865
|
+
if (plugin.verifyConditions) await plugin.verifyConditions(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
866
|
+
}
|
|
867
|
+
let anyRangeRewritten = false;
|
|
868
|
+
for (const release of planned) {
|
|
869
|
+
for (const bump of release.bumps) {
|
|
870
|
+
if (bump.kind !== "rewritten") continue;
|
|
871
|
+
await writeDependencyRange(release.pkg.manifestPath, bump.field, bump.dependency, bump.range);
|
|
872
|
+
anyRangeRewritten = true;
|
|
873
|
+
}
|
|
874
|
+
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
875
|
+
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
876
|
+
if (plugin.prepare) await plugin.prepare(pluginConfig, buildPluginContext(release, shared, capturedCommits, []));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (anyRangeRewritten) await regenerateLockfile({ cwd: workspace.root });
|
|
880
|
+
const touchedPaths = await workingTreeChanges({ cwd: repoRoot });
|
|
881
|
+
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.`);
|
|
882
|
+
const identity = await resolveCommitIdentity({ cwd: repoRoot });
|
|
883
|
+
await commitFiles(touchedPaths, describeCombinedCommit(planned), {
|
|
884
|
+
cwd: repoRoot,
|
|
885
|
+
identity
|
|
886
|
+
});
|
|
887
|
+
const commitSha = (await git(["rev-parse", "HEAD"], { cwd: repoRoot })).trim();
|
|
888
|
+
const tagNames = planned.map((release) => release.gitTag);
|
|
889
|
+
for (const tagName of tagNames) await createTag(tagName, commitSha, { cwd: repoRoot });
|
|
890
|
+
await pushHeadAndTags(tagNames, { cwd: repoRoot });
|
|
891
|
+
log(`${packageName}: committed ${commitSha} and pushed ${tagNames.length} tag(s): ${tagNames.join(", ")}`);
|
|
892
|
+
for (const release of planned) {
|
|
893
|
+
const releases = [];
|
|
894
|
+
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
895
|
+
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
896
|
+
if (plugin.publish) {
|
|
897
|
+
const result = await plugin.publish(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
898
|
+
if (result !== false && result !== void 0) releases.push(result);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
for (const [modulePath, pluginConfig] of resolvedPlugins) {
|
|
902
|
+
const plugin = await loadReleasePlugin(modulePath, moduleCache);
|
|
903
|
+
if (plugin.success) await plugin.success(pluginConfig, buildPluginContext(release, shared, capturedCommits, releases));
|
|
904
|
+
}
|
|
905
|
+
log(`${release.pkg.name}: published ${release.gitTag}`);
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
order,
|
|
909
|
+
packages: outcomes
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Runs one package's analysis with semantic-release's own real `dryRun: true` mode (which still fully computes `nextRelease.version`/`notes` and the branch/repository context; only the plugin steps whose own definition opts out of dry runs -- prepare, publish, addChannel, success, fail -- are skipped), using the exact same path-scoped `analyzeCommits`/`generateNotes` wrapper `commitStrategy: 'per-package'` uses. `dryRun: true` is forced here regardless of the caller's own `dryRun` option: this is always how phase 1 computes what *would* release, whether or not the run goes on to actually commit it.
|
|
914
|
+
*/
|
|
915
|
+
async function analysePackage(pkg, options) {
|
|
916
|
+
const scoped = createScopedPlugins({
|
|
917
|
+
pkg,
|
|
918
|
+
analyzeCommitsConfig: options.analyzeCommitsConfig,
|
|
919
|
+
generateNotesConfig: options.generateNotesConfig,
|
|
920
|
+
bumps: { bumpsFor: () => options.bumpsForThisPackage },
|
|
921
|
+
onCommitsResolved: options.onCommitsResolved
|
|
922
|
+
});
|
|
923
|
+
const semanticReleaseOptions = {
|
|
924
|
+
tagFormat: `${pkg.name}@\${version}`,
|
|
925
|
+
plugins: options.resolvedPlugins,
|
|
926
|
+
dryRun: true,
|
|
927
|
+
async analyzeCommits(pluginConfig, context) {
|
|
928
|
+
if (context.options.repositoryUrl === void 0) throw new ReleaseConfigurationError("Internal error: semantic-release did not resolve a repository URL before analyzeCommits ran.");
|
|
929
|
+
options.onContextCaptured({
|
|
930
|
+
branch: context.branch,
|
|
931
|
+
repositoryUrl: context.options.repositoryUrl
|
|
932
|
+
});
|
|
933
|
+
return scoped.analyzeCommits(pluginConfig, context);
|
|
934
|
+
},
|
|
935
|
+
generateNotes: scoped.generateNotes
|
|
936
|
+
};
|
|
937
|
+
if (options.branches !== void 0) semanticReleaseOptions.branches = options.branches;
|
|
938
|
+
const result = await (0, semantic_release.default)(semanticReleaseOptions, {
|
|
939
|
+
cwd: pkg.directory,
|
|
940
|
+
env: { ...options.env }
|
|
941
|
+
});
|
|
942
|
+
if (result === false) return;
|
|
943
|
+
if (result.nextRelease.notes === void 0) throw new ReleaseConfigurationError(`Internal error: ${pkg.name} released with no notes computed.`);
|
|
944
|
+
return {
|
|
945
|
+
type: result.nextRelease.type,
|
|
946
|
+
version: result.nextRelease.version,
|
|
947
|
+
gitTag: result.nextRelease.gitTag,
|
|
948
|
+
notes: result.nextRelease.notes
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/** Pure classification of what a released package's version does to each dependent's declared range -- the same logic `bumpDependents` in `release.ts` applies, minus the file write/commit/push: `commitStrategy: 'single'` defers every write to phase 3, applying the same classification once analysis has finished for the whole run. */
|
|
952
|
+
function planDependentBumps(released, version, graph) {
|
|
953
|
+
const applied = [];
|
|
954
|
+
const dependents = graph.dependents.get(released.name);
|
|
955
|
+
if (dependents === void 0) return applied;
|
|
956
|
+
for (const edge of dependents) {
|
|
957
|
+
const update = updateDependencyRange(edge.range, version);
|
|
958
|
+
if (update.kind === "wildcard") continue;
|
|
959
|
+
applied.push({
|
|
960
|
+
dependent: edge.dependent,
|
|
961
|
+
dependency: released.name,
|
|
962
|
+
field: edge.field,
|
|
963
|
+
version,
|
|
964
|
+
range: update.kind === "rewritten" ? update.range : edge.range,
|
|
965
|
+
kind: update.kind
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
return applied;
|
|
969
|
+
}
|
|
970
|
+
function describeCombinedCommit(planned) {
|
|
971
|
+
return [
|
|
972
|
+
"chore(release): batch release [skip ci]",
|
|
973
|
+
"",
|
|
974
|
+
...planned.map((release) => {
|
|
975
|
+
const bumpDescriptions = release.bumps.map((bump) => `${bump.dependency} to ${bump.range}`);
|
|
976
|
+
const bumpSuffix = bumpDescriptions.length === 0 ? "" : ` (dependenc${bumpDescriptions.length === 1 ? "y" : "ies"} bumped: ${bumpDescriptions.join(", ")})`;
|
|
977
|
+
return `- ${release.gitTag} (${release.type})${bumpSuffix}`;
|
|
978
|
+
})
|
|
979
|
+
].join("\n");
|
|
980
|
+
}
|
|
981
|
+
function buildPluginContext(release, shared, capturedCommits, releases) {
|
|
982
|
+
const logger = {
|
|
983
|
+
log: (...args) => shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`),
|
|
984
|
+
warn: (...args) => shared.log(`[${release.pkg.name}] warn: ${args.map(String).join(" ")}`),
|
|
985
|
+
error: (...args) => shared.log(`[${release.pkg.name}] error: ${args.map(String).join(" ")}`),
|
|
986
|
+
success: (...args) => shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`)
|
|
987
|
+
};
|
|
988
|
+
return {
|
|
989
|
+
cwd: release.pkg.directory,
|
|
990
|
+
env: { ...shared.env },
|
|
991
|
+
stdout: process.stdout,
|
|
992
|
+
stderr: process.stderr,
|
|
993
|
+
logger,
|
|
994
|
+
options: { repositoryUrl: shared.repositoryUrl },
|
|
995
|
+
branch: shared.branch,
|
|
996
|
+
commits: capturedCommits.get(release.pkg.name) ?? [],
|
|
997
|
+
releases,
|
|
998
|
+
nextRelease: {
|
|
999
|
+
type: release.type,
|
|
1000
|
+
version: release.version,
|
|
1001
|
+
gitTag: release.gitTag,
|
|
1002
|
+
name: release.gitTag,
|
|
1003
|
+
notes: release.notes,
|
|
1004
|
+
channel: channelOrNull(shared.branch.channel)
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
/** Matches semantic-release's own core exactly (`context.branch.channel || null` in its `index.js`): a branch's `channel` is `string | false | undefined`, and any falsy value (including `false` and an empty string) means "the default channel", represented here as `null`. Written as explicit comparisons rather than `||`/`??` so the falsy-to-null collapse (deliberately including `false` and `''`, which `??` alone would not fold) reads as intentional rather than as a fallback for `undefined`/`null` alone. */
|
|
1009
|
+
function channelOrNull(channel) {
|
|
1010
|
+
return channel === void 0 || channel === false || channel === "" ? null : channel;
|
|
1011
|
+
}
|
|
1012
|
+
function isReleaseLifecycleFnOrUndefined(value) {
|
|
1013
|
+
return value === void 0 || typeof value === "function";
|
|
1014
|
+
}
|
|
1015
|
+
function isReleasePluginModule(value) {
|
|
1016
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1017
|
+
if ("verifyConditions" in value && !isReleaseLifecycleFnOrUndefined(value.verifyConditions)) return false;
|
|
1018
|
+
if ("prepare" in value && !isReleaseLifecycleFnOrUndefined(value.prepare)) return false;
|
|
1019
|
+
if ("publish" in value && !isReleaseLifecycleFnOrUndefined(value.publish)) return false;
|
|
1020
|
+
if ("success" in value && !isReleaseLifecycleFnOrUndefined(value.success)) return false;
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
/** Loads a publish plugin module directly (bypassing semantic-release's own step-pipeline machinery, since phase 5 cannot go through it -- see this file's own top-of-file note), caching by resolved path so a plugin shared by several packages is imported once per run rather than once per package per phase. */
|
|
1024
|
+
async function loadReleasePlugin(absolutePath, cache) {
|
|
1025
|
+
const cached = cache.get(absolutePath);
|
|
1026
|
+
if (cached !== void 0) return cached;
|
|
1027
|
+
const loaded = await import((0, node_url.pathToFileURL)(absolutePath).href);
|
|
1028
|
+
if (!isReleasePluginModule(loaded)) throw new ReleaseConfigurationError(`The publish plugin resolved to ${absolutePath} does not export a recognised semantic-release plugin interface (verifyConditions/prepare/publish/success functions), so commitStrategy "single" cannot call its lifecycle steps directly.`);
|
|
1029
|
+
cache.set(absolutePath, loaded);
|
|
1030
|
+
return loaded;
|
|
1031
|
+
}
|
|
1032
|
+
//#endregion
|
|
667
1033
|
//#region src/release.ts
|
|
668
1034
|
/**
|
|
669
1035
|
* Releases every package in a pnpm workspace with independent versions, in dependency order.
|
|
@@ -671,10 +1037,11 @@ async function regenerateLockfile(options) {
|
|
|
671
1037
|
* For each package, in topological order: run semantic-release's programmatic API with `cwd` scoped to the package directory, a `name@version` tag format to keep each package's tags distinct in the one shared tag namespace, and inline `analyzeCommits`/`generateNotes` plugins that filter the release range's commits down to the package's own directory before delegating to the standard plugins. When a package releases, every workspace package that depends on it and has not run yet gets its dependency range rewritten in its manifest and committed immediately -- before its own turn, so its commit analysis and its published manifest both see the new range.
|
|
672
1038
|
*/
|
|
673
1039
|
async function releaseWorkspace(options = {}) {
|
|
1040
|
+
if ((options.commitStrategy ?? "per-package") === "single") return releaseWorkspaceSingleCommit(options);
|
|
674
1041
|
const root = (0, node_path.resolve)(options.root ?? process.cwd());
|
|
675
1042
|
const log = options.log ?? console.log;
|
|
676
1043
|
const dryRun = options.dryRun === true;
|
|
677
|
-
const env = options.env ?? process.env;
|
|
1044
|
+
const env = sanitizeGitEnv(options.env ?? process.env);
|
|
678
1045
|
const workspace = await discoverWorkspace(root);
|
|
679
1046
|
const graph = buildDependencyGraph(workspace.packages);
|
|
680
1047
|
validateDependencyRangeShapes(graph);
|
|
@@ -733,12 +1100,6 @@ async function releaseWorkspace(options = {}) {
|
|
|
733
1100
|
packages: outcomes
|
|
734
1101
|
};
|
|
735
1102
|
}
|
|
736
|
-
/**
|
|
737
|
-
* Checks every workspace dependency edge's range shape before anything releases, so an `UnsupportedDependencyRangeError` stops the run before the first publish rather than after some sibling has already been published, tagged, committed, and pushed. The shape a range supports depends only on the range text itself (see `classifyDependencyRange`), never on which version a sibling ends up releasing, so this can run once up front for the whole graph instead of only being discovered edge by edge as each dependency happens to release.
|
|
738
|
-
*/
|
|
739
|
-
function validateDependencyRangeShapes(graph) {
|
|
740
|
-
for (const edges of graph.dependencies.values()) for (const edge of edges) classifyDependencyRange(edge.range);
|
|
741
|
-
}
|
|
742
1103
|
async function runPackageRelease(pkg, options) {
|
|
743
1104
|
const scoped = createScopedPlugins({
|
|
744
1105
|
pkg,
|
|
@@ -807,16 +1168,12 @@ async function bumpDependents(released, version, graph, options) {
|
|
|
807
1168
|
}
|
|
808
1169
|
return applied;
|
|
809
1170
|
}
|
|
810
|
-
function mustGet(map, key, what) {
|
|
811
|
-
const value = map.get(key);
|
|
812
|
-
if (value === void 0) throw new WorkspaceReleaseError(`Internal error: ${what} "${key}" disappeared from the dependency graph mid-run.`);
|
|
813
|
-
return value;
|
|
814
|
-
}
|
|
815
1171
|
//#endregion
|
|
816
1172
|
exports.DEFAULT_PUBLISH_PLUGINS = DEFAULT_PUBLISH_PLUGINS;
|
|
817
1173
|
exports.DependencyCycleError = DependencyCycleError;
|
|
818
1174
|
exports.GitCommandError = GitCommandError;
|
|
819
1175
|
exports.ReleaseConfigurationError = ReleaseConfigurationError;
|
|
1176
|
+
exports.SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS = SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS;
|
|
820
1177
|
exports.UnsupportedDependencyRangeError = UnsupportedDependencyRangeError;
|
|
821
1178
|
exports.WorkspaceDiscoveryError = WorkspaceDiscoveryError;
|
|
822
1179
|
exports.WorkspaceReleaseError = WorkspaceReleaseError;
|