@williamthorsen/release-kit 11.0.0 → 13.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 (189) hide show
  1. package/.meta/changelog.json +274 -2
  2. package/.readyup/kits/default.js +18 -33
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +98 -0
  6. package/README.md +57 -69
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertCleanWorkingTree.d.ts +1 -5
  9. package/dist/esm/assertTaggedBaseline.d.ts +29 -0
  10. package/dist/esm/assertTaggedBaseline.js +41 -0
  11. package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
  12. package/dist/esm/attachChangelogDiagnostics.js +14 -0
  13. package/dist/esm/bin/release-kit.js +88 -24
  14. package/dist/esm/buildChangelogEntries.d.ts +69 -17
  15. package/dist/esm/buildChangelogEntries.js +222 -120
  16. package/dist/esm/buildDependencyGraph.d.ts +6 -6
  17. package/dist/esm/buildEmptyReleaseEntry.d.ts +1 -8
  18. package/dist/esm/buildReleaseSummary.d.ts +2 -8
  19. package/dist/esm/buildReleaseSummary.js +2 -2
  20. package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
  21. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  22. package/dist/esm/bumpVersion.d.ts +1 -8
  23. package/dist/esm/changelogJsonFile.d.ts +5 -7
  24. package/dist/esm/changelogJsonFile.js +2 -17
  25. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  26. package/dist/esm/changelogOverrides.d.ts +79 -100
  27. package/dist/esm/changelogOverrides.js +138 -58
  28. package/dist/esm/classifyChangelogCommit.d.ts +36 -0
  29. package/dist/esm/classifyChangelogCommit.js +34 -0
  30. package/dist/esm/commitCommand.d.ts +2 -4
  31. package/dist/esm/compareVersions.d.ts +10 -4
  32. package/dist/esm/compareVersions.js +16 -0
  33. package/dist/esm/configFlagSchema.d.ts +1 -1
  34. package/dist/esm/createGithubRelease.d.ts +7 -19
  35. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  36. package/dist/esm/createGithubReleaseCommand.js +9 -9
  37. package/dist/esm/createTags.d.ts +2 -3
  38. package/dist/esm/decideRelease.d.ts +11 -41
  39. package/dist/esm/decideRelease.js +5 -27
  40. package/dist/esm/defaults.d.ts +5 -24
  41. package/dist/esm/defaults.js +7 -5
  42. package/dist/esm/defineConfig.d.ts +1 -1
  43. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  44. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  45. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  46. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  47. package/dist/esm/detectPackageManager.d.ts +2 -4
  48. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  49. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  50. package/dist/esm/determineBumpType.d.ts +11 -11
  51. package/dist/esm/determineBumpType.js +6 -7
  52. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  53. package/dist/esm/discoverWorkspaces.js +12 -12
  54. package/dist/esm/enterRepoRoot.d.ts +12 -0
  55. package/dist/esm/enterRepoRoot.js +16 -0
  56. package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
  57. package/dist/esm/enumerateReleaseWindows.js +124 -0
  58. package/dist/esm/extractMigration.d.ts +6 -8
  59. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  60. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  61. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  62. package/dist/esm/formatExcludedSkip.js +3 -0
  63. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  64. package/dist/esm/generateChangelogs.d.ts +13 -26
  65. package/dist/esm/generateChangelogs.js +0 -14
  66. package/dist/esm/glyphs.d.ts +2 -2
  67. package/dist/esm/help/prepareHelp.d.ts +3 -4
  68. package/dist/esm/help/prepareHelp.js +7 -8
  69. package/dist/esm/index.d.ts +1 -1
  70. package/dist/esm/init/checks.d.ts +3 -3
  71. package/dist/esm/init/checks.js +4 -1
  72. package/dist/esm/init/detectRepoType.d.ts +2 -3
  73. package/dist/esm/init/hasProvenance.d.ts +2 -0
  74. package/dist/esm/init/hasProvenance.js +3 -0
  75. package/dist/esm/init/initCommand.d.ts +3 -6
  76. package/dist/esm/init/initCommand.js +11 -11
  77. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  78. package/dist/esm/init/scaffold.d.ts +1 -3
  79. package/dist/esm/init/scaffold.js +2 -30
  80. package/dist/esm/init/templates.d.ts +9 -11
  81. package/dist/esm/init/templates.js +12 -11
  82. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  83. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  84. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  85. package/dist/esm/injectSection.d.ts +3 -3
  86. package/dist/esm/loadConfig.d.ts +16 -14
  87. package/dist/esm/loadConfig.js +2 -5
  88. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  89. package/dist/esm/loadValidatedConfig.js +6 -1
  90. package/dist/esm/packsPath.d.ts +9 -9
  91. package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
  92. package/dist/esm/parseChangeRecordBlock.js +134 -0
  93. package/dist/esm/parseCommitMessage.d.ts +42 -32
  94. package/dist/esm/parseCommitMessage.js +33 -41
  95. package/dist/esm/parseRequestedTags.d.ts +3 -3
  96. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  97. package/dist/esm/planVersionBump.d.ts +5 -3
  98. package/dist/esm/planVersionBump.js +3 -0
  99. package/dist/esm/prepareCommand.d.ts +4 -7
  100. package/dist/esm/prepareCommand.js +17 -18
  101. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  102. package/dist/esm/propagateBumps.d.ts +6 -7
  103. package/dist/esm/publish.d.ts +1 -1
  104. package/dist/esm/publishCommand.d.ts +4 -3
  105. package/dist/esm/publishCommand.js +8 -8
  106. package/dist/esm/pushCommand.d.ts +4 -3
  107. package/dist/esm/pushCommand.js +8 -2
  108. package/dist/esm/pushRelease.d.ts +2 -2
  109. package/dist/esm/readChangelogSections.d.ts +33 -0
  110. package/dist/esm/readChangelogSections.js +80 -0
  111. package/dist/esm/releaseFiles.d.ts +5 -7
  112. package/dist/esm/releasePlan.d.ts +11 -13
  113. package/dist/esm/releasePrepare.d.ts +25 -25
  114. package/dist/esm/releasePrepare.js +80 -80
  115. package/dist/esm/releasePrepareMono.d.ts +9 -7
  116. package/dist/esm/releasePrepareMono.js +114 -99
  117. package/dist/esm/releasePrepareProject.d.ts +19 -29
  118. package/dist/esm/releasePrepareProject.js +46 -45
  119. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  120. package/dist/esm/renderChangelogMarkdown.js +27 -2
  121. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  122. package/dist/esm/renderReleaseNotes.js +2 -6
  123. package/dist/esm/reportPrepare.d.ts +4 -5
  124. package/dist/esm/reportPrepare.js +67 -42
  125. package/dist/esm/resolveCommandTags.d.ts +15 -11
  126. package/dist/esm/resolveCommandTags.js +35 -5
  127. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  128. package/dist/esm/resolveConfigFlag.js +4 -0
  129. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  130. package/dist/esm/resolveReleaseTags.js +1 -1
  131. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  132. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  133. package/dist/esm/showTagPrefixesCommand.js +4 -4
  134. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  135. package/dist/esm/stripScope.d.ts +2 -2
  136. package/dist/esm/stripScope.js +3 -3
  137. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  138. package/dist/esm/sync-labels/generateCommand.js +11 -8
  139. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  140. package/dist/esm/sync-labels/initCommand.js +2 -2
  141. package/dist/esm/sync-labels/presets.d.ts +5 -4
  142. package/dist/esm/sync-labels/presets.js +9 -9
  143. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  144. package/dist/esm/sync-labels/resolveLabels.js +1 -1
  145. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  146. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  147. package/dist/esm/sync-labels/templates.d.ts +3 -3
  148. package/dist/esm/sync-labels/templates.js +3 -3
  149. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  150. package/dist/esm/sync-labels/typeLabels.js +38 -0
  151. package/dist/esm/sync-labels/types.d.ts +1 -1
  152. package/dist/esm/tagCommand.d.ts +1 -1
  153. package/dist/esm/typeGuards.d.ts +2 -2
  154. package/dist/esm/types.d.ts +185 -116
  155. package/dist/esm/types.js +2 -2
  156. package/dist/esm/validateConfig.d.ts +2 -11
  157. package/dist/esm/validateConfig.js +5 -1
  158. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  159. package/dist/esm/validateOverridesCommand.d.ts +19 -21
  160. package/dist/esm/validateOverridesCommand.js +16 -23
  161. package/docs/api.md +6 -6
  162. package/docs/changelogs.md +90 -28
  163. package/docs/configuration.md +30 -29
  164. package/docs/editorial-overrides.md +90 -31
  165. package/docs/labels.md +21 -19
  166. package/docs/project-releases.md +14 -14
  167. package/docs/publishing.md +5 -5
  168. package/docs/readiness-checks.md +3 -3
  169. package/docs/releasing.md +54 -24
  170. package/docs/work-types.md +46 -58
  171. package/package.json +7 -7
  172. package/presets/labels/common.yaml +1 -41
  173. package/schemas/changelog-overrides.json +39 -0
  174. package/cliff.toml.template +0 -65
  175. package/dist/esm/collectPolicyViolations.d.ts +0 -15
  176. package/dist/esm/collectPolicyViolations.js +0 -12
  177. package/dist/esm/determineBumpFromCommits.d.ts +0 -17
  178. package/dist/esm/determineBumpFromCommits.js +0 -28
  179. package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
  180. package/dist/esm/getCommitsSinceTarget.js +0 -66
  181. package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
  182. package/dist/esm/resolveCliffConfigPath.js +0 -20
  183. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  184. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  185. package/dist/esm/runGitCliff.d.ts +0 -38
  186. package/dist/esm/runGitCliff.js +0 -25
  187. package/dist/esm/workTypesData.d.ts +0 -50
  188. package/dist/esm/workTypesData.js +0 -145
  189. package/schemas/label-map.json +0 -24
