@williamthorsen/release-kit 5.1.0 → 5.2.1

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 (50) hide show
  1. package/CHANGELOG.md +113 -65
  2. package/README.md +147 -70
  3. package/cliff.toml.template +26 -17
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +96 -3
  6. package/dist/esm/buildChangelogEntries.d.ts +1 -0
  7. package/dist/esm/buildChangelogEntries.js +39 -25
  8. package/dist/esm/checkWorkTypesDrift.d.ts +11 -0
  9. package/dist/esm/checkWorkTypesDrift.js +110 -0
  10. package/dist/esm/collectPolicyViolations.d.ts +6 -0
  11. package/dist/esm/collectPolicyViolations.js +15 -0
  12. package/dist/esm/createGithubRelease.d.ts +12 -2
  13. package/dist/esm/createGithubRelease.js +12 -8
  14. package/dist/esm/createGithubReleaseCommand.js +2 -7
  15. package/dist/esm/decideRelease.d.ts +3 -0
  16. package/dist/esm/decideRelease.js +19 -3
  17. package/dist/esm/defaults.d.ts +7 -0
  18. package/dist/esm/defaults.js +41 -20
  19. package/dist/esm/deriveWorkspaceConfig.js +3 -0
  20. package/dist/esm/determineBumpFromCommits.d.ts +6 -1
  21. package/dist/esm/determineBumpFromCommits.js +9 -3
  22. package/dist/esm/generateChangelogs.js +14 -29
  23. package/dist/esm/loadConfig.js +14 -22
  24. package/dist/esm/parseCommitMessage.d.ts +8 -2
  25. package/dist/esm/parseCommitMessage.js +32 -3
  26. package/dist/esm/publishCommand.js +21 -2
  27. package/dist/esm/releasePrepare.js +39 -15
  28. package/dist/esm/releasePrepareMono.js +26 -3
  29. package/dist/esm/releasePrepareProject.js +13 -1
  30. package/dist/esm/renderReleaseNotes.js +2 -1
  31. package/dist/esm/reportPrepare.js +18 -0
  32. package/dist/esm/resolveCommandTags.js +16 -6
  33. package/dist/esm/resolveReleaseTags.d.ts +8 -1
  34. package/dist/esm/resolveReleaseTags.js +11 -7
  35. package/dist/esm/runGitCliff.d.ts +2 -0
  36. package/dist/esm/runGitCliff.js +27 -0
  37. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  38. package/dist/esm/stripEmojiPrefix.js +7 -0
  39. package/dist/esm/syncWorkTypes.d.ts +10 -0
  40. package/dist/esm/syncWorkTypes.js +90 -0
  41. package/dist/esm/types.d.ts +15 -0
  42. package/dist/esm/work-types.json +127 -0
  43. package/dist/esm/work-types.schema.json +73 -0
  44. package/dist/esm/workTypesData.d.ts +14 -0
  45. package/dist/esm/workTypesData.js +59 -0
  46. package/dist/esm/workTypesUtils.d.ts +5 -0
  47. package/dist/esm/workTypesUtils.js +16 -0
  48. package/package.json +6 -3
  49. package/dist/esm/version.d.ts +0 -1
  50. package/dist/esm/version.js +0 -4
package/README.md CHANGED
@@ -5,49 +5,13 @@ Version-bumping and changelog-generation toolkit for release workflows.
5
5
  Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs with `git-cliff`.
6
6
 
7
7
  <!-- section:release-notes -->
8
- ## Release notes — v5.1.0 (2026-04-30)
8
+ ## Release notes — v5.2.1 (2026-05-05)
9
9
 
10
- ### Bug fixes
10
+ ### 🐛 Bug fixes
11
11
 
