@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,8 +1,6 @@
1
1
  import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, truncateToWidth, } from '@williamthorsen/nmr-core';
2
2
  import { bold, dim, sectionHeader } from "./format.js";
3
3
  import { RELEASE_GLYPHS } from "./glyphs.js";
4
- import { GIT_CLIFF_NPX_ARGS } from "./runGitCliff.js";
5
- const CLIFF_DRY_RUN_COMMAND = `npx ${GIT_CLIFF_NPX_ARGS.join(' ')} ...`;
6
4
  const SUBJECT_COLUMN_BUDGET = 72;
7
5
  export function reportPrepare(plan, options) {
8
6
  const isMultiWorkspace = plan.workspaces.some((w) => w.name !== undefined) || plan.project !== undefined;
@@ -27,6 +25,7 @@ function formatSingleWorkspace(result, options) {
27
25
  formatUnparseableWarning(lines, workspace, style);
28
26
  formatPolicyViolations(lines, workspace.policyViolations, style);
29
27
  if (workspace.status === 'skipped') {
28
+ formatChangeRecordWarnings(lines, workspace, style);
30
29
  lines.push(formatStatusLine(style, 'skipped', workspace.skipReason));
31
30
  formatWarnings(lines, result, style);
32
31
  return lines.join('\n');
@@ -49,6 +48,7 @@ function formatSingleWorkspace(result, options) {
49
48
  else if (workspace.releaseType !== undefined) {
50
49
  lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType})`));
51
50
  }
51
+ formatChangeRecordWarnings(lines, workspace, style);
52
52
  formatBumpFiles(lines, workspace, dryRun);
53
53
  lines.push(dim('Generating changelogs...'));
54
54
  formatChangelogFiles(lines, workspace, dryRun);
@@ -87,6 +87,8 @@ function formatProjectSection(lines, project, dryRun, style) {
87
87
  lines.push(dim(` Found ${project.commitCount} commits ${since}`));
88
88
  formatPolicyViolations(lines, project.policyViolations, style, ' ');
89
89
  if (project.status === 'skipped') {
90
+ formatUnparseableWarning(lines, project, style, ' ');
91
+ formatChangeRecordWarnings(lines, project, style, ' ');
90
92
  lines.push(` ${formatStatusLine(style, 'skipped', project.skipReason)}`);
91
93
  return;
92
94
  }
@@ -97,7 +99,8 @@ function formatProjectSection(lines, project, dryRun, style) {
97
99
  if (project.bumpOverride !== undefined) {
98
100
  lines.push(` Using bump override: ${project.bumpOverride}`);
99
101
  }
100
- formatProjectUnparseable(lines, project, style);
102
+ formatUnparseableWarning(lines, project, style, ' ');
103
+ formatChangeRecordWarnings(lines, project, style, ' ');
101
104
  lines.push(dim(` Bumping versions (${releaseType})...`), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${currentVersion} → ${bold(newVersion)} (${releaseType})`)}`);
102
105
  for (const file of project.bumpedFiles) {
103
106
  if (dryRun) {
@@ -110,7 +113,7 @@ function formatProjectSection(lines, project, dryRun, style) {
110
113
  lines.push(dim(' Generating changelogs...'));
111
114
  for (const file of project.changelogFiles) {
112
115
  if (dryRun) {
113
- lines.push(dim(` [dry-run] Would run: ${CLIFF_DRY_RUN_COMMAND} --output ${file}`));
116
+ lines.push(dim(` [dry-run] Would generate changelog: ${file}`));
114
117
  }
115
118
  else {
116
119
  lines.push(dim(` Generating changelog: ${file}`));
@@ -119,21 +122,6 @@ function formatProjectSection(lines, project, dryRun, style) {
119
122
  formatPreviewFiles(lines, project, dryRun, ' ');
120
123
  lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
121
124
  }
122
- function formatProjectUnparseable(lines, project, style) {
123
- const unparseable = project.unparseableCommits;
124
- if (unparseable === undefined || unparseable.length === 0) {
125
- return;
126
- }
127
- const count = unparseable.length;
128
- const isPatchFloor = project.parsedCommitCount === 0;
129
- const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
130
- lines.push(` ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
131
- for (const commit of unparseable) {
132
- const shortHash = commit.hash.slice(0, 7);
133
- const truncatedMessage = truncateSubject(commit.message);
134
- lines.push(` · ${shortHash} ${truncatedMessage}`);
135
- }
136
- }
137
125
  function formatWorkspaceSection(lines, workspace, dryRun, style) {
138
126
  if (workspace.name !== undefined) {
139
127
  lines.push(`\n${sectionHeader(workspace.name)}`);
@@ -141,6 +129,9 @@ function formatWorkspaceSection(lines, workspace, dryRun, style) {
141
129
  const since = workspace.previousTag === undefined ? '(no previous release found)' : `since ${workspace.previousTag}`;
142
130
  lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
143
131
  if (workspace.status === 'skipped') {
132
+ formatUnparseableWarning(lines, workspace, style, ' ');
133
+ formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
134
+ formatChangeRecordWarnings(lines, workspace, style, ' ');
144
135
  lines.push(` ${formatStatusLine(style, 'skipped', workspace.skipReason)}`);
145
136
  return;
146
137
  }
@@ -149,6 +140,7 @@ function formatWorkspaceSection(lines, workspace, dryRun, style) {
149
140
  formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
150
141
  formatUnparseableWarning(lines, workspace, style, ' ');
151
142
  formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
143
+ formatChangeRecordWarnings(lines, workspace, style, ' ');
152
144
  formatBumpLabels(lines, workspace, isPropagatedOnly);
153
145
  formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style);
154
146
  formatBumpFiles(lines, workspace, dryRun, ' ');
@@ -203,7 +195,7 @@ function formatBumpFiles(lines, workspace, dryRun, indent = '') {
203
195
  function formatChangelogFiles(lines, workspace, dryRun, indent = '') {
204
196
  for (const file of workspace.changelogFiles) {
205
197
  if (dryRun) {
206
- lines.push(dim(`${indent} [dry-run] Would run: ${CLIFF_DRY_RUN_COMMAND} --output ${file}`));
198
+ lines.push(dim(`${indent} [dry-run] Would generate changelog: ${file}`));
207
199
  }
208
200
  else {
209
201
  lines.push(dim(`${indent} Generating changelog: ${file}`));
@@ -221,19 +213,17 @@ function formatPreviewFiles(lines, release, dryRun, indent = '') {
221
213
  }
222
214
  }
223
215
  }
224
- function formatUnparseableWarning(lines, workspace, style, indent = '') {
225
- const unparseable = workspace.unparseableCommits;
216
+ function formatUnparseableWarning(lines, release, style, indent = '') {
217
+ const unparseable = release.unparseableCommits;
226
218
  if (unparseable === undefined || unparseable.length === 0) {
227
219
  return;
228
220
  }
229
221
  const count = unparseable.length;
230
- const isPatchFloor = workspace.parsedCommitCount === 0;
231
- const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
232
- lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
222
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed`)}`);
233
223
  for (const commit of unparseable) {
234
224
  const shortHash = commit.hash.slice(0, 7);
235
- const truncatedMessage = truncateSubject(commit.message);
236
- lines.push(`${indent} · ${shortHash} ${truncatedMessage}`);
225
+ const truncatedSubject = truncateSubject(commit.subject);
226
+ lines.push(`${indent} · ${shortHash} ${truncatedSubject}`);
237
227
  }
238
228
  }
239
229
  function formatPolicyViolations(lines, violations, style, indent = '') {
@@ -246,7 +236,28 @@ function formatPolicyViolations(lines, violations, style, indent = '') {
246
236
  const shortHash = violation.commitHash.slice(0, 7);
247
237
  const subject = violation.commitSubject;
248
238
  const truncatedSubject = truncateSubject(subject);
249
- lines.push(`${indent} · ${shortHash} '${truncatedSubject}' — type '${violation.type}' at ${violation.surface} surface`);
239
+ const location = violation.surface === 'entry' ? `entry ${violation.entryPosition ?? '?'}` : `${violation.surface} surface`;
240
+ lines.push(`${indent} · ${shortHash} '${truncatedSubject}' — type '${violation.type}' at ${location}`);
241
+ }
242
+ }
243
+ function formatChangeRecordWarnings(lines, release, style, indent = '') {
244
+ const { malformedBlocks = [], undeclaredEntryTypes = [] } = release;
245
+ if (malformedBlocks.length > 0) {
246
+ const count = malformedBlocks.length;
247
+ const header = `${count} change-record block${count === 1 ? '' : 's'} could not be read (item taken from the title)`;
248
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
249
+ for (const block of malformedBlocks) {
250
+ lines.push(`${indent} · ${block.commitHash.slice(0, 7)} '${truncateSubject(block.commitSubject)}' — ${block.reason}`);
251
+ }
252
+ }
253
+ if (undeclaredEntryTypes.length > 0) {
254
+ const count = undeclaredEntryTypes.length;
255
+ const header = `${count} change-record entr${count === 1 ? 'y has an' : 'ies have'} undeclared type${count === 1 ? '' : 's'} (no item)`;
256
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
257
+ for (const entry of undeclaredEntryTypes) {
258
+ const shortHash = entry.commitHash.slice(0, 7);
259
+ lines.push(`${indent} · ${shortHash} '${truncateSubject(entry.commitSubject)}' — type '${entry.type}' at entry ${entry.entryPosition}`);
260
+ }
250
261
  }
251
262
  }
252
263
  function formatPropagationSuffix(propagatedFrom) {
@@ -11,8 +11,9 @@ export interface ChangelogItem {
11
11
  /**
12
12
  * Whether this item represents a breaking change.
13
13
  *
14
- * `true` when the commit subject carries the `!` prefix (e.g. `feat!:` or `drop(scope)!:`) and
15
- * the commit's work-type policy permits it, so the item agrees with the version bump.
14
+ * `true` when the commit subject carries the `!` prefix (e.g. `feat!:` or `drop(scope)!:`), or the
15
+ * change-record entry from which the item derives sets `breaking: true`, and the work-type policy
16
+ * permits it, so the item agrees with the version bump.
16
17
  * The `BREAKING CHANGE:` body footer is intentionally NOT considered here. Renderers prefix
17
18
  * breaking-item bullets with the marker constructed from `WORK_TYPES_DATA.markers.breaking`
18
19
  * (rendered as `🚨 **Breaking:** ` with the canonical SSOT values) to surface them prominently
@@ -20,29 +21,34 @@ export interface ChangelogItem {
20
21
  */
21
22
  breaking?: boolean;
22
23
  /**
23
- * Migration instruction for a consumer, taken from the `Migration:` paragraph of `body`.
24
+ * Migration instruction for a consumer, taken from the `Migration:` paragraph of `body`, or from
25
+ * the `migration` of the change-record entry from which the item derives.
24
26
  *
25
- * Derived from `body` wherever `body` is set, and absent where the body carries no labeled
26
- * paragraph. Independent of `breaking`: a `deprecate` commit cannot carry `!` under the default
27
+ * On a title-derived item, derived from `body` wherever `body` is set, and absent where the body
28
+ * carries no labeled paragraph. Independent of `breaking`: a `deprecate` commit cannot carry `!` under the default
27
29
  * breaking policy and still calls for a migration. `body` keeps the paragraph, so
28
30
  * `CHANGELOG.md` goes on rendering it in place.
29
31
  */
30
32
  migration?: string;
31
33
  /**
32
- * Full git commit SHA when known. Captured from git-cliff's `--context` output and
34
+ * Full git commit SHA when known. Captured from the commit's release window and
33
35
  * persisted in `changelog.json` so that override files can target items by hash. Synthetic
34
36
  * propagation entries (`buildSyntheticChangelogEntry`, `buildEmptyReleaseEntry`) leave
35
37
  * this field absent — they have no underlying commit.
36
38
  */
37
39
  hash?: string;
40
+ /**
41
+ * 1-based position of the change-record entry from which this item derives, counted over the block's entries as
42
+ * written. Absent for an item derived from the commit's title.
43
+ */
44
+ entry?: number;
38
45
  }
39
46
  /** A grouped section within a changelog entry (e.g., "Features", "Bug fixes"). */
40
47
  export interface ChangelogSection {
41
48
  /**
42
- * Section title carrying the leading emoji prefix used by `cliff.toml.template` group
43
- * definitions (e.g. `"🐛 Bug fixes"`). The `<!-- NN -->` canonical-order HTML comment
44
- * is stripped during transform; the emoji remains. Callers that match against `title`
45
- * (e.g. `sectionOrder` configs) must include the emoji prefix.
49
+ * Section title, read from the work type of the commit or of its change-record entry. A default
50
+ * title carries the type's emoji prefix (e.g. `"🐛 Bug fixes"`), so callers that match against
51
+ * `title` (e.g. `sectionOrder` configs) include that prefix.
46
52
  */
47
53
  title: string;
48
54
  audience: ChangelogAudience;
@@ -61,8 +67,9 @@ export interface ChangelogJsonConfig {
61
67
  devOnlySections: string[];
62
68
  }
63
69
  /**
64
- * Editorial override for a single changelog item, keyed by commit hash (or hash prefix) in
65
- * the override file. All fields are optional; an entry with no fields is a validation error.
70
+ * Editorial override for changelog items, keyed in the override file by `<hash>` (every item of the commit) or
71
+ * `<hash>:<n>` (the item derived from the commit's `n`th change-record entry), where the hash is lowercase hex
72
+ * matched as a prefix. All fields are optional; an entry with no fields is a validation error.
66
73
  *
67
74
  * `audience` declares the full forward-compatible vocabulary, but only `'skip'` is currently
68
75
  * supported at runtime. `'all'` and `'dev'` reclassification is reserved for a v2 follow-up
@@ -75,7 +82,10 @@ export interface ChangelogOverride {
75
82
  body?: string;
76
83
  breaking?: boolean;
77
84
  }
78
- /** On-disk shape of `.meta/changelog-overrides.json`: a flat record keyed by commit hash or prefix. */
85
+ /**
86
+ * Overrides in `.meta/changelog-overrides.json`: a flat record keyed by `<hash>` or `<hash>:<n>`. The file may also contain
87
+ * a top-level `$schema` string, which the loader drops.
88
+ */
79
89
  export type ChangelogOverridesFile = Record<string, ChangelogOverride>;
80
90
  /** Configuration for release notes consumption (README injection). */
81
91
  export interface ReleaseNotesConfig {
@@ -88,8 +98,8 @@ export interface ReleaseNotesConfig {
88
98
  */
89
99
  export interface ResolvedProjectConfig {
90
100
  /**
91
- * Resolved commit window for the project release: the git pathspecs and git-cliff include-paths
92
- * that select which commits the stage considers. Defaults to the union of every contributing
101
+ * Resolved commit window for the project release: the git pathspecs that select which commits
102
+ * the stage considers. Defaults to the union of every contributing
93
103
  * workspace's `paths`.
94
104
  */
95
105
  paths: string[];
@@ -102,7 +112,7 @@ export interface PropagationSource {
102
112
  newVersion: string;
103
113
  }
104
114
  /**
105
- * A `!`-policy violation detected while parsing commits during release preparation.
115
+ * A `!`-policy violation detected while reading the unreleased window during release preparation.
106
116
  *
107
117
  * Surfaces from `releasePrepare`, `releasePrepareMono`, and `releasePrepareProject` via the
108
118
  * `policyViolations` field on each per-workspace/project result. The policy itself is enforced
@@ -115,16 +125,40 @@ export interface PolicyViolation {
115
125
  commitSubject: string;
116
126
  /** Resolved canonical work type whose policy was violated. */
117
127
  type: string;
118
- /** Where the violation was detected: `'prefix'` for `!`, `'body'` for `BREAKING CHANGE:` footer. */
119
- surface: 'prefix' | 'body';
128
+ /**
129
+ * Where the violation was detected: `'prefix'` for `!`, `'body'` for `BREAKING CHANGE:` footer, `'entry'` for a
130
+ * change-record entry's `breaking`.
131
+ */
132
+ surface: 'prefix' | 'body' | 'entry';
133
+ /** 1-based position of the offending change-record entry; present only for surface `'entry'`. */
134
+ entryPosition?: number;
135
+ }
136
+ /** A commit whose `change-record` block could not be read, so that its changelog item came from its title. */
137
+ export interface MalformedChangeRecordBlock {
138
+ /** Full hash of the commit. */
139
+ commitHash: string;
140
+ /** First line of the commit message. */
141
+ commitSubject: string;
142
+ /** The defect that stopped the read. */
143
+ reason: string;
144
+ }
145
+ /** A change-record entry whose type the work-type taxonomy does not declare, so that it yielded no changelog item. */
146
+ export interface UndeclaredEntryType {
147
+ /** Full hash of the commit. */
148
+ commitHash: string;
149
+ /** First line of the commit message. */
150
+ commitSubject: string;
151
+ /** 1-based position of the entry in its block. */
152
+ entryPosition: number;
153
+ /** The type as the entry declares it. */
154
+ type: string;
120
155
  }
121
156
  /**
122
157
  * Result of preparing a single workspace (package) for release when a release was produced.
123
158
  *
124
159
  * `currentVersion`, `newVersion`, `tag`, `bumpedFiles`, and `changelogFiles` are always
125
- * populated. `releaseType` stays optional because it is left undefined for
126
- * `--set-version` workspaces. `parsedCommitCount` stays optional because the legacy
127
- * single-package bump-override path does not populate it. `commits` stays optional
160
+ * populated. `releaseType` and `parsedCommitCount` stay optional because they are left
161
+ * undefined for `--set-version` and propagation-only workspaces. `commits` stays optional
128
162
  * because propagation-only releases have no direct commits.
129
163
  */
130
164
  export interface ReleasedWorkspaceResult {
@@ -134,18 +168,19 @@ export interface ReleasedWorkspaceResult {
134
168
  previousTag?: string;
135
169
  commitCount: number;
136
170
  /**
137
- * Count of commits that parsed into a recognized work type. Always populated for
138
- * results produced by the unified `decideRelease` algorithm (the monorepo path),
139
- * including the no-commits case where it is `0`. May be absent for results
140
- * produced by the legacy single-package executor's bump-override path. Use
141
- * `bumpOverride` (not `parsedCommitCount === undefined`) as the signal for "the
142
- * user supplied --bump=X".
171
+ * Count of commits that yield at least one changelog item — `0` when there are no commits or
172
+ * when none does. Absent for `--set-version` and propagation-only workspaces. Use
173
+ * `bumpOverride` as the signal for "the user supplied --bump=X".
143
174
  */
144
175
  parsedCommitCount?: number;
145
- /** Commits that could not be parsed into a recognized work type. */
176
+ /** Commits that yield no changelog item and that no exclusion or diagnostic accounts for. */
146
177
  unparseableCommits?: Commit[];
147
- /** Policy violations detected while parsing this workspace's commits; omitted when none. */
178
+ /** Policy violations of the unreleased window's titles and change-record entries; omitted when none. */
148
179
  policyViolations?: PolicyViolation[];
180
+ /** Commits of the unreleased window whose `change-record` block could not be read; omitted when none. */
181
+ malformedBlocks?: MalformedChangeRecordBlock[];
182
+ /** Change-record entries of the unreleased window whose type is undeclared; omitted when none. */
183
+ undeclaredEntryTypes?: UndeclaredEntryType[];
149
184
  releaseType?: ReleaseType;
150
185
  currentVersion: string;
151
186
  newVersion: string;
@@ -179,15 +214,16 @@ export interface SkippedWorkspaceResult {
179
214
  name?: string;
180
215
  previousTag?: string;
181
216
  commitCount: number;
182
- /**
183
- * Count of commits that parsed into a recognized work type. May be absent for results
184
- * produced by the legacy single-package executor's bump-override path.
185
- */
186
- parsedCommitCount?: number;
187
- /** Commits that could not be parsed into a recognized work type. */
217
+ /** Count of commits that yield at least one changelog item — `0` when there are no commits or when none does. */
218
+ parsedCommitCount: number;
219
+ /** Commits that yield no changelog item and that no exclusion or diagnostic accounts for. */
188
220
  unparseableCommits?: Commit[];
189
- /** Policy violations detected while parsing this workspace's commits; omitted when none. */
221
+ /** Policy violations of the unreleased window's titles and change-record entries; omitted when none. */
190
222
  policyViolations?: PolicyViolation[];
223
+ /** Commits of the unreleased window whose `change-record` block could not be read; omitted when none. */
224
+ malformedBlocks?: MalformedChangeRecordBlock[];
225
+ /** Change-record entries of the unreleased window whose type is undeclared; omitted when none. */
226
+ undeclaredEntryTypes?: UndeclaredEntryType[];
191
227
  skipReason: string;
192
228
  }
193
229
  /**
@@ -210,16 +246,19 @@ export interface ReleasedProjectResult {
210
246
  previousTag?: string;
211
247
  commitCount: number;
212
248
  /**
213
- * Count of commits that parsed into a recognized work type. Always populated by the
214
- * unified `decideRelease` algorithm — `0` when there are no commits or when none
215
- * parse. Use `bumpOverride` (not `parsedCommitCount === 0`) as the signal for "the
249
+ * Count of commits that yield at least one changelog item — `0` when there are no commits or
250
+ * when none does. Use `bumpOverride` (not `parsedCommitCount === 0`) as the signal for "the
216
251
  * user supplied --bump=X".
217
252
  */
218
253
  parsedCommitCount: number;
219
- /** Commits that could not be parsed into a recognized work type. */
254
+ /** Commits that yield no changelog item and that no exclusion or diagnostic accounts for. */
220
255
  unparseableCommits?: Commit[];
221
- /** Policy violations detected while parsing the project's commits; omitted when none. */
256
+ /** Policy violations of the unreleased window's titles and change-record entries; omitted when none. */
222
257
  policyViolations?: PolicyViolation[];
258
+ /** Commits of the unreleased window whose `change-record` block could not be read; omitted when none. */
259
+ malformedBlocks?: MalformedChangeRecordBlock[];
260
+ /** Change-record entries of the unreleased window whose type is undeclared; omitted when none. */
261
+ undeclaredEntryTypes?: UndeclaredEntryType[];
223
262
  releaseType: ReleaseType;
224
263
  currentVersion: string;
225
264
  newVersion: string;
@@ -247,16 +286,16 @@ export interface SkippedProjectResult {
247
286
  status: 'skipped';
248
287
  previousTag?: string;
249
288
  commitCount: number;
250
- /**
251
- * Count of commits that parsed into a recognized work type. Always populated by the
252
- * unified `decideRelease` algorithm — `0` when there are no commits or when none
253
- * parse.
254
- */
289
+ /** Count of commits that yield at least one changelog item — `0` when there are no commits or when none does. */
255
290
  parsedCommitCount: number;
256
- /** Commits that could not be parsed into a recognized work type. */
291
+ /** Commits that yield no changelog item and that no exclusion or diagnostic accounts for. */
257
292
  unparseableCommits?: Commit[];
258
- /** Policy violations detected while parsing the project's commits; omitted when none. */
293
+ /** Policy violations of the unreleased window's titles and change-record entries; omitted when none. */
259
294
  policyViolations?: PolicyViolation[];
295
+ /** Commits of the unreleased window whose `change-record` block could not be read; omitted when none. */
296
+ malformedBlocks?: MalformedChangeRecordBlock[];
297
+ /** Change-record entries of the unreleased window whose type is undeclared; omitted when none. */
298
+ undeclaredEntryTypes?: UndeclaredEntryType[];
260
299
  skipReason: string;
261
300
  }
262
301
  /**
@@ -287,6 +326,12 @@ export interface WorkTypeConfig {
287
326
  header: string;
288
327
  /** Optional aliases that map to this work type (e.g., 'feature' -> 'feat'). */
289
328
  aliases?: string[] | undefined;
329
+ /**
330
+ * Keeps commits and change-record entries of this type out of every changelog. They yield no
331
+ * item, so they contribute nothing to the version bump, and the release report does not list
332
+ * them as unparseable.
333
+ */
334
+ excludedFromChangelog?: boolean | undefined;
290
335
  }
291
336
  /**
292
337
  * Defines which commit types trigger major or minor version bumps.
@@ -361,6 +406,7 @@ export declare const releaseNotesInputSchema: z.ZodObject<{
361
406
  export declare const workTypeConfigSchema: z.ZodObject<{
362
407
  header: z.ZodString;
363
408
  aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
409
+ excludedFromChangelog: z.ZodOptional<z.ZodBoolean>;
364
410
  }, z.core.$strict>;
365
411
  /** Schema for the `versionPatterns` block. Both arrays are required when the field is provided. */
366
412
  export declare const versionPatternsSchema: z.ZodObject<{
@@ -415,7 +461,6 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
415
461
  outputPath: z.ZodOptional<z.ZodString>;
416
462
  devOnlySections: z.ZodOptional<z.ZodArray<z.ZodString>>;
417
463
  }, z.core.$strict>>;
418
- cliffConfigPath: z.ZodOptional<z.ZodString>;
419
464
  formatCommand: z.ZodOptional<z.ZodString>;
420
465
  project: z.ZodOptional<z.ZodObject<{
421
466
  paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -452,6 +497,7 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
452
497
  workTypes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
453
498
  header: z.ZodString;
454
499
  aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
500
+ excludedFromChangelog: z.ZodOptional<z.ZodBoolean>;
455
501
  }, z.core.$strict>>>;
456
502
  }, z.core.$strict>;
457
503
  /**
@@ -461,14 +507,16 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
461
507
  export type ReleaseKitConfig = z.infer<typeof releaseKitConfigSchema>;
462
508
  /** A raw commit from the git log. */
463
509
  export interface Commit {
464
- /** The full commit message (first line). */
510
+ /** The commit message, subject and body together. */
465
511
  message: string;
512
+ /** The first line of the commit message. */
513
+ subject: string;
466
514
  /** The commit hash. */
467
515
  hash: string;
468
516
  }
469
517
  /** A commit that has been parsed to extract structured metadata. */
470
518
  export interface ParsedCommit {
471
- /** The original commit message. */
519
+ /** The original commit message, subject and body together. */
472
520
  message: string;
473
521
  /** The commit hash. */
474
522
  hash: string;
@@ -530,8 +578,6 @@ export interface MonorepoReleaseConfig {
530
578
  * arguments. Paths are repo-relative; file paths containing spaces are not supported.
531
579
  */
532
580
  formatCommand?: string;
533
- /** Path to the cliff.toml file; defaults to 'cliff.toml' when absent. */
534
- cliffConfigPath?: string;
535
581
  /**
536
582
  * Maps scope shorthand names to their canonical names.
537
583
  * When a commit uses `shorthand|type: description` or `type(shorthand): description`,
@@ -572,8 +618,6 @@ export interface ReleaseConfig {
572
618
  * Paths are repo-relative; file paths containing spaces are not supported.
573
619
  */
574
620
  formatCommand?: string;
575
- /** Path to the cliff.toml file; defaults to 'cliff.toml' when absent. */
576
- cliffConfigPath?: string;
577
621
  /**
578
622
  * Maps scope shorthand names to their canonical names.
579
623
  * When a commit uses `shorthand|type: description` or `type(shorthand): description`,
package/dist/esm/types.js CHANGED
@@ -41,6 +41,7 @@ export const workTypeConfigSchema = z
41
41
  .object({
42
42
  header: z.string(),
43
43
  aliases: z.array(z.string()).optional(),
44
+ excludedFromChangelog: z.boolean().optional(),
44
45
  })
45
46
  .strict();
46
47
  export const versionPatternsSchema = z
@@ -66,7 +67,6 @@ export const releaseKitConfigSchema = z
66
67
  .object({
67
68
  breakingPolicies: z.record(z.string(), breakingPolicyValueSchema).optional(),
68
69
  changelogJson: changelogJsonInputSchema.optional(),
69
- cliffConfigPath: z.string().min(1).optional(),
70
70
  formatCommand: z.string().min(1).optional(),
71
71
  project: projectConfigSchema.optional(),
72
72
  releaseNotes: releaseNotesInputSchema.optional(),
@@ -30,6 +30,10 @@ function preprocessDeprecatedKeys(raw) {
30
30
  return { cleaned: raw, deprecationErrors: [] };
31
31
  const errors = [];
32
32
  const cleaned = { ...raw };
33
+ if (Object.hasOwn(cleaned, 'cliffConfigPath')) {
34
+ errors.push('cliffConfigPath is no longer supported. release-kit reads changelog history from git directly and uses no external config file. Remove this field from your config, and delete the file it names.');
35
+ delete cleaned['cliffConfigPath'];
36
+ }
33
37
  if (isRecord(cleaned['releaseNotes']) && Object.hasOwn(cleaned['releaseNotes'], 'shouldCreateGithubRelease')) {
34
38
  errors.push('releaseNotes.shouldCreateGithubRelease is no longer supported. Adoption is now signaled by installing the create-github-release workflow, which `release-kit init` scaffolds. Remove this field from your config.');
35
39
  const releaseNotesCopy = { ...cleaned['releaseNotes'] };
@@ -1,6 +1,7 @@
1
1
  import { type OutputStyle, type StreamStyles } from '@williamthorsen/nmr-core';
2
2
  import { type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult } from './changelogOverrides.js';
3
3
  import { type WorkspaceDiscovery } from './discoverWorkspaces.js';
4
+ import { type GenerateChangelogOptions } from './generateChangelogs.js';
4
5
  import { type LoadValidatedConfigResult } from './loadValidatedConfig.js';
5
6
  import type { ChangelogEntry, ReleaseConfig } from './types.js';
6
7
  /**
@@ -10,7 +11,7 @@ import type { ChangelogEntry, ReleaseConfig } from './types.js';
10
11
  * Exit codes:
11
12
  * - `0` — clean: no errors, no warnings.
12
13
  * - `1` — only stale-key warnings.
13
- * - `2` — schema/parse or ambiguous-prefix errors (errors dominate when both classes exist).
14
+ * - `2` — schema/parse errors or keys that fail to match (ambiguous prefix, overlapping keys, a bare key setting fields on several items) (errors dominate when both classes exist).
14
15
  */
15
16
  export interface ValidateOverridesCommandResult {
16
17
  exitCode: 0 | 1 | 2;
@@ -22,17 +23,17 @@ export interface ValidateOverridesCommandDependencies {
22
23
  loadValidatedConfig?: () => Promise<LoadValidatedConfigResult>;
23
24
  /**
24
25
  * Build changelog entries for a scope. Defaults to `buildChangelogEntries`, the same path
25
- * `release-kit prepare` uses — anchoring `validate`'s hash universe to `prepare`'s by
26
- * construction. `tagPattern` and `includePaths` are passed straight through to git-cliff.
26
+ * `release-kit prepare` uses — anchoring `validate`'s item universe to `prepare`'s by
27
+ * construction.
27
28
  */
28
- buildEntries?: (config: Pick<ReleaseConfig, 'cliffConfigPath' | 'changelogJson'>, tagPattern?: string, includePaths?: string[]) => ChangelogEntry[];
29
+ buildEntries?: (config: Pick<ReleaseConfig, 'breakingPolicies' | 'changelogJson' | 'workTypes'>, options: GenerateChangelogOptions) => ChangelogEntry[];
29
30
  /** Pluggable validator (default: the production library function). Tests use this to drive specific result shapes through the formatter. */
30
31
  validate?: (inputs: ValidateAllChangelogOverridesInputs) => ValidateAllChangelogOverridesResult;
31
32
  }
32
33
  /**
33
34
  * Validate every changelog override file across the project and per-workspace scopes, and
34
35
  * return a tiered exit-code-plus-message result. Performs workspace discovery, config load,
35
- * and per-scope hash collection, then delegates the actual validation to
36
+ * and per-scope item collection, then delegates the actual validation to
36
37
  * {@link validateAllChangelogOverrides}.
37
38
  *
38
39
  * Single-package and monorepo modes are handled uniformly: single-package collapses to one
@@ -3,7 +3,7 @@ import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
4
4
  import { resolveOverridePath, validateAllChangelogOverrides, } from "./changelogOverrides.js";
5
5
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
6
- import { buildTagPattern, getAllTagPrefixes } from "./generateChangelogs.js";
6
+ import { getAllTagPrefixes } from "./generateChangelogs.js";
7
7
  import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
8
8
  import { loadValidatedConfig } from "./loadValidatedConfig.js";
9
9
  const SYNTHETIC_VALIDATE_TAG = 'validate-only';
@@ -68,28 +68,21 @@ function formatSummaryLine(errorCount, warningCount) {
68
68
  function pluralize(count, noun) {
69
69
  return count === 1 ? `${count} ${noun}` : `${count} ${noun}s`;
70
70
  }
71
- function defaultBuildEntries(config, tagPattern, includePaths) {
72
- const options = {};
73
- if (tagPattern !== undefined) {
74
- options.tagPattern = tagPattern;
75
- }
76
- if (includePaths !== undefined) {
77
- options.includePaths = includePaths;
78
- }
79
- return buildChangelogEntries(config, SYNTHETIC_VALIDATE_TAG, options);
71
+ function defaultBuildEntries(config, options) {
72
+ return buildChangelogEntries(config, SYNTHETIC_VALIDATE_TAG, options).entries;
80
73
  }
81
- function flattenEntriesToHashes(entries) {
82
- const hashes = [];
74
+ function flattenEntriesToItems(entries) {
75
+ const items = [];
83
76
  for (const entry of entries) {
84
77
  for (const section of entry.sections) {
85
- for (const item of section.items) {
86
- if (item.hash !== undefined) {
87
- hashes.push(item.hash);
88
- }
78
+ for (const { hash, entry: position } of section.items) {
79
+ if (hash === undefined)
80
+ continue;
81
+ items.push(position === undefined ? { hash } : { hash, entry: position });
89
82
  }
90
83
  }
91
84
  }
92
- return hashes;
85
+ return items;
93
86
  }
94
87
  function formatConfigProblem(problem) {
95
88
  return problem.kind === 'load'
@@ -98,19 +91,19 @@ function formatConfigProblem(problem) {
98
91
  }
99
92
  function buildSinglePackageInputs(userConfig, buildEntries) {
100
93
  const config = mergeSinglePackageConfig(userConfig);
101
- const hashes = flattenEntriesToHashes(buildEntries(config));
94
+ const items = flattenEntriesToItems(buildEntries(config, { tagPrefixes: [config.tagPrefix] }));
102
95
  return {
103
- project: { filePath: resolveOverridePath('.'), hashes },
96
+ project: { filePath: resolveOverridePath('.'), items },
104
97
  };
105
98
  }
106
99
  function buildMonorepoInputs(discoveredPaths, userConfig, buildEntries) {
107
100
  const rootPackage = readRootPackageVersion();
108
101
  const config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
109
102
  const workspaces = config.workspaces.map((workspace) => {
110
- const tagPattern = buildTagPattern(getAllTagPrefixes(workspace));
103
+ const options = { tagPrefixes: getAllTagPrefixes(workspace), paths: workspace.paths };
111
104
  return {
112
105
  filePath: resolveOverridePath(workspace.workspacePath),
113
- hashes: flattenEntriesToHashes(buildEntries(config, tagPattern, workspace.paths)),
106
+ items: flattenEntriesToItems(buildEntries(config, options)),
114
107
  };
115
108
  });
116
109
  const project = config.project;
@@ -118,8 +111,8 @@ function buildMonorepoInputs(discoveredPaths, userConfig, buildEntries) {
118
111
  filePath: resolveOverridePath('.'),
119
112
  };
120
113
  if (project !== undefined) {
121
- const projectTagPattern = buildTagPattern([project.tagPrefix]);
122
- projectScope.hashes = flattenEntriesToHashes(buildEntries(config, projectTagPattern, project.paths));
114
+ const options = { tagPrefixes: [project.tagPrefix], paths: project.paths };
115
+ projectScope.items = flattenEntriesToItems(buildEntries(config, options));
123
116
  }
124
117
  return { project: projectScope, workspaces };
125
118
  }