@williamthorsen/release-kit 10.2.1 → 10.3.0

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 (63) hide show
  1. package/.readyup/kits/default.js +78 -55
  2. package/.readyup/kits/npm-auto-publish.js +169 -57
  3. package/.readyup/manifest.json +6 -6
  4. package/CHANGELOG.md +54 -0
  5. package/README.md +18 -6
  6. package/bin/release-kit.js +1 -1
  7. package/dist/esm/bin/release-kit.js +4 -4
  8. package/dist/esm/buildChangelogEntries.js +17 -15
  9. package/dist/esm/buildDependencyGraph.js +6 -2
  10. package/dist/esm/buildReleaseSummary.d.ts +1 -1
  11. package/dist/esm/changelogJsonFile.d.ts +1 -3
  12. package/dist/esm/changelogJsonFile.js +4 -18
  13. package/dist/esm/changelogJsonUtils.js +7 -7
  14. package/dist/esm/changelogOverrides.js +13 -12
  15. package/dist/esm/checkWorkTypesDrift.js +1 -1
  16. package/dist/esm/commitCommand.js +3 -13
  17. package/dist/esm/createTags.js +3 -14
  18. package/dist/esm/deriveWorkspaceConfig.js +3 -3
  19. package/dist/esm/detectPackageManager.js +2 -2
  20. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  21. package/dist/esm/discoverWorkspaces.js +1 -1
  22. package/dist/esm/getCommitsSinceTarget.js +2 -2
  23. package/dist/esm/help/prepareHelp.d.ts +1 -1
  24. package/dist/esm/help/prepareHelp.js +2 -2
  25. package/dist/esm/init/checks.js +1 -1
  26. package/dist/esm/init/detectRepoType.js +1 -1
  27. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +2 -0
  28. package/dist/esm/injectReleaseNotesIntoReadme.js +13 -10
  29. package/dist/esm/loadConfig.js +8 -6
  30. package/dist/esm/planReleaseNotesPreviews.d.ts +13 -0
  31. package/dist/esm/planReleaseNotesPreviews.js +43 -0
  32. package/dist/esm/planVersionBump.d.ts +9 -0
  33. package/dist/esm/planVersionBump.js +54 -0
  34. package/dist/esm/prepareCommand.d.ts +0 -4
  35. package/dist/esm/prepareCommand.js +51 -45
  36. package/dist/esm/propagateBumps.js +1 -1
  37. package/dist/esm/releaseFiles.d.ts +4 -0
  38. package/dist/esm/releaseFiles.js +34 -0
  39. package/dist/esm/releasePlan.d.ts +10 -0
  40. package/dist/esm/releasePlan.js +53 -0
  41. package/dist/esm/releasePrepare.d.ts +3 -3
  42. package/dist/esm/releasePrepare.js +53 -54
  43. package/dist/esm/releasePrepareMono.d.ts +3 -2
  44. package/dist/esm/releasePrepareMono.js +50 -53
  45. package/dist/esm/releasePrepareProject.d.ts +3 -0
  46. package/dist/esm/releasePrepareProject.js +33 -37
  47. package/dist/esm/renderChangelogMarkdown.d.ts +0 -7
  48. package/dist/esm/renderChangelogMarkdown.js +0 -13
  49. package/dist/esm/reportPrepare.d.ts +5 -1
  50. package/dist/esm/reportPrepare.js +37 -17
  51. package/dist/esm/sync-labels/generateCommand.js +4 -2
  52. package/dist/esm/sync-labels/presets.js +5 -5
  53. package/dist/esm/sync-labels/resolveLabels.js +5 -3
  54. package/dist/esm/syncWorkTypes.js +1 -1
  55. package/dist/esm/types.d.ts +4 -9
  56. package/dist/esm/validateConfig.js +11 -11
  57. package/dist/esm/workTypesData.d.ts +1 -1
  58. package/dist/esm/workTypesUtils.js +1 -1
  59. package/package.json +3 -3
  60. package/dist/esm/bumpAllVersions.d.ts +0 -3
  61. package/dist/esm/bumpAllVersions.js +0 -61
  62. package/dist/esm/writeReleaseNotesPreviews.d.ts +0 -18
  63. package/dist/esm/writeReleaseNotesPreviews.js +0 -63
