@williamthorsen/release-kit 12.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 (168) hide show
  1. package/.meta/changelog.json +212 -0
  2. package/.readyup/kits/default.js +18 -17
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +58 -0
  6. package/README.md +55 -48
  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 +1 -1
  12. package/dist/esm/attachChangelogDiagnostics.js +3 -0
  13. package/dist/esm/bin/release-kit.js +85 -22
  14. package/dist/esm/buildChangelogEntries.d.ts +23 -13
  15. package/dist/esm/buildChangelogEntries.js +36 -17
  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/buildSyntheticChangelogEntry.d.ts +3 -5
  20. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  21. package/dist/esm/bumpVersion.d.ts +1 -8
  22. package/dist/esm/changelogJsonFile.d.ts +5 -7
  23. package/dist/esm/changelogJsonFile.js +2 -17
  24. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  25. package/dist/esm/changelogOverrides.d.ts +59 -98
  26. package/dist/esm/classifyChangelogCommit.d.ts +7 -9
  27. package/dist/esm/classifyChangelogCommit.js +3 -2
  28. package/dist/esm/commitCommand.d.ts +2 -4
  29. package/dist/esm/compareVersions.d.ts +10 -4
  30. package/dist/esm/compareVersions.js +16 -0
  31. package/dist/esm/configFlagSchema.d.ts +1 -1
  32. package/dist/esm/createGithubRelease.d.ts +7 -19
  33. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  34. package/dist/esm/createGithubReleaseCommand.js +9 -9
  35. package/dist/esm/createTags.d.ts +2 -3
  36. package/dist/esm/decideRelease.d.ts +2 -6
  37. package/dist/esm/defaults.d.ts +5 -24
  38. package/dist/esm/defaults.js +4 -5
  39. package/dist/esm/defineConfig.d.ts +1 -1
  40. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  41. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  42. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  43. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  44. package/dist/esm/detectPackageManager.d.ts +2 -4
  45. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  46. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  47. package/dist/esm/determineBumpType.d.ts +1 -1
  48. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  49. package/dist/esm/discoverWorkspaces.js +12 -12
  50. package/dist/esm/enterRepoRoot.d.ts +12 -0
  51. package/dist/esm/enterRepoRoot.js +16 -0
  52. package/dist/esm/enumerateReleaseWindows.d.ts +8 -8
  53. package/dist/esm/extractMigration.d.ts +6 -8
  54. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  55. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  56. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  57. package/dist/esm/formatExcludedSkip.js +3 -0
  58. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  59. package/dist/esm/generateChangelogs.d.ts +10 -5
  60. package/dist/esm/glyphs.d.ts +1 -1
  61. package/dist/esm/help/prepareHelp.d.ts +3 -4
  62. package/dist/esm/help/prepareHelp.js +4 -3
  63. package/dist/esm/init/checks.d.ts +3 -3
  64. package/dist/esm/init/checks.js +4 -1
  65. package/dist/esm/init/detectRepoType.d.ts +2 -3
  66. package/dist/esm/init/hasProvenance.d.ts +2 -0
  67. package/dist/esm/init/hasProvenance.js +3 -0
  68. package/dist/esm/init/initCommand.d.ts +3 -6
  69. package/dist/esm/init/initCommand.js +10 -10
  70. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  71. package/dist/esm/init/scaffold.d.ts +1 -1
  72. package/dist/esm/init/templates.d.ts +9 -11
  73. package/dist/esm/init/templates.js +12 -11
  74. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  75. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  76. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  77. package/dist/esm/injectSection.d.ts +3 -3
  78. package/dist/esm/loadConfig.d.ts +16 -14
  79. package/dist/esm/loadConfig.js +2 -2
  80. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  81. package/dist/esm/loadValidatedConfig.js +6 -1
  82. package/dist/esm/packsPath.d.ts +9 -9
  83. package/dist/esm/parseChangeRecordBlock.d.ts +2 -2
  84. package/dist/esm/parseCommitMessage.d.ts +17 -32
  85. package/dist/esm/parseCommitMessage.js +28 -37
  86. package/dist/esm/parseRequestedTags.d.ts +3 -3
  87. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  88. package/dist/esm/planVersionBump.d.ts +5 -3
  89. package/dist/esm/planVersionBump.js +3 -0
  90. package/dist/esm/prepareCommand.d.ts +4 -7
  91. package/dist/esm/prepareCommand.js +13 -6
  92. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  93. package/dist/esm/propagateBumps.d.ts +6 -7
  94. package/dist/esm/publish.d.ts +1 -1
  95. package/dist/esm/publishCommand.d.ts +4 -3
  96. package/dist/esm/publishCommand.js +8 -8
  97. package/dist/esm/pushCommand.d.ts +4 -3
  98. package/dist/esm/pushCommand.js +8 -2
  99. package/dist/esm/pushRelease.d.ts +2 -2
  100. package/dist/esm/readChangelogSections.d.ts +33 -0
  101. package/dist/esm/readChangelogSections.js +80 -0
  102. package/dist/esm/releaseFiles.d.ts +5 -7
  103. package/dist/esm/releasePlan.d.ts +11 -13
  104. package/dist/esm/releasePrepare.d.ts +23 -22
  105. package/dist/esm/releasePrepare.js +31 -11
  106. package/dist/esm/releasePrepareMono.d.ts +9 -7
  107. package/dist/esm/releasePrepareMono.js +85 -33
  108. package/dist/esm/releasePrepareProject.d.ts +19 -29
  109. package/dist/esm/releasePrepareProject.js +31 -12
  110. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  111. package/dist/esm/renderChangelogMarkdown.js +27 -2
  112. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  113. package/dist/esm/renderReleaseNotes.js +2 -6
  114. package/dist/esm/reportPrepare.d.ts +4 -5
  115. package/dist/esm/reportPrepare.js +33 -19
  116. package/dist/esm/resolveCommandTags.d.ts +15 -11
  117. package/dist/esm/resolveCommandTags.js +35 -5
  118. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  119. package/dist/esm/resolveConfigFlag.js +4 -0
  120. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  121. package/dist/esm/resolveReleaseTags.js +1 -1
  122. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  123. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  124. package/dist/esm/showTagPrefixesCommand.js +4 -4
  125. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  126. package/dist/esm/stripScope.d.ts +2 -2
  127. package/dist/esm/stripScope.js +3 -3
  128. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  129. package/dist/esm/sync-labels/generateCommand.js +11 -8
  130. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  131. package/dist/esm/sync-labels/initCommand.js +2 -2
  132. package/dist/esm/sync-labels/presets.d.ts +5 -4
  133. package/dist/esm/sync-labels/presets.js +9 -9
  134. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  135. package/dist/esm/sync-labels/resolveLabels.js +1 -1
  136. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  137. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  138. package/dist/esm/sync-labels/templates.d.ts +3 -3
  139. package/dist/esm/sync-labels/templates.js +3 -3
  140. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  141. package/dist/esm/sync-labels/typeLabels.js +38 -0
  142. package/dist/esm/sync-labels/types.d.ts +1 -1
  143. package/dist/esm/tagCommand.d.ts +1 -1
  144. package/dist/esm/typeGuards.d.ts +2 -2
  145. package/dist/esm/types.d.ts +116 -91
  146. package/dist/esm/types.js +1 -1
  147. package/dist/esm/validateConfig.d.ts +2 -11
  148. package/dist/esm/validateConfig.js +2 -2
  149. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  150. package/dist/esm/validateOverridesCommand.d.ts +17 -20
  151. package/dist/esm/validateOverridesCommand.js +1 -1
  152. package/docs/api.md +6 -6
  153. package/docs/changelogs.md +63 -40
  154. package/docs/configuration.md +29 -27
  155. package/docs/editorial-overrides.md +23 -23
  156. package/docs/labels.md +21 -19
  157. package/docs/project-releases.md +13 -13
  158. package/docs/publishing.md +5 -5
  159. package/docs/readiness-checks.md +3 -3
  160. package/docs/releasing.md +50 -25
  161. package/docs/work-types.md +41 -57
  162. package/package.json +7 -6
  163. package/presets/labels/common.yaml +1 -41
  164. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  165. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  166. package/dist/esm/workTypesData.d.ts +0 -50
  167. package/dist/esm/workTypesData.js +0 -145
  168. package/schemas/label-map.json +0 -24
