@williamthorsen/release-kit 11.0.0 → 12.0.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 (69) hide show
  1. package/.meta/changelog.json +62 -2
  2. package/.readyup/kits/default.js +1 -17
  3. package/.readyup/manifest.json +4 -4
  4. package/CHANGELOG.md +40 -0
  5. package/README.md +25 -44
  6. package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
  7. package/dist/esm/attachChangelogDiagnostics.js +11 -0
  8. package/dist/esm/bin/release-kit.js +4 -3
  9. package/dist/esm/buildChangelogEntries.d.ts +59 -17
  10. package/dist/esm/buildChangelogEntries.js +194 -111
  11. package/dist/esm/buildReleaseSummary.js +2 -2
  12. package/dist/esm/changelogOverrides.d.ts +31 -13
  13. package/dist/esm/changelogOverrides.js +138 -58
  14. package/dist/esm/classifyChangelogCommit.d.ts +38 -0
  15. package/dist/esm/classifyChangelogCommit.js +33 -0
  16. package/dist/esm/decideRelease.d.ts +11 -37
  17. package/dist/esm/decideRelease.js +5 -27
  18. package/dist/esm/defaults.d.ts +1 -1
  19. package/dist/esm/defaults.js +3 -0
  20. package/dist/esm/determineBumpType.d.ts +11 -11
  21. package/dist/esm/determineBumpType.js +6 -7
  22. package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
  23. package/dist/esm/enumerateReleaseWindows.js +124 -0
  24. package/dist/esm/generateChangelogs.d.ts +5 -23
  25. package/dist/esm/generateChangelogs.js +0 -14
  26. package/dist/esm/glyphs.d.ts +1 -1
  27. package/dist/esm/help/prepareHelp.d.ts +1 -1
  28. package/dist/esm/help/prepareHelp.js +4 -6
  29. package/dist/esm/index.d.ts +1 -1
  30. package/dist/esm/init/initCommand.js +1 -1
  31. package/dist/esm/init/scaffold.d.ts +0 -2
  32. package/dist/esm/init/scaffold.js +2 -30
  33. package/dist/esm/loadConfig.d.ts +1 -1
  34. package/dist/esm/loadConfig.js +1 -4
  35. package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
  36. package/dist/esm/parseChangeRecordBlock.js +134 -0
  37. package/dist/esm/parseCommitMessage.d.ts +31 -6
  38. package/dist/esm/parseCommitMessage.js +6 -5
  39. package/dist/esm/prepareCommand.js +4 -12
  40. package/dist/esm/releasePrepare.d.ts +7 -8
  41. package/dist/esm/releasePrepare.js +53 -73
  42. package/dist/esm/releasePrepareMono.js +48 -85
  43. package/dist/esm/releasePrepareProject.d.ts +1 -1
  44. package/dist/esm/releasePrepareProject.js +19 -37
  45. package/dist/esm/reportPrepare.js +39 -28
  46. package/dist/esm/types.d.ts +99 -55
  47. package/dist/esm/types.js +1 -1
  48. package/dist/esm/validateConfig.js +4 -0
  49. package/dist/esm/validateOverridesCommand.d.ts +6 -5
  50. package/dist/esm/validateOverridesCommand.js +16 -23
  51. package/docs/changelogs.md +64 -25
  52. package/docs/configuration.md +2 -3
  53. package/docs/editorial-overrides.md +75 -16
  54. package/docs/project-releases.md +4 -4
  55. package/docs/releasing.md +13 -8
  56. package/docs/work-types.md +13 -9
  57. package/package.json +1 -2
  58. package/schemas/changelog-overrides.json +39 -0
  59. package/cliff.toml.template +0 -65
  60. package/dist/esm/collectPolicyViolations.d.ts +0 -15
  61. package/dist/esm/collectPolicyViolations.js +0 -12
  62. package/dist/esm/determineBumpFromCommits.d.ts +0 -17
  63. package/dist/esm/determineBumpFromCommits.js +0 -28
  64. package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
  65. package/dist/esm/getCommitsSinceTarget.js +0 -66
  66. package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
  67. package/dist/esm/resolveCliffConfigPath.js +0 -20
  68. package/dist/esm/runGitCliff.d.ts +0 -38
  69. package/dist/esm/runGitCliff.js +0 -25
@@ -1,26 +1,29 @@
1
1
  # Changelogs
2
2
 
3
- What reaches a published tarball, what each `.meta/changelog.json` item contains, how the git-cliff config is resolved, and how release notes reach a README.
3
+ What reaches a published tarball, what each `.meta/changelog.json` item contains, which commits reach a changelog, how a merge commit's change-record block becomes items, and how release notes reach a README.
4
4
 
5
5
  Neither `CHANGELOG.md` nor `.meta/changelog.json` 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.
6
6
 
7
7
  ## `changelog.json` item schema
8
8
 
9
- Each item under a section in `.meta/changelog.json` carries one required field and four optional ones:
9
+ Each item under a section in `.meta/changelog.json` carries one required field and five optional ones:
10
10
 