package/README.md CHANGED
@@ -5,13 +5,23 @@ 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.3.0 (2026-08-08)
9
+
10
+ ### 🎉 Features
11
+
12
+ - Stop reporting absence checks that pass (#610)
13
+
14
+ A ReadyUp check that checks for a problem (such as an unwanted file or a stale config reference) now stays silent unless it finds one. The `nmr`, `release-kit`, and `v11y-check` ReadyUp kits previously printed a line for every such check on every run.
15
+
16
+ - Stop reporting non-issues in the readyup kits (#613)
17
+
18
+ Modifies the `release-kit` and `nmr` ReadyUp kits to avoid verbose reporting when an initial check determines that further checks aren't applicable. In particular, a repo with no `release-kit` config file gets a single line in the `release-kit` report, and a repo that publishes nothing now bypasses the `npm-auto-publish` kit instead of failing it.
9
19
 
10
20
  ### 🐛 Bug fixes
11
21
 
12
- - Remove the 1 MiB ceiling on captured git output (#596)
22
+ - Publish build output atomically so a failed build cannot destroy it (#631)
13
23
 
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.
24
+ Fixes an issue where a failed or still-running `nmr build` could leave a package with no build output at all, breaking the tooling that a subsequent build depends on. Separately, the error reported when a package's build output is missing now names the correct recovery command.
15
25
  <!-- /section:release-notes -->
16
26
 
17
27
  ## Installation
@@ -565,7 +575,7 @@ Run release preparation with automatic workspace discovery.
565
575
  | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode (rejected when a `project` block is configured). |
566
576
  | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag. In monorepo/project mode, defaults to patch (combine with `--bump=X` for a different level); in single-package mode, a bare `--force` is rejected — pass `--bump=major\|minor\|patch`. |
567
577
  | `--only=name1,name2` | Only process the named workspaces (monorepo only; rejected when a `project` block is configured) |
568
- | `--with-release-notes` | Write per-workspace release-notes previews under `{workspacePath}/docs/` |
578
+ | `--with-release-notes` | Write per-workspace release-notes previews under `<workspacePath>/docs/` |
569
579
  | `--help`, `-h` | Show help |
570
580
 
571
581
  Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
@@ -574,8 +584,8 @@ Workspace names for `--only` match the package directory name (e.g., `arrays`, `
574
584
 
575
585
  `--with-release-notes` writes two versioned files per workspace after each workspace's `changelog.json` is produced:
576
586
 
577
- - `{workspacePath}/docs/README.v{version}.md` — the workspace `README.md` with release notes injected at the `<!-- section:release-notes -->` marker.
578
- - `{workspacePath}/docs/RELEASE_NOTES.v{version}.md` — the standalone release notes for this version.
587
+ - `<workspacePath>/docs/README.v<version>.md` — the workspace `README.md` with release notes injected at the `<!-- section:release-notes -->` marker.
588
+ - `<workspacePath>/docs/RELEASE_NOTES.v<version>.md` — the standalone release notes for this version.
579
589
 
580
590
  The publish-time inject-and-revert lifecycle is unchanged; previews are additive, deterministic, and safe to regenerate. When `changelogJson.enabled` is `false`, the flag logs a warning and skips preview generation. In dry-run mode, planned writes are logged and no files are created.
581
591
 
@@ -890,6 +900,8 @@ rdy list --from npm:@williamthorsen/release-kit # what release-
890
900
 
891
901
  `--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
902
 
903
+ `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.
904
+
893
905
  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
906
 
895
907
  ## External dependencies
@@ -3,7 +3,7 @@ try {
3
3
  await import('../dist/esm/bin/release-kit.js');
4
4
  } catch (error) {
5
5
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('release-kit: build output not found — run `pnpm run build` first\n');
6
+ process.stderr.write('release-kit: build output not found — run `nmr build` first\n');
7
7
  } else {
8
8
  process.stderr.write(`release-kit: failed to load: ${error.message}\n`);
9
9
  }
@@ -263,7 +263,7 @@ if (command === '--version' || command === '-V') {
263
263
  console.info(VERSION);
264
264
  process.exit(0);
265
265
  }
266
- if (command === '--help' || command === '-h' || command === undefined) {
266
+ if (command === undefined || ['--help', '-h'].includes(command)) {
267
267
  showUsage();
268
268
  process.exit(0);
269
269
  }
@@ -350,7 +350,7 @@ if (command === 'init') {
350
350
  if (command === 'sync-labels') {
351
351
  const subcommand = flags[0];
352
352
  const subflags = flags.slice(1);
353
- if (subcommand === '--help' || subcommand === '-h' || subcommand === undefined) {
353
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
354
354
  showSyncLabelsHelp();
355
355
  process.exit(0);
356
356
  }
@@ -398,7 +398,7 @@ if (command === 'sync-labels') {
398
398
  if (command === 'overrides') {
399
399
  const subcommand = flags[0];
400
400
  const subflags = flags.slice(1);
401
- if (subcommand === '--help' || subcommand === '-h' || subcommand === undefined) {
401
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
402
402
  showOverridesHelp();
403
403
  process.exit(0);
404
404
  }
@@ -427,7 +427,7 @@ if (command === 'overrides') {
427
427
  if (command === 'work-types') {
428
428
  const subcommand = flags[0];
429
429
  const subflags = flags.slice(1);
430
- if (subcommand === '--help' || subcommand === '-h' || subcommand === undefined) {
430
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
431
431
  showWorkTypesHelp();
432
432
  process.exit(0);
433
433
  }
@@ -20,7 +20,8 @@ export function buildChangelogEntries(config, tag, options) {
20
20
  if (options?.tagPattern !== undefined) {
21
21
  cliffArgs.push('--tag-pattern', options.tagPattern);
22
22
  }
23
- for (const includePath of options?.includePaths ?? []) {
23
+ const includePaths = options?.includePaths ?? [];
24
+ for (const includePath of includePaths) {
24
25
  cliffArgs.push('--include-path', includePath);
25
26
  }
26
27
  try {
@@ -30,7 +31,7 @@ export function buildChangelogEntries(config, tag, options) {
30
31
  return transformReleases(releases, devOnlySections);
31
32
  }
32
33
  catch (error) {
33
- throw new Error(`Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`);
34
+ throw new Error(`Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
34
35
  }
35
36
  }
36
37
  function parseCliffContext(json) {
@@ -45,14 +46,14 @@ function toCliffContextRelease(value) {
45
46
  return {};
46
47
  }
47
48
  const release = {};
48
- if (typeof value.version === 'string') {
49
- release.version = value.version;
49
+ if (typeof value['version'] === 'string') {
50
+ release.version = value['version'];
50
51
  }
51
- if (typeof value.timestamp === 'number') {
52
- release.timestamp = value.timestamp;
52
+ if (typeof value['timestamp'] === 'number') {
53
+ release.timestamp = value['timestamp'];
53
54
  }
54
- if (isUnknownArray(value.commits)) {
55
- release.commits = value.commits.map(toCliffContextCommit);
55
+ if (isUnknownArray(value['commits'])) {
56
+ release.commits = value['commits'].map(toCliffContextCommit);
56
57
  }
57
58
  return release;
58
59
  }
@@ -61,13 +62,13 @@ function toCliffContextCommit(value) {
61
62
  return { message: '' };
62
63
  }
63
64
  const commit = {
64
- message: typeof value.message === 'string' ? value.message : '',
65
+ message: typeof value['message'] === 'string' ? value['message'] : '',
65
66
  };
66
- if (typeof value.group === 'string') {
67
- commit.group = value.group;
67
+ if (typeof value['group'] === 'string') {
68
+ commit.group = value['group'];
68
69
  }
69
- if (typeof value.id === 'string') {
70
- commit.id = value.id;
70
+ if (typeof value['id'] === 'string') {
71
+ commit.id = value['id'];
71
72
  }
72
73
  return commit;
73
74
  }
@@ -79,9 +80,10 @@ function transformReleases(releases, devOnlySections) {
79
80
  continue;
80
81
  }
81
82
  const version = extractVersion(release.version);
82
- const date = release.timestamp !== undefined ? new Date(release.timestamp * 1000).toISOString().slice(0, 10) : 'unreleased';
83
+ const date = release.timestamp !== undefined ? new Date(release.timestamp * 1_000).toISOString().slice(0, 10) : 'unreleased';
83
84
  const sectionMap = new Map();
84
- for (const commit of release.commits ?? []) {
85
+ const commits = release.commits ?? [];
86
+ for (const commit of commits) {
85
87
  const group = stripCommentPrefix(commit.group ?? 'Other');
86
88
  const description = extractDescription(commit.message);
87
89
  const body = extractBody(commit.message);
@@ -51,14 +51,18 @@ function readPackageJsonSubset(filePath) {
51
51
  content = readFileSync(filePath, 'utf8');
52
52
  }
53
53
  catch (error) {
54
- throw new Error(`Failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
54
+ throw new Error(`Failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
55
+ cause: error,
56
+ });
55
57
  }
56
58
  let parsed;
57
59
  try {
58
60
  parsed = JSON.parse(content);
59
61
  }
60
62
  catch (error) {
61
- throw new Error(`Failed to parse JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
63
+ throw new Error(`Failed to parse JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
64
+ cause: error,
65
+ });
62
66
  }
63
67
  if (!isPackageJsonSubset(parsed)) {
64
68
  throw new Error(`Invalid package.json at ${filePath}`);
@@ -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);
@@ -3,21 +3,21 @@ import { isRecord, isUnknownArray } from "./typeGuards.js";
3
3
  export function isChangelogItem(value) {
4
4
  if (!isRecord(value))
5
5
  return false;
6
- if (typeof value.description !== 'string')
6
+ if (typeof value['description'] !== 'string')
7
7
  return false;
8
- if (value.body !== undefined && typeof value.body !== 'string')
8
+ if (value['body'] !== undefined && typeof value['body'] !== 'string')
9
9
  return false;
10
- if (value.breaking !== undefined && typeof value.breaking !== 'boolean')
10
+ if (value['breaking'] !== undefined && typeof value['breaking'] !== 'boolean')
11
11
  return false;
12
- if (value.hash !== undefined && typeof value.hash !== 'string')
12
+ if (value['hash'] !== undefined && typeof value['hash'] !== 'string')
13
13
  return false;
14
14
  return true;
15
15
  }
16
16
  export function isChangelogEntry(value) {
17
17
  return (isRecord(value) &&
18
- typeof value.version === 'string' &&
19
- typeof value.date === 'string' &&
20
- isUnknownArray(value.sections));
18
+ typeof value['version'] === 'string' &&
19
+ typeof value['date'] === 'string' &&
20
+ isUnknownArray(value['sections']));
21
21
  }
22
22
  export function extractVersion(tag) {
23
23
  const match = /(\d+\.\d+\.\d+.*)$/.exec(tag);
@@ -66,8 +66,8 @@ function validateSingleOverride(key, rawEntry, errors) {
66
66
  entryValid = false;
67
67
  }
68
68
  const result = {};
69
- if (rawEntry.audience !== undefined) {
70
- const audienceResult = validateAudience(key, rawEntry.audience, errors);
69
+ if (rawEntry['audience'] !== undefined) {
70
+ const audienceResult = validateAudience(key, rawEntry['audience'], errors);
71
71
  if (audienceResult === undefined) {
72
72
  entryValid = false;
73
73
  }
@@ -75,31 +75,31 @@ function validateSingleOverride(key, rawEntry, errors) {
75
75
  result.audience = audienceResult;
76
76
  }
77
77
  }
78
- if (rawEntry.description !== undefined) {
79
- if (typeof rawEntry.description !== 'string') {
78
+ if (rawEntry['description'] !== undefined) {
79
+ if (typeof rawEntry['description'] !== 'string') {
80
80
  errors.push(`overrides['${key}']: 'description' must be a string`);
81
81
  entryValid = false;
82
82
  }
83
83
  else {
84
- result.description = rawEntry.description;
84
+ result.description = rawEntry['description'];
85
85
  }
86
86
  }
87
- if (rawEntry.body !== undefined) {
88
- if (typeof rawEntry.body !== 'string') {
87
+ if (rawEntry['body'] !== undefined) {
88
+ if (typeof rawEntry['body'] !== 'string') {
89
89
  errors.push(`overrides['${key}']: 'body' must be a string`);
90
90
  entryValid = false;
91
91
  }
92
92
  else {
93
- result.body = rawEntry.body;
93
+ result.body = rawEntry['body'];
94
94
  }
95
95
  }
96
- if (rawEntry.breaking !== undefined) {
97
- if (typeof rawEntry.breaking !== 'boolean') {
96
+ if (rawEntry['breaking'] !== undefined) {
97
+ if (typeof rawEntry['breaking'] !== 'boolean') {
98
98
  errors.push(`overrides['${key}']: 'breaking' must be a boolean`);
99
99
  entryValid = false;
100
100
  }
101
101
  else {
102
- result.breaking = rawEntry.breaking;
102
+ result.breaking = rawEntry['breaking'];
103
103
  }
104
104
  }
105
105
  if (Object.keys(result).length === 0 && entryValid) {
@@ -236,7 +236,8 @@ export function loadOverridesForScopes(scopes) {
236
236
  project = result.overrides;
237
237
  }
238
238
  }
239
- for (const workspacePath of scopes.workspaces ?? []) {
239
+ const workspacePaths = scopes.workspaces ?? [];
240
+ for (const workspacePath of workspacePaths) {
240
241
  const result = loadChangelogOverrides(resolveOverridePath(workspacePath));
241
242
  if ('errors' in result) {
242
243
  errors.push(...result.errors);
@@ -80,7 +80,7 @@ function stripLocalOnlyFields(value) {
80
80
  return value;
81
81
  }
82
82
  const record = { ...value };
83
- delete record.$schema;
83
+ delete record['$schema'];
84
84
  return record;
85
85
  }
86
86
  function deepEqual(a, b) {
@@ -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
  }
@@ -62,6 +51,6 @@ function assertCleanWorkingTree() {
62
51
  if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
63
52
  throw error;
64
53
  }
65
- throw new Error('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.');
54
+ throw new Error('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.', { cause: error });
66
55
  }
67
56
  }
@@ -10,14 +10,14 @@ export function deriveWorkspaceConfig(workspacePath) {
10
10
  }
11
11
  catch (error) {
12
12
  const message = error instanceof Error ? error.message : String(error);
13
- throw new Error(`Failed to read ${packageJsonPath}: ${message}`);
13
+ throw new Error(`Failed to read ${packageJsonPath}: ${message}`, { cause: error });
14
14
  }
15
- const name = isRecord(parsed) ? parsed.name : undefined;
15
+ const name = isRecord(parsed) ? parsed['name'] : undefined;
16
16
  if (typeof name !== 'string' || name.length === 0) {
17
17
  throw new Error(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
18
18
  }
19
19
  const unscopedName = stripNpmScope(name);
20
- const privateField = isRecord(parsed) ? parsed.private : undefined;
20
+ const privateField = isRecord(parsed) ? parsed['private'] : undefined;
21
21
  const isPublishable = privateField === undefined || privateField === false;
22
22
  return {
23
23
  dir,
@@ -6,8 +6,8 @@ export function detectPackageManager() {
6
6
  try {
7
7
  const content = readFileSync(packageJsonPath, 'utf8');
8
8
  const parsed = JSON.parse(content);
9
- if (isRecord(parsed) && typeof parsed.packageManager === 'string') {
10
- const [name, version] = parsed.packageManager.split('@', 2);
9
+ if (isRecord(parsed) && typeof parsed['packageManager'] === 'string') {
10
+ const [name, version] = parsed['packageManager'].split('@', 2);
11
11
  if (name === 'pnpm' || name === 'npm') {
12
12
  return name;
13
13
  }
@@ -23,7 +23,7 @@ export function detectUndeclaredTagPrefixes(knownPrefixes) {
23
23
  const match = CANDIDATE_TAG_PATTERN.exec(tag);
24
24
  if (match === null)
25
25
  continue;
26
- const prefix = match.groups?.prefix ?? '';
26
+ const prefix = match.groups?.['prefix'] ?? '';
27
27
  if (prefix === '' || known.has(prefix))
28
28
  continue;
29
29
  let tags = grouped.get(prefix);
@@ -20,7 +20,7 @@ export async function discoverWorkspaces() {
20
20
  if (!isRecord(parsed)) {
21
21
  return undefined;
22
22
  }
23
- const packagesField = parsed.packages;
23
+ const packagesField = parsed['packages'];
24
24
  if (!Array.isArray(packagesField)) {
25
25
  return undefined;
26
26
  }
@@ -24,7 +24,7 @@ function findLatestTag(tagPrefixes) {
24
24
  if (isNoTagError(error)) {
25
25
  return undefined;
26
26
  }
27
- throw new Error(`Failed to run 'git describe': ${errorMessage(error)}`);
27
+ throw new Error(`Failed to run 'git describe': ${errorMessage(error)}`, { cause: error });
28
28
  }
29
29
  }
30
30
  const RELEASE_COMMIT_PREFIX = 'release:';
@@ -59,7 +59,7 @@ export function getCommitsSinceTarget(tagPrefixes, paths) {
59
59
  }).trim();
60
60
  }
61
61
  catch (error) {
62
- throw new Error(`Failed to run 'git log' for range '${range}': ${errorMessage(error)}`);
62
+ throw new Error(`Failed to run 'git log' for range '${range}': ${errorMessage(error)}`, { cause: error });
63
63
  }
64
64
  if (logOutput === '') {
65
65
  return { tag, commits: [] };
@@ -1,2 +1,2 @@
1
- export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.\n Requires --only in monorepo mode (rejected when a 'project' block is configured).\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;\n rejected when a 'project' block is configured)\n --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/\n (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
1
+ export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.\n Requires --only in monorepo mode (rejected when a 'project' block is configured).\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;\n rejected when a 'project' block is configured)\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
2
2
  export declare function showPrepareHelp(): void;
@@ -16,8 +16,8 @@ Options:
16
16
  --no-git-checks, -n Skip the clean-working-tree check
17
17
  --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;
18
18
  rejected when a 'project' block is configured)
19
- --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
20
- (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
19
+ --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/
20
+ (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).
21
21
  Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
22
22
  --help, -h Show this help message
23
23
  `;
@@ -23,7 +23,7 @@ export function usesPnpm() {
23
23
  }
24
24
  const raw = readFileSync('package.json', 'utf8');
25
25
  const pkg = parseJsonRecord(raw);
26
- if (pkg !== undefined && typeof pkg.packageManager === 'string' && pkg.packageManager.startsWith('pnpm')) {
26
+ if (pkg !== undefined && typeof pkg['packageManager'] === 'string' && pkg['packageManager'].startsWith('pnpm')) {
27
27
  return { ok: true };
28
28
  }
29
29
  return {
@@ -7,7 +7,7 @@ export function detectRepoType() {
7
7
  if (existsSync('package.json')) {
8
8
  const raw = readFileSync('package.json', 'utf8');
9
9
  const pkg = parseJsonRecord(raw);
10
- if (pkg !== undefined && Array.isArray(pkg.workspaces)) {
10
+ if (pkg !== undefined && Array.isArray(pkg['workspaces'])) {
11
11
  return 'monorepo';
12
12
  }
13
13
  }
@@ -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);
@@ -23,19 +23,19 @@ export function readRootPackageVersion() {
23
23
  contents = readFileSync(absolutePath, 'utf8');
24
24
  }
25
25
  catch (error) {
26
- throw new Error(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`);
26
+ throw new Error(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
27
27
  }
28
28
  let parsed;
29
29
  try {
30
30
  parsed = JSON.parse(contents);
31
31
  }
32
32
  catch (error) {
33
- throw new Error(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`);
33
+ throw new Error(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
34
34
  }
35
35
  if (!isRecord(parsed)) {
36
36
  return { exists: true, version: undefined };
37
37
  }
38
- return { exists: true, version: typeof parsed.version === 'string' ? parsed.version : undefined };
38
+ return { exists: true, version: typeof parsed['version'] === 'string' ? parsed['version'] : undefined };
39
39
  }
40
40
  export const CONFIG_FILE_PATH = '.config/release-kit.config.ts';
41
41
  export async function loadConfig() {
@@ -44,7 +44,7 @@ export async function loadConfig() {
44
44
  return undefined;
45
45
  }
46
46
  const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(absoluteConfigPath).href));
47
- const resolved = isRecord(imported) ? (imported.default ?? imported.config) : undefined;
47
+ const resolved = isRecord(imported) ? (imported['default'] ?? imported['config']) : undefined;
48
48
  if (resolved === undefined) {
49
49
  throw new Error('Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)');
50
50
  }
@@ -177,7 +177,8 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
177
177
  for (const workspace of workspaces) {
178
178
  const owner = `ws:${workspace.dir}`;
179
179
  sources.push({ prefix: workspace.tagPrefix, label: `workspace '${workspace.dir}'`, owner });
180
- for (const identity of workspace.legacyIdentities ?? []) {
180
+ const legacyIdentities = workspace.legacyIdentities ?? [];
181
+ for (const identity of legacyIdentities) {
181
182
  sources.push({
182
183
  prefix: identity.tagPrefix,
183
184
  label: `workspace '${workspace.dir}' legacyIdentities entry (name='${identity.name}')`,
@@ -185,7 +186,8 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
185
186
  });
186
187
  }
187
188
  }
188
- for (const [index, retired] of (retiredPackages ?? []).entries()) {
189
+ const retiredEntries = (retiredPackages ?? []).entries();
190
+ for (const [index, retired] of retiredEntries) {
189
191
  sources.push({
190
192
  prefix: retired.tagPrefix,
191
193
  label: `retiredPackages entry (name='${retired.name}')`,