12
- - Make publish's clean-tree safety gate reachable (#311)
12
+ - Soft-skip tags with no changelog entry under --tags (#366)
13
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.
14
+ Fixes an issue where `release-kit create-github-release --tags <tag>` exited 1 failing the calling CI workflow when the requested tag had no changelog entry. Tooling-only releases (those whose changelog generator legitimately omits an entry) are now soft-skipped with an info-level summary, the same as releases skipped because their entry has no audience-relevant content. Typo protection is preserved: passing an unknown tag to `--tags` still exits 1.
51
15
  <!-- /section:release-notes -->
52
16
 
53
17
  ## Installation
@@ -151,16 +115,17 @@ The config file supports both `export default config` and `export const config =
151
115
 
152
116
  ### `ReleaseKitConfig` reference
153
117
 
154
- | Field | Type | Description |
155
- | ----------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
156
- | `cliffConfigPath` | `string` | Explicit path to cliff config. If omitted, resolved automatically: `.config/git-cliff.toml` → `cliff.toml` → bundled template |
157
- | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
158
- | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
159
- | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
160
- | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commits |
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` |
118
+ | Field | Type | Description |
119
+ | ------------------ | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `cliffConfigPath` | `string` | Explicit path to cliff config. If omitted, resolved automatically: `.config/git-cliff.toml` → `cliff.toml` → bundled template |
121
+ | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
122
+ | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
123
+ | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
124
+ | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commits |
125
+ | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
126
+ | `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 |
127
+ | `retiredPackages` | `RetiredPackage[]` | Packages that once lived in this repo but have been extracted or removed; suppresses undeclared-tag-prefix warnings |
128
+ | `project` | `ProjectConfig` | Opt-in project-level release block. Declaring `project: {}` (even empty) enables a project-release stage in `prepare` |
164
129
 
165
130
  All fields are optional.
166
131
 
@@ -313,30 +278,93 @@ interface VersionPatterns {
313
278
 
314
279
  Default: `{ major: ['!'], minor: ['feat'] }`
315
280
 
316
- ### Default work types
281
+ ### Work types and tiers
282
+
283
+ 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.
284
+
285
+ | Tier | Key | Header | Aliases | `!` policy |
286
+ | -------- | ----------- | ------------------------- | ------------- | ------------ |
287
+ | Public | `feat` | 🎉 Features | `feature` | optional |
288
+ | Public | `drop` | 🪦 Removed | | **required** |
289
+ | Public | `deprecate` | 🗑️ Deprecated | | forbidden |
290
+ | Public | `fix` | 🐛 Bug fixes | `bugfix` | forbidden |
291
+ | Public | `sec` | 🔒 Security | `security` | optional |
292
+ | Public | `perf` | ⚡ Performance | `performance` | forbidden |
293
+ | Internal | `internal` | 🏗️ Internal features | `utility` | forbidden |
294
+ | Internal | `refactor` | ♻️ Refactoring | | forbidden |
295
+ | Internal | `tests` | 🧪 Tests | `test` | forbidden |
296
+ | Process | `tooling` | ⚙️ Tooling | | forbidden |
297
+ | Process | `ci` | 👷 CI | | forbidden |
298
+ | Process | `deps` | 📦 Dependencies | `dep` | forbidden |
299
+ | Process | `ai` | 🤖 Agentic support | | forbidden |
300
+ | Process | `docs` | 📚 Documentation | `doc` | forbidden |
301
+ | Process | `fmt` | (excluded from changelog) | | forbidden |
302
+
303
+ #### Tier semantics
304
+
305
+ - **Public** — visible to all audiences. Public-tier sections appear in both public release notes and dev changelogs.
306
+ - **Internal** — dev-only. Internal-tier sections appear in dev changelogs but not in public-facing release notes.
307
+ - **Process** — dev-only. Same audience treatment as Internal.
308
+
309
+ 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.
310
+
311
+ #### `docs` reclassification
312
+
313
+ `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.
314
+
315
+ #### `utility` alias
316
+
317
+ `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.
318
+
319
+ #### `!` (breaking change) policy
320
+
321
+ Each work-type carries a `breakingPolicy` value:
322
+
323
+ - `optional` (`feat`, `sec`) — `!` is allowed; both `type:` and `type!:` parse cleanly.
324
+ - `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.
325
+ - `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.
326
+
327
+ ##### Two-tier policy enforcement
328
+
329
+ The `!` policy operates at two distinct levels with different semantics:
317
330
 
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) | |
331
+ - **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._
332
+ - **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.
334
333
 
335
- Work types from your config are merged with these defaults by keyyour entries override or extend, they don't replace the full set.
334
+ 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.
336
335
 
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.
336
+ 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:
338
337
 
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.
338
+ ```
339
+ arrays
340
+ Found 1 commits since arrays-v1.0.0
341
+ ⚠️ 1 policy violation:
342
+ · def5678 'internal!: refactor cache' — type 'internal' at prefix surface
343
+ Bumping versions (patch)...
344
+ 📦 1.0.0 → 1.0.1 (patch)
345
+ ```
346
+
347
+ 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.
348
+
349
+ #### `🚨 **Breaking:**` bullet marker
350
+
351
+ Items whose commit subject carries the `!` prefix (e.g. `feat!`, `drop!`, `feat(api)!`) are rendered with a `🚨 **Breaking:** ` prefix on the bullet:
352
+
353
+ ```markdown
354
+ - 🚨 **Breaking:** Drop legacy /v1 endpoint
355
+ ```
356
+
357
+ 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.
358
+
359
+ #### `fmt`
360
+
361
+ `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.
362
+
363
+ #### Custom work types
364
+
365
+ 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.
366
+
367
+ 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.
340
368
 
341
369
  ## CLI reference
342
370
 
@@ -404,6 +432,33 @@ release-kit prepare --only arrays --set-version 1.0.0
404
432
 
405
433
  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
434
 
435
+ ### `release-kit publish`
436
+
437
+ Publish packages that have release tags on HEAD. The publish workflow's reusable workflow `publish.reusable.yaml` invokes this command in CI.
438
+
439
+ | Flag | Description |
440
+ | ---------------------- | ---------------------------------------------------------------------------- |
441
+ | `--dry-run` | Preview without publishing |
442
+ | `--no-git-checks` | Skip the clean-working-tree check |
443
+ | `--tags=tag1,tag2,...` | Only publish the named tags (comma-separated, full tag names) |
444
+ | `--provenance` | Generate provenance statement (requires OIDC, not supported by classic yarn) |
445
+ | `--help`, `-h` | Show help |
446
+
447
+ #### Publishability filter
448
+
449
+ `publish` operates 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`, get a `CHANGELOG.md` entry, and get a GitHub Release via `release-kit create-github-release` — only the registry publish step is skipped. Other commands ignore this filter and operate on private workspaces unchanged.
450
+
451
+ The filter behaves differently depending on whether `--tags` is provided:
452
+
453
+ - **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.
454
+ - **With `--tags`** (explicit naming): if any named tag points at an unpublishable workspace, `release-kit publish` exits 1 with one error line per unpublishable tag, citing `package.json#private`. Explicit naming surfaces the contradiction rather than silently dropping the tag.
455
+
456
+ Example output when an explicit tag is unpublishable:
457
+
458
+ ```
459
+ Error: basic-v1.0.0 (packages/basic) cannot be published: package.json#private is true.
460
+ ```
461
+
407
462
  ### `release-kit create-github-release`
408
463
 
409
464
  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.
@@ -447,6 +502,28 @@ Scaffolded files:
447
502
  - `.config/release-kit.config.ts` — starter config with commented-out customization examples (with `--with-config`)
448
503
  - `.config/git-cliff.toml` — copied from the bundled template (with `--with-config`)
449
504
 
505
+ ### `release-kit work-types`
506
+
507
+ Manage the canonical work-types taxonomy used by changelog and release-notes generation.
508
+
509
+ | Subcommand | Description |
510
+ | ---------- | ----------------------------------------------------------------------------------- |
511
+ | `check` | Compare the local `work-types.json` against the upstream codeassembly canonical |
512
+ | `sync` | Overwrite the local `work-types.json` with the upstream contents (after validation) |
513
+
514
+ `check` exit codes:
515
+
516
+ | Code | Meaning |
517
+ | ---- | --------------------------------------------------------------------------------- |
518
+ | `0` | Match (or upstream missing — transitional warning printed) |
519
+ | `1` | Drift detected |
520
+ | `2` | Network error or non-OK HTTP response |
521
+ | `3` | Schema mismatch (upstream JSON does not parse or fails the top-level shape check) |
522
+
523
+ 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.
524
+
525
+ These commands are also exposed as `nmr work-types:check` / `nmr work-types:sync` from any package directory.
526
+
450
527
  ### `release-kit sync-labels`
451
528
 
452
529
  Manage GitHub label definitions via config-driven YAML files.
@@ -10,6 +10,13 @@
10
10
  # Only ticketed commits (leading #NN, PROJ-NN, or ##) are included.
11
11
  # Unticketed maintenance commits (deps upgrades, tooling tweaks) are skipped.
12
12
  # Use ## as a synthetic ticket prefix for ad-hoc commits that belong in the changelog.
13
+ #
14
+ # Section order: each `group` value carries a hidden `<!-- NN -->` HTML-comment
15
+ # prefix encoding the canonical row position. tera's `group_by` filter sorts
16
+ # groups lexicographically by string, and the body template's `striptags`
17
+ # filter erases the comment from the rendered heading. Numbering is per
18
+ # unique group (not per parser entry) — all parsers routing to the same group
19
+ # share the same prefix.
13
20
 
14
21
  [changelog]
15
22
  header = """
@@ -61,24 +68,26 @@ commit_preprocessors = []
61
68
  commit_parsers = [
62
69
  { message = "^release:", skip = true },
63
70
  { message = "^Merge", skip = true },
64
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?ai(!)?:", group = "Agentic support" },
65
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?bugfix(!)?:", group = "Bug fixes" },
66
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?ci(!)?:", group = "CI" },
67
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?deprecate(!)?:", group = "Deprecated" },
68
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?deps?(!)?:", group = "Dependencies" },
69
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?docs?(!)?:", group = "Documentation" },
70
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?feat(!)?:", group = "Features" },
71
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?feature(!)?:", group = "Features" },
72
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?fix(!)?:", group = "Bug fixes" },
71
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?feat(!)?:", group = "<!-- 01 -->🎉 Features" },
72
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?feature(!)?:", group = "<!-- 01 -->🎉 Features" },
73
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?drop(!)?:", group = "<!-- 02 -->🪦 Removed" },
74
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?deprecate(!)?:", group = "<!-- 03 -->🗑️ Deprecated" },
75
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?fix(!)?:", group = "<!-- 04 -->🐛 Bug fixes" },
76
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?bugfix(!)?:", group = "<!-- 04 -->🐛 Bug fixes" },
77
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?sec(!)?:", group = "<!-- 05 -->🔒 Security" },
78
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?security(!)?:", group = "<!-- 05 -->🔒 Security" },
79
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?perf(!)?:", group = "<!-- 06 -->⚡ Performance" },
80
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?performance(!)?:", group = "<!-- 06 -->⚡ Performance" },
81
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?internal(!)?:", group = "<!-- 07 -->🏗️ Internal features" },
82
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?utility(!)?:", group = "<!-- 07 -->🏗️ Internal features" },
83
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?refactor(!)?:", group = "<!-- 08 -->♻️ Refactoring" },
84
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?tests?(!)?:", group = "<!-- 09 -->🧪 Tests" },
85
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?tooling(!)?:", group = "<!-- 10 -->⚙️ Tooling" },
86
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?ci(!)?:", group = "<!-- 11 -->👷 CI" },
87
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?deps?(!)?:", group = "<!-- 12 -->📦 Dependencies" },
88
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?ai(!)?:", group = "<!-- 13 -->🤖 Agentic support" },
89
+ { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?docs?(!)?:", group = "<!-- 14 -->📚 Documentation" },
73
90
  { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?fmt(!)?:", skip = true },
74
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?internal(!)?:", group = "Internal" },
75
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?perf(!)?:", group = "Performance" },
76
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?performance(!)?:", group = "Performance" },
77
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?refactor(!)?:", group = "Refactoring" },
78
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?sec(!)?:", group = "Security" },
79
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?security(!)?:", group = "Security" },
80
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?tests?(!)?:", group = "Tests" },
81
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([\\w-]+\\|)?tooling(!)?:", group = "Tooling" },
82
91
  # Skip unticketed commits (maintenance, deps, initial scaffolding, etc.)
83
92
  { message = ".*", skip = true },
84
93
  ]
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- fca70d83414ef35352e152c9e916f24705c93c5556b14c16388a5b791752f075
1
+ 704a70608ce284c9f8ef2dda2e6604a5f38ac0c995c9419ae39206ed053775c6
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
2
+ import { parseArgs, readPackageVersion, translateParseError } from "@williamthorsen/nmr-core";
3
+ import { checkWorkTypesDrift } from "../checkWorkTypesDrift.js";
3
4
  import { commitCommand } from "../commitCommand.js";
4
5
  import { createGithubReleaseCommand } from "../createGithubReleaseCommand.js";
5
6
  import { initCommand } from "../init/initCommand.js";
@@ -10,8 +11,9 @@ import { showTagPrefixesCommand } from "../showTagPrefixesCommand.js";
10
11
  import { generateCommand } from "../sync-labels/generateCommand.js";
11
12
  import { syncLabelsInitCommand } from "../sync-labels/initCommand.js";
12
13
  import { syncLabelsCommand } from "../sync-labels/syncCommand.js";
14
+ import { syncWorkTypes } from "../syncWorkTypes.js";
13
15
  import { tagCommand } from "../tagCommand.js";
14
- import { VERSION } from "../version.js";
16
+ const VERSION = readPackageVersion(import.meta.url);
15
17
  function showUsage() {
16
18
  console.info(`
17
19
  Usage: release-kit <command> [options]
@@ -26,6 +28,7 @@ Commands:
26
28
  show-tag-prefixes Show derived and declared legacy tag prefixes per workspace
27
29
  init Initialize release-kit in the current repository
28
30
  sync-labels Manage GitHub label synchronization
31
+ work-types Check for or sync work-type taxonomy drift against the upstream canonical
29
32
 
30
33
  Options:
31
34
  --dry-run Preview changes without writing files
@@ -171,6 +174,49 @@ legacy tag prefixes. Surfaces any release-shaped tags whose prefix is neither a
171
174
  derived prefix nor declared in \`legacyIdentities\`, with a copy-pasteable
172
175
  config snippet.
173
176
 
177
+ Options:
178
+ --help, -h Show this help message
179
+ `);
180
+ }
181
+ function showWorkTypesHelp() {
182
+ console.info(`
183
+ Usage: release-kit work-types <subcommand>
184
+
185
+ Manage the canonical work-types taxonomy used by changelog and release-notes generation.
186
+
187
+ Subcommands:
188
+ check Compare the local work-types.json against the upstream codeassembly canonical
189
+ sync Overwrite the local work-types.json with the upstream contents
190
+
191
+ Exit codes (check):
192
+ 0 Match (or upstream missing \u2014 transitional warning printed)
193
+ 1 Drift detected
194
+ 2 Network error
195
+ 3 Schema mismatch
196
+
197
+ Options:
198
+ --help, -h Show this help message
199
+ `);
200
+ }
201
+ function showWorkTypesCheckHelp() {
202
+ console.info(`
203
+ Usage: release-kit work-types check
204
+
205
+ Compare the local work-types.json against the upstream codeassembly canonical and report
206
+ drift. Exit 0 on match, 1 on drift, 0 + warning when upstream is missing (transitional),
207
+ 2 on network error, 3 on schema mismatch.
208
+
209
+ Options:
210
+ --help, -h Show this help message
211
+ `);
212
+ }
213
+ function showWorkTypesSyncHelp() {
214
+ console.info(`
215
+ Usage: release-kit work-types sync
216
+
217
+ Fetch the upstream work-types.json, validate its top-level shape, and overwrite the local
218
+ copy with the upstream content (formatted with 2-space indent + trailing newline).
219
+
174
220
  Options:
175
221
  --help, -h Show this help message
176
222
  `);
@@ -179,7 +225,9 @@ function showPublishHelp() {
179
225
  console.info(`
180
226
  Usage: release-kit publish [options]
181
227
 
182
- Publish packages that have release tags on HEAD.
228
+ Publish packages that have release tags on HEAD. Operates only on workspaces where
229
+ package.json#private is absent or false. Without --tags, unpublishable workspaces are
230
+ silently filtered out. With --tags, naming an unpublishable tag is an error.
183
231
 
184
232
  Options:
185
233
  --dry-run Preview without publishing
@@ -341,6 +389,51 @@ if (command === "sync-labels") {
341
389
  showSyncLabelsHelp();
342
390
  process.exit(1);
343
391
  }
392
+ if (command === "work-types") {
393
+ const subcommand = flags[0];
394
+ const subflags = flags.slice(1);
395
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === void 0) {
396
+ showWorkTypesHelp();
397
+ process.exit(0);
398
+ }
399
+ if (subcommand === "check") {
400
+ if (subflags.some((f) => f === "--help" || f === "-h")) {
401
+ showWorkTypesCheckHelp();
402
+ process.exit(0);
403
+ }
404
+ if (subflags.length > 0) {
405
+ console.error(`Error: Unknown option: ${subflags[0]}`);
406
+ process.exit(1);
407
+ }
408
+ const result = await checkWorkTypesDrift();
409
+ if (result.exitCode === 0) {
410
+ console.info(result.message);
411
+ } else {
412
+ console.error(result.message);
413
+ }
414
+ process.exit(result.exitCode);
415
+ }
416
+ if (subcommand === "sync") {
417
+ if (subflags.some((f) => f === "--help" || f === "-h")) {
418
+ showWorkTypesSyncHelp();
419
+ process.exit(0);
420
+ }
421
+ if (subflags.length > 0) {
422
+ console.error(`Error: Unknown option: ${subflags[0]}`);
423
+ process.exit(1);
424
+ }
425
+ const result = await syncWorkTypes();
426
+ if (result.exitCode === 0) {
427
+ console.info(result.message);
428
+ } else {
429
+ console.error(result.message);
430
+ }
431
+ process.exit(result.exitCode);
432
+ }
433
+ console.error(`Error: Unknown subcommand: ${subcommand}`);
434
+ showWorkTypesHelp();
435
+ process.exit(1);
436
+ }
344
437
  console.error(`Error: Unknown command: ${command}`);
345
438
  showUsage();
346
439
  process.exit(1);
@@ -1,3 +1,4 @@
1
1
  import type { GenerateChangelogOptions } from './generateChangelogs.ts';
2
2
  import type { ChangelogEntry, ReleaseConfig } from './types.ts';
3
+ export declare function stripGroupDecorations(group: string): string;
3
4
  export declare function buildChangelogEntries(config: Pick<ReleaseConfig, 'cliffConfigPath' | 'changelogJson'>, tag: string, options?: GenerateChangelogOptions): ChangelogEntry[];
@@ -1,31 +1,32 @@
1
- import { execFileSync } from "node:child_process";
2
- import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
1
  import { extractVersion } from "./changelogJsonUtils.js";
2
+ import { DEFAULT_WORK_TYPES } from "./defaults.js";
3
+ import { COMMIT_PREPROCESSOR_PATTERNS } from "./parseCommitMessage.js";
6
4
  import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
5
+ import { runGitCliff } from "./runGitCliff.js";
6
+ import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
7
7
  import { isRecord, isUnknownArray } from "./typeGuards.js";
8
+ const HTML_COMMENT_PREFIX_PATTERN = /^<!--[^>]*-->/;
9
+ const CANONICAL_SECTION_ORDER = new Map(
10
+ Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index])
11
+ );
12
+ function canonicalSectionPriority(title) {
13
+ const index = CANONICAL_SECTION_ORDER.get(stripGroupDecorations(title));
14
+ return index ?? Number.POSITIVE_INFINITY;
15
+ }
16
+ function stripGroupDecorations(group) {
17
+ return stripEmojiPrefix(group.replace(HTML_COMMENT_PREFIX_PATTERN, ""));
18
+ }
8
19
  function buildChangelogEntries(config, tag, options) {
9
20
  const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
10
- let cliffConfigPath = resolvedConfigPath;
11
- let tempDir;
12
- if (resolvedConfigPath.endsWith(".template")) {
13
- tempDir = mkdtempSync(join(tmpdir(), "cliff-"));
14
- cliffConfigPath = join(tempDir, "cliff.toml");
15
- copyFileSync(resolvedConfigPath, cliffConfigPath);
16
- }
17
- const args = ["--config", cliffConfigPath, "--context", "--tag", tag];
21
+ const cliffArgs = ["--context", "--tag", tag];
18
22
  if (options?.tagPattern !== void 0) {
19
- args.push("--tag-pattern", options.tagPattern);
23
+ cliffArgs.push("--tag-pattern", options.tagPattern);
20
24
  }
21
25
  for (const includePath of options?.includePaths ?? []) {
22
- args.push("--include-path", includePath);
26
+ cliffArgs.push("--include-path", includePath);
23
27
  }
24
28
  try {
25
- const contextJson = execFileSync("npx", ["--yes", "git-cliff", ...args], {
26
- encoding: "utf8",
27
- stdio: ["pipe", "pipe", "inherit"]
28
- });
29
+ const contextJson = runGitCliff(resolvedConfigPath, cliffArgs, ["pipe", "pipe", "inherit"]);
29
30
  const releases = parseCliffContext(contextJson);
30
31
  const devOnlySections = new Set(config.changelogJson.devOnlySections);
31
32
  return transformReleases(releases, devOnlySections);
@@ -33,10 +34,6 @@ function buildChangelogEntries(config, tag, options) {
33
34
  throw new Error(
34
35
  `Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`
35
36
  );
36
- } finally {
37
- if (tempDir !== void 0) {
38
- rmSync(tempDir, { recursive: true, force: true });
39
- }
40
37
  }
41
38
  }
42
39
  function parseCliffContext(json) {
@@ -76,6 +73,7 @@ function toCliffContextCommit(value) {
76
73
  }
77
74
  function transformReleases(releases, devOnlySections) {
78
75
  const entries = [];
76
+ const devOnlyNormalised = new Set([...devOnlySections].map(stripGroupDecorations));
79
77
  for (const release of releases) {
80
78
  if (release.version === void 0) {
81
79
  continue;
@@ -84,9 +82,10 @@ function transformReleases(releases, devOnlySections) {
84
82
  const date = release.timestamp !== void 0 ? new Date(release.timestamp * 1e3).toISOString().slice(0, 10) : "unreleased";
85
83
  const sectionMap = /* @__PURE__ */ new Map();
86
84
  for (const commit of release.commits ?? []) {
87
- const group = commit.group ?? "Other";
85
+ const group = stripCommentPrefix(commit.group ?? "Other");
88
86
  const description = extractDescription(commit.message);
89
87
  const body = extractBody(commit.message);
88
+ const breaking = subjectHasBreakingMarker(commit.message);
90
89
  let items = sectionMap.get(group);
91
90
  if (items === void 0) {
92
91
  items = [];
@@ -96,6 +95,9 @@ function transformReleases(releases, devOnlySections) {
96
95
  if (body !== void 0) {
97
96
  item.body = body;
98
97
  }
98
+ if (breaking) {
99
+ item.breaking = true;
100
+ }
99
101
  items.push(item);
100
102
  }
101
103
  const sections = [];
@@ -105,16 +107,27 @@ function transformReleases(releases, devOnlySections) {
105
107
  }
106
108
  sections.push({
107
109
  title,
108
- audience: devOnlySections.has(title) ? "dev" : "all",
110
+ audience: devOnlyNormalised.has(stripGroupDecorations(title)) ? "dev" : "all",
109
111
  items
110
112
  });
111
113
  }
114
+ sections.sort((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
112
115
  if (sections.length > 0) {
113
116
  entries.push({ version, date, sections });
114
117
  }
115
118
  }
116
119
  return entries;
117
120
  }
121
+ function stripCommentPrefix(group) {
122
+ return group.replace(HTML_COMMENT_PREFIX_PATTERN, "");
123
+ }
124
+ function subjectHasBreakingMarker(message) {
125
+ let subject = message.split("\n", 1)[0] ?? "";
126
+ for (const pattern of COMMIT_PREPROCESSOR_PATTERNS) {
127
+ subject = subject.replace(pattern, "");
128
+ }
129
+ return /^(?:[^|]+\|)?\w+(?:\([^)]+\))?!:/.test(subject);
130
+ }
118
131
  function extractDescription(message) {
119
132
  const firstLine = message.split("\n")[0] ?? message;
120
133
  const afterColon = firstLine.split(": ").slice(1).join(": ");
@@ -155,5 +168,6 @@ function extractBody(message) {
155
168
  return lines.slice(start, end).join("\n").trim();
156
169
  }
157
170
  export {
158
- buildChangelogEntries
171
+ buildChangelogEntries,
172
+ stripGroupDecorations
159
173
  };
@@ -0,0 +1,11 @@
1
+ export declare const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json";
2
+ export interface DriftCheckResult {
3
+ exitCode: 0 | 1 | 2 | 3;
4
+ message: string;
5
+ }
6
+ export interface CheckWorkTypesDriftDependencies {
7
+ localPath?: string;
8
+ fetch?: typeof globalThis.fetch;
9
+ upstreamUrl?: string;
10
+ }
11
+ export declare function checkWorkTypesDrift(dependencies?: CheckWorkTypesDriftDependencies): Promise<DriftCheckResult>;