@@ -1,161 +1,263 @@
1
1
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
2
+ import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
2
3
  import { extractVersion } from "./changelogJsonUtils.js";
3
- import { DEFAULT_BREAKING_POLICIES, DEFAULT_WORK_TYPES } from "./defaults.js";
4
+ import { classifyChangelogCommit, isNonChangeSubject, isReleaseSubject } from "./classifyChangelogCommit.js";
5
+ import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
6
+ import { determineBumpType } from "./determineBumpType.js";
7
+ import { enumerateReleaseWindows } from "./enumerateReleaseWindows.js";
4
8
  import { extractMigration } from "./extractMigration.js";
5
- import { COMMIT_PREPROCESSOR_PATTERNS, parseCommitMessage, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
6
- import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
7
- import { runGitCliff } from "./runGitCliff.js";
9
+ import { parseChangeRecordBlock, stripChangeRecordBlocks } from "./parseChangeRecordBlock.js";
10
+ import { evaluateBreakingPolicy, parseSubject, resolveType } from "./parseCommitMessage.js";
8
11
  import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
9
- import { isRecord, isUnknownArray } from "./typeGuards.js";
10
- const HTML_COMMENT_PREFIX_PATTERN = /^<!--[^>]*-->/;
12
+ const UNRELEASED_TAG = 'unreleased';
11
13
  const CANONICAL_SECTION_ORDER = new Map(Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index]));