11
- | Field | Type | Meaning |
12
- | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `description` | `string` | The bullet headline, taken from the commit subject with the ticket ID and type prefix stripped. |
14
- | `body` | `string` | The commit body, with trailing trailer metadata stripped. |
15
- | `breaking` | `boolean` | Present and `true` where the commit subject carried the `!` prefix and the commit's type permits it. See [`!` (breaking change) policy](work-types.md#-breaking-change-policy). |
16
- | `migration` | `string` | The migration step for a consumer. See below. |
17
- | `hash` | `string` | The full commit SHA, and the key on which an override entry matches. Absent on synthetic propagation entries. |
11
+ | Field | Type | Meaning |
12
+ | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
13
+ | `description` | `string` | The bullet headline, taken from the commit subject with the ticket ID and type prefix stripped, or from a change-record entry's `text` (see [Change-record blocks](#change-record-blocks)). |
14
+ | `body` | `string` | The commit body, with trailing trailer metadata and any `change-record` block stripped. Absent on an item derived from a change-record entry. |
15
+ | `breaking` | `boolean` | Present and `true` where the commit subject carried the `!` prefix, or the change-record entry set `breaking: true`, and the type permits it. See [`!` (breaking change) policy](work-types.md#-breaking-change-policy). |
16
+ | `migration` | `string` | The migration step for a consumer. See below. |
17
+ | `hash` | `string` | The full commit SHA, which an override key matches by prefix. Absent on synthetic propagation entries. |
18
+ | `entry` | `number` | The 1-based position of the change-record entry from which the item derives, which an override key `<hash>:<n>` matches. Absent on an item derived from the commit's title. |
18
19
 
19
20
  Every optional field is omitted rather than emitted as `null`, so a consumer tests for presence.
20
21
 
21
22
  ### The `migration` field
22
23
 
23
- 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:
24
+ 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. An item derived from a change-record entry takes `migration` from the entry instead, and has no `body`.
25
+
26
+ A title-derived item looks like this:
24
27
 
25
28
  ```jsonc
26
29
  {
@@ -34,30 +37,66 @@ Three properties are worth knowing:
34
37
 
35
38
  - **It is independent of `breaking`.** A `deprecate` cannot carry `!` under the default breaking policy, and a `deprecate` still calls for a migration. Filter on `migration` to find every step; filter on `breaking` to find every breaking change.
36
39
  - **`body` keeps the paragraph.** The field is an extraction, not a move, so `CHANGELOG.md` and `.meta/changelog.json` go on agreeing.
37
- - **It is derived, not authored.** `migration` is not a field an override file can set (see [File shape](editorial-overrides.md#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`.
40
+ - **An override cannot set it.** `migration` is not a field an override file can set (see [File shape](editorial-overrides.md#file-shape)). On a title-derived item it is re-derived from whatever `body` an override installs, and cleared where that body carries no labeled paragraph, so overriding `body` changes the migration text. An entry-derived item keeps its entry's `migration` whatever `body` an override installs.
38
41
 
39
42
  The label match is exact: `migration:` and `**Migration:**` are not recognized.
40
43
 
41
- ## cliff.toml setup
44
+ ## What reaches a changelog
45
+
46
+ release-kit splits the history reachable from `HEAD` into one window per release tag that matches the scope's tag prefixes, restricted to commits that touch the scope's `paths`. The bump reads the same items: Its level is the highest that the unreleased window's items call for, so a commit that bumps is one that the changelog lists.
47
+
48
+ A commit whose last `change-record` block records entries yields its items from those entries, as [Change-record blocks](#change-record-blocks) describes. Any other commit in a window reaches the changelog when its subject passes three checks:
49
+
50
+ - **A ticket-ID prefix**, such as `#42 `, `TOOL-123 `, or `## `.
51
+ - **A declared work type**, resolved against the merged [work types](work-types.md).
52
+ - **A type not excluded from the changelog** by `excludedFromChangelog: true`.
53
+
54
+ `release:` commits and merge commits whose subject git wrote (`Merge …`) never reach a changelog, whether or not they carry a block. A commit that fails a check yields no item and raises no bump, and a release left with no surviving commit gets no changelog entry. The prepare report lists a commit of the unreleased window that fails the first or the second check as unparseable, unless a malformed-block warning already reports it; it lists none that fails the third. The commit's type decides the section under which its item appears.
55
+
56
+ Every item of a commit reaches every workspace whose window contains the commit, because windows are selected by the paths that the commit touches.
57
+
58
+ ## Change-record blocks
59
+
60
+ A squash-merge commit composed from a pull request can end with a fenced block whose info string is exactly `change-record`. Its payload is YAML, with the grammar that codeassembly's `change-record.md` specifies. release-kit reads these keys and ignores any other:
61
+
62
+ | Key | Type | Meaning |
63
+ | --------------------- | -------- | ------------------------------------------------------------------------------- |
64
+ | `entries` | list | The change entries, one per outcome of the change, in the order that they read. |
65
+ | `entries[].type` | `string` | A work type, resolved through the declared types and their aliases. Required. |
66
+ | `entries[].text` | `string` | The sentence that reports the outcome. Required. |
67
+ | `entries[].breaking` | boolean | Whether the outcome breaks consumers. |
68
+ | `entries[].scopes` | list | The scopes that the outcome touched, each a string. Validated but not yet used. |
69
+ | `entries[].migration` | `string` | The migration step for a consumer. |
70
+ | `pr_number` | integer | The merged pull request's number, a positive integer. |
71
+ | `ticket_ref` | `string` | The ticket that the change serves. Validated but not yet used. |
72
+
73
+ release-kit reads the **last** `change-record` fence in the message. A key whose value is null reads as absent.
74
+
75
+ ### Items from entries
76
+
77
+ Each entry yields one item, whether or not the commit subject carries a ticket-ID prefix or a declared type:
78
+
79
+ - **Section**: the header of the entry's `type`, so the section order and the audience follow as for a title.
80
+ - **`description`**: the entry's `text` as written, followed by ` (#N)` when the block records `pr_number`.
81
+ - **`breaking`**: set when the entry's `breaking` is `true` and the type's breaking policy permits it.
82
+ - **`migration`**: the entry's `migration`, when present. There is no `body`.
83
+ - **`hash`** and **`entry`**: the commit's hash, and the entry's 1-based position among all the block's entries, including those that yield no item.
84
+
85
+ An entry whose type is excluded from the changelog yields no item. An override targets one of these items by the key `<hash>:<n>`, where `n` is the item's `entry`. A bare `<hash>` key matches every item of the commit, and it fails when it sets `description` or `body` on a commit with several items; see [Keys](editorial-overrides.md#keys).
42
86
 
43
- The package includes a bundled `cliff.toml.template` that is used automatically when no custom config is found. The resolution order:
87
+ ### Fallback to the title
44
88
 
45
- | Priority | Path | Notes |
46
- | -------- | ----------------------------- | ----------------------------------------------- |
47
- | 1 | `cliffConfigPath` in config | Explicit path, returned without existence check |
48
- | 2 | `.config/git-cliff.toml` | Project-level override |
49
- | 3 | `cliff.toml` | Repo root fallback |
50
- | 4 | Bundled `cliff.toml.template` | Automatic fallback |
89
+ A commit is read from its title, as [What reaches a changelog](#what-reaches-a-changelog) describes, when it has no block, when the block's `entries` is absent or empty, or when the block is malformed. A block is malformed when it never closes, when its payload is not valid YAML or not a mapping, when `entries` is not a list, when an entry is not a mapping, when an entry's `type` or `text` is missing, blank, or not a string, when `breaking` is not a boolean, when `scopes` is not a list of strings, when `migration` or `ticket_ref` is not a string, or when `pr_number` is not a positive integer. One defect makes the whole block malformed.
51
90
 
52
- The bundled template provides a generic git-cliff configuration that:
91
+ ### Reported diagnostics
53
92
 
54
- - Strips issue-ticket prefixes matching `^[A-Z]+-\d+\s+` (e.g., `TOOL-123 `, `AFG-456 `)
55
- - Handles both `type: description` and `workspace|type: description` commit formats
56
- - Groups commits by work type via `[git].commit_parsers`
93
+ `release-kit prepare` reports these as warnings for the release being prepared, from the commits that no earlier release contains:
57
94
 
58
- The body template is intentionally empty: release-kit reads cliff's `--context` JSON output and renders `CHANGELOG.md` in-process via `renderChangelogMarkdown` (see [How it works](../README.md#how-it-works) for the rationale). The `[git].commit_parsers` section remains load-bearing for `--context` group assignment.
95
+ - **A malformed block**, with the defect that stopped the read.
96
+ - **An entry whose type is not declared**, with its position. It yields no item.
97
+ - **An entry that violates its type's breaking policy**, as a policy violation at the entry's position. Its item is not marked breaking.
59
98
 
60
- 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.
99
+ The result that `prepare` returns carries these as `malformedBlocks`, `undeclaredEntryTypes`, and `policyViolations` entries whose `surface` is `'entry'`, whether the target releases or is skipped. A block whose entries are all undeclared or excluded yields no item, so the commit raises no bump.
61
100
 
62
101
  ## Release-notes injection
63
102
 
@@ -18,7 +18,6 @@ The two paths differ in what an absent file means. An absent default path means
18
18
 
19
19
  | Field | Type | Description |
20
20
  | ------------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | `cliffConfigPath` | `string` | Explicit path to cliff config. If omitted, resolved automatically: `.config/git-cliff.toml` → `cliff.toml` → bundled template |
22
21
  | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
23
22
  | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
24
23
  | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
@@ -89,7 +88,7 @@ Tag prefixes from distinct owners must not be identical or be a strict prefix of
89
88
  - A `retiredPackages[]` entry (one owner per entry).
90
89
  - The `project` block, when configured.
91
90
 
92
- 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.
91
+ release-kit matches a tag to a prefix when the tag name starts with the prefix and continues with a digit, so a strict-prefix overlap between distinct owners can match the wrong owner's tags. For example, a project prefix of `v` collides with a workspace prefix of `v11y-check-v`, since `v11y-check-v1.0.0` starts with `v` followed by a digit. The rule rejects every strict-prefix overlap, including one such as `v` and `vue-helpers-v` that no tag of the longer prefix can reach.
93
92
 
94
93
  The rule is enforced at config load; the resulting error identifies both colliding declarations.
95
94
 
@@ -105,7 +104,7 @@ In single-package mode, prints a single row with `workspacePath = .` and `derive
105
104
 
106
105
  ## `VersionPatterns`
107
106
 
108
- Defines which commit types trigger major or minor bumps. Any recognized type not listed defaults to a patch bump.
107
+ Defines which work types trigger major or minor bumps. `prepare` applies it to each changelog item of the unreleased window and takes the highest level. An item of a type not listed bumps patch.
109
108
 
110
109
  ```typescript
111
110
  interface VersionPatterns {
@@ -1,10 +1,10 @@
1
1
  # Editorial overrides
2
2
 
3
- How to correct a generated changelog entry without rewriting history: where override files live, how they compose, what an entry may set, and how to validate the files.
3
+ How to correct a generated changelog entry without rewriting history: where override files live, how they compose, how a key targets items, what an entry may set, and how to validate the files.
4
4
 
5
5
  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.
6
6
 
7
- 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.
7
+ 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 or by commit hash and entry position, 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.
8
8
 
9
9
  ## File-location convention
10
10
 
@@ -22,8 +22,8 @@ When a workspace's changelog is rendered, both files are consulted:
22
22
 
23
23
  - The root file's overrides apply globally.
24
24
  - The workspace file's overrides apply only to that workspace.
25
- - 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.
26
- - 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.
25
+ - When the **same 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.
26
+ - Non-identical keys that match the same item, such as a root `abc1234` beside a workspace `abc1234:2`, or two prefixes of one commit, do **not** shadow. The run fails with the [overlapping-keys error](#conflicting-keys), which names both keys.
27
27
  - Other keys in the root file still apply for that workspace.
28
28
 
29
29
  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.
@@ -39,9 +39,13 @@ A key that doesn't match any commit gets a stale-reference warning. The warning'
39
39
 
40
40
  ```json
41
41
  {
42
+ "$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/changelog-overrides.json",
42
43
  "82962311": {
43
44
  "audience": "skip"
44
45
  },
46
+ "3f0c9a1:2": {
47
+ "description": "Reworded headline for the second change-record entry"
48
+ },
45
49
  "abc1234d": {
46
50
  "body": "Cleaned-up prose without the original PR-template scaffolding."
47
51
  },
@@ -52,7 +56,7 @@ A key that doesn't match any commit gets a stale-reference warning. The warning'
52
56
  }
53
57
  ```
54
58
 
55
- Per-entry fields are all optional, but at least one must be present per entry:
59
+ The top-level `$schema` key is optional and names the [published JSON Schema](#published-json-schema) for editor support; it is not an override. Per-entry fields are all optional, but at least one must be present per entry:
56
60
 
57
61
  | Field | Type | Effect |
58
62
  | ------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
@@ -61,15 +65,44 @@ Per-entry fields are all optional, but at least one must be present per entry:
61
65
  | `body` | `string` | Replaces the entry's body (the prose that renders below the bullet). Other fields are preserved. |
62
66
  | `breaking` | `boolean` | Toggles the `🚨 **Breaking:** ` marker on the bullet. |
63
67
 
64
- There is no `migration` key: the field is derived from `body`, so an override that replaces `body` re-derives it. See [The `migration` field](changelogs.md#the-migration-field).
68
+ There is no `migration` key: On a title-derived item the field is derived from `body`, so an override that replaces `body` re-derives it, and an item derived from a change-record entry keeps its entry's `migration`. See [The `migration` field](changelogs.md#the-migration-field).
69
+
70
+ ## Keys
71
+
72
+ A key takes one of two forms. In both, the hash part is lowercase hex and matches as a prefix of the commit hash.
73
+
74
+ | Key | Targets |
75
+ | ------------ | ---------------------------------------------------------------- |
76
+ | `<hash>` | Every item of the commit |
77
+ | `<hash>:<n>` | The one item derived from the commit's `n`th change-record entry |
78
+
79
+ `n` counts from 1 over the `entries` of the commit's [change-record block](changelogs.md#change-record-blocks), in the order in which the PR lists them, including entries that yield no item. An item derived from the commit's title has no entry position, so `<hash>:<n>` never matches it. Any other key fails loading.
80
+
81
+ The hash part can be the full 40-character SHA or a prefix. `prepare` resolves it against the distinct commit hashes in the changelog:
82
+
83
+ - **One commit** — the override applies to the items that the key targets. A 7-character prefix is usually unambiguous within a single repo's history; longer prefixes are always safe.
84
+ - **No commit** — the override is treated as a stale reference (probably from a rebase or branch deletion) and prepare reports a warning. The release continues. An ordinal key whose commit has no item at position `n` is stale in the same way.
85
+ - **Several commits** — the release aborts with an error naming the key and the matching hashes. Lengthen the prefix or use the full SHA.
86
+
87
+ ### Conflicting keys
65
88
 
66
- ## Hash-prefix matching
89
+ Two further conditions abort the release. Each counts the items within one changelog.
67
90
 
68
- 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:
91
+ - **A bare key that sets `description` or `body` on a commit with several items.** One headline or body cannot replace several different ones, so the error names the ordinal keys to use instead: `Override key 'abc1234' sets description on a commit with several items; use abc1234:1, abc1234:2`. `audience` and `breaking` on a bare key still apply to every item.
92
+ - **Two non-identical keys that match the same item.** The error names both keys, such as `Override keys 'abc1234' and 'abc1234:2' both match the item at abc1234:2; keep one`. Neither override applies to the item.
93
+
94
+ ### Example: one commit, several items
95
+
96
+ Suppose commit `3f0c9a1` merged a PR whose change record lists three entries. Its changelog contains three items, and each key below targets one of them:
97
+
98
+ ```json
99
+ {
100
+ "3f0c9a1:2": { "description": "Reworded headline for the second entry" },
101
+ "3f0c9a1:3": { "audience": "skip" }
102
+ }
103
+ ```
69
104
 
70
- - **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.
71
- - **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.
72
- - **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.
105
+ A bare `"3f0c9a1": { "audience": "skip" }` would skip all three items instead. Adding it beside the keys above fails the run, because it overlaps each of them.
73
106
 
74
107
  Override application errors abort the run with a non-zero exit; warnings (zero-match keys) are non-fatal and surface on `PrepareResult.warnings`.
75
108
 
@@ -79,12 +112,16 @@ The override file is validated when `release-kit prepare` loads it. Each error n
79
112
 
80
113
  - Missing file → empty map, no error (the no-op default — projects that do not need overrides skip the file entirely).
81
114
  - Malformed JSON → error.
115
+ - A key that is neither `<hash>` nor `<hash>:<n>` (uppercase hex, non-hex characters, `:0`, a leading zero in `n`) → error.
116
+ - A top-level `$schema` that is not a string → error.
82
117
  - Wrong top-level shape (e.g., array, primitive) → error.
83
118
  - Unknown fields on an entry → error.
84
119
  - Wrong field types (e.g., `description` as a number) → error.
85
120
  - An entry with no fields set → error (a copy-paste mistake more often than not).
86
121
  - `audience: 'all'` or `audience: 'dev'` → error in the current release: only `'skip'` is supported (see below).
87
122
 
123
+ Applying the overrides then aborts the run on an ambiguous prefix and on either of the [conflicting-key conditions](#conflicting-keys): a bare key setting `description` or `body` on a commit with several items, and two keys matching the same item.
124
+
88
125
  ### Standalone validation: `release-kit overrides validate`
89
126
 
90
127
  For a focused overrides-only health check (locally or as a CI gate), run:
@@ -95,16 +132,38 @@ pnpm exec release-kit overrides validate
95
132
 
96
133
  This walks every `.meta/changelog-overrides.json` file across the project tier and per-workspace tier, reporting three classes of finding:
97
134
 
98
- | Class | Examples | Exit code |
99
- | ------------------- | ------------------------------------------------------------------------------------------- | --------- |
100
- | Schema/parse errors | malformed JSON, unknown fields, wrong field types, no-field entries, unsupported `audience` | `2` |
101
- | Ambiguous-prefix | an override key resolves to 2+ commit hashes | `2` |
102
- | Stale-key warnings | an override key resolves to no commit in its applicable scope | `1` |
135
+ | Class | Examples | Exit code |
136
+ | ------------------- | ----------------------------------------------------------------------------------------------------------- | --------- |
137
+ | Schema/parse errors | malformed JSON, malformed keys, unknown fields, wrong field types, no-field entries, unsupported `audience` | `2` |
138
+ | Ambiguous-prefix | an override key resolves to 2+ commit hashes | `2` |
139
+ | Conflicting keys | a bare key sets `description` or `body` on a commit with several items; two keys match the same item | `2` |
140
+ | Stale-key warnings | an override key resolves to no item in its applicable scope | `1` |
103
141
 
104
142
  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).
105
143
 
144
+ Conflicting keys are found within one file and across the root and workspace files, as `prepare` composes them. An overlap between a root key and a workspace key is reported against the workspace file.
145
+
106
146
  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.
107
147
 
148
+ ## Published JSON Schema
149
+
150
+ release-kit publishes a JSON Schema for the override file. The schema lives at `packages/release-kit/schemas/changelog-overrides.json` in this repo and is reachable via the stable raw URL:
151
+
152
+ ```
153
+ https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/changelog-overrides.json
154
+ ```
155
+
156
+ An override file references it from its top-level `$schema` key:
157
+
158
+ ```json
159
+ {
160
+ "$schema": "https://github.com/williamthorsen/node-monorepo-tools/raw/release-kit-v<version>/packages/release-kit/schemas/changelog-overrides.json",
161
+ "82962311": { "audience": "skip" }
162
+ }
163
+ ```
164
+
165
+ The schema states the key form and the entry fields. It accepts `audience` values `'all'` and `'dev'`, which the file format declares; the loader still rejects them (see below).
166
+
108
167
  ## Audience semantics: v1 supports `'skip'` only
109
168
 
110
169
  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.
@@ -19,7 +19,7 @@ When configured, each `release-kit prepare` run additionally:
19
19
 
20
20
  - Computes commits since the last project tag (`<tagPrefix><version>`), filtered to `paths` (by default, the union of every contributing workspace's paths).
21
21
  - Bumps the root `package.json`'s `version` field using the same bump-derivation rules as workspaces (or the `--bump=...` override).
22
- - 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.
22
+ - Regenerates the root `./CHANGELOG.md` from the structured `ChangelogEntry[]` built from the project's release history (scoped to the project's `tagPrefix` and the same `paths`) and any matching editorial overrides.
23
23
  - Emits `./.meta/changelog.json` (when `changelogJson.enabled`).
24
24
  - With `--with-release-notes`, additionally emits `./docs/RELEASE_NOTES.v<version>.md`.
25
25
  - Appends the project tag to `tmp/.release-tags` so `release-kit commit` and `release-kit tag` pick it up alongside per-workspace tags.
@@ -42,7 +42,7 @@ interface ProjectConfig {
42
42
 
43
43
  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.
44
44
 
45
- 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.
45
+ Declare `paths` to choose the window yourself. Each entry is a git pathspec, which both the bump and the changelog read. A declared value **replaces** the workspace union rather than extending it: `paths: ['docs/**']` drops every workspace commit from the project release.
46
46
 
47
47
  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:
48
48
 
@@ -54,13 +54,13 @@ const config: ReleaseKitConfig = {
54
54
 
55
55
  `'**'` is the whole-tree pattern, matching root-level files, dotfiles, and nested paths alike.
56
56
 
57
- 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.
57
+ A bare directory such as `'aws'` selects its whole subtree, as `'aws/**'` does.
58
58
 
59
59
  Validation rules:
60
60
 
61
61
  - The root `package.json` must exist and declare a `version` field. release-kit reports an error at config-load time if either is missing.
62
62
  - 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).
63
- - `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.
63
+ - `paths`, when declared, must hold at least one non-empty string. An empty array is rejected: it reaches git as no filter at all, which is the inverse of how it reads.
64
64
  - Unknown fields inside `project` are rejected.
65
65
 
66
66
  CLI flag interactions:
package/docs/releasing.md CHANGED
@@ -14,7 +14,6 @@ Scaffolded files:
14
14
  - `.github/workflows/publish.yaml` — workflow that delegates to a reusable publish workflow
15
15
  - `.github/workflows/release.yaml` — workflow that delegates to a reusable release workflow
16
16
  - `.config/release-kit.config.ts` — starter config with commented-out customization examples (with `--with-config`)
17
- - `.config/git-cliff.toml` — copied from the bundled template (with `--with-config`)
18
17
 
19
18
  ## `release-kit prepare`
20
19
 
@@ -22,22 +21,28 @@ Run release preparation with automatic workspace discovery.
22
21
 
23
22
  Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
24
23
 
25
- `--bump` and `--force` are orthogonal: `--bump` is purely a level chooser; `--force` is purely a release trigger. Examples:
24
+ `prepare` reads each target's history once and builds the changelog items of its unreleased commits. The bump is the highest level that those items call for, under [`versionPatterns`](configuration.md#versionpatterns), so the bump and the changelog describe the same release: A `feat` entry in a [change-record block](changelogs.md#change-record-blocks) bumps minor under a `docs:` title, and a breaking item bumps major.
25
+
26
+ - A commit that yields no item bumps nothing: one without a ticket-ID prefix, one of a type excluded from the changelog (such as `fmt`), and one whose change-record entries are all undeclared or excluded.
27
+ - A `BREAKING CHANGE:` footer raises no major bump. Only a subject's `!` and an entry's `breaking: true` mark an item breaking; on a type whose policy forbids `!`, the footer is reported as a [policy violation](work-types.md#policy-enforcement).
28
+ - A target whose commits yield no item is skipped. The prepare report lists the commits that could not be parsed and the change-record diagnostics, skipped targets included.
29
+
30
+ `--bump` and `--force` are orthogonal in every mode, single-package included: `--bump` is purely a level chooser; `--force` is purely a release trigger. Examples:
26
31
 
27
32
  ```sh
28
33
  # Release every target at its natural bump level (no flags).
29
34
  release-kit prepare
30
35
 
31
- # Force a release even when no bump-worthy commits exist; defaults to patch
32
- # per target, with each target keeping its natural bump if one is derivable.
36
+ # Force a release even when no commit yields a changelog item; defaults to
37
+ # patch per target, with each target keeping its natural bump if it has one.
33
38
  release-kit prepare --force
34
39
 
35
40
  # Force a release at a uniform level across every releasing target.
36
41
  release-kit prepare --force --bump=minor
37
42
 
38
- # --bump=X alone is a level chooser, NOT a trigger. If a target has no
39
- # bump-worthy commits, it skips with a "Pass --force..." reason. If it has
40
- # bump-worthy commits, the override applies.
43
+ # --bump=X alone is a level chooser, NOT a trigger. If no commit of a target
44
+ # yields a changelog item, it skips with a "Pass --force..." reason. Otherwise
45
+ # the override applies.
41
46
  release-kit prepare --bump=minor
42
47
  ```
43
48
 
@@ -64,7 +69,7 @@ docs/*.v*.md
64
69
 
65
70
  ### Setting an explicit version with `--set-version`
66
71
 
67
- 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.
72
+ The `--set-version` flag is a first-class escape hatch for the cases where the derived bump 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.
68
73
 
69
74
  The flag validates that:
70
75
 
@@ -28,7 +28,11 @@ release-kit bundles a copy of the codeassembly canonical taxonomy in `packages/r
28
28
  - **`internal`** — dev-only. `internal`-tier sections appear in dev changelogs but not in public-facing release notes.
29
29
  - **`process`** — dev-only. Same audience treatment as `internal`.
30
30
 
31
- 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.
31
+ Section render order is **tier order (`public` → `internal` → `process`), then row order within tier**. It comes from the declaration order of `work-types.json`: `CANONICAL_SECTION_ORDER` in `buildChangelogEntries.ts` indexes each header by its row, and `transformReleases` sorts a release's sections by that index whatever order the commits arrived in.
32
+
33
+ ## Subject forms
34
+
35
+ `classifyChangelogCommit` resolves a commit's type through `parseCommitMessage`, so a subject reaches its section under any form that parser accepts: `type:`, `scope|type:`, and the conventional-commit `type(scope):`. The type is matched case-insensitively, so `Feat:` and `FEAT:` resolve like `feat:`. The ticket-ID prefix is still required, and an alias resolves before the section is chosen. A commit whose [change-record block](changelogs.md#change-record-blocks) records entries takes its sections from the entries' types instead of from its subject.
32
36
 
33
37
  ## `utility` alias
34
38
 
@@ -44,11 +48,11 @@ Each work-type carries a `breakingPolicy` value:
44
48
 
45
49
  ### Policy enforcement
46
50
 
47
- `parseCommitMessage` enforces the `!` policy at release time and treats a violation as a warning. 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 an `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.
51
+ `parseCommitMessage` enforces the `!` policy at release time and treats a violation as a warning. 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 an `onPolicyViolation` callback fires. `readReleaseHistory` collects these warnings while it builds the items of the unreleased window, and the release report lists them. A single legacy `internal!` in a year-old log does not block releases.
48
52
 
49
- A `BREAKING CHANGE:` body footer on a `forbidden`-policy type triggers the same warning path as the prefix `!` does: A type that cannot be breaking is parsed as non-breaking whichever surface carries the signal.
53
+ A `BREAKING CHANGE:` body footer on a `forbidden`-policy type triggers the same warning path as the prefix `!` does: A type that cannot be breaking is parsed as non-breaking whichever surface carries the signal. The warning is all that a footer triggers; it raises no bump (see [`🚨 **Breaking:**` bullet marker](#-breaking-bullet-marker)).
50
54
 
51
- 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:
55
+ The release-prepare orchestrators (`releasePrepare`, `releasePrepareMono`, `releasePrepareProject`) apply `DEFAULT_BREAKING_POLICIES` automatically. Violations in the titles and change-record entries of each workspace's or project's unreleased window are collected onto the corresponding result's `policyViolations` field, on a skipped result as on a released one, and rendered under the section in the prepare report:
52
56
 
53
57
  ```
54
58
  arrays
@@ -73,9 +77,9 @@ The marker agrees with the version bump:
73
77
 
74
78
  - A `forbidden`-policy type carrying `!`, such as `refactor!`, gets no marker, just as its `!` raises no bump; the prepare report lists it as a policy violation. An [editorial override](editorial-overrides.md) that sets `breaking: true` restores the marker for one entry.
75
79
  - The configured `breakingPolicies` map, `{}` included, decides which types permit the marker.
76
- - A commit whose type the parser cannot resolve has no policy, so its `!` prefix alone decides.
80
+ - A commit whose type the parser cannot resolve reaches no changelog at all, so no marker question arises.
77
81
 
78
- A `BREAKING CHANGE:` body footer on its own does **not** retroactively mark a changelog item as breaking, even on a type whose policy permits `!`; the changelog signal is tied to the commit prefix. This avoids surprise breaking-marker appearances for older commits written under earlier conventions.
82
+ A `BREAKING CHANGE:` body footer on its own does **not** retroactively mark a changelog item as breaking, even on a type whose policy permits `!`; the changelog signal is tied to the commit prefix. This avoids surprise breaking-marker appearances for older commits written under earlier conventions. The bump follows the items, so a footer raises no major bump either: A consumer that marks breaking changes with the footer alone gets the bump of the commit's type.
79
83
 
80
84
  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.
81
85
 
@@ -95,13 +99,13 @@ Entries store plain text only — the SSOT is format-agnostic, so consumers appl
95
99
 
96
100
  ## `fmt`
97
101
 
98
- `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.
102
+ `fmt:` commits are recognized by `parseCommitMessage`, but `fmt` carries `excludedFromChangelog: true`, which `DEFAULT_WORK_TYPES` carries onto its `WorkTypeConfig`. `classifyChangelogCommit` excludes a commit whose type sets the flag, so a `fmt:` commit never appears in `CHANGELOG.md`, `changelog.json`, or release notes. It yields no item, so it raises no bump, and the release report does not list it as unparseable. The label and emoji are present in `work-types.json` for schema parity with the codeassembly upstream but never render.
99
103
 
100
104
  ## Custom work types
101
105
 
102
- 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.
106
+ Work types from a config are merged with these defaults by key: a consumer entry overrides or extends, it does not replace the full set. `classifyChangelogCommit` reads the merged record, so an added type reaches the changelog under the `header` it declares, and an entry setting `excludedFromChangelog: true` keeps its commits out of the changelog and the bump. Release-notes sections are rendered in the declaration order of the merged work-types record, with any unknown titles trailing the known ones.
103
107
 
104
- 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.
108
+ 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`; matching is decorator-tolerant, so a bare-name override like `['Internal features']` keeps working against the emoji-prefixed default titles.
105
109
 
106
110
  ## Maintaining the bundled taxonomy
107
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "11.0.0",
3
+ "version": "12.0.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -43,7 +43,6 @@
43
43
  ".readyup/manifest.json",
44
44
  "CHANGELOG.md",
45
45
  "bin",
46
- "cliff.toml.template",
47
46
  "dist/*",
48
47
  "docs",
49
48
  "!docs/*.v*.md",
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "changelog-overrides",
4
+ "description": "Editorial overrides that release-kit applies to generated changelog items. Each key is a lowercase hex commit hash or prefix, which targets every item of the commit, optionally followed by `:<n>`, which targets the item derived from the commit's `n`th change-record entry.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "JSON Schema reference for editor support."
11
+ }
12
+ },
13
+ "patternProperties": {
14
+ "^[0-9a-f]+(:[1-9][0-9]*)?$": {
15
+ "type": "object",
16
+ "description": "Override for the items that the key targets. At least one field must be set.",
17
+ "additionalProperties": false,
18
+ "minProperties": 1,
19
+ "properties": {
20
+ "audience": {
21
+ "enum": ["all", "dev", "skip"],
22
+ "description": "`skip` removes the items. `all` and `dev` are reserved and not yet supported."
23
+ },
24
+ "description": {
25
+ "type": "string",
26
+ "description": "Replaces the item's headline."
27
+ },
28
+ "body": {
29
+ "type": "string",
30
+ "description": "Replaces the item's body."
31
+ },
32
+ "breaking": {
33
+ "type": "boolean",
34
+ "description": "Sets whether the item is marked as breaking."
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
@@ -1,65 +0,0 @@
1
- # git-cliff configuration
2
- # Generates changelogs from conventional-ish commit messages.
3
- #
4
- # Commit format: [ticket-prefix ]scope|type[!]: description [#PR]
5
- # Examples:
6
- # #19 rdy|feat: Add utility functions for common check patterns (#26)
7
- # AFG-42 panel|fix: Fix button layout (#73)
8
- # ## feat: Quick utility added during interactive session
9
- #
10
- # Only ticketed commits (leading #NN, PROJ-NN, or ##) are included.
11
- # Unticketed maintenance commits (deps upgrades, tooling tweaks) are skipped.
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.
20
-
21
- # Markdown rendering is handled in-process by `renderChangelogMarkdown.ts`. cliff is invoked
22
- # only for `--context` JSON output (see `buildChangelogEntries.ts`), so no `[changelog]` block
23
- # is needed. The `[git].commit_parsers` section below remains load-bearing for group
24
- # assignment in the `--context` payload.
25
-
26
- [git]
27
- conventional_commits = false
28
- filter_unconventional = false
29
- split_commits = false
30
- commit_preprocessors = []
31
- # Require a ticket-ID prefix for inclusion.
32
- # Supported prefixes: "#123 ", "#123-1 ", "#123.1 ", "TOOL-123 ", "## "
33
- commit_parsers = [
34
- { message = "^release:", skip = true },
35
- { message = "^Merge", skip = true },
36
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?feat(!)?:", group = "<!-- 01 -->🎉 Features" },
37
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?feature(!)?:", group = "<!-- 01 -->🎉 Features" },
38
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?drop(!)?:", group = "<!-- 02 -->🪦 Removed" },
39
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?deprecate(!)?:", group = "<!-- 03 -->🗑️ Deprecated" },
40
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?fix(!)?:", group = "<!-- 04 -->🐛 Bug fixes" },
41
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?bugfix(!)?:", group = "<!-- 04 -->🐛 Bug fixes" },
42
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?sec(!)?:", group = "<!-- 05 -->🔒 Security" },
43
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?security(!)?:", group = "<!-- 05 -->🔒 Security" },
44
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?perf(!)?:", group = "<!-- 06 -->⚡ Performance" },
45
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?performance(!)?:", group = "<!-- 06 -->⚡ Performance" },
46
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?internal(!)?:", group = "<!-- 07 -->🏗️ Internal features" },
47
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?utility(!)?:", group = "<!-- 07 -->🏗️ Internal features" },
48
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?refactor(!)?:", group = "<!-- 08 -->♻️ Refactoring" },
49
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?tests?(!)?:", group = "<!-- 09 -->🧪 Tests" },
50
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?tooling(!)?:", group = "<!-- 10 -->⚙️ Tooling" },
51
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?ci(!)?:", group = "<!-- 11 -->👷 CI" },
52
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?deps?(!)?:", group = "<!-- 12 -->📦 Dependencies" },
53
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?ai(!)?:", group = "<!-- 13 -->🤖 Agentic support" },
54
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?docs?(!)?:", group = "<!-- 14 -->📚 Documentation" },
55
- { message = "^(\\#\\d+([.\\-]\\d+)?|\\#\\#|[A-Z]+-\\d+)\\s+([^|]+\\|)?fmt(!)?:", skip = true },
56
- # Skip unticketed commits (maintenance, deps, initial scaffolding, etc.)
57
- { message = ".*", skip = true },
58
- ]
59
- protect_breaking_commits = false
60
- filter_commits = true
61
- tag_pattern = "v[0-9].*"
62
- skip_tags = ""
63
- ignore_tags = ""
64
- topo_order = false
65
- sort_commits = "oldest"