@@ -1,17 +1,18 @@
1
1
  import { join as joinPath } from 'node:path';
2
+ import { assertTaggedBaseline, findUntaggedBaseline } from "./assertTaggedBaseline.js";
2
3
  import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
3
- import { readReleaseHistory, toChangelogEntries } from "./buildChangelogEntries.js";
4
- import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
4
+ import { readReleaseHistory, toReleaseEntries } from "./buildChangelogEntries.js";
5
5
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
6
6
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
7
7
  import { applyChangelogOverrides, formatStaleOverrideKeyWarning, loadOverridesForScopes, } from "./changelogOverrides.js";
8
8
  import { decideRelease } from "./decideRelease.js";
9
+ import { deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
9
10
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
10
11
  import { resolveWorkTypes } from "./loadConfig.js";
11
12
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
12
13
  import { planVersionBump, planVersionSet } from "./planVersionBump.js";
14
+ import { planPreservedSections } from "./readChangelogSections.js";
13
15
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
14
- import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
15
16
  export function releasePrepare(config, options) {
16
17
  const { bumpOverride, force, setVersion, withReleaseNotes } = options;
17
18
  const writes = [];
@@ -21,6 +22,15 @@ export function releasePrepare(config, options) {
21
22
  }
22
23
  const overrides = overridesResult.project;
23
24
  const history = readReleaseHistory(config, { tagPrefixes: [config.tagPrefix] });
25
+ assertTaggedBaseline([
26
+ findUntaggedBaseline({
27
+ label: 'package',
28
+ packageFiles: config.packageFiles,
29
+ changelogPaths: config.changelogPaths,
30
+ tagPrefixes: [config.tagPrefix],
31
+ previousTag: history.previousTag,
32
+ }, config),
33
+ ]);
24
34
  const { commits } = history.unreleased;
25
35
  const tag = history.previousTag;
26
36
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
@@ -60,11 +70,10 @@ export function releasePrepare(config, options) {
60
70
  config,
61
71
  history,
62
72
  newTag,
63
- newVersion: bump.newVersion,
64
73
  overrides,
65
74
  overrideWarnings: planWarnings,
66
75
  });
67
- const { changelogFiles, changelogJsonFiles } = changelogs;
76
+ const { changelogFiles, changelogJsonFiles, changelogPreservation } = changelogs;
68
77
  const previewWrites = planSinglePackagePreviews(withReleaseNotes === true, config, newTag, changelogs.entries, planWarnings);
69
78
  writes.push(...changelogs.writes, ...previewWrites);
70
79
  const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? 'npx prettier --write' : undefined);
