@williamthorsen/release-kit 18.2.0 → 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 (84) hide show
  1. package/.meta/changelog.json +90 -0
  2. package/.readyup/kits/default.js +2 -2
  3. package/.readyup/manifest.json +13 -13
  4. package/CHANGELOG.md +26 -0
  5. package/README.md +11 -12
  6. package/dist/esm/assertCleanWorkingTree.d.ts +2 -1
  7. package/dist/esm/assertCleanWorkingTree.js +4 -8
  8. package/dist/esm/assertReleasesOnNpm.js +2 -9
  9. package/dist/esm/assertTarballsComplete.js +2 -9
  10. package/dist/esm/buildChangelogEntries.d.ts +5 -3
  11. package/dist/esm/buildChangelogEntries.js +9 -4
  12. package/dist/esm/buildChangelogItem.d.ts +1 -1
  13. package/dist/esm/buildDependencyGraph.js +2 -16
  14. package/dist/esm/buildReleasedResult.d.ts +15 -3
  15. package/dist/esm/buildReleasedResult.js +23 -2
  16. package/dist/esm/changelogJsonFile.d.ts +3 -2
  17. package/dist/esm/changelogJsonFile.js +14 -17
  18. package/dist/esm/changelogJsonUtils.d.ts +22 -2
  19. package/dist/esm/changelogJsonUtils.js +19 -10
  20. package/dist/esm/commitCommand.d.ts +2 -1
  21. package/dist/esm/commitCommand.js +8 -11
  22. package/dist/esm/configSchema.d.ts +3 -1
  23. package/dist/esm/configSchema.js +1 -0
  24. package/dist/esm/createGithubRelease.d.ts +13 -10
  25. package/dist/esm/createGithubRelease.js +9 -13
  26. package/dist/esm/createGithubReleaseCommand.d.ts +1 -1
  27. package/dist/esm/createGithubReleaseCommand.js +25 -3
  28. package/dist/esm/createTags.d.ts +2 -1
  29. package/dist/esm/createTags.js +12 -12
  30. package/dist/esm/decideDirectReleases.d.ts +2 -1
  31. package/dist/esm/decideDirectReleases.js +5 -5
  32. package/dist/esm/decideRelease.d.ts +1 -1
  33. package/dist/esm/defaults.d.ts +4 -2
  34. package/dist/esm/defaults.js +1 -0
  35. package/dist/esm/deriveDirectoryConfig.js +5 -4
  36. package/dist/esm/deriveWorkspaceConfig.js +2 -9
  37. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +2 -1
  38. package/dist/esm/detectUndeclaredTagPrefixes.js +4 -13
  39. package/dist/esm/determineBumpType.d.ts +2 -1
  40. package/dist/esm/determineBumpType.js +5 -0
  41. package/dist/esm/enumerateReleaseWindows.d.ts +3 -11
  42. package/dist/esm/enumerateReleaseWindows.js +17 -60
  43. package/dist/esm/findUnroutedEntryScopes.d.ts +1 -1
  44. package/dist/esm/git.d.ts +61 -0
  45. package/dist/esm/git.js +101 -0
  46. package/dist/esm/init/checks.d.ts +2 -1
  47. package/dist/esm/init/checks.js +5 -10
  48. package/dist/esm/init/eligibility.d.ts +12 -0
  49. package/dist/esm/init/eligibility.js +50 -0
  50. package/dist/esm/init/initCommand.d.ts +0 -2
  51. package/dist/esm/init/initCommand.js +4 -45
  52. package/dist/esm/init/templates.js +2 -2
  53. package/dist/esm/init/updateTemplatesCommand.js +4 -31
  54. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +29 -7
  55. package/dist/esm/injectReleaseNotesIntoReadme.js +13 -21
  56. package/dist/esm/loadConfig.js +3 -16
  57. package/dist/esm/mergeConfig.d.ts +2 -1
  58. package/dist/esm/packFromCleanCheckout.js +3 -9
  59. package/dist/esm/planVersionBump.js +2 -16
  60. package/dist/esm/prepareCommand.js +2 -1
  61. package/dist/esm/prepareComposite.js +1 -1
  62. package/dist/esm/prepareReleaseTargets.d.ts +2 -1
  63. package/dist/esm/prepareReleaseTargets.js +3 -2
  64. package/dist/esm/prepareRunContext.d.ts +2 -0
  65. package/dist/esm/prepareTarget.d.ts +6 -4
  66. package/dist/esm/prepareTarget.js +9 -3
  67. package/dist/esm/previewTagPrefixes.d.ts +2 -1
  68. package/dist/esm/previewTagPrefixes.js +16 -21
  69. package/dist/esm/publishCommand.d.ts +1 -1
  70. package/dist/esm/publishCommand.js +27 -2
  71. package/dist/esm/pushRelease.d.ts +2 -1
  72. package/dist/esm/pushRelease.js +11 -11
  73. package/dist/esm/readJsonFile.d.ts +2 -0
  74. package/dist/esm/readJsonFile.js +17 -0
  75. package/dist/esm/reportPrepare.js +68 -114
  76. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  77. package/dist/esm/resolveReleaseTags.js +4 -12
  78. package/dist/esm/types.d.ts +8 -2
  79. package/docs/configuration.md +12 -3
  80. package/docs/labels.md +2 -0
  81. package/docs/releasing.md +3 -2
  82. package/docs/work-types.md +2 -0
  83. package/package.json +3 -3
  84. package/presets/labels/common.yaml +14 -0
