@williamthorsen/release-kit 10.6.0 → 10.8.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 (42) hide show
  1. package/.meta/changelog.json +96 -0
  2. package/.readyup/kits/default.js +17 -3
  3. package/.readyup/kits/npm-auto-publish.js +87 -23
  4. package/.readyup/manifest.json +9 -9
  5. package/CHANGELOG.md +61 -0
  6. package/README.md +53 -1010
  7. package/dist/esm/bin/release-kit.js +7 -6
  8. package/dist/esm/buildChangelogEntries.d.ts +1 -1
  9. package/dist/esm/buildChangelogEntries.js +14 -5
  10. package/dist/esm/buildDependencyGraph.d.ts +6 -5
  11. package/dist/esm/buildDependencyGraph.js +5 -1
  12. package/dist/esm/checkWorkTypesDrift.d.ts +2 -3
  13. package/dist/esm/checkWorkTypesDrift.js +1 -1
  14. package/dist/esm/init/scaffold.js +14 -6
  15. package/dist/esm/parseCommitMessage.d.ts +4 -5
  16. package/dist/esm/planVersionBump.d.ts +2 -1
  17. package/dist/esm/planVersionBump.js +4 -0
  18. package/dist/esm/propagateBumps.d.ts +1 -3
  19. package/dist/esm/propagateBumps.js +2 -2
  20. package/dist/esm/releasePrepare.js +0 -13
  21. package/dist/esm/releasePrepareMono.js +3 -20
  22. package/dist/esm/sync-labels/resolveLabels.d.ts +5 -3
  23. package/dist/esm/sync-labels/resolveLabels.js +21 -6
  24. package/dist/esm/types.d.ts +12 -9
  25. package/dist/esm/validateConfig.js +1 -1
  26. package/dist/esm/work-types.json +68 -52
  27. package/dist/esm/work-types.schema.json +12 -2
  28. package/dist/esm/workTypesData.d.ts +10 -9
  29. package/dist/esm/workTypesData.js +97 -11
  30. package/docs/api.md +73 -0
  31. package/docs/changelogs.md +70 -0
  32. package/docs/configuration.md +105 -0
  33. package/docs/editorial-overrides.md +144 -0
  34. package/docs/labels.md +83 -0
  35. package/docs/project-releases.md +74 -0
  36. package/docs/publishing.md +30 -0
  37. package/docs/readiness-checks.md +18 -0
  38. package/docs/releasing.md +108 -0
  39. package/docs/work-types.md +116 -0
  40. package/package.json +7 -4
  41. package/dist/esm/readCurrentVersion.d.ts +0 -2
  42. package/dist/esm/readCurrentVersion.js +0 -18
package/README.md CHANGED
@@ -1,31 +1,26 @@
1
+ <!-- readme-type: cli -->
2
+
1
3
  # @williamthorsen/release-kit
2
4
 
3
5
  Version-bumping and changelog-generation toolkit for release workflows.
4
6
 
5
- Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
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 `git-cliff --context` output rendered in-process (with optional [editorial overrides](docs/editorial-overrides.md)).
6
8
 
7
9
  <!-- section:release-notes -->
8
- ## Release notes — v10.6.0 (2026-09-08)
10
+ ## Release notes — v10.8.0 (2026-09-15)
9
11
 
10
12
  ### 🎉 Features
11
13
 