@@ -82,6 +91,7 @@ export function releasePrepare(config, options) {
82
91
  bump,
83
92
  newTag,
84
93
  changelogFiles,
94
+ changelogPreservation,
85
95
  releaseType,
86
96
  bumpOverride: setVersion === undefined ? bumpOverride : undefined,
87
97
  setVersion,
@@ -149,13 +159,15 @@ function buildReleasedSinglePackage(args) {
149
159
  if (args.previewFiles.length > 0) {
150
160
  released.previewFiles = args.previewFiles;
151
161
  }
162
+ if (args.changelogPreservation.length > 0) {
163
+ released.changelogPreservation = args.changelogPreservation;
164
+ }
152
165
  return released;
153
166
  }
154
167
  function planSinglePackageChangelogs(args) {
155
- const { config, history, newTag, newVersion, overrides, overrideWarnings } = args;
156
- const isEmptyRange = history.unreleased.commits.length === 0;
168
+ const { config, history, newTag, overrides, overrideWarnings } = args;
157
169
  const today = new Date().toISOString().slice(0, 10);
158
- const builtEntries = isEmptyRange ? [buildEmptyReleaseEntry(newVersion, today)] : toChangelogEntries(history, newTag);
170
+ const builtEntries = toReleaseEntries(history, newTag, today);
159
171
  const applied = applyChangelogOverrides(builtEntries, overrides);
160
172
  if (applied.errors.length > 0) {
161
173
  throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
@@ -170,6 +182,7 @@ function planSinglePackageChangelogs(args) {
170
182
  const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
171
183
  const changelogFiles = [];
172
184
  const changelogJsonFiles = [];
185
+ const changelogPreservation = [];
173
186
  const writes = [];
174
187
  let firstMergedEntries = [];
175
188
  for (const changelogPath of config.changelogPaths) {
@@ -183,10 +196,17 @@ function planSinglePackageChangelogs(args) {
183
196
  changelogJsonFiles.push(jsonPath);
184
197
  }
185
198
  const changelogFile = joinPath(changelogPath, 'CHANGELOG.md');
186
- writes.push({ path: changelogFile, content: renderChangelogMarkdown(mergedEntries, { sectionOrder }) });
199
+ const preserved = planPreservedSections(changelogFile, mergedEntries);
200
+ writes.push({
201
+ path: changelogFile,
202
+ content: renderChangelogMarkdown(mergedEntries, { sectionOrder, preservedSections: preserved.sections }),
203
+ });
187
204
  changelogFiles.push(changelogFile);
205
+ if (preserved.preservation !== undefined) {
206
+ changelogPreservation.push(preserved.preservation);
207
+ }
188
208
  }
189
- return { changelogFiles, changelogJsonFiles, entries: firstMergedEntries, writes };
209
+ return { changelogFiles, changelogJsonFiles, changelogPreservation, entries: firstMergedEntries, writes };
190
210
  }
191
211
  function planSinglePackagePreviews(withReleaseNotes, config, newTag, entries, warnings) {
192
212
  if (!withReleaseNotes) {
@@ -200,7 +220,7 @@ function planSinglePackagePreviews(withReleaseNotes, config, newTag, entries, wa
200
220
  return [];
201
221
  }
202
222
  const previews = planReleaseNotesPreviews({
203
- workspacePath: process.cwd(),
223
+ workspacePath: '.',
204
224
  tag: newTag,
205
225
  entries,
206
226
  sectionOrder: deriveSectionOrder(resolveWorkTypes(config.workTypes)),
@@ -1,12 +1,14 @@
1
1
  import type { ReleasePlan } from './releasePlan.js';
2
2
  import type { ReleasePrepareOptions } from './releasePrepare.js';
3
- import type { MonorepoReleaseConfig } from './types.js';
3
+ import type { MonorepoPrepareConfig } from './types.js';
4
4
  /**
5
- * Orchestrate release preparation for a monorepo with multiple workspaces.
5
+ * Orchestrates release preparation for a monorepo with multiple workspaces.
6
6
  *
7
- * Phase 1: Determine direct bumps from commits for each workspace.
8
- * Phase 2: Build the dependency graph and propagate bumps to dependents.
9
- * Phase 2b: Topologically sort the full release set.
10
- * Phase 3: Execute bumps and generate changelogs in dependency order.
7
+ * Phase 1: Determines direct bumps from commits for each workspace.
8
+ * Phase 2: Builds the dependency graph and propagates bumps to dependents.
9
+ * Phase 2b: Sorts the full release set topologically.
10
+ * Phase 3: Plans bumps and changelogs in dependency order.
11
+ * Phase 3b: Plans the project release.
12
+ * Phase 4: Renders the format command.
11
13
  */
12
- export declare function releasePrepareMono(config: MonorepoReleaseConfig, options: ReleasePrepareOptions): ReleasePlan;
14
+ export declare function releasePrepareMono(config: MonorepoPrepareConfig, options: ReleasePrepareOptions): ReleasePlan;
@@ -1,24 +1,26 @@
1
1
  import { join as joinPath } from 'node:path';
2
2
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
3
+ import { assertTaggedBaseline, findUntaggedBaseline } from "./assertTaggedBaseline.js";
3
4
  import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
4
- import { readReleaseHistory, toChangelogEntries } from "./buildChangelogEntries.js";
5
+ import { readReleaseHistory, toReleaseEntries } from "./buildChangelogEntries.js";
5
6
  import { buildDependencyGraph } from "./buildDependencyGraph.js";
6
- import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
7
7
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
8
8
  import { buildSyntheticChangelogEntry } from "./buildSyntheticChangelogEntry.js";
9
9
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
10
10
  import { applyWorkspaceOverrides, createOverrideContext, formatStaleOverrideKeyWarning, } from "./changelogOverrides.js";
11
11
  import { decideRelease } from "./decideRelease.js";
12
+ import { deriveDependenciesHeader, deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
12
13
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
14
+ import { findUnroutedEntryScopes } from "./findUnroutedEntryScopes.js";
13
15
  import { getAllTagPrefixes } from "./generateChangelogs.js";
14
16
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
15
17
  import { resolveWorkTypes } from "./loadConfig.js";
16
18
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
17
19
  import { planVersionBump, planVersionSet } from "./planVersionBump.js";
18
20
  import { propagateBumps } from "./propagateBumps.js";
21
+ import { planPreservedSections } from "./readChangelogSections.js";
19
22
  import { releasePrepareProject } from "./releasePrepareProject.js";
20
23
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
21
- import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
22
24
  export function releasePrepareMono(config, options) {
23
25
  const { only, withReleaseNotes } = options;
24
26
  const writes = [];
@@ -26,18 +28,18 @@ export function releasePrepareMono(config, options) {
26
28
  if (withReleaseNotes === true && !config.changelogJson.enabled) {
27
29
  warnings.push('--with-release-notes requires changelogJson.enabled; skipping preview generation');
28
30
  }
29
- const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
31
+ const workTypes = resolveWorkTypes(config.workTypes);
32
+ const sectionOrder = deriveSectionOrder(workTypes);
33
+ const dependenciesHeader = deriveDependenciesHeader(workTypes);
30
34
  const overrideContext = createOverrideContext(config.workspaces);
31
- const { directBumps, directResults, skippedResults } = determineDirectBumps(config, options);
32
- const previousTags = new Map();
33
- for (const result of directResults.values()) {
34
- previousTags.set(result.workspace.dir, result.history.previousTag);
35
- }
36
- for (const skipped of skippedResults) {
37
- previousTags.set(skipped.workspace.dir, skipped.history.previousTag);
38
- }
35
+ const { directBumps, directResults, skippedResults, untaggedBaselines } = determineDirectBumps(config, options);
36
+ reportUnroutedEntryScopes([...directResults.values(), ...skippedResults], config, options);
37
+ const skippedHistories = new Map(skippedResults.map((skipped) => [skipped.workspace.dir, skipped.history]));
39
38
  const graph = buildDependencyGraph(config.workspaces);
40
39
  const fullReleaseSet = propagateBumps(directBumps, graph);
40
+ assertTaggedBaseline([...untaggedBaselines]
41
+ .filter(([dir]) => directBumps.has(dir) || !fullReleaseSet.has(dir))
42
+ .map(([, untagged]) => untagged));
41
43
  const { sorted: sortedDirs, cyclicDirs } = topologicalSort(fullReleaseSet, graph);
42
44
  if (cyclicDirs.length > 0) {
43
45
  warnings.push(`Circular workspace dependencies detected among: ${cyclicDirs.join(', ')}. ` +
@@ -53,13 +55,14 @@ export function releasePrepareMono(config, options) {
53
55
  fullReleaseSet,
54
56
  config,
55
57
  directResults,
56
- previousTags,
58
+ skippedHistories,
57
59
  writes,
58
60
  warnings,
59
61
  workspaces,
60
62
  previewOptions,
61
63
  overrideContext,
62
64
  sectionOrder,
65
+ dependenciesHeader,
63
66
  });
64
67
  const configOrder = new Map(config.workspaces.map((w, i) => [w.dir, i]));
65
68
  workspaces.sort((a, b) => {
@@ -112,12 +115,24 @@ function determineDirectBumps(config, options) {
112
115
  const directBumps = new Map();
113
116
  const directResults = new Map();
114
117
  const skippedResults = [];
118
+ const untaggedBaselines = new Map();
115
119
  const hintState = { emitted: false };
116
120
  const knownPrefixes = config.workspaces.flatMap(getAllTagPrefixes);
117
121
  for (const workspace of config.workspaces) {
118
122
  const name = workspace.dir;
119
123
  const stageLabel = workspaceStageLabel(workspace.dir);
120
- const history = tryStage(stageLabel, () => readReleaseHistory(config, { tagPrefixes: getAllTagPrefixes(workspace), paths: workspace.paths }));
124
+ const tagPrefixes = getAllTagPrefixes(workspace);
125
+ const history = tryStage(stageLabel, () => readReleaseHistory(config, { tagPrefixes, paths: workspace.paths, workspaceDir: workspace.dir }));
126
+ const untagged = tryStage(stageLabel, () => findUntaggedBaseline({
127
+ label: `workspace '${workspace.dir}'`,
128
+ packageFiles: workspace.packageFiles,
129
+ changelogPaths: workspace.changelogPaths,
130
+ tagPrefixes,
131
+ previousTag: history.previousTag,
132
+ }, config));
133
+ if (untagged !== undefined) {
134
+ untaggedBaselines.set(workspace.dir, untagged);
135
+ }
121
136
  const tag = history.previousTag;
122
137
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
123
138
  if (tag === undefined) {
@@ -156,7 +171,13 @@ function determineDirectBumps(config, options) {
156
171
  bumpOverride,
157
172
  });
158
173
  }
159
- return { directBumps, directResults, skippedResults };
174
+ return { directBumps, directResults, skippedResults, untaggedBaselines };
175
+ }
176
+ function reportUnroutedEntryScopes(results, config, options) {
177
+ const findings = findUnroutedEntryScopes(results.map(({ workspace, history }) => ({ dir: workspace.dir, commits: history.unreleased.commits })), options.configuredWorkspaceDirs ?? config.workspaces.map((workspace) => workspace.dir), config.scopeAliases ?? {});
178
+ for (const { workspace, history } of results) {
179
+ history.unreleased.diagnostics.unroutedEntryScopes = findings.get(workspace.dir) ?? [];
180
+ }
160
181
  }
161
182
  function collectSkippedWorkspaces(skippedResults, fullReleaseSet) {
162
183
  const workspaces = [];
@@ -184,7 +205,7 @@ function collectSkippedWorkspaces(skippedResults, fullReleaseSet) {
184
205
  return workspaces;
185
206
  }
186
207
  function executeReleaseSet(args) {
187
- const { sortedDirs, fullReleaseSet, config, directResults, previousTags, writes, warnings, workspaces, previewOptions, overrideContext, sectionOrder, } = args;
208
+ const { sortedDirs, fullReleaseSet, config, directResults, skippedHistories, writes, warnings, workspaces, previewOptions, overrideContext, sectionOrder, dependenciesHeader, } = args;
188
209
  const tags = [];
189
210
  const modifiedFiles = [];
190
211
  const today = new Date().toISOString().slice(0, 10);
@@ -202,7 +223,7 @@ function executeReleaseSet(args) {
202
223
  workspace,
203
224
  releaseEntry,
204
225
  directResult: directResults.get(dir),
205
- previousTags,
226
+ skippedHistory: skippedHistories.get(dir),
206
227
  config,
207
228
  today,
208
229
  tags,
@@ -213,12 +234,13 @@ function executeReleaseSet(args) {
213
234
  previewOptions,
214
235
  overrideContext,
215
236
  sectionOrder,
237
+ dependenciesHeader,
216
238
  }));
217
239
  }
218
240
  return { tags, modifiedFiles };
219
241
  }
220
242
  function executeWorkspaceRelease(args) {
221
- const { dir, workspace, releaseEntry, directResult, previousTags, config, today, tags, modifiedFiles, writes, warnings, workspaces, previewOptions, overrideContext, sectionOrder, } = args;
243
+ const { dir, workspace, releaseEntry, directResult, skippedHistory, config, today, tags, modifiedFiles, writes, warnings, workspaces, previewOptions, overrideContext, sectionOrder, dependenciesHeader, } = args;
222
244
  const setVersionTarget = directResult?.setVersion;
223
245
  const bump = setVersionTarget === undefined
224
246
  ? planVersionBump(workspace.packageFiles, releaseEntry.releaseType)
@@ -227,8 +249,8 @@ function executeWorkspaceRelease(args) {
227
249
  tags.push(newTag);
228
250
  writes.push(...bump.writes);
229
251
  modifiedFiles.push(...workspace.packageFiles, ...workspace.changelogPaths.map((changelogPath) => joinPath(changelogPath, 'CHANGELOG.md')));
230
- const directCommits = directResult?.history.unreleased.commits;
231
- const { changelogFiles, previewFiles } = generateWorkspaceChangelogs({
252
+ const phase1History = directResult?.history ?? skippedHistory;
253
+ const { changelogFiles, changelogPreservation, previewFiles } = generateWorkspaceChangelogs({
232
254
  workspace,
233
255
  releaseEntry,
234
256
  newTag,
@@ -242,28 +264,31 @@ function executeWorkspaceRelease(args) {
242
264
  previewOptions,
243
265
  overrideContext,
244
266
  sectionOrder,
267
+ dependenciesHeader,
245
268
  });
246
269
  const released = {
247
270
  name: dir,
248
271
  status: 'released',
249
- commitCount: directCommits?.length ?? 0,
272
+ commitCount: phase1History?.unreleased.commits.length ?? 0,
250
273
  currentVersion: bump.currentVersion,
251
274
  newVersion: bump.newVersion,
252
275
  tag: newTag,
253
276
  bumpedFiles: bump.writes.map((write) => write.path),
254
277
  changelogFiles,
255
278
  ...(previewFiles.length > 0 && { previewFiles }),
279
+ ...(changelogPreservation.length > 0 && { changelogPreservation }),
256
280
  };
257
281
  attachReleasedWorkspaceOptionals(released, {
258
- previousTag: directResult?.history.previousTag ?? previousTags.get(dir),
282
+ previousTag: phase1History?.previousTag,
259
283
  directResult,
284
+ skippedHistory,
260
285
  releaseEntry,
261
286
  setVersionTarget,
262
287
  });
263
288
  workspaces.push(released);
264
289
  }
265
290
  function attachReleasedWorkspaceOptionals(released, args) {
266
- const { previousTag, directResult, releaseEntry, setVersionTarget } = args;
291
+ const { previousTag, directResult, skippedHistory, releaseEntry, setVersionTarget } = args;
267
292
  if (previousTag !== undefined) {
268
293
  released.previousTag = previousTag;
269
294
  }
@@ -273,6 +298,9 @@ function attachReleasedWorkspaceOptionals(released, args) {
273
298
  if (directResult !== undefined) {
274
299
  attachDirectHistory(released, directResult.history.unreleased, setVersionTarget === undefined);
275
300
  }
301
+ else {
302
+ attachPropagatedHistory(released, skippedHistory?.unreleased);
303
+ }
276
304
  if (releaseEntry.propagatedFrom !== undefined) {
277
305
  released.propagatedFrom = releaseEntry.propagatedFrom;
278
306
  }
@@ -293,11 +321,31 @@ function attachDirectHistory(released, unreleased, hasDecidedBump) {
293
321
  }
294
322
  attachChangelogDiagnostics(released, unreleased.diagnostics);
295
323
  }
324
+ function attachPropagatedHistory(released, unreleased) {
325
+ released.propagatedOnly = true;
326
+ if (unreleased === undefined) {
327
+ return;
328
+ }
329
+ released.commits = unreleased.commits;
330
+ if (unreleased.unparseableCommits !== undefined) {
331
+ released.unparseableCommits = unreleased.unparseableCommits;
332
+ }
333
+ attachChangelogDiagnostics(released, unreleased.diagnostics);
334
+ }
296
335
  function generateWorkspaceChangelogs(args) {
297
- const { workspace, releaseEntry, newTag, newVersion, history, config, today, modifiedFiles, writes, warnings, previewOptions, overrideContext, sectionOrder, } = args;
298
- const entries = buildWorkspaceEntries({ workspace, releaseEntry, newTag, newVersion, history, today });
336
+ const { workspace, releaseEntry, newTag, newVersion, history, config, today, modifiedFiles, writes, warnings, previewOptions, overrideContext, sectionOrder, dependenciesHeader, } = args;
337
+ const entries = buildWorkspaceEntries({
338
+ workspace,
339
+ releaseEntry,
340
+ newTag,
341
+ newVersion,
342
+ history,
343
+ today,
344
+ dependenciesHeader,
345
+ });
299
346
  const applied = applyWorkspaceOverrides(entries, workspace.workspacePath, overrideContext);
300
347
  const changelogFiles = [];
348
+ const changelogPreservation = [];
301
349
  let firstMergedEntries;
302
350
  for (const changelogPath of workspace.changelogPaths) {
303
351
  const jsonPath = resolveChangelogJsonPath(config, changelogPath);
@@ -308,25 +356,29 @@ function generateWorkspaceChangelogs(args) {
308
356
  firstMergedEntries ??= mergedEntries;
309
357
  }
310
358
  const changelogFile = joinPath(changelogPath, 'CHANGELOG.md');
311
- writes.push({ path: changelogFile, content: renderChangelogMarkdown(mergedEntries, { sectionOrder }) });
359
+ const preserved = planPreservedSections(changelogFile, mergedEntries);
360
+ writes.push({
361
+ path: changelogFile,
362
+ content: renderChangelogMarkdown(mergedEntries, { sectionOrder, preservedSections: preserved.sections }),
363
+ });
312
364
  changelogFiles.push(changelogFile);
365
+ if (preserved.preservation !== undefined) {
366
+ changelogPreservation.push(preserved.preservation);
367
+ }
313
368
  }
314
369
  const previews = planPreviews(workspace, newTag, firstMergedEntries, previewOptions, warnings);
315
370
  writes.push(...previews);
316
- return { changelogFiles, previewFiles: previews.map((write) => write.path) };
371
+ return { changelogFiles, changelogPreservation, previewFiles: previews.map((write) => write.path) };
317
372
  }
318
373
  function buildWorkspaceEntries(args) {
319
- const { workspace, releaseEntry, newTag, newVersion, history, today } = args;
374
+ const { workspace, releaseEntry, newTag, newVersion, history, today, dependenciesHeader } = args;
320
375
  if (history === undefined) {
321
376
  if (releaseEntry.propagatedFrom === undefined) {
322
377
  throw new Error(`Workspace '${workspace.dir}' has neither a direct release nor a propagation source`);
323
378
  }
324
- return [buildSyntheticChangelogEntry(releaseEntry.propagatedFrom, newVersion, today)];
325
- }
326
- if (history.unreleased.commits.length === 0) {
327
- return [buildEmptyReleaseEntry(newVersion, today)];
379
+ return [buildSyntheticChangelogEntry(releaseEntry.propagatedFrom, newVersion, today, dependenciesHeader)];
328
380
  }
329
- return toChangelogEntries(history, newTag);
381
+ return toReleaseEntries(history, newTag, today);
330
382
  }
331
383
  function planPreviews(workspace, newTag, entries, previewOptions, warnings) {
332
384
  if (!previewOptions.enabled || entries === undefined) {
@@ -1,57 +1,47 @@
1
1
  import type { PlannedWrite } from './releasePlan.js';
2
2
  import type { ReleasePrepareOptions } from './releasePrepare.js';
3
- import type { ChangelogOverride, MonorepoReleaseConfig, ProjectPrepareResult } from './types.js';
3
+ import type { ChangelogOverride, MonorepoPrepareConfig, ProjectPrepareResult } from './types.js';
4
4
  /** Inputs to the project-release stage. */
5
5
  export interface ReleasePrepareProjectArgs {
6
6
  /** Resolved monorepo config; `config.project` must be defined when this function is called. */
7
- config: MonorepoReleaseConfig;
7
+ config: MonorepoPrepareConfig;
8
8
  options: ReleasePrepareOptions;
9
9
  /** Mutated in-place to append project-level files (root package.json, root CHANGELOG.md, root changelog.json). */
10
10
  modifiedFiles: string[];
11
- /** Mutated in-place to append every file this stage intends to write. */
11
+ /** Mutated in-place to append every file that this stage intends to write. */
12
12
  writes: PlannedWrite[];
13
13
  /** Mutated in-place to append the project tag. */
14
14
  tags: string[];
15
15
  /**
16
- * Mutated in-place to surface warnings this stage raises that are not override-specific
17
- * (currently the release-notes previews' skip reasons). Defaults to a discardable sink.
16
+ * Mutated in-place to receive the warnings that this stage raises and that are not override-specific.
17
+ * Defaults to a discardable sink.
18
18
  */
19
19
  warnings?: string[];
20
20
  /**
21
- * Root-tier editorial overrides loaded once at the top of the prepare run. Defaults to an
22
- * empty map when omitted (no overrides applied). The project changelog applies only the
23
- * root-tier file — per-workspace files describe per-workspace editorial intent and have no
24
- * meaning at the aggregated project tier.
21
+ * Root-tier editorial overrides. Defaults to an empty map. The project changelog applies only the
22
+ * root-tier file: Per-workspace files describe per-workspace editorial intent and are
23
+ * meaningless at the aggregated project tier.
25
24
  */
26
25
  rootOverrides?: Map<string, ChangelogOverride>;
27
26
  /**
28
- * Mutated in-place to surface override warnings (currently empty by design — stale-key
29
- * warnings are emitted by the orchestrator after aggregating across batches). Defaults to
30
- * a discardable sink when omitted.
27
+ * Mutated in-place to receive override-application warnings. Stale-key warnings are not among them: Those
28
+ * depend on the matches of every batch, which `globalMatchedRootKeys` collects. Defaults to a discardable sink.
31
29
  */
32
30
  overrideWarnings?: string[];
33
31
  /**
34
- * Mutated in-place: every root-tier override key matched in this stage is added so the
35
- * orchestrator can dedupe stale-key warnings across the run. Defaults to a discardable
36
- * sink when omitted.
32
+ * Mutated in-place to receive every root-tier override key that this stage matches, so that stale-key
33
+ * warnings can cover the whole run. Defaults to a discardable sink.
37
34
  */
38
35
  globalMatchedRootKeys?: Set<string>;
39
36
  }
40
37
  /**
41
- * Run the project-level release stage.
38
+ * Runs the project-level release stage: reads the history, decides the bump, and plans the root `package.json`
39
+ * bump, the root `CHANGELOG.md`, and optionally `changelog.json` and release-notes previews. Contributing paths
40
+ * come from the resolved `project.paths`, which defaults to the union of every (already-filtered) workspace's
41
+ * `paths`.
42
42
  *
43
- * Mirrors the per-workspace pipeline shape — read the history → decide the bump → bump version →
44
- * regenerate CHANGELOG → optionally emit changelog.json and release-notes previews — but
45
- * targets the root `package.json` and the root `CHANGELOG.md`. Contributing paths come from
46
- * the resolved `project.paths`, which defaults to the union of every (already-filtered)
47
- * workspace's `paths`.
48
- *
49
- * Returns a structured `{ status: 'skipped', skipReason, ... }` result when neither
50
- * commits nor `--force` provide a release signal. The caller should attach the returned
51
- * result to `PrepareResult.project`. `undefined` is returned only when there is no
52
- * configured `project` block — handled at the call site, not here.
53
- *
54
- * Caller contract: `prepareCommand` rejects `--only` upstream when a project block is
55
- * configured, so this orchestrator never has to reason about workspace-narrowing flags.
43
+ * Returns a `skipped` result when neither commits nor `--force` provide a release signal, and throws when
44
+ * `config.project` is undefined. A run narrowed by `--only` does not reach this stage, so it doesn't
45
+ * read any narrowing flag.
56
46
  */
57
47
  export declare function releasePrepareProject(args: ReleasePrepareProjectArgs): ProjectPrepareResult;
@@ -1,15 +1,16 @@
1
1
  import { join as joinPath } from 'node:path';
2
+ import { assertTaggedBaseline, findUntaggedBaseline } from "./assertTaggedBaseline.js";
2
3
  import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
3
- import { readReleaseHistory, toChangelogEntries } from "./buildChangelogEntries.js";
4
- import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
4
+ import { readReleaseHistory, toReleaseEntries } from "./buildChangelogEntries.js";
5
5
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
6
6
  import { applyChangelogOverrides } from "./changelogOverrides.js";
7
7
  import { decideRelease } from "./decideRelease.js";
8
8
  import { DEFAULT_WORK_TYPES } from "./defaults.js";
9
+ import { deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
9
10
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
10
11
  import { planVersionBump } from "./planVersionBump.js";
12
+ import { planPreservedSections } from "./readChangelogSections.js";
11
13
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
12
- import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
13
14
  const ROOT_PACKAGE_FILE = './package.json';
14
15
  const ROOT_CHANGELOG_PATH = '.';
15
16
  export function releasePrepareProject(args) {
@@ -22,6 +23,15 @@ export function releasePrepareProject(args) {
22
23
  }
23
24
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
24
25
  const history = readReleaseHistory(config, { tagPrefixes: [project.tagPrefix], paths: project.paths });
26
+ assertTaggedBaseline([
27
+ findUntaggedBaseline({
28
+ label: 'project',
29
+ packageFiles: [ROOT_PACKAGE_FILE],
30
+ changelogPaths: [ROOT_CHANGELOG_PATH],
31
+ tagPrefixes: [project.tagPrefix],
32
+ previousTag: history.previousTag,
33
+ }, config),
34
+ ]);
25
35
  const { commits, diagnostics, parsedCommitCount, unparseableCommits } = history.unreleased;
26
36
  const tag = history.previousTag;
27
37
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
@@ -59,7 +69,6 @@ export function releasePrepareProject(args) {
59
69
  config,
60
70
  history,
61
71
  newTag,
62
- newVersion: bump.newVersion,
63
72
  rootOverrides,
64
73
  overrideWarnings,
65
74
  globalMatchedRootKeys,
@@ -95,6 +104,9 @@ export function releasePrepareProject(args) {
95
104
  if (previewFiles.length > 0) {
96
105
  result.previewFiles = previewFiles;
97
106
  }
107
+ if (changelogs.changelogPreservation.length > 0) {
108
+ result.changelogPreservation = changelogs.changelogPreservation;
109
+ }
98
110
  if (tag !== undefined) {
99
111
  result.previousTag = tag;
100
112
  }
@@ -116,10 +128,9 @@ function resolveOptionalOverrideArgs(args) {
116
128
  };
117
129
  }
118
130
  function planProjectChangelogs(args) {
119
- const { config, history, newTag, newVersion, rootOverrides, overrideWarnings, globalMatchedRootKeys } = args;
120
- const isEmptyRange = history.unreleased.commits.length === 0;
131
+ const { config, history, newTag, rootOverrides, overrideWarnings, globalMatchedRootKeys } = args;
121
132
  const today = new Date().toISOString().slice(0, 10);
122
- const builtEntries = isEmptyRange ? [buildEmptyReleaseEntry(newVersion, today)] : toChangelogEntries(history, newTag);
133
+ const builtEntries = toReleaseEntries(history, newTag, today);
123
134
  const applied = applyChangelogOverrides(builtEntries, rootOverrides);
124
135
  if (applied.errors.length > 0) {
125
136
  throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
@@ -130,9 +141,7 @@ function planProjectChangelogs(args) {
130
141
  }
131
142
  const changelogJsonPath = resolveChangelogJsonPath(config, ROOT_CHANGELOG_PATH);
132
143
  const sectionOrder = deriveSectionOrder(config.workTypes ?? { ...DEFAULT_WORK_TYPES });
133
- const renderEntries = isEmptyRange
134
- ? mergeChangelogEntriesWithDisk(changelogJsonPath, applied.entries)
135
- : applied.entries;
144
+ const renderEntries = mergeChangelogEntriesWithDisk(changelogJsonPath, applied.entries);
136
145
  const writes = [];
137
146
  const changelogJsonFiles = [];
138
147
  if (config.changelogJson.enabled) {
@@ -140,6 +149,16 @@ function planProjectChangelogs(args) {
140
149
  changelogJsonFiles.push(changelogJsonPath);
141
150
  }
142
151
  const changelogFile = joinPath(ROOT_CHANGELOG_PATH, 'CHANGELOG.md');
143
- writes.push({ path: changelogFile, content: renderChangelogMarkdown(renderEntries, { sectionOrder }) });
144
- return { changelogFiles: [changelogFile], changelogJsonFiles, entries: renderEntries, writes };
152
+ const preserved = planPreservedSections(changelogFile, renderEntries);
153
+ writes.push({
154
+ path: changelogFile,
155
+ content: renderChangelogMarkdown(renderEntries, { sectionOrder, preservedSections: preserved.sections }),
156
+ });
157
+ return {
158
+ changelogFiles: [changelogFile],
159
+ changelogJsonFiles,
160
+ changelogPreservation: preserved.preservation === undefined ? [] : [preserved.preservation],
161
+ entries: renderEntries,
162
+ writes,
163
+ };
145
164
  }
@@ -1,22 +1,28 @@
1
+ import type { VersionedMarkdownSection } from './readChangelogSections.js';
1
2
  import type { ChangelogEntry } from './types.js';
2
3
  /** Options for `renderChangelogMarkdown`. */
3
4
  export interface RenderChangelogMarkdownOptions {
4
- /** Section titles in canonical order; passed through to `renderReleaseNotesMulti`. */
5
+ /** Section titles in canonical order; passed through to `renderReleaseNotesSingle`. */
5
6
  sectionOrder?: string[];
7
+ /**
8
+ * Sections of the existing `CHANGELOG.md` to keep verbatim, interleaved by version among the rendered entries. A
9
+ * section's version must not also be an entry's version.
10
+ */
11
+ preservedSections?: readonly VersionedMarkdownSection[];
6
12
  }
7
13
  /**
8
- * Render `ChangelogEntry[]` into a complete `CHANGELOG.md`-shaped string.
14
+ * Renders `ChangelogEntry[]` into a complete `CHANGELOG.md`-shaped string.
9
15
  *
10
16
  * Format:
11
17
  * `# Changelog`
12
18
  * blank line
13
19
  * `All notable changes …`
14
20
  * blank line
15
- * per-entry sections (delegated to `renderReleaseNotesMulti`)
21
+ * per-entry sections (delegated to `renderReleaseNotesSingle`), with preserved sections interleaved by version
16
22
  * blank line
17
23
  * footer comment
18
24
  *
19
25
  * Entries are rendered in the order provided; the caller sorts (typically newest-first via
20
- * `changelogJsonFile`'s SemVer-aware comparator).
26
+ * `compareVersionsDescending`).
21
27
  */
22
28
  export declare function renderChangelogMarkdown(entries: ChangelogEntry[], options?: RenderChangelogMarkdownOptions): string;