@@ -1,4 +1,94 @@
1
1
  [
2
+ {
3
+ "version": "18.3.0",
4
+ "date": "2026-11-14",
5
+ "sections": [
6
+ {
7
+ "title": "🎉 Features",
8
+ "audience": "all",
9
+ "items": [
10
+ {
11
+ "description": "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)",
12
+ "hash": "d060436067879c9934d3dccbbde10c0b34392989",
13
+ "entry": 1
14
+ },
15
+ {
16
+ "description": "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)",
17
+ "hash": "d060436067879c9934d3dccbbde10c0b34392989",
18
+ "entry": 2
19
+ },
20
+ {
21
+ "description": "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)",
22
+ "hash": "64a18b97c8100012dbde3284f4c0c65a2a8ebe4e",
23
+ "entry": 1
24
+ },
25
+ {
26
+ "description": "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)",
27
+ "hash": "64a18b97c8100012dbde3284f4c0c65a2a8ebe4e",
28
+ "entry": 2
29
+ },
30
+ {
31
+ "description": "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)",
32
+ "hash": "998ba6fbaa286eef4960adc5e25523f6b50338d5",
33
+ "entry": 1
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ "title": "🐛 Bug fixes",
39
+ "audience": "all",
40
+ "items": [
41
+ {
42
+ "description": "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)",
43
+ "hash": "4a0c6ee88949cd9a8ff0b91ec6a603caa5a6b692",
44
+ "entry": 1
45
+ },
46
+ {
47
+ "description": "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)",
48
+ "hash": "17acd51ac7f25580a6277b88b1c01bc8fc76ee1d",
49
+ "entry": 1
50
+ },
51
+ {
52
+ "description": "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)",
53
+ "hash": "bbe64c890eb8adff00e9f76aff25fab092fe638c",
54
+ "entry": 1
55
+ },
56
+ {
57
+ "description": "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)",
58
+ "hash": "a7548a236902ff6d2e7968271459e02114de640f",
59
+ "entry": 1
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ "title": "♻️ Refactoring",
65
+ "audience": "dev",
66
+ "items": [
67
+ {
68
+ "description": "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)",
69
+ "hash": "17acd51ac7f25580a6277b88b1c01bc8fc76ee1d",
70
+ "entry": 2
71
+ },
72
+ {
73
+ "description": "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)",
74
+ "hash": "d663fdfe45d03fb9dcadef09f80a32191a531f10",
75
+ "entry": 1
76
+ }
77
+ ]
78
+ },
79
+ {
80
+ "title": "🧪 Tests",
81
+ "audience": "dev",
82
+ "items": [
83
+ {
84
+ "description": "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)",
85
+ "hash": "38e028a3010ee083c1d48f0b22f72a45fc4852a3",
86
+ "entry": 3
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ },
2
92
  {
3
93
  "version": "18.2.0",
4
94
  "date": "2026-11-13",
@@ -805,12 +805,12 @@ ${result.diff ?? ""}` };
805
805
  }
806
806
 
807
807
  // .readyup/lib/workflow-hashes.ts
808
- var COMMON_PRESET_HASH = "945ee6d9f69365b0c8c5e20e9fed5b7ef7eee418fa28bf3602a21a48d293ed03";
808
+ var COMMON_PRESET_HASH = "c39637a3c99735331c46ef0ca29eb5031c02185c53e1cb007ccc50ecefad3fcd";
809
809
  var SYNC_LABELS_WORKFLOW_HASH = "0a88829d154c0de7cdd4246fede99e9311a89ab4199034dbe601663d641ab1e4";
810
810
 
811
811
  // .readyup/kits/default.ts
812
812
  function getMinVersion() {
813
- const picked = { "version": "18.2.0" };
813
+ const picked = { "version": "18.3.0" };
814
814
  if (typeof picked["version"] !== "string") {
815
815
  throw new TypeError("release-kit/package.json: 'version' must be a string");
816
816
  }
@@ -3,12 +3,12 @@
3
3
  "kits": [
4
4
  {
5
5
  "bundledDependencies": {
6
- "@williamthorsen/toolbelt.datetime": "4.1.4",
6
+ "@williamthorsen/toolbelt.datetime": "4.1.5",
7
7
  "@williamthorsen/toolbelt.errors": "0.10.1",
8
- "@williamthorsen/toolbelt.filesystem": "0.10.3",
9
- "@williamthorsen/toolbelt.numbers": "8.0.1",
8
+ "@williamthorsen/toolbelt.filesystem": "0.11.0",
9
+ "@williamthorsen/toolbelt.numbers": "8.0.2",
10
10
  "@williamthorsen/toolbelt.objects": "4.4.3",
11
- "@williamthorsen/toolbelt.packaging": "0.7.3",
11
+ "@williamthorsen/toolbelt.packaging": "0.8.0",
12
12
  "@williamthorsen/toolbelt.terminal": "0.3.1",
13
13
  "ansi-regex": "6.6.0",
14
14
  "ansi-styles": "6.2.3, 7.0.0",
@@ -25,7 +25,7 @@
25
25
  "checklist": "release-kit",
26
26
  "checks": [
27
27
  "@williamthorsen/release-kit in devDependencies",
28
- "@williamthorsen/release-kit >= 18.2.0",
28
+ "@williamthorsen/release-kit >= 18.3.0",
29
29
  "release.yaml workflow exists",
30
30
  "release.yaml matches template",
31
31
  "release.yaml does not reference deprecated tag ref",
@@ -160,12 +160,12 @@
160
160
  "path": "lib/template-checks.ts"
161
161
  },
162
162
  {
163
- "hash": "ec55cfef",
163
+ "hash": "3c6fd66a",
164
164
  "kind": "module",
165
165
  "path": "lib/workflow-hashes.ts"
166
166
  },
167
167
  {
168
- "hash": "ec16f961",
168
+ "hash": "af60087a",
169
169
  "kind": "inline",
170
170
  "path": "../package.json",
171
171
  "paths": [
@@ -203,7 +203,7 @@
203
203
  "path": "../src/init/scaffold.ts"
204
204
  },
205
205
  {
206
- "hash": "e10ddb57",
206
+ "hash": "4d4b66bc",
207
207
  "kind": "module",
208
208
  "path": "../src/init/templates.ts"
209
209
  },
@@ -218,16 +218,16 @@
218
218
  "readyupVersion": "0.46.2",
219
219
  "source": "kits/default.ts",
220
220
  "sourceHash": "ee8d2dc8",
221
- "targetHash": "dfbfab4f"
221
+ "targetHash": "5985226a"
222
222
  },
223
223
  {
224
224
  "bundledDependencies": {
225
- "@williamthorsen/toolbelt.datetime": "4.1.4",
225
+ "@williamthorsen/toolbelt.datetime": "4.1.5",
226
226
  "@williamthorsen/toolbelt.errors": "0.10.1",
227
- "@williamthorsen/toolbelt.filesystem": "0.10.3",
228
- "@williamthorsen/toolbelt.numbers": "8.0.1",
227
+ "@williamthorsen/toolbelt.filesystem": "0.11.0",
228
+ "@williamthorsen/toolbelt.numbers": "8.0.2",
229
229
  "@williamthorsen/toolbelt.objects": "4.4.3",
230
- "@williamthorsen/toolbelt.packaging": "0.7.3",
230
+ "@williamthorsen/toolbelt.packaging": "0.8.0",
231
231
  "@williamthorsen/toolbelt.terminal": "0.3.1",
232
232
  "ansi-regex": "6.6.0",
233
233
  "ansi-styles": "6.2.3, 7.0.0",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
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
+
5
31
  ## 18.2.0 — 2026-11-13
6
32
 
7
33
  ### 🎉 Features
package/README.md CHANGED
@@ -7,23 +7,22 @@ Version-bumping and changelog-generation toolkit for release workflows.
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
9
  <!-- section:release-notes -->
10
- ## Release notes — v18.2.0 (2026-11-13)
10
+ ## Release notes — v18.3.0 (2026-11-14)
11
11
 
12
12
  ### 🎉 Features
13
13
 
14
- - 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)
15
- - 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)
16
- - 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)
17
- - 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)
18
-
19
- ### 🪦 Removed
20
-
21
- - 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)
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)
22
19
 
23
20
  ### 🐛 Bug fixes
24
21
 
25
- - 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)
26
- - 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)
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)
27
26
  <!-- /section:release-notes -->
28
27
 
29
28
  ## Installation
@@ -123,7 +122,7 @@ export default defineConfig({
123
122
  formatCommand: 'npx prettier --write',
124
123
 
125
124
  // Override the default version patterns
126
- versionPatterns: { major: ['!'], minor: ['feat', 'feature'] },
125
+ versionPatterns: { major: ['!'], minor: ['feat', 'feature'], none: ['ai', 'ci', 'docs', 'tests', 'tooling'] },
127
126
 
128
127
  // Add or override work types (merged with defaults by key)
129
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
+ }