@williamthorsen/release-kit 4.8.0 → 5.1.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 (100) hide show
  1. package/CHANGELOG.md +134 -4
  2. package/README.md +404 -40
  3. package/cliff.toml.template +2 -1
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/assertCleanWorkingTree.js +1 -1
  6. package/dist/esm/bin/release-kit.js +45 -14
  7. package/dist/esm/buildChangelogEntries.d.ts +3 -0
  8. package/dist/esm/{generateChangelogJson.js → buildChangelogEntries.js} +40 -77
  9. package/dist/esm/buildDependencyGraph.d.ts +4 -3
  10. package/dist/esm/buildDependencyGraph.js +18 -11
  11. package/dist/esm/buildReleaseSummary.js +12 -4
  12. package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
  13. package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
  14. package/dist/esm/bumpAllVersions.d.ts +1 -0
  15. package/dist/esm/bumpAllVersions.js +16 -2
  16. package/dist/esm/bumpVersion.js +3 -0
  17. package/dist/esm/changelogJsonFile.d.ts +4 -0
  18. package/dist/esm/changelogJsonFile.js +68 -0
  19. package/dist/esm/commitCommand.js +1 -1
  20. package/dist/esm/compareVersions.d.ts +1 -0
  21. package/dist/esm/compareVersions.js +27 -0
  22. package/dist/esm/createGithubRelease.d.ts +6 -2
  23. package/dist/esm/createGithubRelease.js +17 -17
  24. package/dist/esm/createGithubReleaseCommand.d.ts +1 -0
  25. package/dist/esm/createGithubReleaseCommand.js +41 -0
  26. package/dist/esm/decideRelease.d.ts +25 -0
  27. package/dist/esm/decideRelease.js +28 -0
  28. package/dist/esm/defaults.d.ts +1 -0
  29. package/dist/esm/defaults.js +7 -3
  30. package/dist/esm/deriveWorkspaceConfig.d.ts +2 -0
  31. package/dist/esm/deriveWorkspaceConfig.js +37 -0
  32. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +7 -0
  33. package/dist/esm/detectUndeclaredTagPrefixes.js +46 -0
  34. package/dist/esm/generateChangelogs.d.ts +1 -1
  35. package/dist/esm/generateChangelogs.js +14 -3
  36. package/dist/esm/getCommitsSinceTarget.d.ts +1 -1
  37. package/dist/esm/getCommitsSinceTarget.js +8 -4
  38. package/dist/esm/index.d.ts +2 -39
  39. package/dist/esm/index.js +0 -75
  40. package/dist/esm/init/initCommand.js +1 -1
  41. package/dist/esm/init/scaffold.d.ts +1 -1
  42. package/dist/esm/init/scaffold.js +8 -5
  43. package/dist/esm/init/templates.d.ts +1 -0
  44. package/dist/esm/init/templates.js +35 -5
  45. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  46. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  47. package/dist/esm/loadConfig.d.ts +12 -2
  48. package/dist/esm/loadConfig.js +161 -14
  49. package/dist/esm/parseRequestedTags.d.ts +1 -0
  50. package/dist/esm/parseRequestedTags.js +10 -0
  51. package/dist/esm/prepareCommand.d.ts +3 -1
  52. package/dist/esm/prepareCommand.js +121 -31
  53. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  54. package/dist/esm/previewTagPrefixes.js +120 -0
  55. package/dist/esm/propagateBumps.d.ts +1 -0
  56. package/dist/esm/propagateBumps.js +1 -1
  57. package/dist/esm/publish.d.ts +0 -1
  58. package/dist/esm/publish.js +3 -3
  59. package/dist/esm/publishCommand.js +18 -14
  60. package/dist/esm/pushCommand.js +5 -4
  61. package/dist/esm/readCurrentVersion.d.ts +1 -0
  62. package/dist/esm/readCurrentVersion.js +21 -0
  63. package/dist/esm/releasePrepare.d.ts +2 -0
  64. package/dist/esm/releasePrepare.js +140 -54
  65. package/dist/esm/releasePrepareMono.js +312 -143
  66. package/dist/esm/releasePrepareProject.d.ts +9 -0
  67. package/dist/esm/releasePrepareProject.js +109 -0
  68. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  69. package/dist/esm/renderReleaseNotes.js +29 -2
  70. package/dist/esm/reportPrepare.js +146 -73
  71. package/dist/esm/resolveCliffConfigPath.js +1 -1
  72. package/dist/esm/resolveCommandTags.d.ts +1 -1
  73. package/dist/esm/resolveCommandTags.js +17 -13
  74. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  75. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  76. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  77. package/dist/esm/resolveReleaseTags.js +19 -14
  78. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  79. package/dist/esm/showTagPrefixesCommand.js +84 -0
  80. package/dist/esm/sync-labels/initCommand.js +1 -1
  81. package/dist/esm/sync-labels/presets.js +1 -1
  82. package/dist/esm/tagCommand.js +1 -1
  83. package/dist/esm/types.d.ts +77 -19
  84. package/dist/esm/validateConfig.js +205 -36
  85. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
  86. package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
  87. package/dist/esm/version.d.ts +1 -1
  88. package/dist/esm/version.js +1 -1
  89. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  90. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  91. package/package.json +5 -2
  92. package/presets/labels/common.yaml +9 -6
  93. package/schemas/label-map.json +24 -0
  94. package/dist/esm/component.d.ts +0 -2
  95. package/dist/esm/component.js +0 -14
  96. package/dist/esm/findPackageRoot.d.ts +0 -1
  97. package/dist/esm/findPackageRoot.js +0 -17
  98. package/dist/esm/generateChangelogJson.d.ts +0 -7
  99. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  100. package/dist/esm/githubReleaseCommand.js +0 -35