12
14
  function canonicalSectionPriority(title) {
13
15
  const index = CANONICAL_SECTION_ORDER.get(stripGroupDecorations(title));
14
16
  return index ?? Infinity;
15
17
  }
16
18
  export function stripGroupDecorations(group) {
17
- return stripEmojiPrefix(group.replace(HTML_COMMENT_PREFIX_PATTERN, ''));
19
+ return stripEmojiPrefix(group);
18
20
  }
19
21
  export function buildChangelogEntries(config, tag, options) {
20
- const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
21
- const cliffArgs = ['--context', '--tag', tag];
22
- if (options?.tagPattern !== undefined) {
23
- cliffArgs.push('--tag-pattern', options.tagPattern);
24
- }
25
- const includePaths = options?.includePaths ?? [];
26
- for (const includePath of includePaths) {
27
- cliffArgs.push('--include-path', includePath);
28
- }
22
+ const history = readReleaseHistory(config, options);
23
+ return { entries: toChangelogEntries(history, tag), diagnostics: history.unreleased.diagnostics };
24
+ }
25
+ export function readReleaseHistory(config, options) {
29
26
  try {
30
- const contextJson = runGitCliff(resolvedConfigPath, cliffArgs);
31
- const releases = parseCliffContext(contextJson);
32
- const devOnlySections = new Set(config.changelogJson.devOnlySections);
33
- const workTypes = config.workTypes ?? DEFAULT_WORK_TYPES;
34
- const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
35
- return transformReleases(releases, devOnlySections, workTypes, breakingPolicies);
27
+ const windows = enumerateReleaseWindows({
28
+ ...(options.paths !== undefined && { paths: options.paths }),
29
+ tagPrefixes: options.tagPrefixes,
30
+ unreleasedTag: UNRELEASED_TAG,
31
+ });
32
+ const context = {
33
+ breakingPolicies: config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES,
34
+ devOnlySections: new Set(config.changelogJson.devOnlySections.map(stripGroupDecorations)),
35
+ ...(options.workspaceDir !== undefined && {
36
+ routing: { workspaceDir: options.workspaceDir, scopeAliases: config.scopeAliases ?? {} },
37
+ }),
38
+ versionPatterns: config.versionPatterns ?? DEFAULT_VERSION_PATTERNS,
39
+ workTypes: config.workTypes ?? DEFAULT_WORK_TYPES,
40
+ };
41
+ return transformReleases(windows, context);
36
42
  }
37
43
  catch (error) {
38
- throw chainError(`Failed to build changelog entries for tag ${tag}`, error);
44
+ throw chainError(`Failed to read the release history for ${options.tagPrefixes.join(', ')}`, error);
39
45
  }
40
46
  }
