@williamthorsen/release-kit 18.1.1 → 18.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 (95) hide show
  1. package/.meta/changelog.json +150 -0
  2. package/.readyup/kits/default.js +596 -32
  3. package/.readyup/kits/npm-auto-publish.js +1 -1
  4. package/.readyup/manifest.json +55 -33
  5. package/CHANGELOG.md +44 -0
  6. package/README.md +19 -2
  7. package/dist/esm/assertCleanWorkingTree.d.ts +2 -1
  8. package/dist/esm/assertCleanWorkingTree.js +4 -8
  9. package/dist/esm/assertReleasesOnNpm.js +2 -9
  10. package/dist/esm/assertTarballsComplete.js +2 -9
  11. package/dist/esm/buildChangelogEntries.d.ts +5 -3
  12. package/dist/esm/buildChangelogEntries.js +9 -4
  13. package/dist/esm/buildChangelogItem.d.ts +1 -1
  14. package/dist/esm/buildDependencyGraph.js +2 -16
  15. package/dist/esm/buildReleasedResult.d.ts +15 -3
  16. package/dist/esm/buildReleasedResult.js +23 -2
  17. package/dist/esm/changelogJsonFile.d.ts +3 -2
  18. package/dist/esm/changelogJsonFile.js +14 -17
  19. package/dist/esm/changelogJsonUtils.d.ts +22 -2
  20. package/dist/esm/changelogJsonUtils.js +19 -10
  21. package/dist/esm/cli/maintenanceCommands.js +1 -1
  22. package/dist/esm/commitCommand.d.ts +2 -1
  23. package/dist/esm/commitCommand.js +8 -11
  24. package/dist/esm/configSchema.d.ts +3 -1
  25. package/dist/esm/configSchema.js +1 -0
  26. package/dist/esm/createGithubRelease.d.ts +13 -10
  27. package/dist/esm/createGithubRelease.js +9 -13
  28. package/dist/esm/createGithubReleaseCommand.d.ts +1 -1
  29. package/dist/esm/createGithubReleaseCommand.js +25 -3
  30. package/dist/esm/createTags.d.ts +2 -1
  31. package/dist/esm/createTags.js +12 -12
  32. package/dist/esm/decideDirectReleases.d.ts +2 -1
  33. package/dist/esm/decideDirectReleases.js +5 -5
  34. package/dist/esm/decideRelease.d.ts +1 -1
  35. package/dist/esm/defaults.d.ts +4 -2
  36. package/dist/esm/defaults.js +1 -0
  37. package/dist/esm/deriveDirectoryConfig.js +5 -4
  38. package/dist/esm/deriveWorkspaceConfig.js +2 -9
  39. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +2 -1
  40. package/dist/esm/detectUndeclaredTagPrefixes.js +4 -13
  41. package/dist/esm/determineBumpType.d.ts +2 -1
  42. package/dist/esm/determineBumpType.js +5 -0
  43. package/dist/esm/enumerateReleaseWindows.d.ts +3 -11
  44. package/dist/esm/enumerateReleaseWindows.js +17 -60
  45. package/dist/esm/findUnroutedEntryScopes.d.ts +1 -1
  46. package/dist/esm/git.d.ts +61 -0
  47. package/dist/esm/git.js +101 -0
  48. package/dist/esm/init/checks.d.ts +2 -1
  49. package/dist/esm/init/checks.js +5 -10
  50. package/dist/esm/init/eligibility.d.ts +12 -0
  51. package/dist/esm/init/eligibility.js +50 -0
  52. package/dist/esm/init/initCommand.d.ts +0 -2
  53. package/dist/esm/init/initCommand.js +4 -45
  54. package/dist/esm/init/managed-workflows.d.ts +10 -0
  55. package/dist/esm/init/managed-workflows.js +18 -0
  56. package/dist/esm/init/templates.js +2 -2
  57. package/dist/esm/init/updateTemplatesCommand.js +9 -42
  58. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +29 -7
  59. package/dist/esm/injectReleaseNotesIntoReadme.js +13 -21
  60. package/dist/esm/loadConfig.js +3 -16
  61. package/dist/esm/mergeConfig.d.ts +2 -1
  62. package/dist/esm/packFromCleanCheckout.js +3 -9
  63. package/dist/esm/planVersionBump.js +2 -16
  64. package/dist/esm/prepareCommand.js +2 -1
  65. package/dist/esm/prepareComposite.js +1 -1
  66. package/dist/esm/prepareReleaseTargets.d.ts +2 -1
  67. package/dist/esm/prepareReleaseTargets.js +3 -2
  68. package/dist/esm/prepareRunContext.d.ts +2 -0
  69. package/dist/esm/prepareTarget.d.ts +6 -4
  70. package/dist/esm/prepareTarget.js +9 -3
  71. package/dist/esm/previewTagPrefixes.d.ts +9 -2
  72. package/dist/esm/previewTagPrefixes.js +22 -22
  73. package/dist/esm/publishCommand.d.ts +1 -1
  74. package/dist/esm/publishCommand.js +27 -2
  75. package/dist/esm/pushRelease.d.ts +2 -1
  76. package/dist/esm/pushRelease.js +11 -11
  77. package/dist/esm/readJsonFile.d.ts +2 -0
  78. package/dist/esm/readJsonFile.js +17 -0
  79. package/dist/esm/reportPrepare.js +68 -114
  80. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  81. package/dist/esm/resolveReleaseTags.js +4 -12
  82. package/dist/esm/showTagPrefixesCommand.d.ts +6 -6
  83. package/dist/esm/showTagPrefixesCommand.js +94 -39
  84. package/dist/esm/sync-labels/generateCommand.js +0 -4
  85. package/dist/esm/sync-labels/initCommand.js +0 -4
  86. package/dist/esm/sync-labels/syncCommand.js +0 -4
  87. package/dist/esm/types.d.ts +8 -2
  88. package/docs/configuration.md +15 -6
  89. package/docs/labels.md +2 -2
  90. package/docs/releasing.md +3 -2
  91. package/docs/work-types.md +2 -0
  92. package/package.json +7 -7
  93. package/presets/labels/common.yaml +14 -0
  94. package/dist/esm/sync-labels/retiredConfig.d.ts +0 -11
  95. package/dist/esm/sync-labels/retiredConfig.js +0 -12