package/README.md CHANGED
@@ -4,6 +4,52 @@ Version-bumping and changelog-generation toolkit for release workflows.
4
4
 
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 with `git-cliff`.
6
6
 
7
+ <!-- section:release-notes -->
8
+ ## Release notes — v5.1.0 (2026-04-30)
9
+
10
+ ### Bug fixes
11
+
12
+ - Make publish's clean-tree safety gate reachable (#311)
13
+
14
+ Fixes an issue where `release-kit publish` failed with pnpm's "working tree is dirty" error on a clean tree whenever `releaseNotes.shouldInjectIntoReadme: true` was configured. release-kit injects the release notes into the package README before invoking `pnpm publish`, so pnpm's own working-tree check fired on a tree release-kit had just dirtied — even though the user's tree was clean at command start.
15
+
16
+ - Make `--set-version` + `project` rejection explicit (#319)
17
+
18
+ Improves the error users see when invoking `release-kit prepare --set-version` with a `project` block configured. The combination is still rejected — as before — but now produces a single, project-aware message ("--set-version cannot be combined with a project release…") rather than the previous two-step chain (`--set-version requires --only`, then `--only cannot be combined with a project release` after the user added `--only`).
19
+
20
+ - Reject `--only` that would strand excluded dependents (#321)
21
+
22
+ Fixes a silent footgun in `release-kit prepare --only=...`: an excluded internal dependent with its own changes would be left unreleased with no runtime signal, even though the targeted workspace it depends on was being released. The command now rejects such invocations up front, naming every excluded dependent whose changes would be stranded.
23
+
24
+ - Order prerelease versions correctly in changelog sort (#334)
25
+
26
+ Fixes a latent issue in `@williamthorsen/release-kit` where prerelease version tags (e.g., `1.2.3-alpha`, `1.2.3-rc.1`) were sorted as if their prerelease component were absent, causing them to appear in the wrong position relative to releases sharing the same base version. Changelog entries are now ordered per SemVer §11: prerelease versions precede the corresponding release (`1.2.3-alpha < 1.2.3`), build metadata is ignored for ordering, and entries that fail SemVer validation sort deterministically to the bottom of the list rather than collapsing into mid-list positions.
27
+
28
+ ### Features
29
+
30
+ - Add `project` block for project-level release stage (#317)
31
+
32
+ Adds support for monorepos that ship a single combined deliverable to version, changelog, and release-note the project itself rather than only its constituent workspaces.
33
+
34
+ - Publish JSON Schema for `.meta/label-map.json` (#325)
35
+
36
+ Adds a JSON Schema for `.meta/label-map.json` to release-kit, packaged at `packages/release-kit/schemas/label-map.json` and shipped to npm. Consumers reference it via the stable raw URL pattern `https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json` — the same shape audit-deps already uses.
37
+
38
+ - Label prepare errors with the failing stage (#326)
39
+
40
+ Adds stage attribution to errors thrown during `release-kit prepare`. Errors from per-workspace bumps and changelog generation, the project release stage, and the post-release format command now begin with a stage label that identifies the failing stage and (where relevant) the affected workspace.
41
+
42
+ - Make `--force` and `--bump` orthogonal (#328)
43
+
44
+ Decouples `--force` and `--bump` so each flag has a single responsibility, and unifies skip semantics across the per-workspace and project pipelines.
45
+
46
+ ### Documentation
47
+
48
+ - Document tag prefix collisions as general rule (#320)
49
+
50
+ Documents the strict-prefix tag-prefix collision rule as a general validation rule that applies to every release-kit consumer declaring more than one tag prefix: across active workspaces, declared legacy identities, retired packages, and the optional `project` block. Previously, the rule appeared only inside the `Project releases` validation list.
51
+ <!-- /section:release-notes -->
52
+
7
53
  ## Installation
8
54
 
9
55
  ```bash
@@ -54,9 +100,9 @@ That's it for most repos. The CLI auto-discovers workspaces and applies sensible
54
100
 
55
101
  ## How it works
56
102
 
57
- 1. **Workspace discovery**: reads `pnpm-workspace.yaml` and resolves its `packages` globs to find workspace directories. Each directory containing a `package.json` becomes a component. If no workspace file is found, the repo is treated as a single-package project.
103
+ 1. **Workspace discovery**: reads `pnpm-workspace.yaml` and resolves its `packages` globs to find workspace directories. Each directory containing a `package.json` becomes a workspace. If no workspace file is found, the repo is treated as a single-package project.
58
104
  2. **Config loading**: loads `.config/release-kit.config.ts` (if present) via [jiti](https://github.com/unjs/jiti) and merges it with discovered defaults.
59
- 3. **Commit analysis**: for each component, finds commits since the last version tag, parses them for type and scope, and determines the appropriate version bump.
105
+ 3. **Commit analysis**: for each workspace, finds commits since the last version tag, parses them for type and scope, and determines the appropriate version bump.
60
106
  4. **Version bump + changelog**: bumps `package.json` versions and generates changelogs via `git-cliff`.
61
107
  5. **Release tags file**: writes computed tags to `tmp/.release-tags` for the release workflow to read when tagging and pushing.
62
108
 
@@ -73,7 +119,7 @@ scope|type!: description # scoped breaking change
73
119
  type(scope)!: description # conventional scoped breaking change
74
120
  ```
75
121
 
76
- The `scope|type:` format scopes a commit to a specific component in a monorepo. Use `scopeAliases` in your config to map shorthand names to canonical scope names.
122
+ The `scope|type:` format scopes a commit to a specific workspace in a monorepo. Use `scopeAliases` in your config to map shorthand names to canonical scope names.
77
123
 
78
124
  ## Configuration
79
125
 
@@ -85,8 +131,8 @@ Configuration is optional. The CLI works out of the box by auto-discovering work
85
131
  import type { ReleaseKitConfig } from '@williamthorsen/release-kit';
86
132
 
87
133
  const config: ReleaseKitConfig = {
88
- // Exclude a component from release processing
89
- components: [{ dir: 'internal-tools', shouldExclude: true }],
134
+ // Exclude a workspace from release processing
135
+ workspaces: [{ dir: 'internal-tools', shouldExclude: true }],
90
136
 
91
137
  // Run a formatter after changelog generation (modified file paths are appended as arguments)
92
138
  formatCommand: 'npx prettier --write',
@@ -108,23 +154,152 @@ The config file supports both `export default config` and `export const config =
108
154
  | Field | Type | Description |
109
155
  | ----------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
110
156
  | `cliffConfigPath` | `string` | Explicit path to cliff config. If omitted, resolved automatically: `.config/git-cliff.toml` → `cliff.toml` → bundled template |
111
- | `components` | `ComponentOverride[]` | Override or exclude discovered components (matched by `dir`) |
157
+ | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
112
158
  | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
113
159
  | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
114
160
  | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commits |
115
161
  | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
162
+ | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
163
+ | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
116
164
 
117
165
  All fields are optional.
118
166
 
119
- ### `ComponentOverride`
167
+ ### `WorkspaceOverride`
120
168
 
121
169
  ```typescript
122
- interface ComponentOverride {
170
+ interface WorkspaceOverride {
123
171
  dir: string; // Package directory name (e.g., 'arrays')
124
172
  shouldExclude?: boolean; // If true, exclude from release processing
173
+ legacyIdentities?: LegacyIdentity[]; // Prior `(name, tagPrefix)` identities for this workspace
174
+ }
175
+
176
+ interface LegacyIdentity {
177
+ name: string; // Full scoped npm name at the time (e.g., '@scope/pkg')
178
+ tagPrefix: string; // Tag prefix under which historical tags were published (e.g., 'core-v')
179
+ }
180
+ ```
181
+
182
+ `legacyIdentities` captures prior identities of a workspace as complete `(name, tagPrefix)` snapshots. The union of the current `tagPrefix` and each identity's `tagPrefix` is consulted when release-kit searches for the most recent baseline tag and when generating changelogs. Use it when a workspace's historical tags were published under a different npm name, a different tag prefix, or both — typically across a package rename. Both fields are required per identity: each entry must be a complete historical snapshot that stays valid regardless of subsequent renames. Run `release-kit show-tag-prefixes` to detect undeclared candidates and produce a paste-ready config snippet. Listing the current identity (full `(name, tagPrefix)` match) is rejected as a no-op duplicate; an identity whose `tagPrefix` matches the current but whose `name` differs is valid and documents a prior rename that reused the same tag shape. If the workspace no longer exists in this repo at all (the package was extracted or removed), use [`retiredPackages`](#retiredpackage) instead.
183
+
184
+ ### `RetiredPackage`
185
+
186
+ ```typescript
187
+ interface RetiredPackage {
188
+ name: string; // Final scoped npm name while the package lived in this repo
189
+ tagPrefix: string; // Tag prefix under which the package's historical tags were published
190
+ successor?: string; // Optional successor package name (e.g., 'readyup')
191
+ }
192
+ ```
193
+
194
+ `retiredPackages` is the repo-level complement to `legacyIdentities`. Use `legacyIdentities` when the workspace still exists in this repo under a new identity; use `retiredPackages` when no workspace for this package exists in this repo anymore — the package was extracted to another repo or removed outright. Retired entries are inert: release-kit never consults them for baseline lookup or changelog attribution. Their declared `tagPrefix` values are recognized as historical, so `show-tag-prefixes` stops flagging them under "Undeclared tag prefixes."
195
+
196
+ Worked example — `preflight` was extracted from this monorepo and continues as the standalone `readyup` project. Its tags stay in this repo as historical anchors:
197
+
198
+ ```typescript
199
+ import type { ReleaseKitConfig } from '@williamthorsen/release-kit';
200
+
201
+ const config: ReleaseKitConfig = {
202
+ retiredPackages: [{ name: '@scope/preflight', tagPrefix: 'preflight-v', successor: 'readyup' }],
203
+ };
204
+
205
+ export default config;
206
+ ```
207
+
208
+ Validation rules:
209
+
210
+ - `name` and `tagPrefix` are required per entry and must be non-empty strings.
211
+ - `successor` is optional; if present, it must be a non-empty string.
212
+ - Full-tuple `(name, tagPrefix)` duplicates within `retiredPackages` are rejected.
213
+ - Two entries sharing the same `tagPrefix` but different `name`s are accepted — this documents a package renamed within the repo before being retired.
214
+
215
+ `show-tag-prefixes` currently does not render a dedicated "Retired packages" section (deferred). Declaring a retired entry is verifiable by confirming that its `tagPrefix` stops appearing under "Undeclared tag prefixes" in the `show-tag-prefixes` output.
216
+
217
+ ### Tag prefix collisions
218
+
219
+ Tag prefixes from distinct owners must not be identical or be a strict prefix of one another. An owner is one of:
220
+
221
+ - An active workspace, comprising its derived `tagPrefix` plus any declared `legacyIdentities[].tagPrefix`. Identities of the same workspace are one owner, so their prefixes are allowed to overlap (this represents the same package across renames).
222
+ - A `retiredPackages[]` entry (one owner per entry).
223
+ - The `project` block, when configured.
224
+
225
+ release-kit resolves baseline tags via `git describe --match=<prefix>*`, so a strict-prefix overlap between distinct owners would cause that glob to return cross-matches against the wrong owner's history. For example, a project prefix of `v` collides with a workspace prefix of `vue-helpers-v`, since `git describe --match=v*` would return both project tags and `vue-helpers` tags.
226
+
227
+ The rule is enforced at config load; the resulting error identifies both colliding declarations.
228
+
229
+ ### Project releases
230
+
231
+ Some monorepos ship a single combined deliverable — a Chrome extension, a CLI binary, a packaged desktop app — for which the per-workspace tags and changelogs alone do not describe what the user actually receives. Declare the optional `project` block to add a project-level release stage that runs alongside the per-workspace pipeline.
232
+
233
+ ```typescript
234
+ import type { ReleaseKitConfig } from '@williamthorsen/release-kit';
235
+
236
+ const config: ReleaseKitConfig = {
237
+ // Empty object is enough to opt in. Every non-excluded workspace contributes.
238
+ project: {},
239
+ };
240
+
241
+ export default config;
242
+ ```
243
+
244
+ When configured, each `release-kit prepare` run additionally:
245
+
246
+ - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to the union of every contributing workspace's paths.
247
+ - Bumps the root `package.json`'s `version` field using the same bump-derivation rules as workspaces (or the `--bump=...` override).
248
+ - Regenerates the root `./CHANGELOG.md` via `git-cliff`, scoped to the project's `tagPrefix` and contributing paths.
249
+ - Emits `./.meta/changelog.json` (when `changelogJson.enabled`).
250
+ - With `--with-release-notes`, additionally emits `./docs/RELEASE_NOTES.v<version>.md`.
251
+ - Appends the project tag to `tmp/.release-tags` so `release-kit commit` and `release-kit tag` pick it up alongside per-workspace tags.
252
+
253
+ If no contributing workspace has commits since the last project tag, the project release is silently skipped — same behavior as a per-workspace skip.
254
+
255
+ #### `ProjectConfig`
256
+
257
+ ```typescript
258
+ interface ProjectConfig {
259
+ tagPrefix?: string; // Defaults to 'v'
125
260
  }
126
261
  ```
127
262
 
263
+ | Field | Default | Description |
264
+ | ----------- | ------- | -------------------------------------------------------------------- |
265
+ | `tagPrefix` | `'v'` | Prefix for project tags. The full tag is `${tagPrefix}${newVersion}` |
266
+
267
+ Contributing workspaces are implicit: every non-excluded discovered workspace contributes. There is no field to override the contributing set in this initial release; if a future consumer needs to release a workspace as a component but exclude it from the project release, that override can be added then.
268
+
269
+ Validation rules:
270
+
271
+ - The root `package.json` must exist and declare a `version` field. release-kit reports an error at config-load time if either is missing.
272
+ - The `project` block is rejected in single-package mode (the implicit "all non-excluded workspaces contribute" rule is meaningless in a single-package repo).
273
+ - Unknown fields inside `project` are rejected.
274
+
275
+ CLI flag interactions:
276
+
277
+ - `--dry-run` previews project artifacts alongside workspace artifacts; no files are written.
278
+ - `--bump=major|minor|patch` propagates to the project release as a level chooser. It does not trigger a release on its own when there are no commits or no bump-worthy commits.
279
+ - `--force` runs the project release even when no commits or no bump-worthy commits exist since the last project tag. Defaults to patch when `--bump` is not given; combine with `--bump=X` to release at a different level.
280
+ - `--only` is rejected with an error when `project` is configured. `--only` is a surgical, single-workspace operation; combining it with a project release that rolls up every contributing workspace would create ambiguous semantics. To release a single workspace, use a config without a `project` block, or run a full `prepare` (no `--only`) to include the project release.
281
+ - `--set-version` is rejected with an error when `project` is configured. `--set-version` operates on a single workspace, but a project release rolls up every contributing workspace; the two semantics don't compose. To use `--set-version`, run on a config without a `project` block.
282
+
283
+ `--bump` and `--force` are orthogonal: `--bump` is purely a level chooser; `--force` is purely a release trigger. Examples:
284
+
285
+ ```sh
286
+ # Release every target at its natural bump level (no flags).
287
+ release-kit prepare
288
+
289
+ # Force a release even when no bump-worthy commits exist; defaults to patch
290
+ # per target, with each target keeping its natural bump if one is derivable.
291
+ release-kit prepare --force
292
+
293
+ # Force a release at a uniform level across every releasing target.
294
+ release-kit prepare --force --bump=minor
295
+
296
+ # --bump=X alone is a level chooser, NOT a trigger. If a target has no
297
+ # bump-worthy commits, it skips with a "Pass --force..." reason. If it has
298
+ # bump-worthy commits, the override applies. (Behavioral change from earlier
299
+ # release-kit versions, where --bump=X alone would force a release.)
300
+ release-kit prepare --bump=minor
301
+ ```
302
+
128
303
  ### `VersionPatterns`
129
304
 
130
305
  Defines which commit types trigger major or minor bumps. Any recognized type not listed defaults to a patch bump.
@@ -140,21 +315,29 @@ Default: `{ major: ['!'], minor: ['feat'] }`
140
315
 
141
316
  ### Default work types
142
317
 
143
- | Key | Header | Aliases |
144
- | ---------- | ------------- | --------- |
145
- | `fix` | Bug fixes | `bugfix` |
146
- | `feat` | Features | `feature` |
147
- | `internal` | Internal | |
148
- | `refactor` | Refactoring | |
149
- | `tests` | Tests | `test` |
150
- | `tooling` | Tooling | |
151
- | `ci` | CI | |
152
- | `deps` | Dependencies | `dep` |
153
- | `docs` | Documentation | `doc` |
154
- | `fmt` | Formatting | |
318
+ | Key | Header | Aliases |
319
+ | ----------- | --------------- | ------------- |
320
+ | `fix` | Bug fixes | `bugfix` |
321
+ | `deprecate` | Deprecated | |
322
+ | `feat` | Features | `feature` |
323
+ | `internal` | Internal | |
324
+ | `perf` | Performance | `performance` |
325
+ | `refactor` | Refactoring | |
326
+ | `sec` | Security | `security` |
327
+ | `tests` | Tests | `test` |
328
+ | `tooling` | Tooling | |
329
+ | `ci` | CI | |
330
+ | `deps` | Dependencies | `dep` |
331
+ | `docs` | Documentation | `doc` |
332
+ | `ai` | Agentic support | |
333
+ | `fmt` | (skipped) | |
155
334
 
156
335
  Work types from your config are merged with these defaults by key — your entries override or extend, they don't replace the full set.
157
336
 
337
+ `fmt:` commits are recognized for version-bump determination (they contribute to a patch bump) but are skipped by the bundled `cliff.toml.template`, so they do not appear in `CHANGELOG.md` or release notes.
338
+
339
+ Release-notes sections are rendered in the declaration order of the merged work-types record, with any unknown titles trailing the known ones. The default `devOnlySections` (excluded from public release notes but still written to `CHANGELOG.md`) are: `Agentic support`, `CI`, `Dependencies`, `Internal`, `Refactoring`, `Tests`, `Tooling`. Override via `changelogJson.devOnlySections` in your config.
340
+
158
341
  ## CLI reference
159
342
 
160
343
  ### Global options
@@ -168,15 +351,82 @@ Work types from your config are merged with these defaults by key — your entri
168
351
 
169
352
  Run release preparation with automatic workspace discovery.
170
353
 
171
- | Flag | Description |
172
- | ---------------------------- | ---------------------------------------------------------------- |
173
- | `--dry-run` | Preview changes without writing files |
174
- | `--bump=major\|minor\|patch` | Override the bump type for all components |
175
- | `--force` | Bypass the "no commits since last tag" check (requires `--bump`) |
176
- | `--only=name1,name2` | Only process the named components (monorepo only) |
177
- | `--help`, `-h` | Show help |
354
+ | Flag | Description |
355
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
356
+ | `--dry-run` | Preview changes without writing files |
357
+ | `--bump=major\|minor\|patch` | Override the bump type for all workspaces |
358
+ | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode. |
359
+ | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag (defaults to patch; combine with `--bump=X` for a different level) |
360
+ | `--only=name1,name2` | Only process the named workspaces (monorepo only; rejected when a `project` block is configured) |
361
+ | `--with-release-notes` | Write per-workspace release-notes previews under `{workspacePath}/docs/` |
362
+ | `--help`, `-h` | Show help |
363
+
364
+ Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
365
+
366
+ #### Previewing release notes with `--with-release-notes`
367
+
368
+ `--with-release-notes` writes two versioned files per workspace after each workspace's `changelog.json` is produced:
369
+
370
+ - `{workspacePath}/docs/README.v{version}.md` — the workspace `README.md` with release notes injected at the `<!-- section:release-notes -->` marker.
371
+ - `{workspacePath}/docs/RELEASE_NOTES.v{version}.md` — the standalone release notes for this version.
372
+
373
+ 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.
374
+
375
+ Because preview filenames are versioned, committing them will accumulate files over time. The recommended `.gitignore` entry for monorepos is:
376
+
377
+ ```gitignore
378
+ packages/*/docs/*.v*.md
379
+ ```
380
+
381
+ For single-package repos:
382
+
383
+ ```gitignore
384
+ docs/*.v*.md
385
+ ```
386
+
387
+ #### Setting an explicit version with `--set-version`
388
+
389
+ The `--set-version` flag is a first-class escape hatch for the cases where commit-derived bump logic produces the wrong version — most notably, promoting a pre-1.0 package to 1.0.0. Pre-1.0 packages collapse a `feat!` breaking change to a minor bump (matching semantic-release's `initialMajor: false` and release-please's `bump-minor-pre-major`), so a deliberate promotion to 1.0.0 must be requested explicitly.
390
+
391
+ The flag validates that:
392
+
393
+ - The value is canonical `N.N.N` semver (pre-release suffixes are rejected).
394
+ - The target is strictly greater than the current version (numeric comparison on each component).
395
+ - In monorepo mode, `--only` is set and resolves to exactly one workspace.
396
+
397
+ `--set-version` is mutually exclusive with `--bump` and `--force`. The rest of the pipeline (changelog generation, tag creation, commit summary, propagation to dependents) runs unchanged, so dependents receive a propagated patch bump triggered by the overridden version.
398
+
399
+ Promoting a pre-1.0 package to 1.0.0 in a monorepo:
178
400
 
179
- Component names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
401
+ ```sh
402
+ release-kit prepare --only arrays --set-version 1.0.0
403
+ ```
404
+
405
+ An empty changelog section is expected for a bare promotion, because the changelog is generated from commits since the last tag. To include a narrative entry, land a descriptive release commit (e.g., a `feat!` describing the stable API) before running `prepare`.
406
+
407
+ ### `release-kit create-github-release`
408
+
409
+ Create GitHub Releases from `changelog.json` for tags on HEAD. Independent of `npm publish`: invoking this command creates Releases regardless of whether the matching package was published.
410
+
411
+ | Flag | Description |
412
+ | ---------------------- | ------------------------------------------------------------------------- |
413
+ | `--dry-run` | Preview without creating releases |
414
+ | `--tags=tag1,tag2,...` | Only create releases for the named tags (comma-separated, full tag names) |
415
+ | `--help`, `-h` | Show help |
416
+
417
+ When `--tags` is omitted, every release tag pointing at HEAD is processed. The CLI requires the `gh` CLI on `PATH` and `contents: write` permission. The bundled `create-github-release.reusable.yaml` GitHub Actions workflow runs this command in CI.
418
+
419
+ ### `release-kit show-tag-prefixes`
420
+
421
+ Print a per-workspace table of derived tag prefixes, tag counts, and declared legacy prefixes. Also surfaces any release-shaped tag prefix in the repo that is neither a derived prefix nor declared via `legacyIdentities`, along with a copy-pasteable `workspaces: [...]` config snippet. The snippet uses a `TODO-fill-in-legacy-npm-name` placeholder for each identity's `name`; replace it with the package's prior npm name before pasting.
422
+
423
+ | Flag | Description |
424
+ | -------------- | ----------- |
425
+ | `--help`, `-h` | Show help |
426
+
427
+ Exits `0` when every workspace derives a prefix and there are no cross-workspace collisions; exits `1` on any derivation failure or collision. Undeclared candidates do not affect the exit code — they surface as a warning via the `legacy tag prefixes are declared` readyup check.
428
+
429
+ In single-package mode, prints a single row with `workspacePath = .` and `derivedPrefix = v`; legacy entries and undeclared-candidate scanning are not applicable.
180
430
 
181
431
  ### `release-kit init`
182
432
 
@@ -191,6 +441,8 @@ Initialize release-kit in the current repository. By default, scaffolds only the
191
441
 
192
442
  Scaffolded files:
193
443
 
444
+ - `.github/workflows/create-github-release.yaml` — workflow that creates a GitHub Release on tag push, independent of npm publish
445
+ - `.github/workflows/publish.yaml` — workflow that delegates to a reusable publish workflow
194
446
  - `.github/workflows/release.yaml` — workflow that delegates to a reusable release workflow
195
447
  - `.config/release-kit.config.ts` — starter config with commented-out customization examples (with `--with-config`)
196
448
  - `.config/git-cliff.toml` — copied from the bundled template (with `--with-config`)
@@ -207,13 +459,33 @@ Manage GitHub label definitions via config-driven YAML files.
207
459
 
208
460
  `init` scaffolds `.config/sync-labels.config.ts` with auto-detected workspace scope labels and a `.github/workflows/sync-labels.yaml` caller workflow, then generates `.github/labels.yaml`. `generate` reads the config and writes `.github/labels.yaml`. `sync` triggers the workflow remotely — it requires the `gh` CLI and an existing workflow file.
209
461
 
462
+ #### Published JSON Schema for `.meta/label-map.json`
463
+
464
+ release-kit publishes a JSON Schema for `.meta/label-map.json` — a separate, generic data file that maps commit-prefix scopes and types to GitHub label names. The schema lives at `packages/release-kit/schemas/label-map.json` in this repo and is reachable via the stable raw URL:
465
+
466
+ ```
467
+ https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json
468
+ ```
469
+
470
+ Consumers reference it from the top of their `.meta/label-map.json`:
471
+
472
+ ```json
473
+ {
474
+ "$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json",
475
+ "types": { "feat": "feature", "fix": "fix" },
476
+ "scopes": { "audit": "scope:audit" }
477
+ }
478
+ ```
479
+
480
+ release-kit publishes the schema only; it does not generate `.meta/label-map.json`. Generation requires commit-prefix knowledge that lives outside release-kit (in agent-conventions tooling), and is owned by those consumers.
481
+
210
482
  ## GitHub Actions workflow
211
483
 
212
484
  The `init` command scaffolds a release workflow at `.github/workflows/release.yaml` that delegates to a reusable release workflow. The scaffolded workflow accepts these inputs:
213
485
 
214
486
  | Input | Type | Description |
215
487
  | ------ | ------ | ------------------------------------------------------------------- |
216
- | `only` | string | Components to release (comma-separated, leave empty for all) |
488
+ | `only` | string | Workspaces to release (comma-separated, leave empty for all) |
217
489
  | `bump` | choice | Override bump type: `patch`, `minor`, `major` (empty = auto-detect) |
218
490
 
219
491
  For repos that need a self-contained workflow instead of the reusable one, the scaffolded file can be expanded. The key steps are: checkout with full history (`fetch-depth: 0`), run `release-kit prepare` with optional `--only` and `--bump` flags, check for changes, read tags from `tmp/.release-tags`, then commit, tag, and push.
@@ -221,10 +493,10 @@ For repos that need a self-contained workflow instead of the reusable one, the s
221
493
  ### Triggering a release
222
494
 
223
495
  ```sh
224
- # All components
496
+ # All workspaces
225
497
  gh workflow run release.yaml
226
498
 
227
- # Specific component(s)
499
+ # Specific workspace(s)
228
500
  gh workflow run release.yaml -f only=arrays
229
501
  gh workflow run release.yaml -f only=arrays,strings -f bump=minor
230
502
  ```
@@ -257,25 +529,57 @@ This package shells out to two external tools:
257
529
  - **`git`** — must be available on `PATH`. Used to find tags and retrieve commit history.
258
530
  - **`git-cliff`** — automatically downloaded and cached via `npx` on first invocation. No need to install it as a dev dependency.
259
531
 
260
- ## Using `component()` for manual configuration
532
+ ## Upgrading from v4 to v5
533
+
534
+ Release-kit v5 derives each workspace's tag prefix from its unscoped `package.json` `name`, so a package at `packages/core` with `"name": "@scope/nmr-core"` uses tags like `nmr-core-v1.3.0`. Repos that previously tagged under the directory basename (e.g., `core-v1.3.0`) do not need to rewrite history — declare the prior identity in `legacyIdentities` so release-kit recognizes historical tags under both the new and old prefixes.
535
+
536
+ Minimal worked example for a repo whose pre-v5 tags were `core-v0.2.7` and whose npm name has not changed:
537
+
538
+ ```typescript
539
+ // .config/release-kit.config.ts
540
+ import type { ReleaseKitConfig } from '@williamthorsen/release-kit';
541
+
542
+ const config: ReleaseKitConfig = {
543
+ workspaces: [
544
+ {
545
+ dir: 'core',
546
+ legacyIdentities: [{ name: '@scope/nmr-core', tagPrefix: 'core-v' }],
547
+ },
548
+ ],
549
+ };
550
+
551
+ export default config;
552
+ ```
553
+
554
+ Each `legacyIdentity` is a complete `(name, tagPrefix)` snapshot of the workspace at an earlier point. In the common case — the tag-derivation rule changed but the npm name did not — the identity's `name` equals the current `name`. If an earlier publish used a different npm name, use that prior name here.
555
+
556
+ Verify with `release-kit show-tag-prefixes` — it prints the derived prefix per workspace, tag counts under each declared legacy prefix, and any undeclared release-shaped prefixes it finds in the repo (with a copy-pasteable config snippet that includes a `TODO-fill-in-legacy-npm-name` placeholder to replace with the prior npm name). After declaring, `release-kit prepare` consults the union of the current and legacy tag prefixes when searching for the most recent baseline tag, and changelog generation matches tags under either prefix.
261
557
 
262
- If you need to build a `MonorepoReleaseConfig` manually (e.g., for the legacy script-based approach), the exported `component()` helper creates a `ComponentConfig` from a workspace-relative path:
558
+ See the `legacyIdentities` entry in the [`WorkspaceOverride`](#workspaceoverride) section for the config shape.
559
+
560
+ ## Using `deriveWorkspaceConfig()` for manual configuration
561
+
562
+ If you need to build a `MonorepoReleaseConfig` manually (e.g., for the legacy script-based approach), the exported `deriveWorkspaceConfig()` helper creates a `WorkspaceConfig` from a workspace-relative path. It reads the workspace's `package.json` to derive the tag prefix from the package name:
263
563
 
264
564
  ```typescript
265
- import { component } from '@williamthorsen/release-kit';
565
+ import { deriveWorkspaceConfig } from '@williamthorsen/release-kit';
266
566
 
267
- // Accepts the full workspace-relative path
268
- component('packages/arrays');
567
+ // packages/arrays/package.json contains `"name": "@scope/arrays"`
568
+ deriveWorkspaceConfig('packages/arrays');
269
569
  // => {
270
570
  // dir: 'arrays',
571
+ // name: '@scope/arrays',
271
572
  // tagPrefix: 'arrays-v',
573
+ // workspacePath: 'packages/arrays',
272
574
  // packageFiles: ['packages/arrays/package.json'],
273
575
  // changelogPaths: ['packages/arrays'],
274
576
  // paths: ['packages/arrays/**'],
275
577
  // }
276
578
  ```
277
579
 
278
- The `dir` field is derived from `path.basename()`, so `packages/arrays` and `libs/arrays` both produce `dir: 'arrays'`. The `tagPrefix` is always `${dir}-v` it cannot be customized.
580
+ `dir` is the basename of the workspace path and is the stable internal identifier used by `--only`, `WorkspaceOverride.dir`, and the dependency graph. `tagPrefix` is derived from the unscoped `package.json` `name` any leading `@scope/` is stripped — so tags reflect the package identity rather than the directory layout. For example, a workspace at `packages/core` with `"name": "@williamthorsen/nmr-core"` produces `tagPrefix: 'nmr-core-v'`, yielding tags like `nmr-core-v1.3.0`.
581
+
582
+ The workspace's `package.json` must declare a non-empty `name` field; `deriveWorkspaceConfig()` throws otherwise. If two workspaces produce the same `tagPrefix` (because their unscoped names collide), `mergeMonorepoConfig()` throws and names the colliding workspaces so you can rename one.
279
583
 
280
584
  ## Legacy script-based approach
281
585
 
@@ -284,10 +588,10 @@ The CLI-driven approach is recommended for new setups. The script-based approach
284
588
  ```typescript
285
589
  // .github/scripts/release.config.ts
286
590
  import type { MonorepoReleaseConfig } from '@williamthorsen/release-kit';
287
- import { component } from '@williamthorsen/release-kit';
591
+ import { deriveWorkspaceConfig } from '@williamthorsen/release-kit';
288
592
 
289
593
  export const config: MonorepoReleaseConfig = {
290
- components: [component('packages/arrays'), component('packages/strings')],
594
+ workspaces: [deriveWorkspaceConfig('packages/arrays'), deriveWorkspaceConfig('packages/strings')],
291
595
  formatCommand: 'npx prettier --write',
292
596
  };
293
597
  ```
@@ -300,10 +604,70 @@ import { config } from './release.config.ts';
300
604
  runReleasePrepare(config);
301
605
  ```
302
606
 
303
- The key difference: the script-based approach requires manually listing every component, while the CLI auto-discovers them from `pnpm-workspace.yaml`.
607
+ The key difference: the script-based approach requires manually listing every workspace, while the CLI auto-discovers them from `pnpm-workspace.yaml`.
304
608
 
305
609
  ## Breaking changes
306
610
 
611
+ ### `resolveReleaseTags` takes workspaces; `WorkspaceConfig` requires `workspacePath`
612
+
613
+ Tag resolution is now driven by workspace records rather than a caller-supplied directory map, so `resolveReleaseTags` can report both the workspace `dir` and its `workspacePath` for every resolved tag.
614
+
615
+ - `resolveReleaseTags` signature changed from `(workspaceMap?: Map<string, string>)` to `(workspaces?: readonly WorkspaceConfig[])`.
616
+ - `WorkspaceConfig` gained a required `workspacePath: string` field.
617
+
618
+ Replace direct `Map`-based calls with `deriveWorkspaceConfig()`, which now populates `workspacePath` for you:
619
+
620
+ ```diff
621
+ -import { resolveReleaseTags } from '@williamthorsen/release-kit';
622
+ -
623
+ -const workspaceMap = new Map([['core', 'packages/core']]);
624
+ -resolveReleaseTags(workspaceMap);
625
+ +import { deriveWorkspaceConfig, resolveReleaseTags } from '@williamthorsen/release-kit';
626
+ +
627
+ +resolveReleaseTags([deriveWorkspaceConfig('packages/core')]);
628
+ ```
629
+
630
+ If you construct `WorkspaceConfig` objects directly, add `workspacePath` alongside the other required fields.
631
+
632
+ ### `release-kit publish` and `release-kit push` replace `--only` with `--tags`
633
+
634
+ The `--only=<dir>` flag on `release-kit publish` and `release-kit push` has been removed. Both commands now filter by full tag name via `--tags=<tag1>[,<tag2>...]`, matching `release-kit create-github-release`. Passing `--only=...` after upgrading produces an `Unknown option: --only` error.
635
+
636
+ Local usage mapping:
637
+
638
+ ```diff
639
+ -release-kit publish --only=core
640
+ +release-kit publish --tags=core-v1.3.0
641
+
642
+ -release-kit push --only=core,cli
643
+ +release-kit push --tags=core-v1.3.0,cli-v0.5.0
644
+ ```
645
+
646
+ Omitting `--tags` preserves the previous behavior of operating on every release tag at HEAD. The reusable workflow `publish.reusable.yaml` also accepts an optional `tags:` input, and the scaffolded `publish.yaml` now passes `tags: ${{ github.ref_name }}` so the publish scope is explicit rather than relying on `actions/checkout@v6`'s fetch default. Existing callers that do not set `tags:` continue to work unchanged.
647
+
648
+ ### GitHub Release creation moved to its own command and workflow
649
+
650
+ `release-kit publish` no longer creates GitHub Releases as a side effect, and the `releaseNotes.shouldCreateGithubRelease` config field has been removed. Adoption is now signaled by installing the dedicated `create-github-release.reusable.yaml` workflow.
651
+
652
+ If you previously set the field, remove it from `.config/release-kit.config.ts`. The new caller template (scaffolded by `release-kit init`) looks like this:
653
+
654
+ ```yaml
655
+ name: Create GitHub Release
656
+ on:
657
+ push:
658
+ tags:
659
+ - '*-v[0-9]*.[0-9]*.[0-9]*'
660
+ permissions:
661
+ contents: write
662
+ jobs:
663
+ create-github-release:
664
+ uses: williamthorsen/node-monorepo-tools/.github/workflows/create-github-release.reusable.yaml@workflow/create-github-release-v1
665
+ with:
666
+ tag: ${{ github.ref_name }}
667
+ ```
668
+
669
+ The CLI command was renamed from `release-kit github-release` to `release-kit create-github-release`, and its filter flag changed from `--only=<package-name>` to `--tags=<full-tag-name>[,...]`.
670
+
307
671
  ### v1.1.0: `formatCommand` receives file paths as trailing arguments
308
672
 
309
673
  Previously, `formatCommand` was executed as-is (e.g., `pnpm run fmt` would run without arguments). Now, the paths of all modified files (package.json files and changelogs) are appended as trailing arguments.
@@ -61,6 +61,7 @@ commit_preprocessors = []
61
61
  commit_parsers = [
62
62
  { message = "^release:", skip = true },
63
63
  { message = "^Merge", skip = true },
64
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?ai(!)?:", group = "Agentic support" },
64
65
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?bugfix(!)?:", group = "Bug fixes" },
65
66
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?ci(!)?:", group = "CI" },
66
67
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?deprecate(!)?:", group = "Deprecated" },
@@ -69,7 +70,7 @@ commit_parsers = [
69
70
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?feat(!)?:", group = "Features" },
70
71
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?feature(!)?:", group = "Features" },
71
72
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?fix(!)?:", group = "Bug fixes" },
72
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?fmt(!)?:", group = "Formatting" },
73
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?fmt(!)?:", skip = true },
73
74
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?internal(!)?:", group = "Internal" },
74
75
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?perf(!)?:", group = "Performance" },
75
76
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?performance(!)?:", group = "Performance" },
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- d6a6d71f34f00ba31c3dca3b32657832693ff9c12de118fafa96fc3554835ecc
1
+ fca70d83414ef35352e152c9e916f24705c93c5556b14c16388a5b791752f075
@@ -6,7 +6,7 @@ function assertCleanWorkingTree() {
6
6
  }).trim();
7
7
  if (status.length > 0) {
8
8
  throw new Error(
9
- "Working tree has uncommitted changes. Commit or stash them before running prepare, or use --no-git-checks to bypass this check."
9
+ "Working tree has uncommitted changes. Commit or stash them, or use --no-git-checks to bypass this check."
10
10
  );
11
11
  }
12
12
  }