41
- function parseCliffContext(json) {
42
- const parsed = JSON.parse(json);
43
- if (!isUnknownArray(parsed)) {
44
- throw new TypeError('Expected git-cliff --context output to be an array');
47
+ export function toChangelogEntries(history, tag) {
48
+ const { unreleased } = history;
49
+ if (unreleased.sections.length === 0) {
50
+ return [...history.releasedEntries];
45
51
  }
46
- return parsed.map(toCliffContextRelease);
52
+ return [
53
+ { version: extractVersion(tag), date: unreleased.date, sections: unreleased.sections },
54
+ ...history.releasedEntries,
55
+ ];
47
56
  }
48
- function toCliffContextRelease(value) {
49
- if (!isRecord(value)) {
50
- return {};
51
- }
52
- const release = {};
53
- if (typeof value['version'] === 'string') {
54
- release.version = value['version'];
55
- }
56
- if (typeof value['timestamp'] === 'number') {
57
- release.timestamp = value['timestamp'];
58
- }
59
- if (isUnknownArray(value['commits'])) {
60
- release.commits = value['commits'].map(toCliffContextCommit);
57
+ export function toReleaseEntries(history, tag, today) {
58
+ if (history.unreleased.sections.length === 0) {
59
+ return [buildEmptyReleaseEntry(extractVersion(tag), today), ...history.releasedEntries];
61
60
  }
62
- return release;
61
+ return toChangelogEntries(history, tag);
63
62
  }
64
- function toCliffContextCommit(value) {
65
- if (!isRecord(value)) {
66
- return { message: '' };
67
- }
68
- const commit = {
69
- message: typeof value['message'] === 'string' ? value['message'] : '',
63
+ function transformReleases(windows, context) {
64
+ const [unreleasedWindow, baselineWindow] = windows;
65
+ const diagnostics = {
66
+ malformedBlocks: [],
67
+ policyViolations: [],
68
+ undeclaredEntryTypes: [],
69
+ unroutedEntryScopes: [],
70
70
  };
71
- if (typeof value['group'] === 'string') {
72
- commit.group = value['group'];
73
- }
74
- if (typeof value['id'] === 'string') {
75
- commit.id = value['id'];
76
- }
77
- return commit;
78
- }
79
- function transformReleases(releases, devOnlySections, workTypes, breakingPolicies) {
80
- const entries = [];
81
- const devOnlyNormalised = new Set([...devOnlySections].map(stripGroupDecorations));
82
- for (const release of releases) {
83
- if (release.version === undefined) {
84
- continue;
71
+ const signals = [];
72
+ const unparseable = [];
73
+ let parsedCommitCount = 0;
74
+ const sectionMap = new Map();
75
+ const unreleasedCommits = unreleasedWindow?.commits ?? [];
76
+ for (const commit of unreleasedCommits) {
77
+ const reading = readCommit(commit, context, diagnostics);
78
+ for (const derived of reading.items) {
79
+ appendItem(sectionMap, derived.header, derived.item);
80
+ signals.push(derived.signal);
85
81
  }
86
- const version = extractVersion(release.version);
87
- const date = release.timestamp !== undefined ? new Date(release.timestamp * 1_000).toISOString().slice(0, 10) : 'unreleased';
88
- const sectionMap = new Map();
89
- const commits = release.commits ?? [];
90
- for (const commit of commits) {
91
- const group = stripCommentPrefix(commit.group ?? 'Other');
92
- const description = extractDescription(commit.message);
93
- const body = extractBody(commit.message);
94
- const breaking = isBreakingUnderPolicy(commit, workTypes, breakingPolicies);
95
- let items = sectionMap.get(group);
96
- if (items === undefined) {
97
- items = [];
98
- sectionMap.set(group, items);
99
- }
100
- const item = { description };
101
- if (body !== undefined) {
102
- item.body = body;
103
- }
104
- if (breaking) {
105
- item.breaking = true;
106
- }
107
- const migration = extractMigration(body);
108
- if (migration !== undefined) {
109
- item.migration = migration;
110
- }
111
- if (commit.id !== undefined && commit.id !== '') {
112
- item.hash = commit.id;
113
- }
114
- items.push(item);
82
+ if (reading.items.length > 0) {
83
+ parsedCommitCount += 1;
84
+ }
85
+ else if (reading.isUnparseable) {
86
+ unparseable.push(commit);
115
87
  }
116
- const sections = [];
117
- for (const [title, items] of sectionMap) {
118
- if (items.length === 0) {
119
- continue;
88
+ }
89
+ const releasedEntries = [];
90
+ for (const release of windows.slice(1)) {
91
+ const releaseSections = new Map();
92
+ for (const commit of release.commits) {
93
+ for (const derived of readCommit(commit, context, undefined).items) {
94
+ appendItem(releaseSections, derived.header, derived.item);
120
95
  }
121
- sections.push({
122
- title,
123
- audience: devOnlyNormalised.has(stripGroupDecorations(title)) ? 'dev' : 'all',
124
- items,
125
- });
126
96
  }
127
- sections.sort((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
97
+ const sections = buildSections(releaseSections, context.devOnlySections);
128
98
  if (sections.length > 0) {
129
- entries.push({ version, date, sections });
99
+ releasedEntries.push({ version: extractVersion(release.version), date: formatDate(release.timestamp), sections });
130
100
  }
131
101
  }
132
- return entries;
102
+ return {
103
+ previousTag: baselineWindow?.version,
104
+ releasedEntries,
105
+ unreleased: {
106
+ bump: determineBumpType(signals, context.workTypes, context.versionPatterns),
107
+ commits: unreleasedCommits.filter((commit) => !isReleaseSubject(commit.subject)).toReversed(),
108
+ date: formatDate(unreleasedWindow?.timestamp ?? Math.floor(Date.now() / 1_000)),
109
+ diagnostics,
110
+ parsedCommitCount,
111
+ sections: buildSections(sectionMap, context.devOnlySections),
112
+ unparseableCommits: unparseable.length > 0 ? unparseable.toReversed() : undefined,
113
+ },
114
+ };
133
115
  }
134
- function stripCommentPrefix(group) {
135
- return group.replace(HTML_COMMENT_PREFIX_PATTERN, '');
116
+ function readCommit(commit, context, diagnostics) {
117
+ if (isNonChangeSubject(commit.subject)) {
118
+ return { items: [], isUnparseable: false };
119
+ }
120
+ const reading = parseChangeRecordBlock(commit.message);
121
+ if (reading.kind === 'malformed') {
122
+ diagnostics?.malformedBlocks.push({
123
+ commitHash: commit.hash,
124
+ commitSubject: commit.subject,
125
+ reason: reading.reason,
126
+ });
127
+ }
128
+ if (reading.kind === 'read' && reading.entries.length > 0) {
129
+ const items = reading.entries.flatMap((entry, entryIndex) => buildEntryItem(commit, entry, entryIndex + 1, reading.prNumber, context, diagnostics) ?? []);
130
+ return { items, isUnparseable: false };
131
+ }
132
+ const classification = classifyChangelogCommit(commit, context.workTypes, {
133
+ breakingPolicies: context.breakingPolicies,
134
+ ...(diagnostics !== undefined && {
135
+ onPolicyViolation: (violating, type, surface) => {
136
+ diagnostics.policyViolations.push({
137
+ commitHash: violating.hash,
138
+ commitSubject: violating.subject,
139
+ type,
140
+ surface,
141
+ });
142
+ },
143
+ }),
144
+ });
145
+ if (classification.kind !== 'header') {
146
+ return { items: [], isUnparseable: classification.kind === 'unparseable' && reading.kind !== 'malformed' };
147
+ }
148
+ const item = buildTitleItem(commit, classification.breaking, context.workTypes);
149
+ return {
150
+ items: [
151
+ { header: classification.header, item, signal: { type: classification.type, breaking: item.breaking === true } },
152
+ ],
153
+ isUnparseable: false,
154
+ };
155
+ }
156
+ function buildSections(sectionMap, devOnlySections) {
157
+ const sections = [];
158
+ for (const [title, items] of sectionMap) {
159
+ sections.push({
160
+ title,
161
+ audience: devOnlySections.has(stripGroupDecorations(title)) ? 'dev' : 'all',
162
+ items,
163
+ });
164
+ }
165
+ return sections.toSorted((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
136
166
  }
137
- function isBreakingUnderPolicy(commit, workTypes, breakingPolicies) {
138
- if (!subjectHasBreakingMarker(commit.message)) {
139
- return false;
167
+ function isRoutedTo(scopes, routing) {
168
+ if (scopes.length === 0) {
169
+ return true;
140
170
  }
141
- const parsed = parseCommitMessage(commit.message, commit.id ?? '', workTypes, undefined, { breakingPolicies });
142
- return parsed?.breaking ?? true;
171
+ return scopes.some((scope) => {
172
+ const resolved = routing.scopeAliases[scope] ?? scope;
173
+ return resolved === '*' || resolved === routing.workspaceDir;
174
+ });
175
+ }
176
+ function formatDate(timestamp) {
177
+ return new Date(timestamp * 1_000).toISOString().slice(0, 10);
143
178
  }
144
- const SUBJECT_BREAKING_MARKER_PATTERN = new RegExp(String.raw `^(?:${PIPE_SCOPE_SOURCE}\|)?\w+(?:\([^)]+\))?!:`);
145
- function subjectHasBreakingMarker(message) {
146
- let subject = message.split('\n', 1)[0] ?? '';
147
- for (const pattern of COMMIT_PREPROCESSOR_PATTERNS) {
148
- subject = subject.replace(pattern, '');
179
+ function appendItem(sectionMap, header, item) {
180
+ let items = sectionMap.get(header);
181
+ if (items === undefined) {
182
+ items = [];
183
+ sectionMap.set(header, items);
149
184
  }
150
- return SUBJECT_BREAKING_MARKER_PATTERN.test(subject);
185
+ items.push(item);
186
+ }
187
+ function buildEntryItem(commit, entry, position, prNumber, context, diagnostics) {
188
+ const { breakingPolicies, routing, workTypes } = context;
189
+ if (routing !== undefined && !isRoutedTo(entry.scopes, routing)) {
190
+ return undefined;
191
+ }
192
+ const type = resolveType(entry.type, workTypes);
193
+ if (type === undefined) {
194
+ diagnostics?.undeclaredEntryTypes.push({
195
+ commitHash: commit.hash,
196
+ commitSubject: commit.subject,
197
+ entryPosition: position,
198
+ type: entry.type,
199
+ });
200
+ return undefined;
201
+ }
202
+ const config = workTypes[type];
203
+ if (config === undefined || config.excludedFromChangelog === true) {
204
+ return undefined;
205
+ }
206
+ const breaking = evaluateBreakingPolicy({
207
+ commit: { message: commit.message, subject: commit.subject, hash: commit.hash },
208
+ resolvedType: type,
209
+ hasPrefixBreaking: entry.breaking,
210
+ hasFooterBreaking: false,
211
+ policy: breakingPolicies[type] ?? 'optional',
212
+ prefixSurface: 'entry',
213
+ onPolicyViolation: diagnostics === undefined
214
+ ? undefined
215
+ : (violating, violatedType, surface) => {
216
+ diagnostics.policyViolations.push({
217
+ commitHash: violating.hash,
218
+ commitSubject: violating.subject,
219
+ type: violatedType,
220
+ surface,
221
+ entryPosition: position,
222
+ });
223
+ },
224
+ });
225
+ const item = {
226
+ description: prNumber === undefined ? entry.text : `${entry.text} (#${prNumber})`,
227
+ };
228
+ if (breaking) {
229
+ item.breaking = true;
230
+ }
231
+ if (entry.migration !== undefined) {
232
+ item.migration = entry.migration;
233
+ }
234
+ item.hash = commit.hash;
235
+ item.entry = position;
236
+ return { header: config.header, item, signal: { type, breaking } };
237
+ }
238
+ function buildTitleItem(commit, isParsedBreaking, workTypes) {
239
+ const body = extractBody(stripChangeRecordBlocks(commit.message));
240
+ const item = { description: extractDescription(commit.message) };
241
+ if (body !== undefined) {
242
+ item.body = body;
243
+ }
244
+ if (isParsedBreaking && subjectHasBreakingMarker(commit.message, workTypes)) {
245
+ item.breaking = true;
246
+ }
247
+ const migration = extractMigration(body);
248
+ if (migration !== undefined) {
249
+ item.migration = migration;
250
+ }
251
+ item.hash = commit.hash;
252
+ return item;
253
+ }
254
+ function subjectHasBreakingMarker(message, workTypes) {
255
+ return parseSubject(message.split('\n', 1)[0] ?? '', workTypes)?.breaking === true;
151
256
  }
152
257
  function extractDescription(message) {
153
258
  const firstLine = message.split('\n', 1)[0] ?? message;
154
259
  const afterColon = firstLine.split(': ').slice(1).join(': ');
155
- if (afterColon.length > 0) {
156
- return afterColon.charAt(0).toUpperCase() + afterColon.slice(1);
157
- }
158
- return firstLine;
260
+ return afterColon.charAt(0).toUpperCase() + afterColon.slice(1);
159
261
  }
160
262
  const TRAILER_PATTERNS = [
161
263
  /^Change:/i,
@@ -1,17 +1,17 @@
1
1
  import type { WorkspaceConfig } from './types.js';
2
2
  /** Identities, current versions, and `workspace:` dependency edges of the workspaces' packages. */
3
3
  export interface DependencyGraph {
4
- /** Resolve a package name to its workspace `dir`. */
4
+ /** Maps a package name to its workspace `dir`. */
5
5
  packageNameToDir: Map<string, string>;
6
- /** Resolve a workspace `dir` to its package name (inverse of `packageNameToDir`). */
6
+ /** Maps a workspace `dir` to its package name (inverse of `packageNameToDir`). */
7
7
  dirToPackageName: Map<string, string>;
8
- /** Resolve a workspace `dir` to its current version; absent when its `package.json` has no string `version`. */
8
+ /** Maps a workspace `dir` to its current version; absent when its `package.json` doesn't have a string `version`. */
9
9
  dirToVersion: Map<string, string>;
10
- /** Map a package name to the workspaces that depend on it. */
10
+ /** Maps a package name to the workspaces that depend on it. */
11
11
  dependentsOf: Map<string, WorkspaceConfig[]>;
12
12
  /**
13
- * Forward adjacency: map a workspace `dir` to the set of `workspace:`-protocol package
14
- * names it declares in `dependencies` or `peerDependencies`. Complement of `dependentsOf`.
13
+ * Maps a workspace `dir` to the `workspace:`-protocol package names that it declares in `dependencies` or
14
+ * `peerDependencies` (the forward counterpart of `dependentsOf`).
15
15
  */
16
16
  dependenciesOf: Map<string, Set<string>>;
17
17
  }
@@ -1,10 +1,3 @@
1
1
  import type { ChangelogEntry } from './types.js';
2
- /**
3
- * Build a synthetic changelog entry for a forced empty-range release.
4
- *
5
- * Produces a single `ChangelogEntry` with one `'Notes'` section (audience `'dev'`)
6
- * containing a single `'Forced version bump.'` item. Used when `release-kit prepare`
7
- * proceeds via `--force`, `--bump=X`, or `--set-version` against a unit with zero
8
- * qualifying commits since its last tag. Pure function: no I/O.
9
- */
2
+ /** Builds the changelog entry of a forced release whose unreleased window doesn't yield any changelog item. */
10
3
  export declare function buildEmptyReleaseEntry(version: string, date: string): ChangelogEntry;
@@ -1,12 +1,6 @@
1
1
  import type { PrepareResult } from './types.js';
2
2
  /**
3
- * Build a release commit body from a prepare result.
4
- *
5
- * Each released workspace with commits gets a section headed by its tag,
6
- * followed by scope-stripped commit messages as bullet points. Sections
7
- * are separated by blank lines. The project release (when present) is
8
- * appended as a final section using the same format. Returns an empty
9
- * string when no released workspaces have commits and there is no project
10
- * release.
3
+ * Builds a release commit body from a prepare result: one section per released workspace or project that has commits,
4
+ * the project last, each headed by its tag and listing its scope-stripped commit subjects.
11
5
  */
12
6
  export declare function buildReleaseSummary(result: Pick<PrepareResult, 'project' | 'workspaces'>): string;
@@ -11,7 +11,7 @@ export function buildReleaseSummary(result) {
11
11
  }
12
12
  const lines = [workspace.tag];
13
13
  for (const commit of commits) {
14
- lines.push(`- ${stripScope(commit.message)}`);
14
+ lines.push(`- ${stripScope(commit.subject)}`);
15
15
  }
16
16
  sections.push(lines.join('\n'));
17
17
  }
@@ -19,7 +19,7 @@ export function buildReleaseSummary(result) {
19
19
  if (project !== undefined && project.status === 'released' && project.commits.length > 0) {
20
20
  const lines = [project.tag];
21
21
  for (const commit of project.commits) {
22
- lines.push(`- ${stripScope(commit.message)}`);
22
+ lines.push(`- ${stripScope(commit.subject)}`);
23
23
  }
24
24
  sections.push(lines.join('\n'));
25
25
  }
@@ -1,11 +1,9 @@
1
1
  import type { ChangelogEntry } from './types.js';
2
2
  /**
3
- * Build a synthetic changelog entry for a propagation-only bump.
4
- *
5
- * Produces a single `ChangelogEntry` with one `'Dependency updates'` section (audience `'dev'`)
6
- * containing one item per propagated-from dependency. Pure function: no I/O.
3
+ * Builds the changelog entry of a propagation-only bump, with one item per dependency whose bump propagated, under
4
+ * `sectionTitle`, the resolved `deps` work type's header.
7
5
  */
8
6
  export declare function buildSyntheticChangelogEntry(propagatedFrom: ReadonlyArray<{
9
7
  packageName: string;
10
8
  newVersion: string;
11
- }>, version: string, date: string): ChangelogEntry;
9
+ }>, version: string, date: string, sectionTitle: string): ChangelogEntry;
@@ -1,10 +1,10 @@
1
- export function buildSyntheticChangelogEntry(propagatedFrom, version, date) {
1
+ export function buildSyntheticChangelogEntry(propagatedFrom, version, date, sectionTitle) {
2
2
  const items = propagatedFrom.map((dep) => ({
3
3
  description: `Bumped \`${dep.packageName}\` to ${dep.newVersion}`,
4
4
  }));
5
5
  return {
6
6
  version,
7
7
  date,
8
- sections: [{ title: 'Dependency updates', audience: 'dev', items }],
8
+ sections: [{ title: sectionTitle, audience: 'dev', items }],
9
9
  };
10
10
  }
@@ -1,10 +1,3 @@
1
1
  import type { ReleaseType } from './types.js';
2
- /**
3
- * Bumps a semver version string by the given release type.
4
- *
5
- * @param version - A semver version string (e.g., '1.2.3').
6
- * @param releaseType - The type of release bump to apply.
7
- * @returns The bumped version string.
8
- * @throws If the version string is not a valid semver format.
9
- */
2
+ /** Bumps a `major.minor.patch` version by the release type; throws on a version of any other form. */
10
3
  export declare function bumpVersion(version: string, releaseType: ReleaseType): string;
@@ -1,15 +1,13 @@
1
1
  import type { ChangelogEntry, ReleaseConfig } from './types.js';
2
- /** Resolve the absolute output path for the `changelog.json` file under a workspace's changelog directory. */
2
+ /** Resolves the path of the `changelog.json` file under a workspace's changelog directory. */
3
3
  export declare function resolveChangelogJsonPath(config: Pick<ReleaseConfig, 'changelogJson'>, changelogPath: string): string;
4
4
  /** Renders changelog entries as a `changelog.json` file's complete content, sorted newest-first. */
5
5
  export declare function renderChangelogJson(entries: ChangelogEntry[]): string;
6
6
  /**
7
- * Merges `entries` with the on-disk entries at `filePath` and returns the merged set in
8
- * newest-first order, without writing.
7
+ * Merges `entries` with the on-disk entries at `filePath` and returns the merged set in newest-first order, without
8
+ * writing.
9
9
  *
10
- * Preserves entries that exist on disk but are absent from `entries` — load-bearing for
11
- * synthetic-entry preservation across propagation runs. Reads the file when present; treats a
12
- * missing or malformed file as an empty existing set, so a malformed file does not abort the
13
- * release.
10
+ * Keeps an on-disk entry that `entries` lacks, which preserves synthetic entries across propagation runs. A missing or
11
+ * malformed file counts as empty, so that a malformed file does not abort the release.
14
12
  */
15
13
  export declare function mergeChangelogEntriesWithDisk(filePath: string, entries: ChangelogEntry[]): ChangelogEntry[];
@@ -2,8 +2,8 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import stringify from 'json-stringify-pretty-compact';
5
- import semver from 'semver';
6
5
  import { isChangelogEntry } from "./changelogJsonUtils.js";
6
+ import { compareVersionsDescending } from "./compareVersions.js";
7
7
  import { isUnknownArray } from "./typeGuards.js";
8
8
  export function resolveChangelogJsonPath(config, changelogPath) {
9
9
  return join(changelogPath, config.changelogJson.outputPath);
@@ -16,7 +16,7 @@ export function mergeChangelogEntriesWithDisk(filePath, entries) {
16
16
  return mergeEntries(entries, existing);
17
17
  }
18
18
  function sortNewestFirst(entries) {
19
- return [...entries].sort((a, b) => compareVersionsDescending(a.version, b.version));
19
+ return [...entries].toSorted((a, b) => compareVersionsDescending(a.version, b.version));
20
20
  }
21
21
  function readExistingEntries(filePath) {
22
22
  if (!existsSync(filePath)) {
@@ -45,18 +45,3 @@ function mergeEntries(newEntries, existingEntries) {
45
45
  }
46
46
  return sortNewestFirst(versionMap.values());
47
47
  }
48
- function compareVersionsDescending(a, b) {
49
- const aValid = semver.valid(a);
50
- const bValid = semver.valid(b);
51
- if (aValid && bValid)
52
- return semver.rcompare(aValid, bValid);
53
- if (aValid)
54
- return -1;
55
- if (bValid)
56
- return 1;
57
- if (a > b)
58
- return -1;
59
- if (a < b)
60
- return 1;
61
- return 0;
62
- }
@@ -1,19 +1,16 @@
1
1
  import type { ChangelogEntry } from './types.js';
2
2
  /**
3
- * Type guard for `ChangelogEntry` values parsed from JSON.
3
+ * Reports whether a value parsed from JSON is a `ChangelogEntry`.
4
4
  *
5
- * Checks the entry's top level only: `sections` is verified to be an array, and its contents pass
6
- * through unvalidated. `.meta/changelog.json` is written by release-kit and read tolerantly; strict
7
- * validation applies to the human-authored `.meta/changelog-overrides.json`, in `changelogOverrides.ts`.
5
+ * Checks the entry's top level only: `sections` must be an array, and its contents pass through unvalidated.
6
+ * `.meta/changelog.json` is written by release-kit and read tolerantly; strict validation applies to the
7
+ * human-authored `.meta/changelog-overrides.json`, in `changelogOverrides.ts`.
8
8
  */
9
9
  export declare function isChangelogEntry(value: unknown): value is ChangelogEntry;
10
- /** Extract the version from a tag by stripping the prefix up to the first digit. */
10
+ /** Extracts the version from a tag: the text from its first `N.N.N` onward, or the whole tag when it contains none. */
11
11
  export declare function extractVersion(tag: string): string;
12
12
  /**
13
- * Read and parse a changelog JSON file, returning validated entries.
14
- *
15
- * Returns `undefined` if the file cannot be read or does not contain a valid array.
16
- * Silent on parse error — for the warn-and-treat-as-empty variant used by upsert callers,
17
- * see `readExistingEntries` in `./changelogJsonFile.ts`.
13
+ * Reads the changelog entries in a JSON file; undefined when the file is missing or unreadable, or doesn't contain an
14
+ * array.
18
15
  */
19
16
  export declare function readChangelogEntries(filePath: string): ChangelogEntry[] | undefined;