12
- - Report a package whose tarball would omit its changelog (#768)
13
-
14
- Adds ReadyUp checks to verify that every published package bundles its changelog files: the human-readable `CHANGELOG.md` and the machine-readable `changelog.json`.
14
+ - Allow the breaking marker on fix and perf commits (#818)
15
15
 
16
- - Add a migration field to changelog.json items (#770)
17
-
18
- Adds an optional `migration` field to entries in the `.meta/changelog.json` that `release-kit` generates. If a commit body contains a labeled migration step, that content is copied to the `migration` field.
16
+ - Makes `release-kit prepare` bump the major version for a `fix!` or `perf!` commit under the default breaking policies, instead of reporting a policy violation and bumping only the patch version while the changelog marked the entry breaking.
17
+ - Corrects `docs/work-types.md` to state that a `breakingPolicies` map replaces the default policies rather than overriding individual types, and that release-kit treats every type omitted by the map as `optional`.
19
18
 
20
19
  ### 🐛 Bug fixes
21
20
 
22
- - Pin git-cliff and remove the per-run npx cache refresh (#776)
23
-
24
- Fixes an issue where git-cliff's version was unpinned, resulting in a per-run cache refresh. An exact version is now pinned.
21
+ - Omit the breaking marker from changelog entries whose type forbids it (#819)
25
22
 
26
- Removes the per-run cache refresh, which re-ran `npx` without `--prefer-offline` once per prepare and forced a registry round trip. An exact version leaves npm nothing to revalidate, so a prepare run no longer waits on registry latency.
27
-
28
- Also silences git-cliff's INFO output, in particular the crates.io update notice.
23
+ - Makes the configured `breakingPolicies` map decide whether the changelog marks a commit breaking, as it already decides the version bump.
29
24
  <!-- /section:release-notes -->
30
25
 
31
26
  ## Installation
@@ -39,54 +34,52 @@ pnpm add -D @williamthorsen/release-kit
39
34
  ## Quick start
40
35
 
41
36
  ```bash
42
- # 1. Set up release-kit in your repo (scaffolds the release workflow)
37
+ # 1. Scaffold the release workflows
43
38
  npx @williamthorsen/release-kit init
44
39
 
45
40
  # 2. Preview what a release would do
46
41
  npx @williamthorsen/release-kit prepare --dry-run
47
42
  ```
48
43
 
49
- Example output from `prepare --dry-run` in a monorepo:
44
+ Example output from `prepare --dry-run` in a monorepo whose `arrays` workspace has two commits since its last release:
50
45
 
51
46
  ```
52
47
  🔍 DRY RUN — no files will be modified
53
48
 
54
- ── arrays ──────────────────────────────────────
55
- Found 4 commits since arrays-v1.2.0
56
- Parsed 3 typed commits
49
+ ━━━ arrays ━━━
50
+ Found 2 commits since arrays-v1.2.0
51
+ Parsed 2 typed commits
57
52
  Bumping versions (minor)...
58
53
  📦 1.2.0 → 1.3.0 (minor)
59
- [dry-run] Would bump packages/arrays/package.json
54
+ [dry-run] Would bump packages/arrays/package.json
60
55
  Generating changelogs...
61
- [dry-run] Would write packages/arrays/CHANGELOG.md
56
+ [dry-run] Would run: npx --prefer-offline --yes git-cliff@<version> ... --output packages/arrays/CHANGELOG.md
62
57
  🏷️ arrays-v1.3.0
63
58
 
64
- ── strings ─────────────────────────────────────
65
- Found 2 commits since strings-v0.5.1
66
- Parsed 2 typed commits
67
- Bumping versions (patch)...
68
- 📦 0.5.1 → 0.5.2 (patch)
69
- [dry-run] Would bump packages/strings/package.json
70
- Generating changelogs...
71
- [dry-run] Would write packages/strings/CHANGELOG.md
72
- 🏷️ strings-v0.5.2
73
-
74
59
  ✅ Release preparation complete.
75
60
  🏷️ arrays-v1.3.0
76
- 🏷️ strings-v0.5.2
61
+ [dry-run] Would write tmp/.release-tags: arrays-v1.3.0
62
+ [dry-run] Would write tmp/.release-summary
63
+ ```
64
+
65
+ Commit the scaffolded workflows, then start a release from the `release` workflow. It runs `prepare` in CI, then commits, tags, and pushes the result:
66
+
67
+ ```sh
68
+ gh workflow run release.yaml # every workspace with release-worthy changes
69
+ gh workflow run release.yaml -f only=arrays # the named workspaces alone
77
70
  ```
78
71
 
79
- That's it for most repos. The CLI auto-discovers workspaces and applies sensible defaults. The bundled `cliff.toml.template` is used automatically — no need to copy it. Customize only what you need via `.config/release-kit.config.ts`.
72
+ The CLI applies defaults to every discovered workspace and uses the bundled `cliff.toml.template` unless the repository provides a git-cliff config of its own. [Releasing](docs/releasing.md) covers `prepare`'s flags, release-notes previews, and the workflow's inputs.
80
73
 
81
74
  ## How it works
82
75
 
83
76
  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.
84
77
  2. **Config loading**: loads `.config/release-kit.config.ts` (if present) and merges it with discovered defaults.
85
78
  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.
86
- 4. **Version bump + changelog**: bumps `package.json` versions, builds structured `ChangelogEntry[]` from `git-cliff --context`, applies any [editorial overrides](#editorial-overrides) from per-scope `.meta/changelog-overrides.json` files, and renders both `CHANGELOG.md` and `.meta/changelog.json` from that single source. `git-cliff` is invoked only for its `--context` JSON; markdown rendering happens in-process so `.meta/changelog.json` and `CHANGELOG.md` always agree.
79
+ 4. **Version bump + changelog**: bumps `package.json` versions, builds structured `ChangelogEntry[]` from `git-cliff --context`, applies any [editorial overrides](docs/editorial-overrides.md) from per-scope `.meta/changelog-overrides.json` files, and renders both `CHANGELOG.md` and `.meta/changelog.json` from that single source. `git-cliff` is invoked only for its `--context` JSON; markdown rendering happens in-process so `.meta/changelog.json` and `CHANGELOG.md` always agree.
87
80
  5. **Release tags file**: writes computed tags to `tmp/.release-tags` for the release workflow to read when tagging and pushing.
88
81
 
89
- Neither changelog reaches a published tarball on its own. npm stopped including `CHANGELOG` automatically in npm 7, so a package that declares a `files` field ships a changelog only where that field names `CHANGELOG.md` and `.meta/changelog.json`. release-kit's readyup kit reports a publishable workspace whose `files` field omits either.
82
+ [Changelogs](docs/changelogs.md) covers what reaches a published tarball, the `changelog.json` item schema, and the git-cliff config.
90
83
 
91
84
  ## Commit format
92
85
 
@@ -103,12 +96,12 @@ type(scope)!: description # conventional scoped breaking change
103
96
 
104
97
  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.
105
98
 
99
+ [Work types and tiers](docs/work-types.md) lists the recognized types and how each one affects the bump and the changelog.
100
+
106
101
  ## Configuration
107
102
 
108
103
  Configuration is optional. The CLI works out of the box by auto-discovering workspaces and applying defaults. Create `.config/release-kit.config.ts` only when you need to customize behavior.
109
104
 
110
- ### Config file
111
-
112
105
  ```typescript
113
106
  import { defineConfig } from '@williamthorsen/release-kit/config';
114
107
 
@@ -131,815 +124,18 @@ export default defineConfig({
131
124
 
132
125
  Node loads the file directly, so a relative import inside it needs an explicit file extension — `./work-types.ts`, not `./work-types`.
133
126
 
134
- ### `ReleaseKitConfig` reference
135
-
136
- | Field | Type | Description |
137
- | ------------------ | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
138
- | `cliffConfigPath` | `string` | Explicit path to cliff config. If omitted, resolved automatically: `.config/git-cliff.toml` → `cliff.toml` → bundled template |
139
- | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
140
- | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
141
- | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
142
- | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commits |
143
- | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
144
- | `breakingPolicies` | `Record<string, 'forbidden' \| 'optional' \| 'required'>` | Per-type `!`-policy lookup. Defaults to `DEFAULT_BREAKING_POLICIES`. Replaces the default entirely when provided. Set to `{}` to disable enforcement |
145
- | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
146
- | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
147
- | `repoLabels` | `RepoLabelsConfig` | Declares the repository's label registry for `sync-labels`; see [Label configuration](#label-configuration) |
148
-
149
- All fields are optional.
150
-
151
- ### `WorkspaceOverride`
152
-
153
- ```typescript
154
- interface WorkspaceOverride {
155
- dir: string; // Package directory name (e.g., 'arrays')
156
- shouldExclude?: boolean; // If true, exclude from release processing
157
- legacyIdentities?: LegacyIdentity[]; // Prior `(name, tagPrefix)` identities for this workspace
158
- }
159
-
160
- interface LegacyIdentity {
161
- name: string; // Full scoped npm name at the time (e.g., '@scope/pkg')
162
- tagPrefix: string; // Tag prefix under which historical tags were published (e.g., 'core-v')
163
- }
164
- ```
165
-
166
- `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.
167
-
168
- ### `RetiredPackage`
169
-
170
- ```typescript
171
- interface RetiredPackage {
172
- name: string; // Final scoped npm name while the package lived in this repo
173
- tagPrefix: string; // Tag prefix under which the package's historical tags were published
174
- successor?: string; // Optional successor package name (e.g., 'readyup')
175
- }
176
- ```
177
-
178
- `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."
179
-
180
- Worked example — `preflight` was extracted from this monorepo and continues as the standalone `readyup` project. Its tags stay in this repo as historical anchors:
181
-
182
- ```typescript
183
- import { defineConfig } from '@williamthorsen/release-kit/config';
184
-
185
- export default defineConfig({
186
- retiredPackages: [{ name: '@scope/preflight', tagPrefix: 'preflight-v', successor: 'readyup' }],
187
- });
188
- ```
189
-
190
- Validation rules:
191
-
192
- - `name` and `tagPrefix` are required per entry and must be non-empty strings.
193
- - `successor` is optional; if present, it must be a non-empty string.
194
- - Full-tuple `(name, tagPrefix)` duplicates within `retiredPackages` are rejected.
195
- - Two entries sharing the same `tagPrefix` but different `name`s are accepted — this documents a package renamed within the repo before being retired.
196
-
197
- `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.
198
-
199
- ### Tag prefix collisions
200
-
201
- Tag prefixes from distinct owners must not be identical or be a strict prefix of one another. An owner is one of:
202
-
203
- - 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).
204
- - A `retiredPackages[]` entry (one owner per entry).
205
- - The `project` block, when configured.
206
-
207
- 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.
208
-
209
- The rule is enforced at config load; the resulting error identifies both colliding declarations.
210
-
211
- ### Project releases
212
-
213
- 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.
214
-
215
- ```typescript
216
- import type { ReleaseKitConfig } from '@williamthorsen/release-kit/config';
217
-
218
- const config: ReleaseKitConfig = {
219
- // Empty object is enough to opt in. Every non-excluded workspace contributes.
220
- project: {},
221
- };
222
-
223
- export default config;
224
- ```
225
-
226
- When configured, each `release-kit prepare` run additionally:
227
-
228
- - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to `paths` (by default, the union of every contributing workspace's paths).
229
- - Bumps the root `package.json`'s `version` field using the same bump-derivation rules as workspaces (or the `--bump=...` override).
230
- - Regenerates the root `./CHANGELOG.md` from the structured `ChangelogEntry[]` produced by `git-cliff --context` (scoped to the project's `tagPrefix` and the same `paths`) and any matching editorial overrides.
231
- - Emits `./.meta/changelog.json` (when `changelogJson.enabled`).
232
- - With `--with-release-notes`, additionally emits `./docs/RELEASE_NOTES.v<version>.md`.
233
- - Appends the project tag to `tmp/.release-tags` so `release-kit commit` and `release-kit tag` pick it up alongside per-workspace tags.
234
-
235
- If no commit under `paths` has landed since the last project tag, the project release is silently skipped — same behavior as a per-workspace skip.
236
-
237
- #### `ProjectConfig`
238
-
239
- ```typescript
240
- interface ProjectConfig {
241
- paths?: string[]; // Defaults to the union of every contributing workspace's paths
242
- tagPrefix?: string; // Defaults to 'v'
243
- }
244
- ```
245
-
246
- | Field | Default | Description |
247
- | ----------- | ----------------------- | -------------------------------------------------------------------- |
248
- | `paths` | Contributing-path union | Patterns selecting the commits the project release considers |
249
- | `tagPrefix` | `'v'` | Prefix for project tags. The full tag is `${tagPrefix}${newVersion}` |
250
-
251
- By default the contributing workspaces are implicit: every non-excluded discovered workspace contributes its `<dir>/**` glob, and the project release considers commits under their union.
252
-
253
- Declare `paths` to choose the window yourself. Each entry reaches two matchers: `git log` as a pathspec, and `git-cliff --include-path` as a glob. A declared value **replaces** the workspace union rather than extending it: `paths: ['docs/**']` drops every workspace commit from the project release.
254
-
255
- A repo whose content lives at the root — where no commit matches any `<dir>/**` glob, so the project release would skip every run — declares the whole tree:
256
-
257
- ```typescript
258
- const config: ReleaseKitConfig = {
259
- project: { paths: ['**'] },
260
- };
261
- ```
262
-
263
- `'**'` is the whole-tree pattern, matching root-level files, dotfiles, and nested paths alike.
264
-
265
- Terminate a directory scope with `/**`. The two matchers agree on `'aws/**'` but diverge on a bare `'aws'`, which git reads as the whole subtree and git-cliff matches against nothing; `'.'` is the same trap for the repo root. A release under such an entry finds commits, bumps the version, and writes the tag, while its changelog gains no entry for them — and nothing reports an error.
266
-
267
- Validation rules:
268
-
269
- - The root `package.json` must exist and declare a `version` field. release-kit reports an error at config-load time if either is missing.
270
- - The `project` block is rejected in single-package mode (the package's own release already covers the whole repo, so a project tier would only duplicate it).
271
- - `paths`, when declared, must hold at least one non-empty string. An empty array is rejected: it reaches git and git-cliff as no filter at all, which is the inverse of how it reads.
272
- - Unknown fields inside `project` are rejected.
273
-
274
- CLI flag interactions:
275
-
276
- - `--dry-run` previews project artifacts alongside workspace artifacts; no files are written.
277
- - `--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.
278
- - `--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.
279
- - `--only` narrows the run to the named workspaces and skips the project release, which is reported as a warning. The project release rolls up every contributing workspace, and `--only` changes which workspaces those are, so rolling up a narrowed set would release a project version covering work the run excluded. Run a full `prepare` (no `--only`) to include the project release.
280
- - `--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.
281
-
282
- `--bump` and `--force` are orthogonal: `--bump` is purely a level chooser; `--force` is purely a release trigger. Examples:
283
-
284
- ```sh
285
- # Release every target at its natural bump level (no flags).
286
- release-kit prepare
287
-
288
- # Force a release even when no bump-worthy commits exist; defaults to patch
289
- # per target, with each target keeping its natural bump if one is derivable.
290
- release-kit prepare --force
291
-
292
- # Force a release at a uniform level across every releasing target.
293
- release-kit prepare --force --bump=minor
294
-
295
- # --bump=X alone is a level chooser, NOT a trigger. If a target has no
296
- # bump-worthy commits, it skips with a "Pass --force..." reason. If it has
297
- # bump-worthy commits, the override applies. (Behavioral change from earlier
298
- # release-kit versions, where --bump=X alone would force a release.)
299
- release-kit prepare --bump=minor
300
- ```
301
-
302
- ### `VersionPatterns`
303
-
304
- Defines which commit types trigger major or minor bumps. Any recognized type not listed defaults to a patch bump.
305
-
306
- ```typescript
307
- interface VersionPatterns {
308
- major: string[]; // Patterns triggering a major bump ('!' = any breaking change)
309
- minor: string[]; // Commit types triggering a minor bump
310
- }
311
- ```
312
-
313
- Default: `{ major: ['!'], minor: ['feat'] }`
314
-
315
- ### Work types and tiers
316
-
317
- The canonical taxonomy lives in `packages/release-kit/src/work-types.json` and is split into three tiers that drive section rendering and audience classification.
318
-
319
- | Tier | Key | Header | Aliases | `!` policy |
320
- | -------- | ----------- | ------------------------- | ------------- | ------------ |
321
- | public | `feat` | 🎉 Features | `feature` | optional |
322
- | public | `drop` | 🪦 Removed | | **required** |
323
- | public | `deprecate` | 🗑️ Deprecated | | forbidden |
324
- | public | `fix` | 🐛 Bug fixes | `bugfix` | forbidden |
325
- | public | `sec` | 🔒 Security | `security` | optional |
326
- | public | `perf` | ⚡ Performance | `performance` | forbidden |
327
- | internal | `internal` | 🏗️ Internal features | `utility` | forbidden |
328
- | internal | `refactor` | ♻️ Refactoring | | forbidden |
329
- | internal | `tests` | 🧪 Tests | `test` | forbidden |
330
- | process | `tooling` | ⚙️ Tooling | | forbidden |
331
- | process | `ci` | 👷 CI | | forbidden |
332
- | process | `deps` | 📦 Dependencies | `dep` | forbidden |
333
- | process | `ai` | 🤖 Agentic support | | forbidden |
334
- | process | `docs` | 📚 Documentation | `doc` | forbidden |
335
- | process | `fmt` | (excluded from changelog) | | forbidden |
336
-
337
- #### Tier semantics
338
-
339
- - **`public`** — visible to all audiences. `public`-tier sections appear in both public release notes and dev changelogs.
340
- - **`internal`** — dev-only. `internal`-tier sections appear in dev changelogs but not in public-facing release notes.
341
- - **`process`** — dev-only. Same audience treatment as `internal`.
342
-
343
- Section render order is **tier order (`public` → `internal` → `process`), then row order within tier**. The bundled `cliff.toml.template` encodes this order via hidden `<!-- NN -->` HTML-comment prefixes on each parser's `group` value; tera's `group_by` filter sorts groups lexicographically (now monotonic by row number), and the body template's `striptags` filter erases the prefix from rendered headings.
344
-
345
- #### `docs` reclassification
346
-
347
- `docs`/Documentation has moved from the all-audience tier (where it lived before this taxonomy was formalised) to the dev-only `process` tier. **Documentation commits no longer appear in public-facing release notes.** They still appear in `CHANGELOG.md` and `changelog.json` under the `audience: 'dev'` classification.
348
-
349
- #### `utility` alias
350
-
351
- `utility:` is a backward-compat alias for `internal:`. Both forms parse to the same canonical type, route to the same `🏗️ Internal features` section, and are subject to the same `!` policy.
352
-
353
- #### `!` (breaking change) policy
354
-
355
- Each work-type carries a `breakingPolicy` value:
356
-
357
- - `optional` (`feat`, `sec`) — `!` is allowed; both `type:` and `type!:` parse cleanly.
358
- - `forbidden` (most types) — `!` is a policy violation. The premise: types like `internal!`, `perf!`, `fix!` are contradictory; an internal change cannot break a consumer contract, a pure perf change preserves the contract, and a bug-fix is by definition not a contract change.
359
- - `required` (`drop`) — bare `drop:` is a policy violation; only `drop!:` is accepted. The premise: removing a feature always breaks consumers; the `!` form makes that explicit.
360
-
361
- ##### Two-tier policy enforcement
362
-
363
- The `!` policy operates at two distinct levels with different semantics:
364
-
365
- - **Write-time** (commit-msg hook) — strict rejection. Policy violations are blocked at the gate where the author can act on them immediately. _Hook-based enforcement is tracked separately and is not yet shipped._
366
- - **Release-time** (`parseCommitMessage`) — tolerant warn-and-continue. Commits already in the log cannot be rewritten, so a policy-violating commit is parsed using its canonical type with `breaking: false` (the `!` is dropped from the parse) and a `onPolicyViolation` callback fires. Callers (`decideRelease` etc.) can collect these warnings and surface them in the release report. A single legacy `internal!` in a year-old log does not block releases.
367
-
368
- A `BREAKING CHANGE:` body footer on a `forbidden`-policy type triggers the same warning path as the prefix `!` does — the spirit of the policy is "internal/perf/etc. cannot be breaking", which must apply to both surfaces.
369
-
370
- The release-prepare orchestrators (`releasePrepare`, `releasePrepareMono`, `releasePrepareProject`) apply `DEFAULT_BREAKING_POLICIES` automatically. Violations encountered while parsing each workspace's or project's commit window are collected onto the corresponding result's `policyViolations` field and rendered under the section in the prepare report:
371
-
372
- ```
373
- arrays
374
- Found 1 commits since arrays-v1.0.0
375
- ⚠️ 1 policy violation:
376
- · def5678 'internal!: refactor cache' — type 'internal' at prefix surface
377
- Bumping versions (patch)...
378
- 📦 1.0.0 → 1.0.1 (patch)
379
- ```
380
-
381
- To customize, set `breakingPolicies` in `release-kit.config.ts` — provide a partial map to override individual types, or `{}` to disable enforcement entirely (the parser falls back to `'optional'` for any missing type). Violations remain warnings, never failures.
382
-
383
- #### `🚨 **Breaking:**` bullet marker
384
-
385
- Items whose commit subject carries the `!` prefix (e.g. `feat!`, `drop!`, `feat(api)!`) are rendered with a `🚨 **Breaking:** ` prefix on the bullet:
386
-
387
- ```markdown
388
- - 🚨 **Breaking:** Drop legacy /v1 endpoint
389
- ```
390
-
391
- Only the prefix `!` triggers this marker. A `BREAKING CHANGE:` body footer on its own does **not** retroactively mark a changelog item as breaking — the changelog signal is tied to the commit-prefix policy. This avoids surprise breaking-marker appearances for older commits written under earlier conventions.
392
-
393
- The emoji and label of this marker are sourced from the `markers.breaking` entry in `work-types.json` (see [Section markers](#section-markers)) so consumers that render their own breaking-changes section draw from the same SSOT.
394
-
395
- ### Section markers
396
-
397
- Alongside `tiers` and `types`, `work-types.json` exposes a top-level `markers` object for cross-cutting section markers — visual indicators that aren't tied to a specific work type. Today the canonical entry is `breaking`; additional keys (e.g., security advisories, migration notices) can be added without a schema change.
398
-
399
- ```jsonc
400
- {
401
- "markers": {
402
- "breaking": { "emoji": "🚨", "label": "Breaking" },
403
- },
404
- }
405
- ```
406
-
407
- Entries store plain text only — the SSOT is format-agnostic, so consumers apply their own emphasis (Markdown bold, ANSI escape, HTML `<strong>`) when constructing the rendered form. release-kit's own renderer constructs the per-bullet prefix as `${emoji} **${label}:** ` from this entry.
408
-
409
- #### `fmt`
410
-
411
- `fmt:` commits are recognized by `parseCommitMessage` (they contribute to a patch bump) but `fmt` carries `excludedFromChangelog: true`. The bundled `cliff.toml.template` skips `fmt:` commits at the parser level, so they never appear in `CHANGELOG.md`, `changelog.json`, or release notes. The label and emoji are present in `work-types.json` for schema parity with the codeassembly upstream but never render.
412
-
413
- #### Custom work types
414
-
415
- Work types from your config are merged with these defaults by key — your entries override or extend, they don't replace the full set. Release-notes sections are rendered in the declaration order of the merged work-types record, with any unknown titles trailing the known ones.
416
-
417
- The default `devOnlySections` (excluded from public release notes but still written to `CHANGELOG.md`) are derived from the `internal` and `process` tiers (excluding `fmt`). Override via `changelogJson.devOnlySections` in your config; matching is decorator-tolerant, so a bare-name override like `['Internal features']` keeps working against the emoji-prefixed and prefix-decorated default titles.
418
-
419
- ## `changelog.json` item schema
420
-
421
- Each item under a section in `.meta/changelog.json` carries one required field and four optional ones:
422
-
423
- | Field | Type | Meaning |
424
- | ------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- |
425
- | `description` | `string` | The bullet headline, taken from the commit subject with the ticket ID and type prefix stripped. |
426
- | `body` | `string` | The commit body, with trailing trailer metadata stripped. |
427
- | `breaking` | `boolean` | Present and `true` where the commit subject carried the `!` prefix. See [`!` (breaking change) policy](#-breaking-change-policy). |
428
- | `migration` | `string` | The migration step for a consumer. See below. |
429
- | `hash` | `string` | The full commit SHA, and the key on which an override entry matches. Absent on synthetic propagation entries. |
430
-
431
- Every optional field is omitted rather than emitted as `null`, so a consumer tests for presence.
432
-
433
- ### The `migration` field
434
-
435
- A commit body states a migration step as a paragraph opening with the literal label `Migration:`. Where one is present, `migration` carries that paragraph with the label stripped:
436
-
437
- ```jsonc
438
- {
439
- "description": "Rename the workspace field",
440
- "body": "Renames `name` to `id`.\n\nMigration: Change any uses of `name` to `id`.",
441
- "migration": "Change any uses of `name` to `id`.",
442
- }
443
- ```
444
-
445
- Three properties are worth knowing:
446
-
447
- - **It is independent of `breaking`.** A `fix` cannot carry `!` under the default breaking policy, and a `fix` that tightens validation still imposes a migration. Filter on `migration` to find every step; filter on `breaking` to find every breaking change.
448
- - **`body` keeps the paragraph.** The field is an extraction, not a move, so `CHANGELOG.md` and `.meta/changelog.json` go on agreeing.
449
- - **It is derived, not authored.** `migration` is not a field an override file can set (see [File shape](#file-shape)); it is re-derived from whatever `body` an override installs, and cleared where that body carries no labeled paragraph. To change the migration text, override `body`.
450
-
451
- The label match is exact: `migration:` and `**Migration:**` are not recognized.
452
-
453
- ## Editorial overrides
454
-
455
- Generated changelogs occasionally need editorial correction — typos, redacted scope, reworded entries, or historical commits whose bodies carry verbatim PR-template scaffolding (`## What`, `## Why`, etc.) that renders as literal text in user-facing release notes. Rewriting git history is not viable, and any in-place edit to `CHANGELOG.md` or `.meta/changelog.json` is overwritten on the next release because release-kit regenerates both artifacts from scratch.
456
-
457
- Override files are the supported escape hatch. Drop a checked-in JSON file at the conventional path for the scope you want to influence, keyed by commit hash, and `release-kit prepare` applies the overrides between `buildChangelogEntries` and serialization. Both `CHANGELOG.md` and `.meta/changelog.json` reflect the post-override view, so downstream consumers (the GitHub Release body, the in-app release-notes page, etc.) see the same content.
458
-
459
- ### File-location convention
460
-
461
- | Scope | Path | Applies to |
462
- | ------------------- | ---------------------------------------------- | ------------------------------------------------------- |
463
- | Project (root) | `.meta/changelog-overrides.json` | The project changelog and every workspace's changelog |
464
- | Workspace | `packages/<ws>/.meta/changelog-overrides.json` | Only that workspace's changelog |
465
- | Single-package mode | `.meta/changelog-overrides.json` | The package's changelog (collapses to the project case) |
466
-
467
- Filenames have no leading dot — the `.meta/` directory already provides the visibility property and parallels its sibling artifacts (`changelog.json`, `label-map.json`).
468
-
469
- ### Composition: per-key shadowing
470
-
471
- When a workspace's changelog is rendered, both files are consulted:
472
-
473
- - The root file's overrides apply globally.
474
- - The workspace file's overrides apply only to that workspace.
475
- - When the **same hash key** (string-equal, byte-for-byte) appears in both files, the **workspace entry wins entirely** for that workspace's changelog — no field-level merge, the workspace entry replaces the root entry.
476
- - Different prefix strings that happen to resolve to the same commit do **not** shadow; they fall through to the existing ambiguous-prefix error so you can correct your override file.
477
- - Other keys in the root file still apply for that workspace.
478
-
479
- The project-level changelog applies only the root file. Per-workspace files describe per-workspace editorial intent and have no meaning at the aggregated project tier.
480
-
481
- ### Stale-key warnings
482
-
483
- A key that doesn't match any commit gets a stale-reference warning. The warning's scope mirrors the file's scope:
484
-
485
- - **Per-workspace files** are warned against their own apply context. A key in `packages/foo/.meta/changelog-overrides.json` that doesn't match any commit in foo's changelog is unambiguously stale and is warned immediately.
486
- - **Root file** keys are aggregated globally — a root key that matches in any workspace or in the project changelog is non-stale; a root key matched nowhere is warned exactly once after all batches complete.
487
-
488
- ### File shape
489
-
490
- ```json
491
- {
492
- "82962311": {
493
- "audience": "skip"
494
- },
495
- "abc1234d": {
496
- "body": "Cleaned-up prose without the original PR-template scaffolding."
497
- },
498
- "ef567890": {
499
- "description": "Rewritten headline that fixes the typo",
500
- "body": "Optional replacement body."
501
- }
502
- }
503
- ```
504
-
505
- Per-entry fields are all optional, but at least one must be present per entry:
506
-
507
- | Field | Type | Effect |
508
- | ------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
509
- | `audience` | `'all' \| 'dev' \| 'skip'` | `'skip'` removes the entry entirely. `'all'` and `'dev'` are reserved for a future audience-reclassification feature (see below). |
510
- | `description` | `string` | Replaces the entry's bullet headline. Other fields are preserved. |
511
- | `body` | `string` | Replaces the entry's body (the prose that renders below the bullet). Other fields are preserved. |
512
- | `breaking` | `boolean` | Toggles the `🚨 **Breaking:** ` marker on the bullet. |
513
-
514
- There is no `migration` key: the field is derived from `body`, so an override that replaces `body` re-derives it. See [The `migration` field](#the-migration-field).
515
-
516
- ### Hash-prefix matching
517
-
518
- Keys can be either the full 40-character commit SHA or a non-ambiguous prefix. The matcher walks every `ChangelogItem.hash` value present in the entry tree and resolves each override key to its set of matching hashes:
519
-
520
- - **Exact prefix match (1 hit)** — the override applies. A 7-character prefix is usually unambiguous within a single repo's history; longer prefixes are always safe.
521
- - **No matches (0 hits)** — the override is treated as a stale reference (probably from a rebase or branch deletion) and prepare reports a warning. The release continues.
522
- - **Ambiguous prefix (2+ hits)** — the release aborts with an error naming the key and the matching hashes. Lengthen the prefix or use the full SHA.
523
-
524
- Override application errors abort the run with a non-zero exit; warnings (zero-match keys) are non-fatal and surface on `PrepareResult.warnings`.
525
-
526
- ### Validation
527
-
528
- The override file is validated when `release-kit prepare` loads it. Each error names the offending key so you can locate it in your file:
529
-
530
- - Missing file → empty map, no error (the no-op default — projects that do not need overrides skip the file entirely).
531
- - Malformed JSON → error.
532
- - Wrong top-level shape (e.g., array, primitive) → error.
533
- - Unknown fields on an entry → error.
534
- - Wrong field types (e.g., `description` as a number) → error.
535
- - An entry with no fields set → error (a copy-paste mistake more often than not).
536
- - `audience: 'all'` or `audience: 'dev'` → error in the current release: only `'skip'` is supported (see below).
537
-
538
- #### Standalone validation: `release-kit overrides validate`
539
-
540
- For a focused overrides-only health check (locally or as a CI gate), run:
541
-
542
- ```sh
543
- pnpm exec release-kit overrides validate
544
- ```
545
-
546
- This walks every `.meta/changelog-overrides.json` file across the project tier and per-workspace tier, reporting three classes of finding:
547
-
548
- | Class | Examples | Exit code |
549
- | ------------------- | ------------------------------------------------------------------------------------------- | --------- |
550
- | Schema/parse errors | malformed JSON, unknown fields, wrong field types, no-field entries, unsupported `audience` | `2` |
551
- | Ambiguous-prefix | an override key resolves to 2+ commit hashes | `2` |
552
- | Stale-key warnings | an override key resolves to no commit in its applicable scope | `1` |
553
-
554
- Exit code semantics:
555
-
556
- - `0` — clean (no errors, no stale keys).
557
- - `1` — only stale-key warnings.
558
- - `2` — schema/parse or ambiguous-prefix errors (errors dominate when both classes are present).
559
-
560
- Tier-aware stale-key semantics match `release-kit prepare`'s match-set exactly: a workspace-tier key is stale if it does not match in its own workspace's history; a root-tier key is stale only if it matches in **no** scope (no workspace AND not the project release window).
561
-
562
- The same logic is also exposed programmatically via the `validateAllChangelogOverrides` function exported from `@williamthorsen/release-kit`, for callers that want to integrate the check into their own tooling.
563
-
564
- ### Audience semantics: v1 supports `'skip'` only
565
-
566
- The on-disk format declares the full `'all' | 'dev' | 'skip'` audience vocabulary so the file format will not need to change when the v2 reclassification feature ships. In the current release, only `'skip'` is supported at runtime; `'all'` and `'dev'` are rejected with an explicit "not yet supported" error.
567
-
568
- The eventual v2 behavior will let an override move a single item to a different audience section (e.g., reclassifying a `Documentation` entry as `Internal features` to keep it out of public-facing release notes). v1 deliberately leaves that as a separate change so the override mechanism can ship now and the section-split logic can land additively later.
569
-
570
- ### Worked example 1: cleaning up scaffolded historical commits (root file)
571
-
572
- Suppose a year-old commit `82962311` was authored from a PR template that left `## What` / `## Why` headings in the body, and that commit now appears in your in-app release notes as literal Markdown headings. Add an override at the project tier:
573
-
574
- ```json
575
- // .meta/changelog-overrides.json
576
- {
577
- "82962311": {
578
- "body": "Add the in-app release-notes page with version-aware navigation."
579
- }
580
- }
581
- ```
582
-
583
- On the next `release-kit prepare` run, the matched item's body is replaced before the JSON and Markdown artifacts are written. The original git history is untouched.
584
-
585
- ### Worked example 2: suppressing a cross-attribution spillover (workspace file)
586
-
587
- Release-kit attributes commits to workspaces by file path, so a commit that primarily belongs to one workspace can land in another's changelog if it touched files there. Suppose commit `1ce3d2f` renamed the `audit-deps` package to `v11y-check` (scope `v11y-check`) but also edited `packages/nmr/src/default-scripts.ts` and `packages/nmr/README.md`. The commit correctly appears in `packages/v11y-check/CHANGELOG.md`, but it also spills into `packages/nmr/CHANGELOG.md` where it isn't the right editorial framing.
588
-
589
- Drop a workspace-tier override at `packages/nmr/.meta/changelog-overrides.json`:
590
-
591
- ```json
592
- // packages/nmr/.meta/changelog-overrides.json
593
- {
594
- "1ce3d2f": {
595
- "audience": "skip"
596
- }
597
- }
598
- ```
599
-
600
- The commit is now suppressed in nmr's changelog only — it still appears in v11y-check's, where it belongs. A root-tier `'skip'` would have removed it from both, which is the wrong outcome.
601
-
602
- ### Rendering pipeline change
603
-
604
- Prior versions of release-kit shelled out to `git-cliff` for both structured `--context` JSON and rendered Markdown (cliff's body template). After this change, `git-cliff` is invoked only for `--context` JSON; release-kit's in-process `renderChangelogMarkdown` produces `CHANGELOG.md` from the same `ChangelogEntry[]` that drives `.meta/changelog.json`. The two artifacts can no longer disagree.
605
-
606
- The bundled `cliff.toml.template`'s body template has been emptied (the `[git].commit_parsers` section is still load-bearing for `--context` group assignment); custom `.config/git-cliff.toml` files no longer need a body template.
607
-
608
- Observable output differences from the prior cliff-rendered format:
609
-
610
- - Trailers (`Signed-off-by:`, `Co-authored-by:`, `Closes #N`, GitHub PR URLs) are stripped from rendered bodies.
611
- - Items whose commit subject carries the `!` breaking marker render with a `🚨 **Breaking:** ` prefix.
612
- - Empty version entries (releases with no commits routed to a section) are omitted; the prior cliff template rendered them as bare headings.
613
- - The footer comment is now `<!-- Generated by release-kit. Do not edit this file. Use .meta/changelog-overrides.json to override entries. -->`.
614
-
615
- The first release that ships under the new renderer will produce a one-time noisy diff in `CHANGELOG.md` (whitespace, trailers, breaking markers). Subsequent releases stabilize.
616
-
617
- ## CLI reference
618
-
619
- ### Global options
620
-
621
- | Flag | Description |
622
- | ----------------- | ------------------- |
623
- | `--help`, `-h` | Show help message |
624
- | `--version`, `-V` | Show version number |
625
-
626
- ### `release-kit prepare`
627
-
628
- Run release preparation with automatic workspace discovery.
127
+ [Configuration](docs/configuration.md) lists every field and covers legacy identities, retired packages, and tag prefixes. [Project releases](docs/project-releases.md) covers releasing a monorepo as one combined deliverable.
629
128
 
630
- | Flag | Description |
631
- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
632
- | `--dry-run` | Preview changes without writing files |
633
- | `--bump=major\|minor\|patch` | Override the bump type for all workspaces |
634
- | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. In monorepo mode it requires `--only`, and is rejected when a `project` block is configured. |
635
- | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag. In monorepo/project mode, defaults to patch (combine with `--bump=X` for a different level); in single-package mode, a bare `--force` is rejected — pass `--bump=major\|minor\|patch`. |
636
- | `--only=name1,name2` | Only process the named workspaces (monorepo only). When a `project` block is configured, the project release is skipped. |
637
- | `--with-release-notes` | Write per-workspace release-notes previews under `<workspacePath>/docs/` |
638
- | `--help`, `-h` | Show help |
639
-
640
- Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
641
-
642
- #### Previewing release notes with `--with-release-notes`
643
-
644
- `--with-release-notes` writes two versioned files per workspace after each workspace's `changelog.json` is produced:
645
-
646
- - `<workspacePath>/docs/README.v<version>.md` — the workspace `README.md` with release notes injected at the `<!-- section:release-notes -->` marker.
647
- - `<workspacePath>/docs/RELEASE_NOTES.v<version>.md` — the standalone release notes for this version.
648
-
649
- The publish-time inject-and-revert lifecycle is unchanged; previews are additive, deterministic, and safe to regenerate. When `changelogJson.enabled` is `false`, prepare reports a warning and skips preview generation. In dry-run mode, planned writes are logged and no files are created.
650
-
651
- Because preview filenames are versioned, committing them will accumulate files over time. The recommended `.gitignore` entry for monorepos is:
652
-
653
- ```gitignore
654
- packages/*/docs/*.v*.md
655
- ```
656
-
657
- For single-package repos:
658
-
659
- ```gitignore
660
- docs/*.v*.md
661
- ```
662
-
663
- #### Setting an explicit version with `--set-version`
664
-
665
- 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.
666
-
667
- The flag validates that:
668
-
669
- - The value is canonical `N.N.N` semver (pre-release suffixes are rejected).
670
- - The target is strictly greater than the current version (numeric comparison on each component).
671
- - In monorepo mode, `--only` is set and resolves to exactly one workspace.
672
-
673
- `--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.
674
-
675
- Promoting a pre-1.0 package to 1.0.0 in a monorepo:
676
-
677
- ```sh
678
- release-kit prepare --only arrays --set-version 1.0.0
679
- ```
680
-
681
- 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`.
682
-
683
- ### `release-kit publish`
684
-
685
- Publish packages that have release tags on HEAD. The publish workflow's reusable workflow `publish.reusable.yaml` invokes this command in CI.
686
-
687
- | Flag | Description |
688
- | ---------------------- | ---------------------------------------------------------------------------- |
689
- | `--dry-run` | Preview without publishing |
690
- | `--no-git-checks` | Skip the clean-working-tree check |
691
- | `--tags=tag1,tag2,...` | Only publish the named tags (comma-separated, full tag names) |
692
- | `--provenance` | Generate provenance statement (requires OIDC, not supported by classic yarn) |
693
- | `--help`, `-h` | Show help |
694
-
695
- #### Publishability filter
696
-
697
- `publish` and `create-github-release` operate only on workspaces where `package.json#private` is absent or `false`. A workspace marked `private: true` is "versioned but not published": it can still be tagged by `release-kit tag` and get a `CHANGELOG.md` entry, but it is skipped by both `release-kit publish` (no registry publish) and `release-kit create-github-release` (no GitHub Release). `private: true` alone routes a workspace out of publish and announce — no extra release config is required. Other commands ignore this filter and operate on private workspaces unchanged.
698
-
699
- An unpublishable tag is always skipped cleanly, never fatal. For `release-kit publish`, whether the skip is announced depends on how the tag was resolved:
700
-
701
- - **Without `--tags`** (implicit resolution): unpublishable tags on HEAD are silently filtered. The pre-publish listing shows only the publishable subset. If the filter empties the set, `release-kit publish` prints `Nothing to publish.` and exits 0.
702
- - **With `--tags`** (explicit naming): each named tag pointing at an unpublishable workspace is skipped with a warning, and any publishable tags named in the same command still publish.
703
-
704
- `release-kit create-github-release` skips unpublishable workspaces with a warning regardless of `--tags`; an all-private tag set is a clean no-op.
705
-
706
- Example warning when an explicit tag is unpublishable:
707
-
708
- ```
709
- Skipping basic-v1.0.0 (packages/basic): package.json#private is true.
710
- ```
711
-
712
- ### `release-kit create-github-release`
713
-
714
- Create GitHub Releases from `changelog.json` for tags on HEAD. Independent of `npm publish`: invoking this command creates Releases for publishable packages whether or not they were published to a registry. Private workspaces are skipped (see [Publishability filter](#publishability-filter)).
715
-
716
- | Flag | Description |
717
- | ---------------------- | ------------------------------------------------------------------------- |
718
- | `--dry-run` | Preview without creating releases |
719
- | `--tags=tag1,tag2,...` | Only create releases for the named tags (comma-separated, full tag names) |
720
- | `--help`, `-h` | Show help |
721
-
722
- 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.
723
-
724
- ### `release-kit show-tag-prefixes`
725
-
726
- 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.
727
-
728
- | Flag | Description |
729
- | -------------- | ----------- |
730
- | `--help`, `-h` | Show help |
731
-
732
- 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.
733
-
734
- In single-package mode, prints a single row with `workspacePath = .` and `derivedPrefix = v`; legacy entries and undeclared-candidate scanning are not applicable.
735
-
736
- ### `release-kit init`
737
-
738
- Initialize release-kit in the current repository. By default, scaffolds only the GitHub Actions workflow file. Use `--with-config` to also scaffold configuration files.
739
-
740
- | Flag | Description |
741
- | --------------- | -------------------------------------------------------------------------- |
742
- | `--with-config` | Also scaffold `.config/release-kit.config.ts` and `.config/git-cliff.toml` |
743
- | `--force` | Overwrite existing files instead of skipping them |
744
- | `--dry-run` | Preview changes without writing files |
745
- | `--help`, `-h` | Show help |
746
-
747
- Scaffolded files:
748
-
749
- - `.github/workflows/create-github-release.yaml` — workflow that creates a GitHub Release on tag push, independent of npm publish
750
- - `.github/workflows/publish.yaml` — workflow that delegates to a reusable publish workflow
751
- - `.github/workflows/release.yaml` — workflow that delegates to a reusable release workflow
752
- - `.config/release-kit.config.ts` — starter config with commented-out customization examples (with `--with-config`)
753
- - `.config/git-cliff.toml` — copied from the bundled template (with `--with-config`)
754
-
755
- ### `release-kit work-types`
756
-
757
- Manage the canonical work-types taxonomy used by changelog and release-notes generation.
758
-
759
- | Subcommand | Description |
760
- | ---------- | ----------------------------------------------------------------------------------- |
761
- | `check` | Compare the local `work-types.json` against the upstream codeassembly canonical |
762
- | `sync` | Overwrite the local `work-types.json` with the upstream contents (after validation) |
763
-
764
- `check` exit codes:
765
-
766
- | Code | Meaning |
767
- | ---- | --------------------------------------------------------------------------------- |
768
- | `0` | Match (or upstream missing — transitional warning printed) |
769
- | `1` | Drift detected |
770
- | `2` | Network error or non-OK HTTP response |
771
- | `3` | Schema mismatch (upstream JSON does not parse or fails the top-level shape check) |
772
-
773
- The check is non-blocking initially: until codeassembly publishes its `work-types.json`, the upstream URL returns 404 and `check` exits 0 with a warning. CI flip to a blocking check is tracked as a follow-up once the upstream ships.
774
-
775
- These commands are also exposed as `nmr work-types:check` / `nmr work-types:sync` from any package directory.
776
-
777
- #### Authenticated fetches
778
-
779
- When the upstream codeassembly repo is private, both `check` and `sync` need a GitHub token to fetch the canonical `work-types.json`. Set `GITHUB_TOKEN` in the environment and the commands send `Authorization: Bearer <token>` automatically; without it, requests are unauthenticated and a private upstream will return 404.
780
-
781
- ```sh
782
- # Source from `gh auth` for local runs:
783
- export GITHUB_TOKEN=$(gh auth token)
784
- pnpm exec release-kit work-types check
785
- ```
786
-
787
- The token needs `contents: read` on the codeassembly repo (fine-grained PAT scope) or the equivalent classic-PAT scope. A token without sufficient scope still produces a 404 — same response as a missing upstream — so a misconfigured token degrades to the transitional-warning path rather than failing loudly. CI wiring against private upstream is deferred until either codeassembly is publicly readable or a cross-repo PAT is provisioned as a workflow secret.
788
-
789
- ### `release-kit sync-labels`
790
-
791
- Manage GitHub label definitions via the `repoLabels` block of `.config/release-kit.config.ts`.
792
-
793
- | Subcommand | Description | Flags |
794
- | ---------- | -------------------------------------------------------------- | ---------------------- |
795
- | `init` | Scaffold the caller workflow, seed config, and generate labels | `--dry-run`, `--force` |
796
- | `generate` | Regenerate `.github/labels.yaml` from config | `--check` |
797
- | `sync` | Trigger the `sync-labels` GitHub Actions workflow via `gh` CLI | — |
798
-
799
- `init` scaffolds the `.github/workflows/sync-labels.yaml` caller workflow and seeds a `repoLabels` block with scope labels discovered from workspaces and declared `retiredPackages`. When `.config/release-kit.config.ts` does not exist, `init` writes it; when it does, `init` prints the block for manual paste — a hand-authored config is never rewritten. `generate` resolves the block and writes `.github/labels.yaml`; with `--check` it regenerates in memory and exits non-zero if the committed file is stale or missing, writing nothing. `sync` triggers the workflow remotely — it requires the `gh` CLI and an existing workflow file.
800
-
801
- Every `sync-labels` subcommand refuses to run while the retired `.config/sync-labels.config.ts` is present, so custom labels cannot be silently dropped mid-migration; see [Migrating from `.config/sync-labels.config.ts`](#migrating-from-configsync-labelsconfigts).
802
-
803
- #### Label configuration
804
-
805
- ```typescript
806
- import { defineConfig } from '@williamthorsen/release-kit/config';
807
-
808
- export default defineConfig({
809
- repoLabels: {
810
- extends: ['common'],
811
- labels: {
812
- 'scope:my-package': { color: '00ff96' }, // add, with no description
813
- bug: { color: 'b60205', description: 'Something broken' }, // replace the preset's `bug`
814
- wontfix: null, // remove the preset's `wontfix`
815
- },
816
- },
817
- });
818
- ```
819
-
820
- The block declares the repository's label registry — the set of labels defined on the GitHub repo, distinct from labels applied to PRs and issues. Resolution is an ordered fold with last-writer-wins:
821
-
822
- 1. Presets, in `extends` order — a later preset wins on a shared name.
823
- 2. The `labels` record — an entry adds a label, replaces one an earlier layer defined, or removes it (`null`). Replacement is wholesale: an entry omitting `description` resolves to no description rather than inheriting the one an earlier layer supplied.
824
-
825
- `description` is optional throughout, in a preset as in the `labels` record, and `sync-labels init` generates none for a scope label. The generated file spells an absent description `''` because `github-label-sync` reads an omitted description as "leave the label's current one alone"; the empty form clears it.
826
-
827
- Overlaps are never errors; order resolves them, and the committed `.github/labels.yaml` diff is where an unexpected change surfaces at review. The one config error is a dangling `null` — removing a name no preset defines — because that misstatement is invisible in the output diff.
828
-
829
- #### Migrating from `.config/sync-labels.config.ts`
830
-
831
- Label configuration formerly lived in a standalone `.config/sync-labels.config.ts` (`presets` + a `labels` array). To migrate:
832
-
833
- 1. Move the preset list to `repoLabels.extends` in `.config/release-kit.config.ts`.
834
- 2. Convert each `labels` array entry to a `'name': { color }` record entry under `repoLabels.labels`, adding `description` where the label carries one.
835
- 3. Delete `.config/sync-labels.config.ts` and run `release-kit sync-labels generate`.
836
-
837
- The regenerated `.github/labels.yaml` differs only in its `# Source:` header line. Name collisions with preset labels, which the old format rejected, now mean your entry replaces the preset label.
838
-
839
- #### When labels are applied
840
-
841
- The scaffolded workflow carries three triggers:
842
-
843
- | Trigger | Job | Token | Effect |
844
- | --------------------------------------------------------- | ------- | --------------- | --------------------------------- |
845
- | Push to the default branch touching `.github/labels.yaml` | `sync` | `issues: write` | Applies the labels |
846
- | Manual dispatch (`release-kit sync-labels sync`) | `sync` | `issues: write` | Applies the labels |
847
- | Pull request touching `.github/labels.yaml` | `check` | `issues: read` | Logs the diff without applying it |
848
-
849
- Applying on merge closes the window in which a regenerated `.github/labels.yaml` sits unapplied — a window in which a later manual dispatch would apply a label set nobody reviewed.
850
-
851
- The `check` job runs the same sync in dry-run. Its **Label diff** log group lists every create, edit, rename, and deletion the sync would perform, including deletions of labels the file does not declare — the destructive edits a diff of `.github/labels.yaml` alone cannot show, because a label created by hand or by another workflow is absent from the file both before and after. Read that log; the check reports success whether or not the diff is destructive, so a green check is not evidence that nothing will be deleted.
852
-
853
- The two jobs are split because a job's permissions are fixed when the run is created and cannot vary by trigger. The split is what keeps a write-scoped token out of pull-request runs.
854
-
855
- The push trigger filters on the path alone; the `sync` job compares `github.ref_name` against the repository's default branch. The workflow therefore needs no per-repo edit whatever that branch is named — and a push touching `.github/labels.yaml` on any other branch produces a run whose jobs all skip.
856
-
857
- Manual dispatch is not a preview. It matches the `sync` job, so it applies the labels, deletions included; only the pull-request path runs in dry-run.
858
-
859
- #### Dry-run checks on fork pull requests
860
-
861
- GitHub issues a read-only `GITHUB_TOKEN` to pull requests from forks, and by default holds runs from first-time contributors until a maintainer approves them. The dry-run reads labels and writes nothing, so the check normally runs once approved. A repo that disables workflows on fork pull requests gets no check at all; to preview such a change, push the branch to the base repo and open the pull request from there.
862
-
863
- #### Migrating an existing repo
864
-
865
- The caller workflow gained the `push` and `pull_request` triggers after its first release. A repo scaffolded before that keeps working on manual dispatch alone — nothing breaks — but it applies labels only when someone remembers to. To adopt the new shape:
866
-
867
- ```sh
868
- pnpm exec release-kit sync-labels init --force
869
- ```
870
-
871
- `readyup` prompts for this: its `sync-labels.yaml matches template` check fails once the scaffolded workflow no longer matches the current template.
872
-
873
- #### Published JSON Schema for `.meta/label-map.json`
874
-
875
- 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:
876
-
877
- ```
878
- https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json
879
- ```
880
-
881
- Consumers reference it from the top of their `.meta/label-map.json`:
882
-
883
- ```json
884
- {
885
- "$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/label-map.json",
886
- "types": { "feat": "feature", "fix": "fix" },
887
- "scopes": { "audit": "scope:audit" }
888
- }
889
- ```
890
-
891
- 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.
892
-
893
- ## GitHub Actions workflow
894
-
895
- 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:
896
-
897
- | Input | Type | Description |
898
- | ------ | ------ | ------------------------------------------------------------------- |
899
- | `only` | string | Workspaces to release (comma-separated, leave empty for all) |
900
- | `bump` | choice | Override bump type: `patch`, `minor`, `major` (empty = auto-detect) |
901
-
902
- 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.
903
-
904
- ### Triggering a release
905
-
906
- ```sh
907
- # All workspaces
908
- gh workflow run release.yaml
909
-
910
- # Specific workspace(s)
911
- gh workflow run release.yaml -f only=arrays
912
- gh workflow run release.yaml -f only=arrays,strings -f bump=minor
913
- ```
914
-
915
- Or use the GitHub UI: Actions > Release > Run workflow.
916
-
917
- ## cliff.toml setup
918
-
919
- The package includes a bundled `cliff.toml.template` that is used automatically when no custom config is found. The resolution order:
920
-
921
- | Priority | Path | Notes |
922
- | -------- | ----------------------------- | ----------------------------------------------- |
923
- | 1 | `cliffConfigPath` in config | Explicit path, returned without existence check |
924
- | 2 | `.config/git-cliff.toml` | Project-level override |
925
- | 3 | `cliff.toml` | Repo root fallback |
926
- | 4 | Bundled `cliff.toml.template` | Automatic fallback |
927
-
928
- The bundled template provides a generic git-cliff configuration that:
929
-
930
- - Strips issue-ticket prefixes matching `^[A-Z]+-\d+\s+` (e.g., `TOOL-123 `, `AFG-456 `)
931
- - Handles both `type: description` and `workspace|type: description` commit formats
932
- - Groups commits by work type via `[git].commit_parsers`
129
+ ## External dependencies
933
130
 
934
- The body template is intentionally empty: release-kit reads cliff's `--context` JSON output and renders `CHANGELOG.md` in-process via `renderChangelogMarkdown` (see [Editorial overrides](#editorial-overrides) for the rationale). The `[git].commit_parsers` section remains load-bearing for `--context` group assignment.
131
+ This package shells out to two external tools:
935
132
 
936
- To customize, scaffold a local copy with `release-kit init --with-config` and edit `.config/git-cliff.toml`. Edit only the `[git]` section — body-template changes have no effect.
133
+ - **`git`** — must be available on `PATH`. Used to find tags and retrieve commit history.
134
+ - **`git-cliff`** — pinned to an exact version and fetched via `npx` on first invocation, then served from npx's cache. No need to install it as a dev dependency. The pin moves only when a new release-kit version names a newer git-cliff, so two releases cut months apart run the same changelog generator.
937
135
 
938
136
  ## Readiness checks
939
137
 
940
- release-kit publishes two [readyup](https://www.npmjs.com/package/readyup) kits that check a consuming repo against the release it has installed: the `default` kit covers release-kit's own setup — workflows matching the current templates, config free of removed fields, sync-labels wiring — and `npm-auto-publish` covers a repo's OIDC-based npm publishing setup. Both ship inside the package, so they check against the version you installed rather than whatever a repository ref happens to point at, and a check added in a release reaches your repo on upgrade.
941
-
942
- Add `readyup` as a devDependency, then name release-kit in its config:
138
+ release-kit publishes two [readyup](https://www.npmjs.com/package/readyup) kits: `default` checks release-kit's own setup, and `npm-auto-publish` checks OIDC-based npm publishing. Add `readyup` as a devDependency, then name release-kit in its config:
943
139
 
944
140
  ```ts
945
141
  // .config/readyup.config.ts
@@ -951,186 +147,33 @@ export default defineRdyConfig({
951
147
  ```
952
148
 
953
149
  ```bash
954
- rdy run --packages # every kit each listed package publishes
955
- rdy run --from npm:@williamthorsen/release-kit # the default kit alone
956
- rdy run --from npm:@williamthorsen/release-kit npm-auto-publish # the npm-auto-publish kit
957
- rdy list --from npm:@williamthorsen/release-kit # what release-kit publishes
958
- ```
959
-
960
- `--packages` is the form that survives release-kit publishing further kits. Every form needs `readyup` 0.23 or later, and `@williamthorsen/release-kit` as a _direct_ devDependency: a strict pnpm layout links nothing else into the project, so a transitive copy is unreachable.
961
-
962
- `npm-auto-publish` queries the npm registry for each package's trusted publisher, so it needs an authenticated npm session and is meant to be invoked deliberately rather than swept up by an unattended run. Without a login it reports one error naming the missing login and blocks the rest of the `packages` checklist, rather than reporting a trusted-publisher failure for every package.
963
-
964
- These kits are no longer reachable through `rdy run --from github:williamthorsen/node-monorepo-tools`. Repos still using that form should switch to one of the invocations above.
965
-
966
- ## External dependencies
967
-
968
- This package shells out to two external tools:
969
-
970
- - **`git`** — must be available on `PATH`. Used to find tags and retrieve commit history.
971
- - **`git-cliff`** — pinned to an exact version and fetched via `npx` on first invocation, then served from npx's cache. No need to install it as a dev dependency. The pin moves only when a new release-kit version names a newer git-cliff, so two releases cut months apart run the same changelog generator.
972
-
973
- ## Upgrading from v4 to v5
974
-
975
- 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.
976
-
977
- Minimal worked example for a repo whose pre-v5 tags were `core-v0.2.7` and whose npm name has not changed:
978
-
979
- ```typescript
980
- // .config/release-kit.config.ts
981
- import type { ReleaseKitConfig } from '@williamthorsen/release-kit/config';
982
-
983
- const config: ReleaseKitConfig = {
984
- workspaces: [
985
- {
986
- dir: 'core',
987
- legacyIdentities: [{ name: '@scope/nmr-core', tagPrefix: 'core-v' }],
988
- },
989
- ],
990
- };
991
-
992
- export default config;
150
+ rdy run --packages
993
151
  ```
994
152
 
995
- 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.
996
-
997
- 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.
998
-
999
- See the `legacyIdentities` entry in the [`WorkspaceOverride`](#workspaceoverride) section for the config shape.
153
+ `rdy run` needs `readyup` 0.23 or later, and `@williamthorsen/release-kit` as a _direct_ devDependency: a strict pnpm layout links nothing else into the project; therefore, a transitive copy is unreachable.
1000
154
 
1001
- ## Using `deriveWorkspaceConfig()` for manual configuration
1002
-
1003
- 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:
1004
-
1005
- ```typescript
1006
- import { deriveWorkspaceConfig } from '@williamthorsen/release-kit';
1007
-
1008
- // packages/arrays/package.json contains `"name": "@scope/arrays"`
1009
- deriveWorkspaceConfig('packages/arrays');
1010
- // => {
1011
- // dir: 'arrays',
1012
- // name: '@scope/arrays',
1013
- // tagPrefix: 'arrays-v',
1014
- // workspacePath: 'packages/arrays',
1015
- // packageFiles: ['packages/arrays/package.json'],
1016
- // changelogPaths: ['packages/arrays'],
1017
- // paths: ['packages/arrays/**'],
1018
- // }
1019
- ```
1020
-
1021
- `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`.
1022
-
1023
- 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.
1024
-
1025
- ## Legacy script-based approach
1026
-
1027
- The CLI-driven approach is recommended for new setups. The script-based approach (using `runReleasePrepare` with a manually maintained config) is still supported for backward compatibility.
1028
-
1029
- ```typescript
1030
- // .github/scripts/release.config.ts
1031
- import type { MonorepoReleaseConfig } from '@williamthorsen/release-kit';
1032
- import { deriveWorkspaceConfig } from '@williamthorsen/release-kit';
1033
-
1034
- export const config: MonorepoReleaseConfig = {
1035
- workspaces: [deriveWorkspaceConfig('packages/arrays'), deriveWorkspaceConfig('packages/strings')],
1036
- formatCommand: 'npx prettier --write',
1037
- };
1038
- ```
1039
-
1040
- ```typescript
1041
- // .github/scripts/release-prepare.ts
1042
- import { runReleasePrepare } from '@williamthorsen/release-kit';
1043
- import { config } from './release.config.ts';
1044
-
1045
- runReleasePrepare(config);
1046
- ```
1047
-
1048
- The key difference: the script-based approach requires manually listing every workspace, while the CLI auto-discovers them from `pnpm-workspace.yaml`.
1049
-
1050
- ## Breaking changes
1051
-
1052
- ### `resolveReleaseTags` takes workspaces; `WorkspaceConfig` requires `workspacePath`
1053
-
1054
- 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.
1055
-
1056
- - `resolveReleaseTags` signature changed from `(workspaceMap?: Map<string, string>)` to `(workspaces?: readonly WorkspaceConfig[])`.
1057
- - `WorkspaceConfig` gained a required `workspacePath: string` field.
1058
-
1059
- Replace direct `Map`-based calls with `deriveWorkspaceConfig()`, which now populates `workspacePath` for you:
1060
-
1061
- ```diff
1062
- -import { resolveReleaseTags } from '@williamthorsen/release-kit';
1063
- -
1064
- -const workspaceMap = new Map([['core', 'packages/core']]);
1065
- -resolveReleaseTags(workspaceMap);
1066
- +import { deriveWorkspaceConfig, resolveReleaseTags } from '@williamthorsen/release-kit';
1067
- +
1068
- +resolveReleaseTags([deriveWorkspaceConfig('packages/core')]);
1069
- ```
1070
-
1071
- If you construct `WorkspaceConfig` objects directly, add `workspacePath` alongside the other required fields.
1072
-
1073
- ### `release-kit publish` and `release-kit push` replace `--only` with `--tags`
1074
-
1075
- 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.
1076
-
1077
- Local usage mapping:
1078
-
1079
- ```diff
1080
- -release-kit publish --only=core
1081
- +release-kit publish --tags=core-v1.3.0
1082
-
1083
- -release-kit push --only=core,cli
1084
- +release-kit push --tags=core-v1.3.0,cli-v0.5.0
1085
- ```
1086
-
1087
- 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.
1088
-
1089
- ### GitHub Release creation moved to its own command and workflow
1090
-
1091
- `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.
1092
-
1093
- 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:
1094
-
1095
- ```yaml
1096
- name: Create GitHub Release
1097
- on:
1098
- push:
1099
- tags:
1100
- - '*-v[0-9]*.[0-9]*.[0-9]*'
1101
- permissions:
1102
- contents: write
1103
- jobs:
1104
- create-github-release:
1105
- uses: williamthorsen/node-monorepo-tools/.github/workflows/create-github-release.reusable.yaml@workflow/create-github-release-v1
1106
- with:
1107
- tag: ${{ github.ref_name }}
1108
- ```
1109
-
1110
- 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>[,...]`.
1111
-
1112
- ### v1.1.0: `formatCommand` receives file paths as trailing arguments
1113
-
1114
- 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.
1115
-
1116
- If your format command does not accept file arguments, update it to one that does:
1117
-
1118
- ```diff
1119
- -formatCommand: 'pnpm run fmt',
1120
- +formatCommand: 'npx prettier --write',
1121
- ```
1122
-
1123
- ### v1.1.0: `git-cliff` is no longer a required dev dependency
1124
-
1125
- `git-cliff` is now invoked via `npx` instead of requiring it as a dev dependency. You can remove it from your `devDependencies`. release-kit names an exact version in the invocation, so the version you get is decided by the release-kit version you install rather than by whatever the registry serves that day.
155
+ [Readiness checks](docs/readiness-checks.md) covers running each kit alone and what `npm-auto-publish` needs from the npm session.
1126
156
 
1127
157
  ## Migration from changesets
1128
158
 
1129
159
  1. Add `@williamthorsen/release-kit` as a dev dependency.
1130
160
  2. Remove `@changesets/cli` from dev dependencies. The [default readyup kit](#readiness-checks) reports a repo that still declares it.
1131
161
  3. Delete the `.changeset/` directory.
1132
- 4. Run `npx @williamthorsen/release-kit init` to scaffold workflow and config files.
162
+ 4. Run `npx @williamthorsen/release-kit init` to scaffold the release workflows.
1133
163
  5. Remove `changeset:*` scripts from `package.json` (no replacement needed — the CLI handles everything).
1134
164
  6. Create an initial version tag for each package (e.g., `git tag v1.0.0` or `git tag arrays-v1.0.0`).
1135
165
 
1136
166
  No cliff config copy is needed — the bundled template is used automatically. To customize, run `release-kit init --with-config`.
167
+
168
+ ## Documentation
169
+
170
+ - [Configuration](docs/configuration.md): `ReleaseKitConfig` fields, workspace overrides and legacy identities, retired packages, tag prefixes, and version patterns
171
+ - [Project releases](docs/project-releases.md): the `project` block, and how a project release interacts with `prepare`'s flags
172
+ - [Releasing](docs/releasing.md): `init`, `prepare`'s flags and release-notes previews, and the release workflow
173
+ - [Publishing](docs/publishing.md): `publish`, the publishability filter, and `create-github-release`
174
+ - [Changelogs](docs/changelogs.md): the `changelog.json` item schema, the git-cliff config, and release-notes injection
175
+ - [Editorial overrides](docs/editorial-overrides.md): correcting generated changelog entries, and `overrides validate`
176
+ - [Work types and tiers](docs/work-types.md): tiers, the `!` policy, markers, custom work types, and `work-types check` and `sync`
177
+ - [Labels](docs/labels.md): `sync-labels`, label configuration, the workflow's triggers, and the `label-map.json` schema
178
+ - [Readiness checks](docs/readiness-checks.md): both readyup kits, and how to run each
179
+ - [Programmatic API](docs/api.md): `deriveWorkspaceConfig()`, the script-based approach, and `resolveReleaseTags`