@williamthorsen/release-kit 12.0.0 → 13.1.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 +229 -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 +64 -0
  6. package/README.md +27 -51
  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 +17 -9
  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 +2 -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 +4 -2
  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 +122 -93
  146. package/dist/esm/types.js +2 -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 +37 -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,5 +1,5 @@
1
1
  import type { PrepareResult } from './types.js';
2
- /** One file a release will write, carried as its complete intended content. */
2
+ /** One file that a release will write, with its complete intended content. */
3
3
  export interface PlannedWrite {
4
4
  path: string;
5
5
  content: string;
@@ -7,12 +7,11 @@ export interface PlannedWrite {
7
7
  /**
8
8
  * A release computed in full but not yet written.
9
9
  *
10
- * Holding every intended file up front is what lets `prepare` fail during computation without
11
- * leaving anything on disk. {@link applyReleasePlan} is the only step that mutates the tree, so
12
- * a dry run is this same plan with the apply step skipped rather than a separate code path.
10
+ * Because the plan contains every intended file up front, `prepare` can fail during computation
11
+ * without leaving anything on disk. {@link applyReleasePlan} is the only step that mutates the
12
+ * tree, so a dry run is this same plan with the apply step skipped.
13
13
  *
14
- * Extends the reporting view with the two fields only the apply step needs, so `reportPrepare`
15
- * can render a plan without knowing about file content.
14
+ * Extends the reporting view with the two fields that only the apply step reads.
16
15
  */
17
16
  export interface ReleasePlan extends PrepareResult {
18
17
  writes: readonly PlannedWrite[];
@@ -20,14 +19,13 @@ export interface ReleasePlan extends PrepareResult {
20
19
  summary: string;
21
20
  }
22
21
  /**
23
- * Writes every file a plan describes: content files first, then the summary, then the tags file.
22
+ * Writes every file that a plan describes: content files first, then the summary, then the tags file.
24
23
  *
25
- * The tags file goes last so its presence means the whole plan landed. Both `release-kit commit`
26
- * and the reusable release workflow read it as the signal that a release is ready to commit, so
27
- * writing it earlier would let a failure produce a tree that claims to be releasable.
24
+ * The tags file goes last so that its presence means that the whole plan was written to disk.
25
+ * Both `release-kit commit` and the reusable release workflow read it as the signal that a
26
+ * release is ready to commit, so writing it earlier would let a failure produce a tree that
27
+ * looks releasable.
28
28
  *
29
- * Returns the paths written, in order.
30
- *
31
- * @throws If any write fails, naming what reached disk and what did not.
29
+ * Returns the paths written, in order. Throws if any write fails, naming what was written and what was not.
32
30
  */
33
31
  export declare function applyReleasePlan(plan: ReleasePlan): string[];
@@ -1,18 +1,13 @@
1
1
  import type { ReleasePlan } from './releasePlan.js';
2
- import type { ReleaseConfig, ReleaseType } from './types.js';
3
- /**
4
- * Options for the release preparation workflow.
5
- *
6
- * Carries no dry-run flag: preparation only ever computes a plan, and whether that plan is
7
- * applied is the caller's decision.
8
- */
2
+ import type { PrepareConfig, ReleaseType } from './types.js';
3
+ /** Options for the release preparation workflow. */
9
4
  export interface ReleasePrepareOptions {
10
5
  /**
11
- * Release even when no commits or no bump-worthy commits exist since the last tag.
12
- * Orthogonal to `bumpOverride`: when `bumpOverride` is not given, the release falls back to `patch`.
6
+ * Release even when the range since the last tag doesn't contain any commits or any bump-worthy commits.
7
+ * Orthogonal to `bumpOverride`: When `bumpOverride` is not given, the release falls back to `patch`.
13
8
  */
14
9
  force?: boolean;
15
- /** Choose the level of a release instead of the level that the changelog items call for; triggers no release. */
10
+ /** Level of the release, in place of the level that the changelog items call for; it doesn't trigger a release. */
16
11
  bumpOverride?: ReleaseType;
17
12
  /**
18
13
  * Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic.
@@ -21,29 +16,35 @@ export interface ReleasePrepareOptions {
21
16
  */
22
17
  setVersion?: string;
23
18
  /**
24
- * Workspace directories the run was narrowed to by `--only` (monorepo only), with
19
+ * Workspace directories to which `--only` narrowed the run (monorepo only), with
25
20
  * `config.workspaces` already filtered to match. Present only for a narrowed run, which
26
- * skips the project release: the project tier rolls up every contributing workspace, and
21
+ * skips the project release: The project tier rolls up every contributing workspace, and
27
22
  * the narrowing has changed which workspaces those are.
28
23
  */
29
24
  only?: string[];
30
25
  /**
31
- * If true, write per-workspace release-notes previews under `{workspacePath}/docs/`
32
- * (`README.v{version}.md` and `RELEASE_NOTES.v{version}.md`) after each workspace's
33
- * `changelog.json` is produced. Requires `config.changelogJson.enabled`; when disabled,
34
- * a warning is recorded on the plan and no previews are generated.
26
+ * Every configured workspace `dir`, taken before `--only` narrowed `config.workspaces`; the unrouted-scope report
27
+ * does not report a scope naming one of them that the run did not read. Defaults to the `dir`s of
28
+ * `config.workspaces`.
29
+ */
30
+ configuredWorkspaceDirs?: readonly string[];
31
+ /**
32
+ * If true, plan per-workspace release-notes previews under `{workspacePath}/docs/`
33
+ * (`README.v{version}.md` and `RELEASE_NOTES.v{version}.md`) from each workspace's
34
+ * changelog entries. Requires `config.changelogJson.enabled`; when disabled,
35
+ * the planner records a warning on the plan and doesn't plan any previews.
35
36
  */
36
37
  withReleaseNotes?: boolean;
37
38
  }
38
39
  /**
39
- * Orchestrate the release preparation workflow for a single package.
40
+ * Orchestrates the release preparation workflow for a single package.
40
41
  *
41
- * 1. Reads the release history once.
42
+ * 1. Reads the release history once, and stops when the current version is recorded but untagged.
42
43
  * 2. Decides the release from the history's bump, `--force`, and `--bump` (or takes `--set-version`).
43
- * 3. Bumps all configured package.json version fields.
44
- * 4. Generates changelogs from the same history.
44
+ * 3. Plans the bump of every configured `package.json` version field.
45
+ * 4. Plans the changelogs from the same history, and the release-notes previews when requested.
45
46
  * 5. Renders the optional format command.
46
47
  *
47
- * Returns a structured `PrepareResult` with all data needed for presentation.
48
+ * Returns the plan without writing any file.
48
49
  */
49
- export declare function releasePrepare(config: ReleaseConfig, options: ReleasePrepareOptions): ReleasePlan;
50
+ export declare function releasePrepare(config: PrepareConfig, options: ReleasePrepareOptions): ReleasePlan;
@@ -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;