@exadev/semantic-release-workspace 1.3.4 → 1.3.6

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/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");
@@ -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) delete sanitized[key];
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
- const { stdout } = await execFileAsync$1("git", [...args], {
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 || entry.length < 4) continue;
265
+ if (entry === void 0) continue;
243
266
  const statusCode = entry.slice(0, 2);
244
- paths.push(entry.slice(3));
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;
@@ -306,7 +331,7 @@ async function readManifest(path) {
306
331
  const { name, version } = parsed;
307
332
  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
333
  const validity = (0, validate_npm_package_name.default)(name);
309
- if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${(validity.errors ?? []).join("; ")}`);
334
+ if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${validity.errors.join("; ")}`);
310
335
  if (typeof version !== "string" || version.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "version".`);
311
336
  const dependencies = /* @__PURE__ */ new Map();
312
337
  for (const field of DEPENDENCY_FIELDS) {
@@ -609,6 +634,10 @@ function matchTrailerLine(message, key) {
609
634
  }
610
635
  //#endregion
611
636
  //#region src/plugins.ts
637
+ /** 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. */
638
+ function hasGitHead(lastRelease) {
639
+ return typeof lastRelease === "object" && lastRelease !== null && "gitHead" in lastRelease && typeof lastRelease.gitHead === "string";
640
+ }
612
641
  /** 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
642
  const DEFAULT_PUBLISH_PLUGINS = [
614
643
  "@semantic-release/changelog",
@@ -619,7 +648,7 @@ const DEFAULT_PUBLISH_PLUGINS = [
619
648
  message: "chore(release): ${nextRelease.gitTag} [skip ci]"
620
649
  }]
621
650
  ];
622
- /** 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. */
651
+ /** 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
652
  const SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS = [
624
653
  "@semantic-release/changelog",
625
654
  "@semantic-release/npm",
@@ -629,14 +658,14 @@ const STEP_PLUGINS_THE_ORCHESTRATOR_OWNS = /* @__PURE__ */ new Set(["@semantic-r
629
658
  /**
630
659
  * Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
631
660
  *
632
- * 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.
661
+ * 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
662
  *
634
663
  * 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
664
  */
636
665
  function createScopedPlugins(scope) {
637
666
  let cached;
638
667
  async function commitsForPackage(context) {
639
- const from = context.lastRelease?.gitHead ?? void 0;
668
+ const from = hasGitHead(context.lastRelease) ? context.lastRelease.gitHead : void 0;
640
669
  if (cached === void 0) cached = {
641
670
  from,
642
671
  paths: changedPathsSince(from, { cwd: context.cwd })
@@ -656,10 +685,10 @@ function createScopedPlugins(scope) {
656
685
  ...context,
657
686
  commits
658
687
  });
659
- if (type) return type;
688
+ if (typeof type === "string") return type;
660
689
  const bumps = mergeDependencyBumps(scope.bumps.bumpsFor(scope.pkg.name), commits);
661
690
  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.`);
691
+ 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
692
  return "patch";
664
693
  },
665
694
  async generateNotes(_pluginConfig, context) {
@@ -675,7 +704,7 @@ function createScopedPlugins(scope) {
675
704
  "",
676
705
  ...bumps.map((bump) => describeDependencyBump(bump))
677
706
  ].join("\n");
678
- return notes ? `${notes}\n\n${section}` : section;
707
+ return typeof notes === "string" ? `${notes}\n\n${section}` : section;
679
708
  }
680
709
  };
681
710
  }
@@ -747,7 +776,6 @@ function parsePublishPluginSpec(spec) {
747
776
  }
748
777
  //#endregion
749
778
  //#region src/pnpm.ts
750
- const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
751
779
  /**
752
780
  * 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
781
  *
@@ -755,7 +783,7 @@ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
755
783
  */