@@ -1,6 +1,6 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.45.1";
3
+ export const __readyupVersion = "0.46.2";
4
4
 
5
5
 
6
6
  // .readyup/kits/npm-auto-publish.ts
@@ -3,20 +3,19 @@
3
3
  "kits": [
4
4
  {
5
5
  "bundledDependencies": {
6
- "@williamthorsen/toolbelt.datetime": "4.1.2",
7
- "@williamthorsen/toolbelt.errors": "0.9.1",
8
- "@williamthorsen/toolbelt.filesystem": "0.10.1",
9
- "@williamthorsen/toolbelt.numbers": "7.3.2",
10
- "@williamthorsen/toolbelt.objects": "4.4.1",
11
- "@williamthorsen/toolbelt.packaging": "0.7.1",
12
- "@williamthorsen/toolbelt.terminal": "0.3.0",
6
+ "@williamthorsen/toolbelt.datetime": "4.1.5",
7
+ "@williamthorsen/toolbelt.errors": "0.10.1",
8
+ "@williamthorsen/toolbelt.filesystem": "0.11.0",
9
+ "@williamthorsen/toolbelt.numbers": "8.0.2",
10
+ "@williamthorsen/toolbelt.objects": "4.4.3",
11
+ "@williamthorsen/toolbelt.packaging": "0.8.0",
12
+ "@williamthorsen/toolbelt.terminal": "0.3.1",
13
13
  "ansi-regex": "6.6.0",
14
- "ansi-styles": "6.2.3",
15
- "cli-truncate": "6.1.1",
14
+ "ansi-styles": "6.2.3, 7.0.0",
15
+ "cli-truncate": "6.2.0",
16
16
  "diff": "9.0.0",
17
17
  "get-east-asian-width": "1.7.0",
18
- "is-fullwidth-code-point": "5.1.0",
19
- "slice-ansi": "9.0.1",
18
+ "slice-ansi": "10.0.1",
20
19
  "string-width": "8.3.0",
21
20
  "strip-ansi": "7.2.0",
22
21
  "wrap-ansi": "10.0.2"
@@ -26,7 +25,7 @@
26
25
  "checklist": "release-kit",
27
26
  "checks": [
28
27
  "@williamthorsen/release-kit in devDependencies",
29
- "@williamthorsen/release-kit >= 18.1.1",
28
+ "@williamthorsen/release-kit >= 18.3.0",
30
29
  "release.yaml workflow exists",
31
30
  "release.yaml matches template",
32
31
  "release.yaml does not reference deprecated tag ref",
@@ -51,8 +50,7 @@
51
50
  "@changesets/cli not in devDependencies",
52
51
  "sync-labels.yaml workflow exists",
53
52
  "sync-labels.yaml matches template",
54
- "sync-labels.yaml does not reference deprecated tag ref",
55
- "retired .config/sync-labels.config.ts is absent"
53
+ "sync-labels.yaml does not reference deprecated tag ref"
56
54
  ]
57
55
  }
58
56
  ],
@@ -142,7 +140,7 @@
142
140
  "path": "../../nmr-core/dist/esm/timer.js"
143
141
  },
144
142
  {
145
- "hash": "56566d0b",
143
+ "hash": "ee8d2dc8",
146
144
  "kind": "module",
147
145
  "path": "kits/default.ts"
148
146
  },
@@ -157,12 +155,17 @@
157
155
  "path": "lib/config-checks.ts"
158
156
  },
159
157
  {
160
- "hash": "9a4023ad",
158
+ "hash": "70633d7f",
159
+ "kind": "module",
160
+ "path": "lib/template-checks.ts"
161
+ },
162
+ {
163
+ "hash": "3c6fd66a",
161
164
  "kind": "module",
162
165
  "path": "lib/workflow-hashes.ts"
163
166
  },
