@exadev/semantic-release-workspace 1.3.5 → 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/cli.js CHANGED
@@ -6,7 +6,6 @@ import { Command, InvalidArgumentError } from "commander";
6
6
  import { dirname, relative, resolve, sep } from "node:path";
7
7
  import { detachRelease, resumeRelease } from "@exadev/release-gate";
8
8
  import { execFile } from "node:child_process";
9
- import { promisify } from "node:util";
10
9
  import validateNpmPackageName from "validate-npm-package-name";
11
10
  import { glob } from "tinyglobby";
12
11
  import { parse } from "yaml";
@@ -15,7 +14,7 @@ import { generateNotes } from "@semantic-release/release-notes-generator";
15
14
  import semanticRelease from "semantic-release";
16
15
  import { pathToFileURL } from "node:url";
17
16
  //#region package.json
18
- var version = "1.3.5";
17
+ var version = "1.3.6";
19
18
  //#endregion
20
19
  //#region src/errors.ts
21
20
  /**
@@ -48,7 +47,7 @@ var ReleaseConfigurationError = class extends WorkspaceReleaseError {};
48
47
  var GitCommandError = class extends WorkspaceReleaseError {
49
48
  exitCode;
50
49
  constructor(args, cwd, exitCode, detail) {
51
- super(`git ${args.join(" ")} failed in ${cwd}${exitCode === void 0 ? "" : ` (exit ${exitCode})`}: ${detail}`);
50
+ super(`git ${args.join(" ")} failed in ${cwd}${exitCode === void 0 ? "" : ` (exit ${String(exitCode)})`}: ${detail}`);
52
51
  this.exitCode = exitCode;
53
52
  }
54
53
  };
@@ -61,10 +60,30 @@ var PnpmCommandError = class extends WorkspaceReleaseError {
61
60
  }
62
61
  };
63
62
  //#endregion
63
+ //#region src/exec-file.ts
64
+ /**
65
+ * A hand-written promise wrapper around `child_process.execFile`, in place of `util.promisify(execFile)`: `execFile` synchronously returns a `ChildProcess` in addition to invoking its callback, which trips `@typescript-eslint/strict-void-return` when the whole function is handed to `promisify` (a value-returning function used where a void-returning one is contextually expected there) -- exactly the void-return contravariance leniency that rule exists to catch, even though `tsc` itself accepts the pattern. Calling `execFile` directly with our own callback, whose own return type really is `void`, sidesteps the mismatch instead of suppressing it.
66
+ */
67
+ async function execFile$1(command, args, options) {
68
+ return new Promise((resolve, reject) => {
69
+ execFile(command, [...args], {
70
+ cwd: options.cwd,
71
+ env: options.env,
72
+ maxBuffer: options.maxBuffer
73
+ }, (error, stdout, stderr) => {
74
+ if (error) {
75
+ reject(error instanceof Error ? error : new Error(error.message));
76
+ return;
77
+ }
78
+ resolve({
79
+ stdout,
80
+ stderr
81
+ });
82
+ });
83
+ });
84
+ }
85
+ //#endregion
64
86
  //#region src/git.ts
65
- const execFileAsync$1 = promisify(execFile);
66
- /** `git log --name-only` over everything since a package's last release tag can legitimately produce tens of megabytes of path output on a long-lived monorepo, well past execFile's default buffer, failing on exactly the big workspaces this tool exists for. */
67
- const GIT_MAX_BUFFER_BYTES = 104857600;
68
87
  /** Separates one commit's record in `git log --format` output. Chosen from the C0 control range so it can never appear in a hash or a file path. */
69
88
  const COMMIT_RECORD_SEPARATOR = "";
70
89
  /** The identity semantic-release's own core writes release commits under in CI when nothing else is configured (its COMMIT_NAME/COMMIT_EMAIL constants); dependency-bump commits use the same fallback so every commit a release run produces has a consistent author when the repository declares none. */
