@williamthorsen/release-kit 10.2.1 → 10.2.2

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.
Files changed (39) hide show
  1. package/.readyup/kits/default.js +1 -1
  2. package/.readyup/kits/npm-auto-publish.js +135 -23
  3. package/.readyup/manifest.json +3 -3
  4. package/CHANGELOG.md +22 -0
  5. package/README.md +13 -3
  6. package/dist/esm/buildChangelogEntries.js +1 -1
  7. package/dist/esm/buildReleaseSummary.d.ts +1 -1
  8. package/dist/esm/changelogJsonFile.d.ts +1 -3
  9. package/dist/esm/changelogJsonFile.js +4 -18
  10. package/dist/esm/commitCommand.js +3 -13
  11. package/dist/esm/createTags.js +2 -13
  12. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +2 -0
  13. package/dist/esm/injectReleaseNotesIntoReadme.js +13 -10
  14. package/dist/esm/planReleaseNotesPreviews.d.ts +13 -0
  15. package/dist/esm/planReleaseNotesPreviews.js +43 -0
  16. package/dist/esm/planVersionBump.d.ts +9 -0
  17. package/dist/esm/{bumpAllVersions.js → planVersionBump.js} +15 -26
  18. package/dist/esm/prepareCommand.d.ts +0 -4
  19. package/dist/esm/prepareCommand.js +51 -45
  20. package/dist/esm/releaseFiles.d.ts +4 -0
  21. package/dist/esm/releaseFiles.js +34 -0
  22. package/dist/esm/releasePlan.d.ts +10 -0
  23. package/dist/esm/releasePlan.js +53 -0
  24. package/dist/esm/releasePrepare.d.ts +3 -3
  25. package/dist/esm/releasePrepare.js +53 -54
  26. package/dist/esm/releasePrepareMono.d.ts +3 -2
  27. package/dist/esm/releasePrepareMono.js +48 -52
  28. package/dist/esm/releasePrepareProject.d.ts +3 -0
  29. package/dist/esm/releasePrepareProject.js +33 -37
  30. package/dist/esm/renderChangelogMarkdown.d.ts +0 -7
  31. package/dist/esm/renderChangelogMarkdown.js +0 -13
  32. package/dist/esm/reportPrepare.d.ts +5 -1
  33. package/dist/esm/reportPrepare.js +36 -17
  34. package/dist/esm/types.d.ts +4 -9
  35. package/dist/esm/workTypesData.d.ts +1 -1
  36. package/package.json +2 -2
  37. package/dist/esm/bumpAllVersions.d.ts +0 -3
  38. package/dist/esm/writeReleaseNotesPreviews.d.ts +0 -18
  39. package/dist/esm/writeReleaseNotesPreviews.js +0 -63