164
167
  {
165
- "hash": "73ff9801",
168
+ "hash": "af60087a",
166
169
  "kind": "inline",
167
170
  "path": "../package.json",
168
171
  "paths": [
@@ -179,11 +182,31 @@
179
182
  "kind": "module",
180
183
  "path": "../src/init/detectRepoType.ts"
181
184
  },
185
+ {
186
+ "hash": "d360febf",
187
+ "kind": "module",
188
+ "path": "../src/init/hasProvenance.ts"
189
+ },
190
+ {
191
+ "hash": "3238f7e0",
192
+ "kind": "module",
193
+ "path": "../src/init/managed-workflows.ts"
194
+ },
182
195
  {
183
196
  "hash": "133be525",
184
197
  "kind": "module",
185
198
  "path": "../src/init/parseJsonRecord.ts"
186
199
  },
200
+ {
201
+ "hash": "5950f562",
202
+ "kind": "module",
203
+ "path": "../src/init/scaffold.ts"
204
+ },
205
+ {
206
+ "hash": "4d4b66bc",
207
+ "kind": "module",
208
+ "path": "../src/init/templates.ts"
209
+ },
187
210
  {
188
211
  "hash": "2210df36",
189
212
  "kind": "module",
@@ -192,27 +215,26 @@
192
215
  ],
193
216
  "name": "default",
194
217
  "path": "kits/default.js",
195
- "readyupVersion": "0.45.1",
218
+ "readyupVersion": "0.46.2",
196
219
  "source": "kits/default.ts",
197
- "sourceHash": "56566d0b",
198
- "targetHash": "b8e72e90"
220
+ "sourceHash": "ee8d2dc8",
221
+ "targetHash": "5985226a"
199
222
  },
200
223
  {
201
224
  "bundledDependencies": {
202
- "@williamthorsen/toolbelt.datetime": "4.1.2",
203
- "@williamthorsen/toolbelt.errors": "0.9.1",
204
- "@williamthorsen/toolbelt.filesystem": "0.10.1",
205
- "@williamthorsen/toolbelt.numbers": "7.3.2",
206
- "@williamthorsen/toolbelt.objects": "4.4.1",
207
- "@williamthorsen/toolbelt.packaging": "0.7.1",
208
- "@williamthorsen/toolbelt.terminal": "0.3.0",
225
+ "@williamthorsen/toolbelt.datetime": "4.1.5",
226
+ "@williamthorsen/toolbelt.errors": "0.10.1",
227
+ "@williamthorsen/toolbelt.filesystem": "0.11.0",
228
+ "@williamthorsen/toolbelt.numbers": "8.0.2",
229
+ "@williamthorsen/toolbelt.objects": "4.4.3",
230
+ "@williamthorsen/toolbelt.packaging": "0.8.0",
231
+ "@williamthorsen/toolbelt.terminal": "0.3.1",
209
232
  "ansi-regex": "6.6.0",
210
- "ansi-styles": "6.2.3",
211
- "cli-truncate": "6.1.1",
233
+ "ansi-styles": "6.2.3, 7.0.0",
234
+ "cli-truncate": "6.2.0",
212
235
  "diff": "9.0.0",
213
236
  "get-east-asian-width": "1.7.0",
214
- "is-fullwidth-code-point": "5.1.0",
215
- "slice-ansi": "9.0.1",
237
+ "slice-ansi": "10.0.1",
216
238
  "string-width": "8.3.0",
217
239
  "strip-ansi": "7.2.0",
218
240
  "wrap-ansi": "10.0.2"
@@ -345,10 +367,10 @@
345
367
  ],
346
368
  "name": "npm-auto-publish",
347
369
  "path": "kits/npm-auto-publish.js",
348
- "readyupVersion": "0.45.1",
370
+ "readyupVersion": "0.46.2",
349
371
  "source": "kits/npm-auto-publish.ts",
350
372
  "sourceHash": "682fdd4a",
351
- "targetHash": "884c7f0a"
373
+ "targetHash": "2bfaa0b5"
352
374
  }
353
375
  ]