@@ -92,18 +111,22 @@ const GIT_REPOSITORY_DISCOVERY_ENV_KEYS = [
92
111
  */
93
112
  function sanitizeGitEnv(env) {
94
113
  const sanitized = { ...env };
95
- for (const key of GIT_REPOSITORY_DISCOVERY_ENV_KEYS) delete sanitized[key];
114
+ for (const key of GIT_REPOSITORY_DISCOVERY_ENV_KEYS) Reflect.deleteProperty(sanitized, key);
96
115
  return sanitized;
97
116
  }
98
117
  const SANITIZED_PROCESS_GIT_ENV = sanitizeGitEnv(process.env);
118
+ /** `git log --name-only` over everything since a package's last release tag can legitimately produce tens of megabytes of path output on a long-lived monorepo, well past execFile's default buffer, failing on exactly the big workspaces this tool exists for -- hence the generous 100 MiB default, rather than execFile's own. */
119
+ async function execGit(args, cwd, maxBuffer = 104857600) {
120
+ const { stdout } = await execFile$1("git", args, {
121
+ cwd,
122
+ maxBuffer,
123
+ env: SANITIZED_PROCESS_GIT_ENV
124
+ });
125
+ return stdout;
126
+ }
99
127
  async function git(args, options) {
100
128
  try {
101
- const { stdout } = await execFileAsync$1("git", [...args], {
102
- cwd: options.cwd,
103
- maxBuffer: GIT_MAX_BUFFER_BYTES,
104
- env: SANITIZED_PROCESS_GIT_ENV
105
- });
106
- return stdout;
129
+ return await execGit(args, options.cwd);
107
130
  } catch (cause) {
108
131
  throw toGitCommandError(args, options.cwd, cause);
109
132
  }
@@ -217,9 +240,11 @@ async function workingTreeChanges(options) {
217
240
  const paths = [];
218
241
  for (let index = 0; index < tokens.length; index += 1) {
219
242
  const entry = tokens[index];
220
- if (entry === void 0 || entry.length < 4) continue;
243
+ if (entry === void 0) continue;
221
244
  const statusCode = entry.slice(0, 2);
222
- paths.push(entry.slice(3));
245
+ const path = entry.slice(3);
246
+ if (path === "") continue;
247
+ paths.push(path);
223
248
  if (statusCode.includes("R") || statusCode.includes("C")) index += 1;
224
249
  }
225
250
  return paths;
@@ -290,7 +315,7 @@ async function readManifest(path) {
290
315
  const { name, version } = parsed;
291
316
  if (typeof name !== "string" || name.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "name". Every workspace package needs a name: releases are ordered, tagged, and matched to dependents by it.`);
292
317
  const validity = validateNpmPackageName(name);
293
- if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${(validity.errors ?? []).join("; ")}`);
318
+ if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${validity.errors.join("; ")}`);
294
319
  if (typeof version !== "string" || version.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "version".`);
295
320
  const dependencies = /* @__PURE__ */ new Map();
296
321
  for (const field of DEPENDENCY_FIELDS) {
@@ -596,6 +621,10 @@ function matchTrailerLine(message, key) {
596
621
  }
597
622
  //#endregion
598
623
  //#region src/plugins.ts
624
+ /** 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. */
625
+ function hasGitHead(lastRelease) {
626
+ return typeof lastRelease === "object" && lastRelease !== null && "gitHead" in lastRelease && typeof lastRelease.gitHead === "string";
627
+ }
599
628
  /** The standard publish pipeline this orchestrator coordinates when a workspace configures none of its own. Every entry reuses the corresponding official plugin -- the orchestrator scopes and sequences them per package, it does not reimplement npm publishing, GitHub release creation, or changelog writing. */
600
629
  const DEFAULT_PUBLISH_PLUGINS = [
601
630
  "@semantic-release/changelog",
@@ -606,7 +635,7 @@ const DEFAULT_PUBLISH_PLUGINS = [
606
635
  message: "chore(release): ${nextRelease.gitTag} [skip ci]"
607
636
  }]
608
637
  ];
609
- /** 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. */
638
+ /** The standard publish pipeline for `commitStrategy: 'single'`: the same as `DEFAULT_PUBLISH_PLUGINS` minus `@semantic-release/git`, which that mode never runs -- see `resolvePublishPlugins`'s `forbidGitPlugin` option for why it is rejected outright rather than merely unused. Single-commit mode does its own committing (one combined commit for every released package), so a `prepare`-step git plugin here would create the very per-package commits that mode exists to avoid. */
610
639
  const SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS = [
611
640
  "@semantic-release/changelog",
612
641
  "@semantic-release/npm",
@@ -616,14 +645,14 @@ const STEP_PLUGINS_THE_ORCHESTRATOR_OWNS = /* @__PURE__ */ new Set(["@semantic-r
616
645
  /**
617
646
  * Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
618
647
  *
619
- * 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.
648
+ * Both apply the same path scoping before delegating to the real `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator`: the commit list semantic-release already fetched for the release range is filtered down to commits whose `git log --name-only` file list intersects the package's own directory, and only the filtered list reaches the standard plugin. Conventional-commit parsing and changelog formatting stay entirely inside the standard plugins.
620
649
  *
621
650
  * The `analyzeCommits` wrapper carries one addition beyond filtering: when the standard analyzer finds no releasable commits but a workspace dependency range of the package's has changed, it returns 'patch' anyway. A dependent whose only change is a dependency bump still needs a release for that range to reach the registry. "Has changed" is read from two sources, merged: bumps recorded in memory earlier in the current run (`scope.bumps`), and bumps recorded in the package's own filtered commit history via the trailer `dependency-bump-commit.ts` writes and reads -- the latter is what lets a run that starts after a previous run already committed and pushed the bump (a crash recovery, or simply a later run) reach the same decision, rather than depending on state that existed only inside the process that made the commit.
622
651
  */
623
652
  function createScopedPlugins(scope) {
624
653
  let cached;
625
654
  async function commitsForPackage(context) {
626
- const from = context.lastRelease?.gitHead ?? void 0;
655
+ const from = hasGitHead(context.lastRelease) ? context.lastRelease.gitHead : void 0;
627
656
  if (cached === void 0) cached = {
628
657
  from,
629
658
  paths: changedPathsSince(from, { cwd: context.cwd })
@@ -643,10 +672,10 @@ function createScopedPlugins(scope) {
643
672
  ...context,
644
673
  commits
645
674
  });
646
- if (type) return type;
675
+ if (typeof type === "string") return type;
647
676
  const bumps = mergeDependencyBumps(scope.bumps.bumpsFor(scope.pkg.name), commits);
648
677
  if (bumps.length === 0) return false;
649
- context.logger.log(`No releasable commits under ${scope.pkg.relativeDirectory}, but ${bumps.length === 1 ? "a workspace dependency range changed" : `${bumps.length} workspace dependency ranges changed`}; forcing a patch release.`);
678
+ context.logger.log(`No releasable commits under ${scope.pkg.relativeDirectory}, but ${bumps.length === 1 ? "a workspace dependency range changed" : `${String(bumps.length)} workspace dependency ranges changed`}; forcing a patch release.`);
650
679
  return "patch";
651
680
  },
652
681
  async generateNotes(_pluginConfig, context) {
@@ -662,7 +691,7 @@ function createScopedPlugins(scope) {
662
691
  "",
663
692
  ...bumps.map((bump) => describeDependencyBump(bump))
664
693
  ].join("\n");
665
- return notes ? `${notes}\n\n${section}` : section;
694
+ return typeof notes === "string" ? `${notes}\n\n${section}` : section;
666
695
  }
667
696
  };
668
697
  }
@@ -734,7 +763,6 @@ function parsePublishPluginSpec(spec) {
734
763
  }
735
764
  //#endregion
736
765
  //#region src/pnpm.ts
737
- const execFileAsync = promisify(execFile);
738
766
  /**
739
767
  * Regenerates `pnpm-lock.yaml` for the whole workspace from the manifests currently on disk, without touching `node_modules` or installing anything -- the same lockfile-refresh step a contributor runs by hand after editing a `package.json` dependency range.
740
768
  *
@@ -742,7 +770,7 @@ const execFileAsync = promisify(execFile);
742
770
  */
743
771
  async function regenerateLockfile(options) {
744
772
  try {
745
- await execFileAsync("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
773
+ await execFile$1("pnpm", ["install", "--lockfile-only"], { cwd: options.cwd });
746
774
  } catch (cause) {
747
775
  const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
748
776
  const detail = stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause);
@@ -758,7 +786,7 @@ async function regenerateLockfile(options) {
758
786
  *
759
787
  * 1. **Analyse** (this file's `analysePackage`): for every package, in topological order, run semantic-release with `dryRun: true` forced (regardless of the caller's own `dryRun` option) using the same path-scoped `analyzeCommits`/`generateNotes` wrapper `commitStrategy: 'per-package'` uses -- computing each package's next version and notes without writing, committing, tagging, or publishing anything. Cross-package dependency bumps are tracked purely in memory during this phase (`pendingBumps`), exactly as the per-package strategy tracks them for the span of one run; nothing is committed yet for a later run to recover from, because this strategy never leaves a partial commit for a crash to recover from in the first place -- either the whole combined commit lands, or nothing does.
760
788
  * 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.
761
- * 3. **Prepare**: for every released package, in topological order, apply any dependency-range bump its own manifest received (writing `package.json` directly, the same `writeDependencyRange` the per-package strategy uses), then run every configured publish plugin's own `prepare` step generically (whichever it defines -- @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.
789
+ * 3. **Prepare**: for every released package, in topological order, apply any dependency-range bump its own manifest received (writing `package.json` directly, the same `writeDependencyRange` the per-package strategy uses), then run every configured publish plugin's own `prepare` step generically (whichever it defines -- `@semantic-release/npm` bumps `package.json`'s version, `@semantic-release/changelog` writes `CHANGELOG.md`). `@semantic-release/git` is rejected outright from this mode's plugin list (see `resolvePublishPlugins`'s `forbidGitPlugin`), since its own `prepare` step would create exactly the per-package commit this mode exists to avoid. The lockfile is regenerated once at the end, not once per bump, since `pnpm install --lockfile-only` recomputes it from whatever is on disk regardless of how many manifests changed.
762
790
  * 4. **Commit**: discover every file phase 3 touched via `git status` (rather than predicting filenames per plugin), make one commit, tag it once per released package (`name@version`, lightweight, matching semantic-release's own tag form), and push the commit and every tag together.
763
791
  * 5. **Publish**: for every released package, in topological order, call each configured plugin's own `publish` step directly (not through semantic-release's top-level orchestrator -- see the note below), then `success`.
764
792
  *
@@ -775,7 +803,7 @@ async function releaseWorkspaceSingleCommit(options) {
775
803
  const graph = buildDependencyGraph(workspace.packages);
776
804
  validateDependencyRangeShapes(graph);
777
805
  const order = topologicalOrder(graph);
778
- log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
806
+ log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")} (commitStrategy: single)`);
779
807
  const resolvedPlugins = resolvePublishPlugins(options.plugins ?? SINGLE_COMMIT_DEFAULT_PUBLISH_PLUGINS, workspace.root, {
780
808
  requireGitPlugin: false,
781
809
  forbidGitPlugin: true
@@ -801,19 +829,26 @@ async function releaseWorkspaceSingleCommit(options) {
801
829
  bumpsForThisPackage,
802
830
  env,
803
831
  branches: options.branches,
804
- onCommitsResolved: (commits) => capturedCommits.set(name, commits),
832
+ onCommitsResolved: (commits) => {
833
+ capturedCommits.set(name, commits);
834
+ },
805
835
  onContextCaptured: (context) => {
806
836
  captured.branch = context.branch;
807
837
  captured.repositoryUrl = context.repositoryUrl;
808
838
  }
809
839
  });
810
- outcomes.push({
840
+ outcomes.push(nextRelease === void 0 ? {
811
841
  name,
812
842
  directory: pkg.directory,
813
- released: nextRelease !== void 0,
814
- version: nextRelease?.version,
815
- gitTag: nextRelease?.gitTag,
816
- type: nextRelease?.type,
843
+ released: false,
844
+ dependencyBumps: bumpsForThisPackage
845
+ } : {
846
+ name,
847
+ directory: pkg.directory,
848
+ released: true,
849
+ version: nextRelease.version,
850
+ gitTag: nextRelease.gitTag,
851
+ type: nextRelease.type,
817
852
  dependencyBumps: bumpsForThisPackage
818
853
  });
819
854
  if (nextRelease === void 0) {
@@ -841,7 +876,7 @@ async function releaseWorkspaceSingleCommit(options) {
841
876
  };
842
877
  const branch = captured.branch;
843
878
  const repositoryUrl = captured.repositoryUrl;
844
- if (branch === void 0 || repositoryUrl === void 0) throw new ReleaseConfigurationError(`Internal error: ${packageName} analysed ${planned.length} package release(s) but never captured a branch/repositoryUrl from semantic-release's own context. This should be impossible when at least one package releases.`);
879
+ if (branch === void 0 || repositoryUrl === void 0) throw new ReleaseConfigurationError(`Internal error: ${packageName} analysed ${String(planned.length)} package release(s) but never captured a branch/repositoryUrl from semantic-release's own context. This should be impossible when at least one package releases.`);
845
880
  const shared = {
846
881
  env,
847
882
  branch,
@@ -867,7 +902,7 @@ async function releaseWorkspaceSingleCommit(options) {
867
902
  }
868
903
  if (anyRangeRewritten) await regenerateLockfile({ cwd: workspace.root });
869
904
  const touchedPaths = await workingTreeChanges({ cwd: repoRoot });
870
- if (touchedPaths.length === 0) throw new ReleaseConfigurationError(`${packageName}: analysis planned ${planned.length} release(s), but no files changed while preparing them. Every configured publish plugin's own "prepare" step (bumping package.json, writing CHANGELOG.md) produced nothing to commit -- check the plugin list includes something that writes the version, e.g. @semantic-release/npm.`);
905
+ if (touchedPaths.length === 0) throw new ReleaseConfigurationError(`${packageName}: analysis planned ${String(planned.length)} release(s), but no files changed while preparing them. Every configured publish plugin's own "prepare" step (bumping package.json, writing CHANGELOG.md) produced nothing to commit -- check the plugin list includes something that writes the version, e.g. @semantic-release/npm.`);
871
906
  const identity = await resolveCommitIdentity({ cwd: repoRoot });
872
907
  await commitFiles(touchedPaths, describeCombinedCommit(planned), {
873
908
  cwd: repoRoot,
@@ -877,7 +912,7 @@ async function releaseWorkspaceSingleCommit(options) {
877
912
  const tagNames = planned.map((release) => release.gitTag);
878
913
  for (const tagName of tagNames) await createTag(tagName, commitSha, { cwd: repoRoot });
879
914
  await pushHeadAndTags(tagNames, { cwd: repoRoot });
880
- log(`${packageName}: committed ${commitSha} and pushed ${tagNames.length} tag(s): ${tagNames.join(", ")}`);
915
+ log(`${packageName}: committed ${commitSha} and pushed ${String(tagNames.length)} tag(s): ${tagNames.join(", ")}`);
881
916
  for (const release of planned) {
882
917
  const releases = [];
883
918
  for (const [modulePath, pluginConfig] of resolvedPlugins) {
@@ -969,10 +1004,18 @@ function describeCombinedCommit(planned) {
969
1004
  }
970
1005
  function buildPluginContext(release, shared, capturedCommits, releases) {
971
1006
  const logger = {
972
- log: (...args) => shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`),
973
- warn: (...args) => shared.log(`[${release.pkg.name}] warn: ${args.map(String).join(" ")}`),
974
- error: (...args) => shared.log(`[${release.pkg.name}] error: ${args.map(String).join(" ")}`),
975
- success: (...args) => shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`)
1007
+ log: (...args) => {
1008
+ shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
1009
+ },
1010
+ warn: (...args) => {
1011
+ shared.log(`[${release.pkg.name}] warn: ${args.map(String).join(" ")}`);
1012
+ },
1013
+ error: (...args) => {
1014
+ shared.log(`[${release.pkg.name}] error: ${args.map(String).join(" ")}`);
1015
+ },
1016
+ success: (...args) => {
1017
+ shared.log(`[${release.pkg.name}] ${args.map(String).join(" ")}`);
1018
+ }
976
1019
  };
977
1020
  return {
978
1021
  cwd: release.pkg.directory,
@@ -1039,7 +1082,7 @@ async function releaseWorkspace(options = {}) {
1039
1082
  const graph = buildDependencyGraph(workspace.packages);
1040
1083
  validateDependencyRangeShapes(graph);
1041
1084
  const order = topologicalOrder(graph);
1042
- log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")}`);
1085
+ log(`${packageName}: ${String(order.length)} packages in release order: ${order.join(" -> ")}`);
1043
1086
  const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
1044
1087
  const analyzeCommitsConfig = options.analyzeCommits ?? {};
1045
1088
  const generateNotesConfig = options.generateNotes ?? {};
@@ -1063,13 +1106,18 @@ async function releaseWorkspace(options = {}) {
1063
1106
  };
1064
1107
  })).map((entry) => {
1065
1108
  const nextRelease = entry.result === false ? void 0 : entry.result.nextRelease;
1066
- return {
1109
+ return nextRelease === void 0 ? {
1067
1110
  name: entry.name,
1068
1111
  directory: entry.directory,
1069
- released: nextRelease !== void 0,
1070
- version: nextRelease?.version,
1071
- gitTag: nextRelease?.gitTag,
1072
- type: nextRelease?.type,
1112
+ released: false,
1113
+ dependencyBumps: entry.dependencyBumps
1114
+ } : {
1115
+ name: entry.name,
1116
+ directory: entry.directory,
1117
+ released: true,
1118
+ version: nextRelease.version,
1119
+ gitTag: nextRelease.gitTag,
1120
+ type: nextRelease.type,
1073
1121
  dependencyBumps: entry.dependencyBumps
1074
1122
  };
1075
1123
  })
@@ -1197,7 +1245,7 @@ async function detachWorkspaceRelease(options) {
1197
1245
  const graph = buildDependencyGraph(workspace.packages);
1198
1246
  validateDependencyRangeShapes(graph);
1199
1247
  const order = topologicalOrder(graph);
1200
- log(`${packageName}: ${order.length} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
1248
+ log(`${packageName}: ${String(order.length)} packages in release order (gated -- tag only, publish deferred): ${order.join(" -> ")}`);
1201
1249
  const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
1202
1250
  const analyzeCommitsConfig = options.analyzeCommits ?? {};
1203
1251
  const generateNotesConfig = options.generateNotes ?? {};
@@ -1219,15 +1267,20 @@ async function detachWorkspaceRelease(options) {
1219
1267
  });
1220
1268
  return {
1221
1269
  order,
1222
- packages: entries.map((entry) => ({
1270
+ packages: entries.map((entry) => entry.result === null ? {
1223
1271
  name: entry.name,
1224
1272
  directory: entry.directory,
1225
- released: entry.result !== null,
1226
- version: entry.result?.nextRelease.version,
1227
- gitTag: entry.result?.nextRelease.gitTag,
1228
- type: entry.result?.nextRelease.type,
1273
+ released: false,
1274
+ dependencyBumps: entry.dependencyBumps
1275
+ } : {
1276
+ name: entry.name,
1277
+ directory: entry.directory,
1278
+ released: true,
1279
+ version: entry.result.nextRelease.version,
1280
+ gitTag: entry.result.nextRelease.gitTag,
1281
+ type: entry.result.nextRelease.type,
1229
1282
  dependencyBumps: entry.dependencyBumps
1230
- })),
1283
+ }),
1231
1284
  detached: entries.map((entry) => ({
1232
1285
  name: entry.name,
1233
1286
  relativeDirectory: entry.relativeDirectory,
@@ -1289,9 +1342,6 @@ async function resumeWorkspaceRelease(options) {
1289
1342
  name: entry.name,
1290
1343
  directory: resolve(root, entry.relativeDirectory),
1291
1344
  released: false,
1292
- version: void 0,
1293
- gitTag: void 0,
1294
- type: void 0,
1295
1345
  dependencyBumps: entry.dependencyBumps
1296
1346
  });
1297
1347
  continue;
@@ -1307,7 +1357,7 @@ async function resumeWorkspaceRelease(options) {
1307
1357
  } catch (cause) {
1308
1358
  throw new WorkspaceReleaseError(`Resuming ${entry.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
1309
1359
  }
1310
- log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${releases.length} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
1360
+ log(`${entry.name}: published ${entry.state.nextRelease.gitTag} (${String(releases.length)} publish plugin${releases.length === 1 ? "" : "s"} ran)`);
1311
1361
  packages.push({
1312
1362
  name: entry.name,
1313
1363
  directory,
@@ -1396,7 +1446,7 @@ async function runRelease(flags) {
1396
1446
  if (gatePublish) {
1397
1447
  if (flags.gateStateFile === void 0) throw new WorkspaceReleaseError("gatePublish was true but no --gate-state-file was resolved -- this should be unreachable.");
1398
1448
  await writeFile(flags.gateStateFile, JSON.stringify(outcome.detached ?? [], null, 2));
1399
- console.log(`${packageName}: wrote gate state for ${(outcome.detached ?? []).length} package(s) to ${flags.gateStateFile}`);
1449
+ console.log(`${packageName}: wrote gate state for ${String((outcome.detached ?? []).length)} package(s) to ${flags.gateStateFile}`);
1400
1450
  }
1401
1451
  }
1402
1452
  async function runResume(flags) {