@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
@@ -4,14 +4,15 @@ import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
6
6
  import { configFlagSchema } from "./configFlagSchema.js";
7
+ import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
7
8
  import { detectPackageManager } from "./detectPackageManager.js";
8
9
  import { formatPrivateSkip } from "./formatPrivateSkip.js";
9
10
  import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
10
- import { assertConfigUsable } from "./loadValidatedConfig.js";
11
+ import { loadUsableConfig } from "./loadValidatedConfig.js";
11
12
  import { parseRequestedTags } from "./parseRequestedTags.js";
12
13
  import { publishPackage } from "./publish.js";
13
14
  import { resolveCommandTags } from "./resolveCommandTags.js";
14
- import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
15
+ import { resolveConfigFlag } from "./resolveConfigFlag.js";
15
16
  const publishFlagSchema = {
16
17
  ...configFlagSchema,
17
18
  dryRun: { long: '--dry-run', type: 'boolean' },
@@ -19,10 +20,11 @@ const publishFlagSchema = {
19
20
  provenance: { long: '--provenance', type: 'boolean' },
20
21
  tags: { long: '--tags', type: 'string' },
21
22
  };
22
- export async function publishCommand(argv, styles) {
23
+ export async function publishCommand(argv, styles, invocationDir) {
23
24
  const parsed = parseArgsOrExit(argv, publishFlagSchema);
25
+ const configPath = resolveConfigFlag(parsed.flags.config, invocationDir);
24
26
  const { dryRun, noGitChecks, provenance } = parsed.flags;
25
- await assertConfigUsable(styles.stderr, parsed.flags.config);
27
+ const userConfig = await loadUsableConfig(styles.stderr, configPath);
26
28
  if (!dryRun && !noGitChecks) {
27
29
  try {
28
30
  assertCleanWorkingTree();
@@ -33,7 +35,7 @@ export async function publishCommand(argv, styles) {
33
35
  }
34
36
  }
35
37
  const requestedTags = parseRequestedTags(parsed.flags.tags);
36
- const resolvedTags = resolveCommandTags(requestedTags);
38
+ const resolvedTags = resolveCommandTags(requestedTags, userConfig);
37
39
  if (resolvedTags.length === 0) {
38
40
  return;
39
41
  }
@@ -43,9 +45,7 @@ export async function publishCommand(argv, styles) {
43
45
  return;
44
46
  }
45
47
  const packageManager = detectPackageManager();
46
- const { releaseNotes, changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig(styles.stderr, {
47
- ...(parsed.flags.config !== undefined && { configPath: parsed.flags.config }),
48
- });
48
+ const { releaseNotes, changelogJsonOutputPath, sectionOrder } = deriveReleaseNotesConfig(userConfig);
49
49
  const shouldInject = releaseNotes.shouldInjectIntoReadme;
50
50
  console.info(dryRun ? '[dry-run] Would publish:' : 'Publishing:');
51
51
  for (const { tag, workspacePath } of publishableTags) {
@@ -1,5 +1,6 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  /**
2
- * Orchestrate the CLI `push` command: parse flags, resolve tags from HEAD, and push
3
- * the release commit and each tag individually.
3
+ * Orchestrates the CLI `push` command: parses flags, loads the config, resolves tags from HEAD, and pushes
4
+ * the release commit and each tag individually. A relative `--config` resolves against `invocationDir`.
4
5
  */
5
- export declare function pushCommand(argv: string[]): void;
6
+ export declare function pushCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
@@ -1,18 +1,24 @@
1
1
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
+ import { configFlagSchema } from "./configFlagSchema.js";
4
+ import { loadUsableConfig } from "./loadValidatedConfig.js";
3
5
  import { parseRequestedTags } from "./parseRequestedTags.js";
4
6
  import { pushRelease } from "./pushRelease.js";
5
7
  import { resolveCommandTags } from "./resolveCommandTags.js";
8
+ import { resolveConfigFlag } from "./resolveConfigFlag.js";
6
9
  const pushFlagSchema = {
10
+ ...configFlagSchema,
7
11
  dryRun: { long: '--dry-run', type: 'boolean' },
8
12
  tags: { long: '--tags', type: 'string' },
9
13
  tagsOnly: { long: '--tags-only', type: 'boolean' },
10
14
  };
11
- export function pushCommand(argv) {
15
+ export async function pushCommand(argv, styles, invocationDir) {
12
16
  const parsed = parseArgsOrExit(argv, pushFlagSchema);
17
+ const configPath = resolveConfigFlag(parsed.flags.config, invocationDir);
13
18
  const { dryRun, tagsOnly } = parsed.flags;
19
+ const userConfig = await loadUsableConfig(styles.stderr, configPath);
14
20
  const requestedTags = parseRequestedTags(parsed.flags.tags);
15
- const resolvedTags = resolveCommandTags(requestedTags);
21
+ const resolvedTags = resolveCommandTags(requestedTags, userConfig);
16
22
  if (resolvedTags.length === 0) {
17
23
  return;
18
24
  }
@@ -9,9 +9,9 @@ export interface PushStep {
9
9
  command: readonly [string, ...string[]];
10
10
  }
11
11
  /**
12
- * Push the release commit and each tag individually so that GitHub Actions fires
12
+ * Pushes the release commit and each tag individually so that GitHub Actions fires
13
13
  * a separate workflow run per tag.
14
14
  *
15
- * Returns the list of push steps that were executed (or would be executed in dry-run mode).
15
+ * Returns the push steps, which it runs only outside dry-run mode.
16
16
  */
17
17
  export declare function pushRelease(resolvedTags: ResolvedTag[], options?: PushReleaseOptions): PushStep[];
@@ -0,0 +1,33 @@
1
+ import type { ChangelogEntry, ChangelogPreservation } from './types.js';
2
+ /** A `##` section of a `CHANGELOG.md` whose heading names a version. */
3
+ export interface VersionedMarkdownSection {
4
+ version: string;
5
+ /** The section as written, heading included, with trailing blank lines trimmed. */
6
+ text: string;
7
+ }
8
+ /** The `##` sections of a `CHANGELOG.md`, split by whether their heading names a version. */
9
+ export interface ChangelogMarkdownSections {
10
+ versioned: VersionedMarkdownSection[];
11
+ /** Headings of the `##` sections that do not name a version, as written. */
12
+ unversionedHeadings: string[];
13
+ }
14
+ /**
15
+ * Plans which sections of the existing `CHANGELOG.md` at `changelogFile` to keep when the file is regenerated from
16
+ * `entries`: every versioned section whose version matches none of the entries. Also returns what to report about the
17
+ * file, or undefined when nothing was kept and nothing was dropped.
18
+ */
19
+ export declare function planPreservedSections(changelogFile: string, entries: readonly ChangelogEntry[]): {
20
+ sections: VersionedMarkdownSection[];
21
+ preservation: ChangelogPreservation | undefined;
22
+ };
23
+ /**
24
+ * Reads the `##` sections of the `CHANGELOG.md` at `filePath`, excluding the header above the first section and the
25
+ * release-kit footer comment. For a missing file, returns empty section lists.
26
+ */
27
+ export declare function readChangelogSections(filePath: string): ChangelogMarkdownSections;
28
+ /**
29
+ * Splits `CHANGELOG.md` content at its `##` headings. A section runs to the next `##` heading or the release-kit
30
+ * footer comment; `###` and deeper headings, and any heading inside a fenced code block, belong to the enclosing
31
+ * section.
32
+ */
33
+ export declare function parseChangelogSections(content: string): ChangelogMarkdownSections;
@@ -0,0 +1,80 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import semver from 'semver';
3
+ export function planPreservedSections(changelogFile, entries) {
4
+ const { versioned, unversionedHeadings } = readChangelogSections(changelogFile);
5
+ const renderedVersions = new Set(entries.map((entry) => entry.version));
6
+ const sections = versioned.filter((section) => !renderedVersions.has(section.version));
7
+ if (sections.length === 0 && unversionedHeadings.length === 0) {
8
+ return { sections, preservation: undefined };
9
+ }
10
+ return {
11
+ sections,
12
+ preservation: {
13
+ file: changelogFile,
14
+ preservedVersions: sections.map((section) => section.version),
15
+ droppedUnversionedHeadings: unversionedHeadings,
16
+ },
17
+ };
18
+ }
19
+ export function readChangelogSections(filePath) {
20
+ if (!existsSync(filePath)) {
21
+ return { versioned: [], unversionedHeadings: [] };
22
+ }
23
+ return parseChangelogSections(readFileSync(filePath, 'utf8'));
24
+ }
25
+ export function parseChangelogSections(content) {
26
+ const sections = [];
27
+ let current;
28
+ let fence;
29
+ for (const line of content.split(/\r?\n/)) {
30
+ if (fence === undefined) {
31
+ if (FOOTER_PATTERN.test(line)) {
32
+ current = undefined;
33
+ continue;
34
+ }
35
+ if (SECTION_HEADING_PATTERN.test(line)) {
36
+ current = { heading: line.trimEnd(), lines: [] };
37
+ sections.push(current);
38
+ }
39
+ }
40
+ fence = updateFence(fence, line);
41
+ current?.lines.push(line);
42
+ }
43
+ const versioned = [];
44
+ const unversionedHeadings = [];
45
+ for (const { heading, lines } of sections) {
46
+ const version = extractHeadingVersion(heading);
47
+ if (version === undefined) {
48
+ unversionedHeadings.push(heading);
49
+ }
50
+ else {
51
+ versioned.push({ version, text: trimTrailingBlankLines(lines).join('\n') });
52
+ }
53
+ }
54
+ return { versioned, unversionedHeadings };
55
+ }
56
+ const FOOTER_PATTERN = /^<!-- Generated by release-kit\b/;
57
+ const SECTION_HEADING_PATTERN = /^## /;
58
+ const FENCE_PATTERN = /^ {0,3}(`{3,}|~{3,})/;
59
+ const HEADING_VERSION_PATTERN = /(?<![\w.])v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)(?!\.?\d)/;
60
+ function extractHeadingVersion(heading) {
61
+ const token = HEADING_VERSION_PATTERN.exec(heading)?.[1];
62
+ if (token === undefined)
63
+ return undefined;
64
+ return semver.valid(token) ?? undefined;
65
+ }
66
+ function trimTrailingBlankLines(lines) {
67
+ let end = lines.length;
68
+ while (end > 0 && lines[end - 1]?.trim() === '') {
69
+ end--;
70
+ }
71
+ return lines.slice(0, end);
72
+ }
73
+ function updateFence(fence, line) {
74
+ const marker = FENCE_PATTERN.exec(line)?.[1];
75
+ if (marker === undefined)
76
+ return fence;
77
+ if (fence === undefined)
78
+ return marker;
79
+ return marker[0] === fence[0] && marker.length >= fence.length ? undefined : fence;
80
+ }
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * File written by the release preparation step, containing one tag per line.
3
3
  *
4
- * Relative to the project root so it works identically in CI and local runs. The reusable
4
+ * Relative to the project root so that it works identically in CI and local runs. The reusable
5
5
  * release workflow reads it at this path, so the path and the line-delimited format are a
6
- * published contract rather than an internal detail.
6
+ * published contract.
7
7
  */
8
8
  export declare const RELEASE_TAGS_FILE = "tmp/.release-tags";
9
9
  /**
10
10
  * File written by the release preparation step, containing the commit body summary.
11
- * Relative to the project root so it works identically in CI and local runs.
11
+ * Relative to the project root so that it works identically in CI and local runs.
12
12
  */
13
13
  export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
14
14
  /** Resolves the tags file against the current working directory, for use in diagnostics. */
@@ -16,10 +16,8 @@ export declare function resolveReleaseTagsPath(): string;
16
16
  /**
17
17
  * Reads the tag names written by `prepare`, one per line, discarding blank lines.
18
18
  *
19
- * Both consumers — `commit` and `tag` — share this reader so their diagnostics cannot drift.
20
- * Failure messages name the path as resolved against `process.cwd()`, which is what separates
21
- * a genuinely absent file from one the command looked for under a different worktree, and
22
- * they preserve the underlying errno instead of reporting every failure as a missing file.
19
+ * A failure message names the path as resolved against `process.cwd()`, which distinguishes an
20
+ * absent file from one that the command looked for under a different worktree.
23
21
  *
24
22
  * An empty or blank file yields an empty array; callers decide whether that is an error.
25
23
  */
@@ -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,19 +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
- * (monorepo only). Orthogonal to `bumpOverride`: when `bumpOverride` is not given,
13
- * 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`.
14
8
  */
15
9
  force?: boolean;
16
- /** Override the bump type instead of determining it from commits. */
10
+ /** Level of the release, in place of the level that the changelog items call for; it doesn't trigger a release. */
17
11
  bumpOverride?: ReleaseType;
18
12
  /**
19
13
  * Explicit target version (canonical `N.N.N`) that bypasses commit-derived bump logic.
@@ -22,29 +16,35 @@ export interface ReleasePrepareOptions {
22
16
  */
23
17
  setVersion?: string;
24
18
  /**
25
- * Workspace directories the run was narrowed to by `--only` (monorepo only), with
19
+ * Workspace directories to which `--only` narrowed the run (monorepo only), with
26
20
  * `config.workspaces` already filtered to match. Present only for a narrowed run, which
27
- * 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
28
22
  * the narrowing has changed which workspaces those are.
29
23
  */
30
24
  only?: string[];
31
25
  /**
32
- * If true, write per-workspace release-notes previews under `{workspacePath}/docs/`
33
- * (`README.v{version}.md` and `RELEASE_NOTES.v{version}.md`) after each workspace's
34
- * `changelog.json` is produced. Requires `config.changelogJson.enabled`; when disabled,
35
- * 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.
36
36
  */
37
37
  withReleaseNotes?: boolean;
38
38
  }
39
39
  /**
40
- * Orchestrate the release preparation workflow for a single package.
40
+ * Orchestrates the release preparation workflow for a single package.
41
41
  *
42
- * 1. Gets commits since the last tag.
43
- * 2. Determines the bump type from commits (or uses the override).
44
- * 3. Bumps all configured package.json version fields.
45
- * 4. Generates changelogs via git-cliff.
46
- * 5. Runs the optional format command.
42
+ * 1. Reads the release history once, and stops when the current version is recorded but untagged.
43
+ * 2. Decides the release from the history's bump, `--force`, and `--bump` (or takes `--set-version`).
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.
46
+ * 5. Renders the optional format command.
47
47
  *
48
- * Returns a structured `PrepareResult` with all data needed for presentation.
48
+ * Returns the plan without writing any file.
49
49
  */
50
- export declare function releasePrepare(config: ReleaseConfig, options: ReleasePrepareOptions): ReleasePlan;
50
+ export declare function releasePrepare(config: PrepareConfig, options: ReleasePrepareOptions): ReleasePlan;
@@ -1,60 +1,54 @@
1
1
  import { join as joinPath } from 'node:path';
2
- import { buildChangelogEntries } from "./buildChangelogEntries.js";
3
- import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
2
+ import { assertTaggedBaseline, findUntaggedBaseline } from "./assertTaggedBaseline.js";
3
+ import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
4
+ import { readReleaseHistory, toReleaseEntries } from "./buildChangelogEntries.js";
4
5
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
5
6
  import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
6
7
  import { applyChangelogOverrides, formatStaleOverrideKeyWarning, loadOverridesForScopes, } from "./changelogOverrides.js";
7
- import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
8
- import { DEFAULT_BREAKING_POLICIES, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
9
- import { determineBumpFromCommits } from "./determineBumpFromCommits.js";
10
- import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
8
+ import { decideRelease } from "./decideRelease.js";
9
+ import { deriveSectionOrder } from "./deriveReleaseNotesConfig.js";
11
10
  import { hasPrettierConfig } from "./hasPrettierConfig.js";
12
11
  import { resolveWorkTypes } from "./loadConfig.js";
13
12
  import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
14
13
  import { planVersionBump, planVersionSet } from "./planVersionBump.js";
14
+ import { planPreservedSections } from "./readChangelogSections.js";
15
15
  import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
16
- import { deriveSectionOrder } from "./resolveReleaseNotesConfig.js";
17
16
  export function releasePrepare(config, options) {
18
- const { bumpOverride, setVersion, withReleaseNotes } = options;
17
+ const { bumpOverride, force, setVersion, withReleaseNotes } = options;
19
18
  const writes = [];
20
- const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
21
- const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
22
- const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
23
19
  const overridesResult = loadOverridesForScopes({ project: '.' });
24
20
  if (overridesResult.errors.length > 0) {
25
21
  throw new Error(`Failed to load changelog overrides:\n - ${overridesResult.errors.join('\n - ')}`);
26
22
  }
27
23
  const overrides = overridesResult.project;
28
- const { tag, commits } = getCommitsSinceTarget([config.tagPrefix]);
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
+ ]);
34
+ const { commits } = history.unreleased;
35
+ const tag = history.previousTag;
36
+ const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
29
37
  let releaseType;
30
- let parsedCommitCount;
31
- let unparseableCommits;
32
- const collector = createPolicyViolationCollector();
33
38
  let bump;
34
- if (setVersion !== undefined) {
35
- bump = planVersionSet(config.packageFiles, setVersion);
36
- }
37
- else {
38
- if (bumpOverride === undefined) {
39
- const determination = determineBumpFromCommits(commits, workTypes, versionPatterns, config.scopeAliases, {
40
- breakingPolicies,
41
- onPolicyViolation: collector.onPolicyViolation,
42
- });
43
- parsedCommitCount = determination.parsedCommitCount;
44
- unparseableCommits = determination.unparseableCommits;
45
- releaseType = determination.releaseType;
46
- }
47
- else {
48
- releaseType = bumpOverride;
49
- }
50
- if (releaseType === undefined) {
51
- const skipped = buildSkippedSinglePackage({
52
- commitCount: commits.length,
53
- previousTag: tag,
54
- parsedCommitCount,
55
- unparseableCommits,
56
- policyViolations: collector.violations.length > 0 ? collector.violations : undefined,
57
- });
39
+ if (setVersion === undefined) {
40
+ const decision = decideRelease({
41
+ naturalBump: history.unreleased.bump,
42
+ commitCount: commits.length,
43
+ force,
44
+ bumpOverride,
45
+ skipReasons: {
46
+ noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
47
+ noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
48
+ },
49
+ });
50
+ if (decision.outcome === 'skip') {
51
+ const skipped = buildSkippedSinglePackage(history, decision.skipReason);
58
52
  return {
59
53
  workspaces: [skipped],
60
54
  tags: [],
@@ -63,20 +57,23 @@ export function releasePrepare(config, options) {
63
57
  formatCommand: undefined,
64
58
  };
65
59
  }
60
+ releaseType = decision.releaseType;
66
61
  bump = planVersionBump(config.packageFiles, releaseType);
67
62
  }
63
+ else {
64
+ bump = planVersionSet(config.packageFiles, setVersion);
65
+ }
68
66
  writes.push(...bump.writes);
69
67
  const newTag = `${config.tagPrefix}${bump.newVersion}`;
70
68
  const planWarnings = [];
71
69
  const changelogs = planSinglePackageChangelogs({
72
70
  config,
73
- commits,
71
+ history,
74
72
  newTag,
75
- newVersion: bump.newVersion,
76
73
  overrides,
77
74
  overrideWarnings: planWarnings,
78
75
  });
79
- const { changelogFiles, changelogJsonFiles } = changelogs;
76
+ const { changelogFiles, changelogJsonFiles, changelogPreservation } = changelogs;
80
77
  const previewWrites = planSinglePackagePreviews(withReleaseNotes === true, config, newTag, changelogs.entries, planWarnings);
81
78
  writes.push(...changelogs.writes, ...previewWrites);
82
79
  const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? 'npx prettier --write' : undefined);
@@ -90,15 +87,13 @@ export function releasePrepare(config, options) {
90
87
  formatCommand = { command: `${formatCommandStr} ${modifiedFiles.join(' ')}`, files: modifiedFiles };
91
88
  }
92
89
  const released = buildReleasedSinglePackage({
93
- commits,
90
+ history,
94
91
  bump,
95
92
  newTag,
96
93
  changelogFiles,
97
- previousTag: tag,
98
- parsedCommitCount,
94
+ changelogPreservation,
99
95
  releaseType,
100
- unparseableCommits,
101
- policyViolations: collector.violations.length > 0 ? collector.violations : undefined,
96
+ bumpOverride: setVersion === undefined ? bumpOverride : undefined,
102
97
  setVersion,
103
98
  previewFiles: previewWrites.map((write) => write.path),
104
99
  });
@@ -114,28 +109,26 @@ export function releasePrepare(config, options) {
114
109
  }
115
110
  return plan;
116
111
  }
117
- function buildSkippedSinglePackage(args) {
112
+ function buildSkippedSinglePackage(history, skipReason) {
113
+ const { unreleased } = history;
118
114
  const skipped = {
119
115
  status: 'skipped',
120
- commitCount: args.commitCount,
121
- skipReason: 'No release-worthy changes found. Skipping.',
116
+ commitCount: unreleased.commits.length,
117
+ parsedCommitCount: unreleased.parsedCommitCount,
118
+ skipReason,
122
119
  };
123
- if (args.previousTag !== undefined) {
124
- skipped.previousTag = args.previousTag;
125
- }
126
- if (args.parsedCommitCount !== undefined) {
127
- skipped.parsedCommitCount = args.parsedCommitCount;
128
- }
129
- if (args.unparseableCommits !== undefined) {
130
- skipped.unparseableCommits = args.unparseableCommits;
120
+ if (history.previousTag !== undefined) {
121
+ skipped.previousTag = history.previousTag;
131
122
  }
132
- if (args.policyViolations !== undefined) {
133
- skipped.policyViolations = args.policyViolations;
123
+ if (unreleased.unparseableCommits !== undefined) {
124
+ skipped.unparseableCommits = unreleased.unparseableCommits;
134
125
  }
126
+ attachChangelogDiagnostics(skipped, unreleased.diagnostics);
135
127
  return skipped;
136
128
  }
137
129
  function buildReleasedSinglePackage(args) {
138
- const { commits, bump, newTag, changelogFiles, previousTag, parsedCommitCount, releaseType, unparseableCommits, policyViolations, setVersion, } = args;
130
+ const { history, bump, newTag, changelogFiles, releaseType, bumpOverride, setVersion } = args;
131
+ const { commits, diagnostics, parsedCommitCount, unparseableCommits } = history.unreleased;
139
132
  const released = {
140
133
  status: 'released',
141
134
  commitCount: commits.length,
@@ -146,37 +139,36 @@ function buildReleasedSinglePackage(args) {
146
139
  changelogFiles,
147
140
  commits,
148
141
  };
149
- if (previousTag !== undefined) {
150
- released.previousTag = previousTag;
151
- }
152
- if (parsedCommitCount !== undefined) {
153
- released.parsedCommitCount = parsedCommitCount;
142
+ if (history.previousTag !== undefined) {
143
+ released.previousTag = history.previousTag;
154
144
  }
155
145
  if (releaseType !== undefined) {
156
146
  released.releaseType = releaseType;
147
+ released.parsedCommitCount = parsedCommitCount;
148
+ if (unparseableCommits !== undefined) {
149
+ released.unparseableCommits = unparseableCommits;
150
+ }
157
151
  }
158
- if (unparseableCommits !== undefined) {
159
- released.unparseableCommits = unparseableCommits;
160
- }
161
- if (policyViolations !== undefined) {
162
- released.policyViolations = policyViolations;
152
+ if (bumpOverride !== undefined) {
153
+ released.bumpOverride = bumpOverride;
163
154
  }
155
+ attachChangelogDiagnostics(released, diagnostics);
164
156
  if (setVersion !== undefined) {
165
157
  released.setVersion = setVersion;
166
158
  }
167
159
  if (args.previewFiles.length > 0) {
168
160
  released.previewFiles = args.previewFiles;
169
161
  }
162
+ if (args.changelogPreservation.length > 0) {
163
+ released.changelogPreservation = args.changelogPreservation;
164
+ }
170
165
  return released;
171
166
  }
172
167
  function planSinglePackageChangelogs(args) {
173
- const { config, commits, newTag, newVersion, overrides, overrideWarnings } = args;
174
- const isEmptyRange = commits.length === 0;
168
+ const { config, history, newTag, overrides, overrideWarnings } = args;
175
169
  const today = new Date().toISOString().slice(0, 10);
176
- const baseEntries = isEmptyRange
177
- ? [buildEmptyReleaseEntry(newVersion, today)]
178
- : buildChangelogEntries(config, newTag);
179
- const applied = applyChangelogOverrides(baseEntries, overrides);
170
+ const builtEntries = toReleaseEntries(history, newTag, today);
171
+ const applied = applyChangelogOverrides(builtEntries, overrides);
180
172
  if (applied.errors.length > 0) {
181
173
  throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
182
174
  }
@@ -190,6 +182,7 @@ function planSinglePackageChangelogs(args) {
190
182
  const sectionOrder = deriveSectionOrder(resolveWorkTypes(config.workTypes));
191
183
  const changelogFiles = [];
192
184
  const changelogJsonFiles = [];
185
+ const changelogPreservation = [];
193
186
  const writes = [];
194
187
  let firstMergedEntries = [];
195
188
  for (const changelogPath of config.changelogPaths) {
@@ -203,10 +196,17 @@ function planSinglePackageChangelogs(args) {
203
196
  changelogJsonFiles.push(jsonPath);
204
197
  }
205
198
  const changelogFile = joinPath(changelogPath, 'CHANGELOG.md');
206
- 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
+ });
207
204
  changelogFiles.push(changelogFile);
205
+ if (preserved.preservation !== undefined) {
206
+ changelogPreservation.push(preserved.preservation);
207
+ }
208
208
  }
209
- return { changelogFiles, changelogJsonFiles, entries: firstMergedEntries, writes };
209
+ return { changelogFiles, changelogJsonFiles, changelogPreservation, entries: firstMergedEntries, writes };
210
210
  }
211
211
  function planSinglePackagePreviews(withReleaseNotes, config, newTag, entries, warnings) {
212
212
  if (!withReleaseNotes) {
@@ -220,7 +220,7 @@ function planSinglePackagePreviews(withReleaseNotes, config, newTag, entries, wa
220
220
  return [];
221
221
  }
222
222
  const previews = planReleaseNotesPreviews({
223
- workspacePath: process.cwd(),
223
+ workspacePath: '.',
224
224
  tag: newTag,
225
225
  entries,
226
226
  sectionOrder: deriveSectionOrder(resolveWorkTypes(config.workTypes)),