@@ -52,7 +52,7 @@ function detectRepoType() {
52
52
 
53
53
  // .readyup/kits/default.ts
54
54
  function getMinVersion() {
55
- const picked = { "version": "10.2.1" };
55
+ const picked = { "version": "10.2.2" };
56
56
  if (typeof picked.version !== "string") {
57
57
  throw new TypeError("release-kit/package.json: 'version' must be a string");
58
58
  }
@@ -7,7 +7,11 @@ export const __readyupVersion = "0.23.0";
7
7
  import { execSync } from "node:child_process";
8
8
  import { existsSync, readdirSync, readFileSync } from "node:fs";
9
9
  import path from "node:path";
10
- import { defineRdyChecklist, defineRdyKit, defineRdyStagedChecklist } from "readyup";
10
+ import {
11
+ defineRdyChecklist,
12
+ defineRdyKit,
13
+ defineRdyStagedChecklist
14
+ } from "readyup";
11
15
  import {
12
16
  discoverWorkspaces,
13
17
  fileContains,
@@ -17,7 +21,17 @@ import {
17
21
  readFile,
18
22
  readJsonFile
19
23
  } from "readyup/check-utils";
24
+ var AUTH_ERROR_CODES = /* @__PURE__ */ new Set(["E401", "ENEEDAUTH"]);
20
25
  var PUBLISH_WORKFLOW_FILE = "publish.yaml";
26
+ var UNREACHABLE_ERROR_CODES = /* @__PURE__ */ new Set([
27
+ "EAI_AGAIN",
28
+ "ECONNREFUSED",
29
+ "ECONNRESET",
30
+ "ENETUNREACH",
31
+ "ENOTFOUND",
32
+ "ERR_SOCKET_TIMEOUT",
33
+ "ETIMEDOUT"
34
+ ]);
21
35
  var repoChecklist = defineRdyStagedChecklist({
22
36
  name: "repo",
23
37
  preconditions: [
@@ -62,6 +76,16 @@ var packagesChecklist = defineRdyChecklist({
62
76
  name: "At least one workspace discovered",
63
77
  check: () => discoverWorkspaces().length > 0,
64
78
  fix: "Ensure pnpm-workspace.yaml lists package globs, or that a root package.json exists"
79
+ },
80
+ {
81
+ name: "npm session is usable",
82
+ check: () => {
83
+ const auth = getCachedNpmAuthStatus();
84
+ return auth.status === "authenticated" ? { ok: true } : { ok: false, detail: auth.detail };
85
+ },
86
+ get fix() {
87
+ return getCachedNpmAuthStatus().status === "unreachable" ? "Restore access to the npm registry, then re-run; the trusted-publisher check queries it directly" : "Log in to npm: npm login";
88
+ }
65
89
  }
66
90
  ],
67
91
  get checks() {
@@ -100,7 +124,7 @@ function buildWorkspaceCheck(workspace) {
100
124
  checks: [
101
125
  {
102
126
  name: "trusted publisher configured",
103
- check: () => hasTrustedPublisher(displayName, getCachedOwnerRepo(), PUBLISH_WORKFLOW_FILE),
127
+ check: () => checkTrustedPublisher(displayName),
104
128
  get fix() {
105
129
  return `Run: npm trust github ${displayName} --repo ${getCachedOwnerRepo()} --file ${PUBLISH_WORKFLOW_FILE}`;
106
130
  }
@@ -148,6 +172,76 @@ function checkProvenanceMatchesVisibility() {
148
172
  }
149
173
  return { ok: true };
150
174
  }
175
+ function checkTrustedPublisher(packageName) {
176
+ const result = classifyTrustQuery(
177
+ runNpmJson(`npm trust list ${packageName} --json`),
178
+ getCachedOwnerRepo(),
179
+ PUBLISH_WORKFLOW_FILE
180
+ );
181
+ switch (result.status) {
182
+ case "configured":
183
+ return { ok: true };
184
+ case "not-configured":
185
+ return { ok: false, detail: "No trusted publisher is configured for this package" };
186
+ default:
187
+ return { ok: false, detail: result.detail };
188
+ }
189
+ }
190
+ function classifyNpmAuth(result) {
191
+ if (result.exitOk) {
192
+ return { status: "authenticated" };
193
+ }
194
+ const error = readNpmError(result.stdout);
195
+ if (error === void 0) {
196
+ return { status: "unreachable", detail: "The npm registry query failed without a readable error payload" };
197
+ }
198
+ if (AUTH_ERROR_CODES.has(error.code)) {
199
+ return { status: "unauthenticated", detail: `The npm registry rejected the session (${error.code})` };
200
+ }
201
+ if (UNREACHABLE_ERROR_CODES.has(error.code)) {
202
+ return { status: "unreachable", detail: `Cannot reach the npm registry (${error.code})` };
203
+ }
204
+ return { status: "unreachable", detail: `The npm registry query failed (${error.code}): ${error.summary}` };
205
+ }
206
+ function classifyTrustQuery(result, expectedRepo, expectedFile) {
207
+ if (!result.exitOk) {
208
+ const error = readNpmError(result.stdout);
209
+ if (error === void 0) {
210
+ return { status: "error", detail: "The npm trust query failed without a readable error payload" };
211
+ }
212
+ if (error.code === "E404") {
213
+ return { status: "not-configured" };
214
+ }
215
+ return { status: "error", detail: `The npm trust query failed (${error.code}): ${error.summary}` };
216
+ }
217
+ const relationships = readTrustRelationships(result.stdout);
218
+ if (relationships === void 0) {
219
+ return { status: "error", detail: "The npm trust query returned a payload this check cannot read" };
220
+ }
221
+ if (relationships.length === 0) {
222
+ return { status: "not-configured" };
223
+ }
224
+ const isMatch = relationships.some(
225
+ (relationship) => relationship.type === "github" && relationship.repository === expectedRepo && relationship.file === expectedFile
226
+ );
227
+ return isMatch ? { status: "configured" } : {
228
+ status: "mismatched",
229
+ detail: `Expected github ${expectedRepo} (${expectedFile}); found ${describeTrustRelationships(relationships)}`
230
+ };
231
+ }
232
+ function describeTrustRelationships(relationships) {
233
+ return relationships.map((relationship) => {
234
+ const type = typeof relationship.type === "string" ? relationship.type : "(unknown)";
235
+ const repository = typeof relationship.repository === "string" ? relationship.repository : "(unknown)";
236
+ const file = typeof relationship.file === "string" ? relationship.file : "(unknown)";
237
+ return `${type} ${repository} (${file})`;
238
+ }).join(", ");
239
+ }
240
+ var cachedNpmAuthStatus;
241
+ function getCachedNpmAuthStatus() {
242
+ cachedNpmAuthStatus ??= classifyNpmAuth(runNpmJson("npm whoami --json"));
243
+ return cachedNpmAuthStatus;
244
+ }
151
245
  var cachedOwnerRepo;
152
246
  function getCachedOwnerRepo() {
153
247
  if (cachedOwnerRepo === void 0) {
@@ -183,27 +277,6 @@ function hasTokenReferences() {
183
277
  }
184
278
  return false;
185
279
  }
186
- function hasTrustedPublisher(packageName, expectedRepo, expectedFile) {
187
- let output;
188
- try {
189
- output = execSync(`npm trust list ${packageName} --json`, {
190
- encoding: "utf8",
191
- stdio: "pipe"
192
- });
193
- } catch {
194
- return false;
195
- }
196
- let parsed;
197
- try {
198
- parsed = JSON.parse(output);
199
- } catch {
200
- return false;
201
- }
202
- if (!isRecord(parsed)) {
203
- return false;
204
- }
205
- return parsed.type === "github" && parsed.repository === expectedRepo && parsed.file === expectedFile;
206
- }
207
280
  function isPublishedToNpm(packageName) {
208
281
  try {
209
282
  execSync(`npm view ${packageName} version`, {
@@ -225,11 +298,50 @@ function isRepoPrivate() {
225
298
  function parseProvenanceSetting(workflowContent) {
226
299
  return /^[^#]*provenance:\s*['"]?true['"]?/im.test(workflowContent);
227
300
  }
301
+ function readNpmError(stdout) {
302
+ let parsed;
303
+ try {
304
+ parsed = JSON.parse(stdout);
305
+ } catch {
306
+ return void 0;
307
+ }
308
+ if (!isRecord(parsed) || !isRecord(parsed.error) || typeof parsed.error.code !== "string") {
309
+ return void 0;
310
+ }
311
+ const { code, summary } = parsed.error;
312
+ return { code, summary: typeof summary === "string" ? summary : "" };
313
+ }
314
+ function readTrustRelationships(stdout) {
315
+ let parsed;
316
+ try {
317
+ parsed = JSON.parse(stdout);
318
+ } catch {
319
+ return void 0;
320
+ }
321
+ if (Array.isArray(parsed)) {
322
+ return parsed.filter((entry) => isRecord(entry) && "type" in entry);
323
+ }
324
+ if (!isRecord(parsed)) {
325
+ return void 0;
326
+ }
327
+ return "type" in parsed ? [parsed] : [];
328
+ }
329
+ function runNpmJson(command) {
330
+ try {
331
+ return { exitOk: true, stdout: execSync(command, { encoding: "utf8", stdio: "pipe" }) };
332
+ } catch (error) {
333
+ const stdout = isRecord(error) && typeof error.stdout === "string" ? error.stdout : "";
334
+ return { exitOk: false, stdout };
335
+ }
336
+ }
228
337
  function skipIfNotPublishable(workspace) {
229
338
  return workspace.isPackage ? false : "package.json#private is true";
230
339
  }
231
340
  export {
232
341
  buildWorkspaceCheck,
342
+ classifyNpmAuth,
343
+ classifyTrustQuery,
233
344
  npm_auto_publish_default as default,
345
+ packagesChecklist,
234
346
  skipIfNotPublishable
235
347
  };
@@ -10,7 +10,7 @@
10
10
  "readyupVersion": "0.23.0",
11
11
  "source": "kits/default.ts",
12
12
  "sourceHash": "afe26920",
13
- "targetHash": "5ac6d830"
13
+ "targetHash": "fecaa9b1"
14
14
  },
15
15
  {
16
16
  "checklists": [
@@ -21,8 +21,8 @@
21
21
  "path": "kits/npm-auto-publish.js",
22
22
  "readyupVersion": "0.23.0",
23
23
  "source": "kits/npm-auto-publish.ts",
24
- "sourceHash": "71e894e6",
25
- "targetHash": "c1c05255"
24
+ "sourceHash": "5b58afac",
25
+ "targetHash": "610ed8a7"
26
26
  }
27
27
  ]
28
28
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 10.2.2 — 2026-08-05
6
+
7
+ ### 🐛 Bug fixes
8
+
9
+ - Report the missing npm login once, not per package (#598)
10
+
11
+ Fixes an issue where running release-kit's publish-readiness checks without an npm login reported a trusted-publisher failure for every published package. The missing login is now reported once, with `npm login` as the fix. An unreachable registry now has its own check. The trusted publisher check produces more informative failure messages.
12
+
13
+ - Make prepare all-or-nothing and report the tags file by path (#601)
14
+
15
+ Fixes an issue where a failed `prepare` could leave the release flow in a broken intermediate state. A release now lands completely or not at all, and a formatting failure no longer discards one that already landed. When a write fails partway, the error names every file that reached disk, every file that did not, and the commands that undo them.
16
+
17
+ `commit` and `tag` now report the full path of the tags file they looked for, and distinguish a missing file from one that could not be read, so that running from the wrong directory or worktree is no longer reported as a release that never happened.
18
+
19
+ Runs with `--with-release-notes` now list the preview files they write, and `--dry-run` names the ones a real run would write.
20
+
21
+ ### 🧪 Tests
22
+
23
+ - Clear the deferred vitest test rules and enforce them (#602)
24
+
25
+ Fixes a subset of previously deferred lint violations and restores the severity of Vitest lint rules to "error" for purposes strict linting.
26
+
5
27
  ## 10.2.1 — 2026-08-04
6
28
 
7
29
  ### 🐛 Bug fixes
package/README.md CHANGED
@@ -5,13 +5,21 @@ Version-bumping and changelog-generation toolkit for release workflows.
5
5
  Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes — v10.2.1 (2026-08-04)
8
+ ## Release notes — v10.2.2 (2026-08-05)
9
9
 
10
10
  ### 🐛 Bug fixes
11
11
 
12
- - Remove the 1 MiB ceiling on captured git output (#596)
12
+ - Report the missing npm login once, not per package (#598)
13
13
 
14
- Fixes an issue where `release-kit prepare` began aborting once a repository's release history grew long enough, leaving that repository unable to cut another release. The fix covers, in addition to `prepare`, every other release-kit command that reads from git.
14
+ Fixes an issue where running release-kit's publish-readiness checks without an npm login reported a trusted-publisher failure for every published package. The missing login is now reported once, with `npm login` as the fix. An unreachable registry now has its own check. The trusted publisher check produces more informative failure messages.
15
+
16
+ - Make prepare all-or-nothing and report the tags file by path (#601)
17
+
18
+ Fixes an issue where a failed `prepare` could leave the release flow in a broken intermediate state. A release now lands completely or not at all, and a formatting failure no longer discards one that already landed. When a write fails partway, the error names every file that reached disk, every file that did not, and the commands that undo them.
19
+
20
+ `commit` and `tag` now report the full path of the tags file they looked for, and distinguish a missing file from one that could not be read, so that running from the wrong directory or worktree is no longer reported as a release that never happened.
21
+
22
+ Runs with `--with-release-notes` now list the preview files they write, and `--dry-run` names the ones a real run would write.
15
23
  <!-- /section:release-notes -->
16
24
 
17
25
  ## Installation
@@ -890,6 +898,8 @@ rdy list --from npm:@williamthorsen/release-kit # what release-
890
898
 
891
899
  `--packages` is the form that survives release-kit publishing further kits. Every form needs `readyup` 0.23 or later, and `@williamthorsen/release-kit` as a _direct_ devDependency: a strict pnpm layout links nothing else into the project, so a transitive copy is unreachable.
892
900
 
901
+ `npm-auto-publish` queries the npm registry for each package's trusted publisher, so it needs an authenticated npm session and is meant to be invoked deliberately rather than swept up by an unattended run. Without a login it reports one error naming the missing login and blocks the rest of the `packages` checklist, rather than reporting a trusted-publisher failure for every package.
902
+
893
903
  These kits are no longer reachable through `rdy run --from github:williamthorsen/node-monorepo-tools`. Repos still using that form should switch to one of the invocations above.
894
904
 
895
905
  ## External dependencies
@@ -79,7 +79,7 @@ function transformReleases(releases, devOnlySections) {
79
79
  continue;
80
80
  }
81
81
  const version = extractVersion(release.version);
82
- const date = release.timestamp !== undefined ? new Date(release.timestamp * 1000).toISOString().slice(0, 10) : 'unreleased';
82
+ const date = release.timestamp !== undefined ? new Date(release.timestamp * 1_000).toISOString().slice(0, 10) : 'unreleased';
83
83
  const sectionMap = new Map();
84
84
  for (const commit of release.commits ?? []) {
85
85
  const group = stripCommentPrefix(commit.group ?? 'Other');
@@ -1,2 +1,2 @@
1
1
  import type { PrepareResult } from './types.js';
2
- export declare function buildReleaseSummary(result: PrepareResult): string;
2
+ export declare function buildReleaseSummary(result: Pick<PrepareResult, 'project' | 'workspaces'>): string;
@@ -1,6 +1,4 @@
1
1
  import type { ChangelogEntry, ReleaseConfig } from './types.js';
2
2
  export declare function resolveChangelogJsonPath(config: Pick<ReleaseConfig, 'changelogJson'>, changelogPath: string): string;
3
- export declare function writeChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
4
- export declare function upsertChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
5
- export declare function upsertChangelogJsonAndReturn(filePath: string, entries: ChangelogEntry[]): ChangelogEntry[];
3
+ export declare function renderChangelogJson(entries: ChangelogEntry[]): string;
6
4
  export declare function mergeChangelogEntriesWithDisk(filePath: string, entries: ChangelogEntry[]): ChangelogEntry[];
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
3
  import stringify from 'json-stringify-pretty-compact';
4
4
  import semver from 'semver';
5
5
  import { isChangelogEntry } from "./changelogJsonUtils.js";
@@ -7,22 +7,8 @@ import { isUnknownArray } from "./typeGuards.js";
7
7
  export function resolveChangelogJsonPath(config, changelogPath) {
8
8
  return join(changelogPath, config.changelogJson.outputPath);
9
9
  }
10
- export function writeChangelogJson(filePath, entries) {
11
- const sorted = sortNewestFirst(entries);
12
- mkdirSync(dirname(filePath), { recursive: true });
13
- writeFileSync(filePath, stringify(sorted, { maxLength: 100 }) + '\n', 'utf8');
14
- return filePath;
15
- }
16
- export function upsertChangelogJson(filePath, entries) {
17
- upsertChangelogJsonAndReturn(filePath, entries);
18
- return filePath;
19
- }
20
- export function upsertChangelogJsonAndReturn(filePath, entries) {
21
- const existing = readExistingEntries(filePath);
22
- const merged = mergeEntries(entries, existing);
23
- mkdirSync(dirname(filePath), { recursive: true });
24
- writeFileSync(filePath, stringify(merged, { maxLength: 100 }) + '\n', 'utf8');
25
- return merged;
10
+ export function renderChangelogJson(entries) {
11
+ return stringify(sortNewestFirst(entries), { maxLength: 100 }) + '\n';
26
12
  }
27
13
  export function mergeChangelogEntriesWithDisk(filePath, entries) {
28
14
  const existing = readExistingEntries(filePath);
@@ -1,25 +1,15 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { GIT_OUTPUT_LIMIT, parseArgsOrExit } from '@williamthorsen/nmr-core';
4
- import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./prepareCommand.js";
4
+ import { readReleaseTags, RELEASE_SUMMARY_FILE, resolveReleaseTagsPath } from "./releaseFiles.js";
5
5
  const commitFlagSchema = {
6
6
  dryRun: { long: '--dry-run', type: 'boolean' },
7
7
  };
8
8
  export function commitCommand(argv) {
9
9
  const dryRun = parseArgsOrExit(argv, commitFlagSchema).flags.dryRun;
10
- let tagsContent;
11
- try {
12
- tagsContent = readFileSync(RELEASE_TAGS_FILE, 'utf8');
13
- }
14
- catch {
15
- throw new Error('No tags file found. Run `release-kit prepare` first.');
16
- }
17
- const tags = tagsContent
18
- .split('\n')
19
- .map((line) => line.trim())
20
- .filter((line) => line.length > 0);
10
+ const tags = readReleaseTags();
21
11
  if (tags.length === 0) {
22
- throw new Error('Tags file is empty. Run `release-kit prepare` first.');
12
+ throw new Error(`Tags file at ${resolveReleaseTagsPath()} is empty. Run \`release-kit prepare\` first.`);
23
13
  }
24
14
  let summary = '';
25
15
  try {
@@ -1,21 +1,10 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
2
  import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
4
3
  import { deleteFileIfExists } from "./deleteFileIfExists.js";
5
- import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./prepareCommand.js";
4
+ import { readReleaseTags, RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
6
5
  export function createTags(options) {
7
6
  const { dryRun, noGitChecks } = options;
8
- let content;
9
- try {
10
- content = readFileSync(RELEASE_TAGS_FILE, 'utf8');
11
- }
12
- catch {
13
- throw new Error('No tags file found. Run `release-kit prepare` first.');
14
- }
15
- const tags = content
16
- .split('\n')
17
- .map((line) => line.trim())
18
- .filter((line) => line.length > 0);
7
+ const tags = readReleaseTags();
19
8
  if (tags.length === 0) {
20
9
  return [];
21
10
  }
@@ -1,7 +1,9 @@
1
+ import type { ChangelogEntry } from './types.js';
1
2
  export interface RenderedInjectedReadme {
2
3
  injectedReadme: string;
3
4
  releaseNotesMarkdown: string;
4
5
  }
6
+ export declare function renderInjectedReadmeFromEntries(readme: string, entries: readonly ChangelogEntry[], tag: string, sectionOrder?: string[]): RenderedInjectedReadme | undefined;
5
7
  export declare function renderInjectedReadme(readme: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): RenderedInjectedReadme | undefined;
6
8
  export declare function injectReleaseNotesIntoReadme(readmePath: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): string | undefined;
7
9
  export declare function resolveReadmePath(workspacePath: string): string | undefined;
@@ -3,17 +3,8 @@ import { join } from 'node:path';
3
3
  import { extractVersion, readChangelogEntries } from "./changelogJsonUtils.js";
4
4
  import { injectSection } from "./injectSection.js";
5
5
  import { matchesAudience, renderReleaseNotesSingle } from "./renderReleaseNotes.js";
6
- export function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrder) {
7
- if (!existsSync(changelogJsonPath)) {
8
- console.warn(`Warning: ${changelogJsonPath} not found; skipping README injection`);
9
- return undefined;
10
- }
6
+ export function renderInjectedReadmeFromEntries(readme, entries, tag, sectionOrder) {
11
7
  const version = extractVersion(tag);
12
- const entries = readChangelogEntries(changelogJsonPath);
13
- if (entries === undefined) {
14
- console.warn(`Warning: could not parse ${changelogJsonPath}; skipping README injection`);
15
- return undefined;
16
- }
17
8
  const entry = entries.find((e) => e.version === version);
18
9
  if (entry === undefined) {
19
10
  console.warn(`Warning: no changelog entry for version ${version}; skipping README injection`);
@@ -33,6 +24,18 @@ export function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrde
33
24
  const injectedReadme = injectSection(readme, 'release-notes', releaseNotesMarkdown);
34
25
  return { injectedReadme, releaseNotesMarkdown };
35
26
  }
27
+ export function renderInjectedReadme(readme, changelogJsonPath, tag, sectionOrder) {
28
+ if (!existsSync(changelogJsonPath)) {
29
+ console.warn(`Warning: ${changelogJsonPath} not found; skipping README injection`);
30
+ return undefined;
31
+ }
32
+ const entries = readChangelogEntries(changelogJsonPath);
33
+ if (entries === undefined) {
34
+ console.warn(`Warning: could not parse ${changelogJsonPath}; skipping README injection`);
35
+ return undefined;
36
+ }
37
+ return renderInjectedReadmeFromEntries(readme, entries, tag, sectionOrder);
38
+ }
36
39
  export function injectReleaseNotesIntoReadme(readmePath, changelogJsonPath, tag, sectionOrder) {
37
40
  const originalReadme = readFileSync(readmePath, 'utf8');
38
41
  const rendered = renderInjectedReadme(originalReadme, changelogJsonPath, tag, sectionOrder);
@@ -0,0 +1,13 @@
1
+ import type { PlannedWrite } from './releasePlan.js';
2
+ import type { ChangelogEntry } from './types.js';
3
+ export interface PlanReleaseNotesPreviewsOptions {
4
+ workspacePath: string;
5
+ tag: string;
6
+ entries: readonly ChangelogEntry[];
7
+ sectionOrder: string[];
8
+ }
9
+ export interface ReleaseNotesPreviewsPlan {
10
+ writes: PlannedWrite[];
11
+ warnings: string[];
12
+ }
13
+ export declare function planReleaseNotesPreviews(options: PlanReleaseNotesPreviewsOptions): ReleaseNotesPreviewsPlan;
@@ -0,0 +1,43 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { extractVersion } from "./changelogJsonUtils.js";
4
+ import { renderInjectedReadmeFromEntries } from "./injectReleaseNotesIntoReadme.js";
5
+ export function planReleaseNotesPreviews(options) {
6
+ const { workspacePath, tag, entries, sectionOrder } = options;
7
+ const warnings = [];
8
+ const readmePath = path.join(workspacePath, 'README.md');
9
+ const readme = readWorkspaceReadme(readmePath, warnings);
10
+ const rendered = renderInjectedReadmeFromEntries(readme.content, entries, tag, sectionOrder);
11
+ if (rendered === undefined) {
12
+ return { writes: [], warnings };
13
+ }
14
+ if (!readme.exists && !readme.unreadable) {
15
+ warnings.push(`${readmePath} not found; skipping injected-README preview but still writing standalone release notes`);
16
+ }
17
+ const version = extractVersion(tag);
18
+ const docsDir = path.join(workspacePath, 'docs');
19
+ const writes = [];
20
+ if (readme.exists) {
21
+ writes.push({ path: path.join(docsDir, `README.v${version}.md`), content: rendered.injectedReadme });
22
+ }
23
+ writes.push({
24
+ path: path.join(docsDir, `RELEASE_NOTES.v${version}.md`),
25
+ content: rendered.releaseNotesMarkdown.endsWith('\n')
26
+ ? rendered.releaseNotesMarkdown
27
+ : `${rendered.releaseNotesMarkdown}\n`,
28
+ });
29
+ return { writes, warnings };
30
+ }
31
+ function readWorkspaceReadme(readmePath, warnings) {
32
+ if (!existsSync(readmePath)) {
33
+ return { content: '', exists: false, unreadable: false };
34
+ }
35
+ try {
36
+ return { content: readFileSync(readmePath, 'utf8'), exists: true, unreadable: false };
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ warnings.push(`failed to read ${readmePath}: ${message}; skipping injected-README preview`);
41
+ return { content: '', exists: false, unreadable: true };
42
+ }
43
+ }
@@ -0,0 +1,9 @@
1
+ import type { PlannedWrite } from './releasePlan.js';
2
+ import type { ReleaseType } from './types.js';
3
+ export interface VersionBumpPlan {
4
+ currentVersion: string;
5
+ newVersion: string;
6
+ writes: PlannedWrite[];
7
+ }
8
+ export declare function planVersionBump(packageFiles: readonly string[], releaseType: ReleaseType): VersionBumpPlan;
9
+ export declare function planVersionSet(packageFiles: readonly string[], newVersion: string): VersionBumpPlan;
@@ -1,43 +1,32 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { bumpVersion } from "./bumpVersion.js";
3
3
  function isPackageJson(value) {
4
4
  return typeof value === 'object' && value !== null && 'version' in value && typeof value.version === 'string';
5
5
  }
6
- export function bumpAllVersions(packageFiles, releaseType, dryRun) {
7
- const firstFile = packageFiles[0];
8
- if (firstFile === undefined) {
9
- throw new Error('No package files specified');
10
- }
11
- const firstPkg = readPackageJson(firstFile);
6
+ export function planVersionBump(packageFiles, releaseType) {
7
+ const { firstFile, firstPkg } = readPrimaryPackage(packageFiles);
12
8
  const currentVersion = firstPkg.version;
13
9
  const newVersion = bumpVersion(currentVersion, releaseType);
14
- writeVersionToAllFiles(packageFiles, firstFile, firstPkg, newVersion, dryRun);
15
- return { currentVersion, newVersion, files: [...packageFiles] };
10
+ return { currentVersion, newVersion, writes: renderVersionWrites(packageFiles, firstFile, firstPkg, newVersion) };
16
11
  }
17
- export function setAllVersions(packageFiles, newVersion, dryRun) {
12
+ export function planVersionSet(packageFiles, newVersion) {
13
+ const { firstFile, firstPkg } = readPrimaryPackage(packageFiles);
14
+ const currentVersion = firstPkg.version;
15
+ return { currentVersion, newVersion, writes: renderVersionWrites(packageFiles, firstFile, firstPkg, newVersion) };
16
+ }
17
+ function readPrimaryPackage(packageFiles) {
18
18
  const firstFile = packageFiles[0];
19
19
  if (firstFile === undefined) {
20
20
  throw new Error('No package files specified');
21
21
  }
22
- const firstPkg = readPackageJson(firstFile);
23
- const currentVersion = firstPkg.version;
24
- writeVersionToAllFiles(packageFiles, firstFile, firstPkg, newVersion, dryRun);
25
- return { currentVersion, newVersion, files: [...packageFiles] };
22
+ return { firstFile, firstPkg: readPackageJson(firstFile) };
26
23
  }
27
- function writeVersionToAllFiles(packageFiles, firstFile, firstPkg, newVersion, dryRun) {
28
- for (const filePath of packageFiles) {
29
- if (dryRun) {
30
- continue;
31
- }
24
+ function renderVersionWrites(packageFiles, firstFile, firstPkg, newVersion) {
25
+ return packageFiles.map((filePath) => {
32
26
  const pkg = filePath === firstFile ? firstPkg : readPackageJson(filePath);
33
27
  pkg.version = newVersion;
34
- try {
35
- writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
36
- }
37
- catch (error) {
38
- throw new Error(`Failed to write ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
39
- }
40
- }
28
+ return { path: filePath, content: JSON.stringify(pkg, null, 2) + '\n' };
29
+ });
41
30
  }
42
31
  function readPackageJson(filePath) {
43
32
  let content;
@@ -1,7 +1,4 @@
1
- import type { WriteResult } from '@williamthorsen/nmr-core';
2
1
  import type { ReleaseType } from './types.js';
3
- export declare const RELEASE_TAGS_FILE = "tmp/.release-tags";
4
- export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
5
2
  export declare const prepareFlagSchema: {
6
3
  dryRun: {
7
4
  long: string;
@@ -42,5 +39,4 @@ export declare function parseArgs(argv: string[]): {
42
39
  setVersion: string | undefined;
43
40
  withReleaseNotes: boolean;
44
41
  };
45
- export declare function writeReleaseTags(tags: string[], dryRun: boolean): WriteResult | undefined;
46
42
  export declare function prepareCommand(argv: string[]): Promise<void>;