354
376
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 18.3.0 — 2026-11-14
6
+
7
+ ### 🎉 Features
8
+
9
+ - Adds the `versionPatterns.none` setting to `release-kit`, whose default list (`ai`, `ci`, `docs`, `tests`, `tooling`) names the types that do not bump a version unless an item is breaking, so that `prepare` skips a window containing only those items; `none: []` restores a patch bump for them. (#1340)
10
+ - Makes `release-kit` list a dependent's own unreleased items, such as `tests` or `ci` items, together with its dependencies in the changelog entry for a release triggered by propagation alone, rather than the dependencies only. (#1340)
11
+ - Changes `create-github-release` to print a stderr warning that names the tag and the file when a tag's `changelog.json` is missing or cannot be parsed, instead of listing that tag in its `Skipped N tag(s) without releasable content` summary. (#1343)
12
+ - Adds the tag and its workspace to the warning that `publish` prints when it skips injecting release notes into a README, alongside the reason: a missing or unparseable `changelog.json`, a version without an entry, or an entry without user-facing release notes. (#1343)
13
+ - Adds the labels `effort:1`, `effort:2`, `effort:3`, `effort:5`, `effort:8`, and `effort:13` to the `common` label preset, which state an item's size in points apart from its urgency and value. (#1355)
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - Fixes the issue that `release-kit prepare` treated an unreadable, unparseable, or non-array `changelog.json` as empty and silently dropped elements that are not changelog entries, which overwrote the history in the file; `prepare` now stops with an error that names the file and tells the user to repair or delete it. (#1346)
18
+ - Fixes the issue that `release-kit tag` reported a failed git command during its clean-tree check as a dirty working tree rather than as git's own error. (#1348)
19
+ - Fixes the issue that `release-kit` reported a JSON file that it could not read as one that it could not parse, or the reverse, by giving each failure the message `Failed to read <path>` or `Failed to parse JSON in <path>`. (#1350)
20
+ - Fixes the issue that the `release-kit prepare` report printed a single-package release's and a project's warnings, bump override, and parsed count in a different order from a workspace's, and printed "Parsed 0 typed commits" for a single-package release or a count for one that skipped. (#1354)
21
+
22
+ ### ♻️ Refactoring
23
+
24
+ - Routes release-kit's git calls, apart from those in `packFromCleanCheckout.ts`, through a `Git` adapter in `git.ts` that each module receives as a parameter, so that unit tests pass a fake git instead of mocking `node:child_process`. (#1348)
25
+ - Moves the dry-run notice, eligibility checks, and repo-type detection that `release-kit`'s `init` and `update-templates` commands each repeated into `resolveEligibleRepoType` in `src/init/eligibility.ts`, so that `updateTemplatesCommand.ts` no longer imports from `initCommand.ts`. (#1353)
26
+
27
+ ### 🧪 Tests
28
+
29
+ - Replaces the file-wide `silenceConsole` calls in the release-kit and v11y-check tests with a `using` silencer in each test that logs, restricted to the channels that the test fires, and reads asserted output through `listConsoleLines` rather than through the `console` mocks. (#1349)
30
+
31
+ ## 18.2.0 — 2026-11-13
32
+
33
+ ### 🎉 Features
34
+
35
+ - Changes `release-kit show-tag-prefixes` to print one block per target that lists the derived prefix and each declared legacy identity with their tag counts in aligned columns, and to print the "Undeclared tag prefixes" section on every run, showing `(none)` when the scan does not find any candidates. (#1332)
36
+ - Adds a "Retired packages" section to `release-kit show-tag-prefixes`, which lists each entry declared in `retiredPackages` with its tag prefix, tag count, name, and successor. (#1332)
37
+ - Adds a closing note to `release-kit show-tag-prefixes` when a derived prefix does not have any tags yet, which explains that `release-kit tag` creates the first tag from the tags file that `release-kit prepare` writes. (#1332)
38
+ - Makes the template checks for `release.yaml`, `publish.yaml`, and `create-github-release.yaml` report the differing hunks, and distinguish a missing file from an unreadable one, rather than report only that a hash differs. (#1336)
39
+
40
+ ### 🪦 Removed
41
+
42
+ - Removes the check for the retired `.config/sync-labels.config.ts`, which made `sync-labels init`, `generate`, and `sync` exit with an error and made the release-kit readyup kit fail while the file existed. (#1333)
43
+
44
+ ### 🐛 Bug fixes
45
+
46
+ - Fixes the issue that `release-kit`'s ReadyUp kit reported the warning `releaseNotes.shouldInjectIntoReadme is true`, which the consumer could not clear, in a repo without any publishable package, by making the check skip with the reason `no publishable packages`. (#1334)
47
+ - Fixes the issue that the `publish.yaml matches template` check in `rdy run --from npm:@williamthorsen/release-kit` failed a `publish.yaml` that omits provenance, a setting that `release-kit update-templates` preserves. (#1336)
48
+
5
49
  ## 18.1.1 — 2026-11-12
6
50
 
7
51
  ### ⚙️ Tooling
package/README.md CHANGED
@@ -6,7 +6,24 @@ Version-bumping and changelog-generation toolkit for release workflows.
6
6
 
7
7
  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 the same commit history that decides each bump (with optional [editorial overrides](docs/editorial-overrides.md)).
8
8
 
9
- <!-- section:release-notes --><!-- /section:release-notes -->
9
+ <!-- section:release-notes -->
10
+ ## Release notes — v18.3.0 (2026-11-14)
11
+
12
+ ### 🎉 Features
13
+
14
+ - Adds the `versionPatterns.none` setting to `release-kit`, whose default list (`ai`, `ci`, `docs`, `tests`, `tooling`) names the types that do not bump a version unless an item is breaking, so that `prepare` skips a window containing only those items; `none: []` restores a patch bump for them. (#1340)
15
+ - Makes `release-kit` list a dependent's own unreleased items, such as `tests` or `ci` items, together with its dependencies in the changelog entry for a release triggered by propagation alone, rather than the dependencies only. (#1340)
16
+ - Changes `create-github-release` to print a stderr warning that names the tag and the file when a tag's `changelog.json` is missing or cannot be parsed, instead of listing that tag in its `Skipped N tag(s) without releasable content` summary. (#1343)
17
+ - Adds the tag and its workspace to the warning that `publish` prints when it skips injecting release notes into a README, alongside the reason: a missing or unparseable `changelog.json`, a version without an entry, or an entry without user-facing release notes. (#1343)
18
+ - Adds the labels `effort:1`, `effort:2`, `effort:3`, `effort:5`, `effort:8`, and `effort:13` to the `common` label preset, which state an item's size in points apart from its urgency and value. (#1355)
19
+
20
+ ### 🐛 Bug fixes
21
+
22
+ - Fixes the issue that `release-kit prepare` treated an unreadable, unparseable, or non-array `changelog.json` as empty and silently dropped elements that are not changelog entries, which overwrote the history in the file; `prepare` now stops with an error that names the file and tells the user to repair or delete it. (#1346)
23
+ - Fixes the issue that `release-kit tag` reported a failed git command during its clean-tree check as a dirty working tree rather than as git's own error. (#1348)
24
+ - Fixes the issue that `release-kit` reported a JSON file that it could not read as one that it could not parse, or the reverse, by giving each failure the message `Failed to read <path>` or `Failed to parse JSON in <path>`. (#1350)
25
+ - Fixes the issue that the `release-kit prepare` report printed a single-package release's and a project's warnings, bump override, and parsed count in a different order from a workspace's, and printed "Parsed 0 typed commits" for a single-package release or a count for one that skipped. (#1354)
26
+ <!-- /section:release-notes -->
10
27
 
11
28
  ## Installation
12
29
 
@@ -105,7 +122,7 @@ export default defineConfig({
105
122
  formatCommand: 'npx prettier --write',
106
123
 
107
124
  // Override the default version patterns
108
- versionPatterns: { major: ['!'], minor: ['feat', 'feature'] },
125
+ versionPatterns: { major: ['!'], minor: ['feat', 'feature'], none: ['ai', 'ci', 'docs', 'tests', 'tooling'] },
109
126
 
110
127
  // Add or override work types (merged with defaults by key)
111
128
  workTypes: { perf: { header: 'Performance' } },
@@ -1,2 +1,3 @@
1
+ import { type Git } from './git.js';
1
2
  /** Throws if `git status --porcelain` reports any uncommitted change in the working tree. */
2
- export declare function assertCleanWorkingTree(): void;
3
+ export declare function assertCleanWorkingTree(git?: Git): void;
@@ -1,14 +1,10 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { GIT_OUTPUT_LIMIT_BYTES, UserError } from '@williamthorsen/nmr-core';
1
+ import { UserError } from '@williamthorsen/nmr-core';
2
+ import { realGit } from "./git.js";
3
3
  import { asUserError } from "./user-error-chaining.js";
4
- export function assertCleanWorkingTree() {
4
+ export function assertCleanWorkingTree(git = realGit) {
5
5
  let status;
6
6
  try {
7
- status = execFileSync('git', ['status', '--porcelain'], {
8
- encoding: 'utf8',
9
- maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
10
- stdio: ['pipe', 'pipe', 'pipe'],
11
- }).trim();
7
+ status = git.readStatus();
12
8
  }
13
9
  catch (error) {
14
10
  throw asUserError(error);
@@ -1,9 +1,8 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
2
  import { isRecord, UserError } from '@williamthorsen/nmr-core';
4
3
  import { isPublishableManifest } from "./isPublishableManifest.js";
5
4
  import { lookUpNpmPackage } from "./npmRegistry.js";
6
- import { chainUserError } from "./user-error-chaining.js";
5
+ import { readJsonFile } from "./readJsonFile.js";
7
6
  export function assertReleasesOnNpm(workspacePaths) {
8
7
  const unpublishedCandidates = [];
9
8
  const unverifiableCandidates = [];
@@ -27,13 +26,7 @@ export function assertReleasesOnNpm(workspacePaths) {
27
26
  }
28
27
  function buildNpmCandidate(workspacePath) {
29
28
  const manifestPath = join(workspacePath, 'package.json');
30
- let manifest;
31
- try {
32
- manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
33
- }
34
- catch (error) {
35
- throw chainUserError(`Failed to parse ${manifestPath}`, error);
36
- }
29
+ const manifest = readJsonFile(manifestPath);
37
30
  if (!isRecord(manifest) || !isPublishableManifest(manifest) || typeof manifest['name'] !== 'string') {
38
31
  return undefined;
39
32
  }
@@ -1,4 +1,3 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
2
  import { isAnticipatedFailure, isRecord, UserError } from '@williamthorsen/nmr-core';
4
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
@@ -6,7 +5,7 @@ import { detectPackageManager } from "./detectPackageManager.js";
6
5
  import { findMissingManifestPaths } from "./findMissingManifestPaths.js";
7
6
  import { isPublishableManifest } from "./isPublishableManifest.js";
8
7
  import { packFromCleanCheckout } from "./packFromCleanCheckout.js";
9
- import { chainUserError } from "./user-error-chaining.js";
8
+ import { readJsonFile } from "./readJsonFile.js";
10
9
  export function assertTarballsComplete(workspacePaths, devExportConditions, pack = packFromCleanCheckout) {
11
10
  const candidates = workspacePaths
12
11
  .map((workspacePath) => ({ workspacePath, manifest: readManifest(workspacePath) }))
@@ -52,12 +51,6 @@ function describeRefusal(incompletePackages) {
52
51
  }
53
52
  function readManifest(workspacePath) {
54
53
  const manifestPath = join(workspacePath, 'package.json');
55
- let manifest;
56
- try {
57
- manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
58
- }
59
- catch (error) {
60
- throw chainUserError(`Failed to parse ${manifestPath}`, error);
61
- }
54
+ const manifest = readJsonFile(manifestPath);
62
55
  return isRecord(manifest) ? manifest : {};
63
56
  }
@@ -1,13 +1,13 @@
1
1
  import { type ChangelogDiagnostics } from './buildChangelogItem.js';
2
- import { type RawCommit } from './enumerateReleaseWindows.js';
3
2
  import type { GenerateChangelogOptions } from './generateChangelogs.js';
3
+ import { type Git, type RawCommit } from './git.js';
4
4
  import type { ChangelogEntry, ChangelogSection, ReleaseSettings, ReleaseType } from './types.js';
5
5
  /**
6
6
  * Builds structured changelog entries from the release windows of git history, naming the unreleased window `tag`.
7
7
  *
8
8
  * Composes `readReleaseHistory` and `convertToChangelogEntries` for a caller that knows the tag before it reads.
9
9
  */
10
- export declare function buildChangelogEntries(config: ReleaseHistoryConfig, tag: string, options: GenerateChangelogOptions): {
10
+ export declare function buildChangelogEntries(config: ReleaseHistoryConfig, tag: string, options: GenerateChangelogOptions, git?: Git): {
11
11
  entries: ChangelogEntry[];
12
12
  diagnostics: ChangelogDiagnostics;
13
13
  };
@@ -38,7 +38,9 @@ export declare function convertToReleaseEntries(history: ReleaseHistory, tag: st
38
38
  * `classifyChangelogCommit`. A `release:` or merge subject yields nothing either way. The read records diagnostics
39
39
  * only for the unreleased window, since the released windows were reported when they were prepared.
40
40
  */
41
- export declare function readReleaseHistory(config: ReleaseHistoryConfig, options: GenerateChangelogOptions): ReleaseHistory;
41
+ export declare function readReleaseHistory(config: ReleaseHistoryConfig, options: GenerateChangelogOptions, git: Git): ReleaseHistory;
42
+ /** Returns `sections` in canonical order; a stable sort keeps unknown sections, which sort last, in encounter order. */
43
+ export declare function sortSectionsCanonically(sections: readonly ChangelogSection[]): ChangelogSection[];
42
44
  /**
43
45
  * Strips a section title down to its bare name.
44
46
  *
@@ -5,13 +5,14 @@ import { classifyChangelogCommit, isNonChangeSubject, isReleaseSubject } from ".
5
5
  import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
6
6
  import { determineBumpType } from "./determineBumpType.js";
7
7
  import { enumerateReleaseWindows } from "./enumerateReleaseWindows.js";
8
+ import { realGit } from "./git.js";
8
9
  import { parseChangeRecordBlock } from "./parseChangeRecordBlock.js";
9
10
  import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
10
11
  import { prefixUserError } from "./user-error-chaining.js";
11
12
  const UNRELEASED_TAG = 'unreleased';
12
13
  const CANONICAL_SECTION_ORDER = new Map(Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index]));
13
- export function buildChangelogEntries(config, tag, options) {
14
- const history = readReleaseHistory(config, options);
14
+ export function buildChangelogEntries(config, tag, options, git = realGit) {
15
+ const history = readReleaseHistory(config, options, git);
15
16
  return { entries: convertToChangelogEntries(history, tag), diagnostics: history.unreleasedReading.diagnostics };
16
17
  }
17
18
  export function convertToChangelogEntries(history, tag) {
@@ -30,9 +31,10 @@ export function convertToReleaseEntries(history, tag, today) {
30
31
  }
31
32
  return convertToChangelogEntries(history, tag);
32
33
  }
33
- export function readReleaseHistory(config, options) {
34
+ export function readReleaseHistory(config, options, git) {
34
35
  try {
35
36
  const windows = enumerateReleaseWindows({
37
+ git,
36
38
  ...(options.paths !== undefined && { paths: options.paths }),
37
39
  tagPrefixes: options.tagPrefixes,
38
40
  unreleasedTag: UNRELEASED_TAG,
@@ -52,6 +54,9 @@ export function readReleaseHistory(config, options) {
52
54
  throw prefixUserError(`Failed to read the release history for ${options.tagPrefixes.join(', ')}`, error);
53
55
  }
54
56
  }
57
+ export function sortSectionsCanonically(sections) {
58
+ return sections.toSorted((a, b) => getCanonicalSectionPriority(a.title) - getCanonicalSectionPriority(b.title));
59
+ }
55
60
  export function stripGroupDecorations(group) {
56
61
  return stripEmojiPrefix(group);
57
62
  }
@@ -72,7 +77,7 @@ function buildSections(sectionMap, devOnlySections) {
72
77
  items,
73
78
  });
74
79
  }
75
- return sections.toSorted((a, b) => getCanonicalSectionPriority(a.title) - getCanonicalSectionPriority(b.title));
80
+ return sortSectionsCanonically(sections);
76
81
  }
77
82
  function formatDate(timestampSec) {
78
83
  return new Date(timestampSec * 1000).toISOString().slice(0, 10);
@@ -1,5 +1,5 @@
1
1
  import type { BumpSignal } from './determineBumpType.js';
2
- import type { RawCommit } from './enumerateReleaseWindows.js';
2
+ import type { RawCommit } from './git.js';
3
3
  import { type ChangeRecordEntry } from './parseChangeRecordBlock.js';
4
4
  import type { ChangelogItem, MalformedChangeRecordBlock, PolicyViolation, ReleaseSettings, UndeclaredEntryType, UnroutedEntryScope, VersionPatterns, WorkTypeConfig } from './types.js';
5
5
  /**
@@ -1,6 +1,5 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import { UserError } from '@williamthorsen/nmr-core';
3
- import { chainUserError } from "./user-error-chaining.js";
2
+ import { readJsonFile } from "./readJsonFile.js";
4
3
  function isPackageJsonSubset(value) {
5
4
  return typeof value === 'object' && value !== null;
6
5
  }
@@ -52,20 +51,7 @@ export function buildDependencyGraph(targets) {
52
51
  return { packageNameToDir, dirToPackageName, dirToVersion, dependentsOf, dependenciesOf };
53
52
  }
54
53
  function readPackageJsonSubset(filePath) {
55
- let content;
56
- try {
57
- content = readFileSync(filePath, 'utf8');
58
- }
59
- catch (error) {
60
- throw chainUserError(`Failed to read ${filePath}`, error);
61
- }
62
- let parsedJson;
63
- try {
64
- parsedJson = JSON.parse(content);
65
- }
66
- catch (error) {
67
- throw chainUserError(`Failed to parse JSON in ${filePath}`, error);
68
- }
54
+ const parsedJson = readJsonFile(filePath);
69
55
  if (!isPackageJsonSubset(parsedJson)) {
70
56
  throw new UserError(`Invalid package.json at ${filePath}`);
71
57
  }
@@ -1,4 +1,4 @@
1
- import type { ReleaseHistory } from './buildChangelogEntries.js';
1
+ import { type ReleaseHistory } from './buildChangelogEntries.js';
2
2
  import type { ChangelogEntry, PropagationSource, ReleasedCompositeResult, ReleasedTargetResult, ReleaseTarget, ReleaseType } from './types.js';
3
3
  /** The release level that a target takes, whether its own commits or a dependency's bump decided it. */
4
4
  export interface TargetRelease {
@@ -13,8 +13,20 @@ interface ReleaseDecisionFields {
13
13
  }
14
14
  /** The fields that a released result of either kind takes from the plan. */
15
15
  type ReleaseResultFields = Omit<ReleasedCompositeResult, 'commits' | 'kind' | 'parsedCommitCount' | 'releaseType' | 'status'>;
16
- /** Builds the one synthetic entry of a release that propagation alone set, under the dependencies header. */
17
- export declare function buildPropagationEntries(target: ReleaseTarget, release: TargetRelease, newVersion: string, today: string, dependenciesHeader: string): ChangelogEntry[];
16
+ /** The inputs from which `buildPropagationEntries` builds a propagated release's entries. */
17
+ export interface PropagationEntryArgs {
18
+ history: ReleaseHistory;
19
+ tag: string;
20
+ newVersion: string;
21
+ today: string;
22
+ dependenciesHeader: string;
23
+ }
24
+ /**
25
+ * Builds the entries of a release that propagation alone set: the window's own entries with the dependencies added to
26
+ * the new version's section under the dependencies header, or, when the window doesn't yield any item, the one
27
+ * synthetic entry under that header.
28
+ */
29
+ export declare function buildPropagationEntries(target: ReleaseTarget, release: TargetRelease, args: PropagationEntryArgs): ChangelogEntry[];
18
30
  /** Builds the released result of a target from the plan's fields, by the target's kind. */
19
31
  export declare function buildReleasedResult(target: ReleaseTarget, fields: ReleaseResultFields, release: TargetRelease, decision: ReleaseDecisionFields | undefined, history: ReleaseHistory): ReleasedTargetResult;
20
32
  export {};
@@ -1,10 +1,17 @@
1
1
  import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
2
+ import { convertToChangelogEntries, sortSectionsCanonically, stripGroupDecorations, } from "./buildChangelogEntries.js";
2
3
  import { buildSyntheticChangelogEntry } from "./buildSyntheticChangelogEntry.js";
3
- export function buildPropagationEntries(target, release, newVersion, today, dependenciesHeader) {
4
+ export function buildPropagationEntries(target, release, args) {
4
5
  if (release.propagatedFrom === undefined) {
5
6
  throw new Error(`Workspace '${target.id}' has neither a direct release nor a propagation source`);
6
7
  }
7
- return [buildSyntheticChangelogEntry(release.propagatedFrom, newVersion, today, dependenciesHeader)];
8
+ const { history, tag, newVersion, today, dependenciesHeader } = args;
9
+ const synthetic = buildSyntheticChangelogEntry(release.propagatedFrom, newVersion, today, dependenciesHeader);
10
+ const [newEntry, ...releasedEntries] = convertToChangelogEntries(history, tag);
11
+ if (history.unreleasedReading.sections.length === 0 || newEntry === undefined) {
12
+ return [synthetic];
13
+ }
14
+ return [{ ...newEntry, sections: mergeSections(newEntry.sections, synthetic.sections) }, ...releasedEntries];
8
15
  }
9
16
  export function buildReleasedResult(target, fields, release, decision, history) {
10
17
  switch (target.kind) {
@@ -78,3 +85,17 @@ function buildReleasedPackage(target, fields, release, decision, history) {
78
85
  attachChangelogDiagnostics(result, unreleasedReading.diagnostics);
79
86
  return result;
80
87
  }
88
+ function mergeSections(sections, added) {
89
+ const merged = [...sections];
90
+ for (const section of added) {
91
+ const index = merged.findIndex((existing) => stripGroupDecorations(existing.title) === stripGroupDecorations(section.title));
92
+ const existing = merged[index];
93
+ if (existing === undefined) {
94
+ merged.push(section);
95
+ }
96
+ else {
97
+ merged[index] = { ...existing, items: [...existing.items, ...section.items] };
98
+ }
99
+ }
100
+ return sortSectionsCanonically(merged);
101
+ }
@@ -7,7 +7,8 @@ export declare function renderChangelogJson(entries: ChangelogEntry[]): string;
7
7
  * Merges `entries` with the on-disk entries at `filePath` and returns the merged set in newest-first order, without
8
8
  * writing.
9
9
  *
10
- * Keeps an on-disk entry that `entries` lacks, which preserves synthetic entries across propagation runs. A missing or
11
- * malformed file counts as empty, so that a malformed file does not abort the release.
10
+ * Keeps an on-disk entry that `entries` lacks, which preserves synthetic entries across propagation runs. A missing file
11
+ * counts as empty. Throws a `UserError` when the file cannot be read as entries or contains an element that is not an
12
+ * entry, because the merged set would replace the history that the file holds.
12
13
  */
13
14
  export declare function mergeChangelogEntriesWithDisk(filePath: string, entries: ChangelogEntry[]): ChangelogEntry[];
@@ -1,10 +1,8 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
- import { describeError } from '@williamthorsen/toolbelt.errors';
2
+ import { UserError } from '@williamthorsen/nmr-core';
4
3
  import stringify from 'json-stringify-pretty-compact';
5
- import { isChangelogEntry } from "./changelogJsonUtils.js";
4
+ import { loadChangelogEntries } from "./changelogJsonUtils.js";
6
5
  import { compareVersionsDescending } from "./compareVersions.js";
7
- import { isUnknownArray } from "./typeGuards.js";
8
6
  export function resolveChangelogJsonPath(config, changelogPath) {
9
7
  return join(changelogPath, config.changelogJson.outputPath);
10
8
  }
@@ -12,28 +10,24 @@ export function renderChangelogJson(entries) {
12
10
  return stringify(sortNewestFirst(entries), { maxLength: 100 }) + '\n';
13
11
  }
14
12
  export function mergeChangelogEntriesWithDisk(filePath, entries) {
15
- const existingEntries = readExistingEntries(filePath);
16
- return mergeEntries(entries, existingEntries);
13
+ return mergeEntries(entries, readExistingEntries(filePath));
17
14
  }
18
15
  function sortNewestFirst(entries) {
19
16
  return [...entries].toSorted((a, b) => compareVersionsDescending(a.version, b.version));
20
17
  }
21
18
  function readExistingEntries(filePath) {
22
- if (!existsSync(filePath)) {
19
+ const loaded = loadChangelogEntries(filePath);
20
+ if (loaded.status === 'missing') {
23
21
  return [];
24
22
  }
25
- try {
26
- const content = readFileSync(filePath, 'utf8');
27
- const parsedJson = JSON.parse(content);
28
- if (!isUnknownArray(parsedJson)) {
29
- return [];
30
- }
31
- return parsedJson.filter(isChangelogEntry);
23
+ if (loaded.status === 'unreadable') {
24
+ throw buildRefusal(filePath, loaded.reason);
32
25
  }
33
- catch (error) {
34
- console.warn(`Warning: could not parse existing ${filePath}: ${describeError(error)}; treating as empty`);
35
- return [];
26
+ if (loaded.rejectedCount > 0) {
27
+ const elements = loaded.rejectedCount === 1 ? 'element is not a changelog entry' : 'elements are not changelog entries';
28
+ throw buildRefusal(filePath, `${loaded.rejectedCount} ${elements}`);
36
29
  }
30
+ return loaded.entries;
37
31
  }
38
32
  function mergeEntries(newEntries, existingEntries) {
39
33
  const versionMap = new Map();
@@ -45,3 +39,6 @@ function mergeEntries(newEntries, existingEntries) {
45
39
  }
46
40
  return sortNewestFirst(versionMap.values());
47
41
  }
42
+ function buildRefusal(filePath, problem) {
43
+ return new UserError(`Cannot prepare the changelog from ${filePath}: ${problem}. Repair or delete the file, then rerun 'release-kit prepare'.`);
44
+ }
@@ -3,12 +3,32 @@ import type { ChangelogEntry } from './types.js';
3
3
  * Reports whether a value parsed from JSON is a `ChangelogEntry`.
4
4
  *
5
5
  * Checks the entry's top level only: `sections` must be an array, and its contents pass through unvalidated.
6
- * `.meta/changelog.json` is written by release-kit and read tolerantly; strict validation applies to the
7
- * human-authored `.meta/changelog-overrides.json`, in `loadChangelogOverrides.ts`.
6
+ * `.meta/changelog.json` is written by release-kit. Its readers skip a rejected entry, but `prepare`, which rewrites
7
+ * the file, refuses one. Strict validation applies to the human-authored `.meta/changelog-overrides.json`, in
8
+ * `loadChangelogOverrides.ts`.
8
9
  */
9
10
  export declare function isChangelogEntry(value: unknown): value is ChangelogEntry;
10
11
  /** Extracts the version from a tag: the text from its first `N.N.N` onward, or the whole tag when it contains none. */
11
12
  export declare function extractVersion(tag: string): string;
13
+ /** Outcome of loading a changelog JSON file, telling a missing file apart from one that cannot be read as entries. */
14
+ export type ChangelogLoadResult = {
15
+ status: 'loaded';
16
+ entries: ChangelogEntry[];
17
+ /** The number of array elements that `isChangelogEntry` rejected, which `entries` leaves out. */
18
+ rejectedCount: number;
19
+ } | {
20
+ status: 'missing';
21
+ } | {
22
+ status: 'unreadable';
23
+ reason: string;
24
+ };
25
+ /** Describes a changelog JSON file that could not be loaded, as a phrase naming the file. */
26
+ export declare function describeChangelogFileProblem(status: 'missing' | 'unreadable', filePath: string): string;
27
+ /**
28
+ * Loads the changelog entries in a JSON file. A file that cannot be read, is not valid JSON, or doesn't contain an
29
+ * array is `unreadable`.
30
+ */
31
+ export declare function loadChangelogEntries(filePath: string): ChangelogLoadResult;
12
32
  /**
13
33
  * Reads the changelog entries in a JSON file; undefined when the file is missing or unreadable, or doesn't contain an
14
34
  * array.