756
784
  async function regenerateLockfile(options) {
757
785
  try {
758
- await execFileAsync("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
786
+ await execFile("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
759
787
  } catch (cause) {
760
788
  const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
761
789
  const detail = stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause);
@@ -771,7 +799,7 @@ async function regenerateLockfile(options) {
771
799
  *
772
800
  * 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
801
  * 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.
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 -- @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.
802
+ * 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
803
  * 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
804
  * 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
805
  *
@@ -788,7 +816,7 @@ async function releaseWorkspaceSingleCommit(options) {
788
816
  const graph = buildDependencyGraph(workspace.packages);
789
817
  validateDependencyRangeShapes(graph);
790
818
  const order = topologicalOrder(graph);
791
- log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
819
+ log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
792
820
  const resolvedPlugins = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
793
821
  requireGitPlugin: false,
794
822
  forbidGitPlugin: true
@@ -814,19 +842,26 @@ async function releaseWorkspaceSingleCommit(options) {
814
842
  bumpsForThisPackage,
815
843
  env,
816
844
  branches: options.branches,
817
- onCommitsResolved: (commits) => capturedCommits.set(name, commits),
845
+ onCommitsResolved: (commits) => {
846
+ capturedCommits.set(name, commits);
847
+ },
818
848
  onContextCaptured: (context) => {
819
849
  captured.branch = context.branch;
820
850
  captured.repositoryUrl = context.repositoryUrl;
821
851
  }
822
852
  });
823
- outcomes.push({
853
+ outcomes.push(nextRelease === void 0 ? {
824
854
  name,
825
855
  directory: pkg.directory,
826
- released: nextRelease !== void 0,
827
- version: nextRelease?.version,
828
- gitTag: nextRelease?.gitTag,
829
- type: nextRelease?.type,
856
+ released: false,
857
+ dependencyBumps: bumpsForThisPackage
858
+ } : {
859
+ name,
860
+ directory: pkg.directory,
861
+ released: true,
862
+ version: nextRelease.version,
863
+ gitTag: nextRelease.gitTag,
864
+ type: nextRelease.type,
830
865
  dependencyBumps: bumpsForThisPackage
831
866
  });
832
867
  if (nextRelease === void 0) {
@@ -854,7 +889,7 @@ async function releaseWorkspaceSingleCommit(options) {
854
889
  };
855
890
  const branch = captured.branch;
856
891
  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.`);
892
+ 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
893
  const shared = {
859
894
  env,
860
895
  branch,
@@ -880,7 +915,7 @@ async function releaseWorkspaceSingleCommit(options) {
880
915
  }
881
916
  if (anyRangeRewritten) await regenerateLockfile({ cwd: workspace.root });
882
917
  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.`);
918
+ 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
919
  const identity = await resolveCommitIdentity({ cwd: repoRoot });
885
920
  await commitFiles(touchedPaths, describeCombinedCommit(planned), {
886
921
  cwd: repoRoot,
@@ -890,7 +925,7 @@ async function releaseWorkspaceSingleCommit(options) {
890
925
  const tagNames = planned.map((release) => release.gitTag);
891
926
  for (const tagName of tagNames) await createTag(tagName, commitSha, { cwd: repoRoot });
892
927
  await pushHeadAndTags(tagNames, { cwd: repoRoot });
893
- log(`${packageName}: committed ${commitSha} and pushed ${tagNames.length} tag(s): ${tagNames.join(", ")}`);
928
+ log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
894
929
  for (const release of planned) {
895
930
  const releases = [];
896
931
  for (const [modulePath, pluginConfig] of resolvedPlugins) {
@@ -982,10 +1017,18 @@ function describeCombinedCommit(planned) {
982
1017
  }
983
1018
  function buildPluginContext(release, shared, capturedCommits, releases) {
984
1019
  const logger = {
985
- log: (...args) => shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`),
986
- warn: (...args) => shared.log(`[${release.pkg.name}] warn: ${args.map(String).join(" ")}`),
987
- error: (...args) => shared.log(`[${release.pkg.name}] error: ${args.map(String).join(" ")}`),
988
- success: (...args) => shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`)
1020
+ log: (...args) => {
1021
+ shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
1022
+ },
1023
+ warn: (...args) => {
1024
+ shared.log(`[${release.pkg.name}] warn: ${args.map(String).join(" ")}`);
1025
+ },
1026
+ error: (...args) => {
1027
+ shared.log(`[${release.pkg.name}] error: ${args.map(String).join(" ")}`);
1028
+ },
1029
+ success: (...args) => {
1030
+ shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
1031
+ }
989
1032
  };
990
1033
  return {
991
1034
  cwd: release.pkg.directory,
@@ -1045,7 +1088,7 @@ async function detachWorkspaceRelease(options) {
1045
1088
  const graph = buildDependencyGraph(workspace.packages);
1046
1089
  validateDependencyRangeShapes(graph);
1047
1090
  const order = topologicalOrder(graph);
1048
- log(`${packageName}: ${order.length} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
1091
+ log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
1049
1092
  const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
1050
1093
  const analyzeCommitsConfig = options.analyzeCommits ?? {};
1051
1094
  const generateNotesConfig = options.generateNotes ?? {};
@@ -1067,15 +1110,20 @@ async function detachWorkspaceRelease(options) {
1067
1110
  });
1068
1111
  return {
1069
1112
  order,
1070
- packages: entries.map((entry) => ({
1113
+ packages: entries.map((entry) => entry.result === null ? {
1114
+ name: entry.name,
1115
+ directory: entry.directory,
1116
+ released: false,
1117
+ dependencyBumps: entry.dependencyBumps
1118
+ } : {
1071
1119
  name: entry.name,
1072
1120
  directory: entry.directory,
1073
- released: entry.result !== null,
1074
- version: entry.result?.nextRelease.version,
1075
- gitTag: entry.result?.nextRelease.gitTag,
1076
- type: entry.result?.nextRelease.type,
1121
+ released: true,
1122
+ version: entry.result.nextRelease.version,
1123
+ gitTag: entry.result.nextRelease.gitTag,
1124
+ type: entry.result.nextRelease.type,
1077
1125
  dependencyBumps: entry.dependencyBumps
1078
- })),
1126
+ }),
1079
1127
  detached: entries.map((entry) => ({
1080
1128
  name: entry.name,
1081
1129
  relativeDirectory: entry.relativeDirectory,
@@ -1125,9 +1173,6 @@ async function resumeWorkspaceRelease(options) {
1125
1173
  name: entry.name,
1126
1174
  directory: (0, node_path.resolve)(root, entry.relativeDirectory),
1127
1175
  released: false,
1128
- version: void 0,
1129
- gitTag: void 0,
1130
- type: void 0,
1131
1176
  dependencyBumps: entry.dependencyBumps
1132
1177
  });
1133
1178
  continue;
@@ -1143,7 +1188,7 @@ async function resumeWorkspaceRelease(options) {
1143
1188
  } catch (cause) {
1144
1189
  throw new WorkspaceReleaseError(`Resuming ${entry.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
1145
1190
  }
1146
- log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${releases.length} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
1191
+ log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${String(releases.length)} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
1147
1192
  packages.push({
1148
1193
  name: entry.name,
1149
1194
  directory,
@@ -1180,7 +1225,7 @@ async function releaseWorkspace(options = {}) {
1180
1225
  const graph = buildDependencyGraph(workspace.packages);
1181
1226
  validateDependencyRangeShapes(graph);
1182
1227
  const order = topologicalOrder(graph);
1183
- log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")}`);
1228
+ log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
1184
1229
  const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
1185
1230
  const analyzeCommitsConfig = options.analyzeCommits ?? {};
1186
1231
  const generateNotesConfig = options.generateNotes ?? {};
@@ -1204,13 +1249,18 @@ async function releaseWorkspace(options = {}) {
1204
1249
  };
1205
1250
  })).map((entry) => {
1206
1251
  const nextRelease = entry.result === false ? void 0 : entry.result.nextRelease;
1207
- return {
1252
+ return nextRelease === void 0 ? {
1208
1253
  name: entry.name,
1209
1254
  directory: entry.directory,
1210
- released: nextRelease !== void 0,
1211
- version: nextRelease?.version,
1212
- gitTag: nextRelease?.gitTag,
1213
- type: nextRelease?.type,
1255
+ released: false,
1256
+ dependencyBumps: entry.dependencyBumps
1257
+ } : {
1258
+ name: entry.name,
1259
+ directory: entry.directory,
1260
+ released: true,
1261
+ version: nextRelease.version,
1262
+ gitTag: nextRelease.gitTag,
1263
+ type: nextRelease.type,
1214
1264
  dependencyBumps: entry.dependencyBumps
1215
1265
  };
1216
1266
  })
package/dist/index.d.cts CHANGED
@@ -139,13 +139,13 @@ interface DependencyBump {
139
139
  }
140
140
  /** What the scoped plugins need to know about bumps recorded so far in the run, for the package they are about to analyse. */
141
141
  interface DependencyBumpSource {
142
- bumpsFor(dependent: string): readonly DependencyBump[];
142
+ bumpsFor: (dependent: string) => readonly DependencyBump[];
143
143
  }
144
144
  /** A publish-pipeline plugin entry as the orchestrator accepts it: a module name, optionally with a config object. */
145
145
  type PublishPluginSpec = string | readonly [string] | readonly [string, Record<string, unknown>];
146
146
  /** 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. */
147
147
  export declare const DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
148
- /** 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. */
148
+ /** 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. */
149
149
  export declare const SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
150
150
  interface ScopedPlugins {
151
151
  readonly analyzeCommits: (pluginConfig: Record<string, unknown>, context: AnalyzeCommitsContext & {
@@ -158,7 +158,7 @@ interface ScopedPlugins {
158
158
  /**
159
159
  * Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
160
160
  *
161
- * 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.
161
+ * 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.
162
162
  *
163
163
  * 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.
164
164
  */
@@ -226,11 +226,11 @@ interface ReleaseWorkspaceOptions {
226
226
  readonly dryRun?: boolean;
227
227
  /** Release branch configuration for semantic-release. Defaults to semantic-release's own default branch list. */
228
228
  readonly branches?: readonly BranchSpec[];
229
- /** Publish-pipeline plugins (changelog, npm, GitHub, git), each scoped per package by semantic-release's own `cwd`. Defaults to the standard pipeline in DEFAULT_PUBLISH_PLUGINS for `commitStrategy: 'per-package'`, or SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS (the same list minus @semantic-release/git) for `commitStrategy: 'single'`. */
229
+ /** Publish-pipeline plugins (changelog, npm, GitHub, git), each scoped per package by semantic-release's own `cwd`. Defaults to the standard pipeline in DEFAULT_PUBLISH_PLUGINS for `commitStrategy: 'per-package'`, or SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS (the same list minus `@semantic-release/git`) for `commitStrategy: 'single'`. */
230
230
  readonly plugins?: readonly PublishPluginSpec[];
231
- /** Options for the wrapped @semantic-release/commit-analyzer, applied per package after path filtering. */
231
+ /** Options for the wrapped `@semantic-release/commit-analyzer`, applied per package after path filtering. */
232
232
  readonly analyzeCommits?: Record<string, unknown>;
233
- /** Options for the wrapped @semantic-release/release-notes-generator, applied per package after path filtering. */
233
+ /** Options for the wrapped `@semantic-release/release-notes-generator`, applied per package after path filtering. */
234
234
  readonly generateNotes?: Record<string, unknown>;
235
235
  /** Progress sink for the orchestrator's own narration (semantic-release logs its own detail). Defaults to `console.log`. */
236
236
  readonly log?: (message: string) => void;
@@ -247,17 +247,22 @@ interface AppliedDependencyBump extends DependencyBump {
247
247
  /** Which manifest field held the range that was rewritten. */
248
248
  readonly field: DependencyField;
249
249
  }
250
- interface PackageReleaseOutcome {
250
+ type PackageReleaseOutcome = {
251
251
  readonly name: string;
252
252
  readonly directory: string;
253
- readonly released: boolean;
254
- readonly version: string | undefined;
255
- readonly gitTag: string | undefined;
256
- /** The semantic-release release type ('minor', 'patch', ...), including the forced 'patch' of a dependency-bump-only release. */
257
- readonly type: string | undefined;
258
253
  /** Dependency ranges rewritten in this package's own manifest because a workspace dependency released earlier in the run. */
259
254
  readonly dependencyBumps: readonly AppliedDependencyBump[];
260
- }
255
+ } & ({
256
+ readonly released: true;
257
+ readonly version: string;
258
+ readonly gitTag: string;
259
+ readonly type: string;
260
+ } | {
261
+ readonly released: false;
262
+ readonly version?: undefined;
263
+ readonly gitTag?: undefined;
264
+ readonly type?: undefined;
265
+ });
261
266
  interface WorkspaceReleaseOutcome {
262
267
  /** The topological order the packages were released in. */
263
268
  readonly order: readonly string[];
package/dist/index.d.ts CHANGED
@@ -139,13 +139,13 @@ interface DependencyBump {
139
139
  }
140
140
  /** What the scoped plugins need to know about bumps recorded so far in the run, for the package they are about to analyse. */
141
141
  interface DependencyBumpSource {
142
- bumpsFor(dependent: string): readonly DependencyBump[];
142
+ bumpsFor: (dependent: string) => readonly DependencyBump[];
143
143
  }
144
144
  /** A publish-pipeline plugin entry as the orchestrator accepts it: a module name, optionally with a config object. */
145
145
  type PublishPluginSpec = string | readonly [string] | readonly [string, Record<string, unknown>];
146
146
  /** 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. */
147
147
  export declare const DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
148
- /** 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. */
148
+ /** 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. */
149
149
  export declare const SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS: readonly PublishPluginSpec[];
150
150
  interface ScopedPlugins {
151
151
  readonly analyzeCommits: (pluginConfig: Record<string, unknown>, context: AnalyzeCommitsContext & {
@@ -158,7 +158,7 @@ interface ScopedPlugins {
158
158
  /**
159
159
  * Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
160
160
  *
161
- * 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.
161
+ * 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.
162
162
  *
163
163
  * 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.
164
164
  */
@@ -226,11 +226,11 @@ interface ReleaseWorkspaceOptions {
226
226
  readonly dryRun?: boolean;
227
227
  /** Release branch configuration for semantic-release. Defaults to semantic-release's own default branch list. */
228
228
  readonly branches?: readonly BranchSpec[];
229
- /** Publish-pipeline plugins (changelog, npm, GitHub, git), each scoped per package by semantic-release's own `cwd`. Defaults to the standard pipeline in DEFAULT_PUBLISH_PLUGINS for `commitStrategy: 'per-package'`, or SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS (the same list minus @semantic-release/git) for `commitStrategy: 'single'`. */
229
+ /** Publish-pipeline plugins (changelog, npm, GitHub, git), each scoped per package by semantic-release's own `cwd`. Defaults to the standard pipeline in DEFAULT_PUBLISH_PLUGINS for `commitStrategy: 'per-package'`, or SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS (the same list minus `@semantic-release/git`) for `commitStrategy: 'single'`. */
230
230
  readonly plugins?: readonly PublishPluginSpec[];
231
- /** Options for the wrapped @semantic-release/commit-analyzer, applied per package after path filtering. */
231
+ /** Options for the wrapped `@semantic-release/commit-analyzer`, applied per package after path filtering. */
232
232
  readonly analyzeCommits?: Record<string, unknown>;
233
- /** Options for the wrapped @semantic-release/release-notes-generator, applied per package after path filtering. */
233
+ /** Options for the wrapped `@semantic-release/release-notes-generator`, applied per package after path filtering. */
234
234
  readonly generateNotes?: Record<string, unknown>;
235
235
  /** Progress sink for the orchestrator's own narration (semantic-release logs its own detail). Defaults to `console.log`. */
236
236
  readonly log?: (message: string) => void;
@@ -247,17 +247,22 @@ interface AppliedDependencyBump extends DependencyBump {
247
247
  /** Which manifest field held the range that was rewritten. */
248
248
  readonly field: DependencyField;
249
249
  }
250
- interface PackageReleaseOutcome {
250
+ type PackageReleaseOutcome = {
251
251
  readonly name: string;
252
252
  readonly directory: string;
253
- readonly released: boolean;
254
- readonly version: string | undefined;
255
- readonly gitTag: string | undefined;
256
- /** The semantic-release release type ('minor', 'patch', ...), including the forced 'patch' of a dependency-bump-only release. */
257
- readonly type: string | undefined;
258
253
  /** Dependency ranges rewritten in this package's own manifest because a workspace dependency released earlier in the run. */
259
254
  readonly dependencyBumps: readonly AppliedDependencyBump[];
260
- }
255
+ } & ({
256
+ readonly released: true;
257
+ readonly version: string;
258
+ readonly gitTag: string;
259
+ readonly type: string;
260
+ } | {
261
+ readonly released: false;
262
+ readonly version?: undefined;
263
+ readonly gitTag?: undefined;
264
+ readonly type?: undefined;
265
+ });
261
266
  interface WorkspaceReleaseOutcome {
262
267
  /** The topological order the packages were released in. */
263
268
  readonly order: readonly string[];