@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
@@ -0,0 +1,34 @@
1
+ import { TICKET_PREFIX_PATTERNS } from '@williamthorsen/change-grammar';
2
+ import { parseCommitMessage } from "./parseCommitMessage.js";
3
+ const RELEASE_SUBJECT_PATTERN = /^release:/;
4
+ const MERGE_SUBJECT_PATTERN = /^Merge/;
5
+ export function classifyChangelogCommit(commit, workTypes, options) {
6
+ const subject = commit.message.split('\n', 1)[0] ?? '';
7
+ if (isNonChangeSubject(subject)) {
8
+ return { kind: 'excluded' };
9
+ }
10
+ if (!hasTicketPrefix(subject)) {
11
+ return { kind: 'unparseable' };
12
+ }
13
+ const parsed = parseCommitMessage(commit.message, commit.hash, workTypes, undefined, options);
14
+ if (parsed === undefined) {
15
+ return { kind: 'unparseable' };
16
+ }
17
+ const config = workTypes[parsed.type];
18
+ if (config === undefined) {
19
+ return { kind: 'unparseable' };
20
+ }
21
+ if (config.excludedFromChangelog === true) {
22
+ return { kind: 'excluded' };
23
+ }
24
+ return { kind: 'header', header: config.header, type: parsed.type, breaking: parsed.breaking };
25
+ }
26
+ export function isNonChangeSubject(subject) {
27
+ return isReleaseSubject(subject) || MERGE_SUBJECT_PATTERN.test(subject);
28
+ }
29
+ export function isReleaseSubject(subject) {
30
+ return RELEASE_SUBJECT_PATTERN.test(subject);
31
+ }
32
+ function hasTicketPrefix(subject) {
33
+ return TICKET_PREFIX_PATTERNS.some((pattern) => pattern.test(subject));
34
+ }
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Orchestrate the CLI `commit` command.
3
- *
4
- * Reads the tags and summary files produced by `prepare`, stages all
5
- * changes, and creates a release commit with a formatted message.
2
+ * Runs the CLI `commit` command: reads the tags and summary files written by `prepare`, stages all changes, and
3
+ * creates the release commit.
6
4
  */
7
5
  export declare function commitCommand(argv: string[]): void;
@@ -1,8 +1,14 @@
1
1
  /**
2
- * Return true when `target` is strictly greater than `current`, comparing major, minor, then patch.
2
+ * Compares two version strings in descending order (newest first).
3
3
  *
4
- * Equal versions return false. Comparison is numeric (not lexical), so `0.10.0` is greater than `0.9.0`.
5
- *
6
- * @throws If either version is not canonical `N.N.N` semver.
4
+ * Valid SemVer inputs are ordered per SemVer §11 (delegated to `semver.rcompare`): prerelease
5
+ * versions precede the corresponding release (`1.2.3-alpha < 1.2.3`), and build metadata is
6
+ * ignored for ordering. Inputs that fail `semver.valid` sort to the bottom of the descending
7
+ * list, ordered lexically among themselves. The comparator never throws.
8
+ */
9
+ export declare function compareVersionsDescending(a: string, b: string): number;
10
+ /**
11
+ * Reports whether `target` is strictly greater than `current`, comparing major, minor, then patch numerically; throws
12
+ * unless both are canonical `N.N.N` semver.
7
13
  */
8
14
  export declare function isForwardVersion(current: string, target: string): boolean;
@@ -1,3 +1,4 @@
1
+ import semver from 'semver';
1
2
  const CANONICAL_SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
2
3
  function parseCanonicalSemver(version) {
3
4
  const match = version.match(CANONICAL_SEMVER_PATTERN);
@@ -11,6 +12,21 @@ function parseCanonicalSemver(version) {
11
12
  patch: Number.parseInt(patch, 10),
12
13
  };
13
14
  }
15
+ export function compareVersionsDescending(a, b) {
16
+ const aValid = semver.valid(a);
17
+ const bValid = semver.valid(b);
18
+ if (aValid && bValid)
19
+ return semver.rcompare(aValid, bValid);
20
+ if (aValid)
21
+ return -1;
22
+ if (bValid)
23
+ return 1;
24
+ if (a > b)
25
+ return -1;
26
+ if (a < b)
27
+ return 1;
28
+ return 0;
29
+ }
14
30
  export function isForwardVersion(current, target) {
15
31
  const currentParsed = parseCanonicalSemver(current);
16
32
  const targetParsed = parseCanonicalSemver(target);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The `--config` flag, shared by every subcommand that reads the config file.
3
3
  *
4
- * Spread into a subcommand's own schema rather than repeated, so that seven subcommands cannot drift to seven
4
+ * Spread into a subcommand's own schema rather than repeated, so that the subcommands cannot drift to different
5
5
  * spellings of one flag.
6
6
  */
7
7
  export declare const configFlagSchema: {
@@ -7,12 +7,8 @@ export interface CreateGithubReleaseOptions {
7
7
  sectionOrder?: string[];
8
8
  }
9
9
  /**
10
- * Discriminated reason a single tag was skipped.
11
- *
12
- * `'no-entry'` is the umbrella for all "data missing" sub-cases (changelog file not found,
13
- * JSON unparseable, version not in changelog) — distinguishing them programmatically adds no
14
- * value to the consumer, while the existing `console.warn` messages already discriminate them
15
- * for human diagnostics.
10
+ * Why a tag was skipped. `'no-entry'` covers every case of missing data (no changelog file, unparseable JSON, no entry
11
+ * for the version); the warnings that `createGithubRelease` prints tell them apart.
16
12
  */
17
13
  export type CreateReleaseSkipReason = 'no-entry' | 'no-audience-content' | 'empty-body';
18
14
  /** Discriminated outcome of attempting to create a single GitHub Release. */
@@ -23,13 +19,11 @@ export type CreateReleaseResult = {
23
19
  reason: CreateReleaseSkipReason;
24
20
  };
25
21
  /**
26
- * Create a GitHub Release from changelog.json using the `gh` CLI.
22
+ * Creates a GitHub Release for a tag through the `gh` CLI, with the all-audience notes of the tag's `changelog.json`
23
+ * entry as its body.
27
24
  *
28
- * Reads the changelog JSON, finds the entry matching the tag's version, renders all-audience
29
- * release notes, and creates the release. Returns a discriminated `{status: 'skipped', reason}`
30
- * (with a warning) when the changelog entry is missing, the entry has no all-audience content,
31
- * or the rendered body is empty. Throws when the `gh` CLI invocation itself fails so callers
32
- * can surface the failure rather than exit 0 silently.
25
+ * Returns a skip when the entry is missing (with a warning), doesn't have any all-audience content, or renders an empty
26
+ * body. Throws when `gh` fails, so that the caller does not exit 0 on a failed release.
33
27
  */
34
28
  export declare function createGithubRelease(options: CreateGithubReleaseOptions): CreateReleaseResult;
35
29
  /** Outcome of a `createGithubReleases` invocation. */
@@ -42,13 +36,7 @@ export interface CreateGithubReleasesOutcome {
42
36
  reason: CreateReleaseSkipReason;
43
37
  }>;
44
38
  }
45
- /**
46
- * Create GitHub Releases for each provided tag.
47
- *
48
- * Per-tag soft-fails (missing changelog entry, no all-audience content, empty rendered body) are
49
- * recorded in `skipped` with their discriminated reason and do not throw. Hard failures from the
50
- * `gh` CLI itself throw and short-circuit the loop so callers can surface them.
51
- */
39
+ /** Creates a GitHub Release for each tag and records each skip with its reason; throws at once when `gh` fails. */
52
40
  export declare function createGithubReleases(tags: Array<{
53
41
  tag: string;
54
42
  workspacePath: string;
@@ -1,9 +1,9 @@
1
1
  import { type StreamStyles } from '@williamthorsen/nmr-core';
2
2
  /**
3
- * Orchestrate the CLI `create-github-release` command: Create GitHub Releases from changelog.json
4
- * for tags on HEAD (or a comma-separated `--tags` subset), without requiring npm publish.
3
+ * Runs the CLI `create-github-release` command: creates GitHub Releases from `changelog.json` for the tags on HEAD, or
4
+ * a comma-separated `--tags` subset, without publishing to npm.
5
5
  *
6
- * Private/unpublishable workspaces are skipped with a warning and never get a Release, matching
7
- * `release-kit publish`; an all-private tag set is a clean no-op.
6
+ * A private workspace's tag is skipped with a warning and doesn't get a Release; an all-private tag set is a clean
7
+ * no-op. A relative `--config` resolves against `invocationDir`.
8
8
  */
9
- export declare function createGithubReleaseCommand(argv: string[], styles: StreamStyles): Promise<void>;
9
+ export declare function createGithubReleaseCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
@@ -2,22 +2,24 @@ import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { configFlagSchema } from "./configFlagSchema.js";
4
4
  import { createGithubReleases } from "./createGithubRelease.js";
5
+ import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
5
6
  import { formatPrivateSkip } from "./formatPrivateSkip.js";
6
- import { assertConfigUsable } from "./loadValidatedConfig.js";
7
+ import { loadUsableConfig } from "./loadValidatedConfig.js";
7
8
  import { parseRequestedTags } from "./parseRequestedTags.js";
8
9
  import { resolveCommandTags } from "./resolveCommandTags.js";
9
- import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
10
+ import { resolveConfigFlag } from "./resolveConfigFlag.js";
10
11
  const createGithubReleaseFlagSchema = {
11
12
  ...configFlagSchema,
12
13
  dryRun: { long: '--dry-run', type: 'boolean' },
13
14
  tags: { long: '--tags', type: 'string' },
14
15
  };
15
- export async function createGithubReleaseCommand(argv, styles) {
16
+ export async function createGithubReleaseCommand(argv, styles, invocationDir) {
16
17
  const parsed = parseArgsOrExit(argv, createGithubReleaseFlagSchema);
18
+ const configPath = resolveConfigFlag(parsed.flags.config, invocationDir);
17
19
  const { dryRun } = parsed.flags;
18
- await assertConfigUsable(styles.stderr, parsed.flags.config);
20
+ const userConfig = await loadUsableConfig(styles.stderr, configPath);
19
21
  const requestedTags = parseRequestedTags(parsed.flags.tags);
20
- const resolvedTags = resolveCommandTags(requestedTags);
22
+ const resolvedTags = resolveCommandTags(requestedTags, userConfig);
21
23
  const publishableTags = resolvedTags.filter((resolvedTag) => resolvedTag.isPublishable);
22
24
  for (const resolvedTag of resolvedTags) {
23
25
  if (!resolvedTag.isPublishable) {
@@ -27,9 +29,7 @@ export async function createGithubReleaseCommand(argv, styles) {
27
29
  if (publishableTags.length === 0) {
28
30
  return;
29
31
  }
30
- const { changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig(styles.stderr, {
31
- ...(parsed.flags.config !== undefined && { configPath: parsed.flags.config }),
32
- });
32
+ const { changelogJsonOutputPath, sectionOrder } = deriveReleaseNotesConfig(userConfig);
33
33
  let outcome;
34
34
  try {
35
35
  outcome = createGithubReleases(publishableTags, changelogJsonOutputPath, dryRun, sectionOrder);
@@ -40,6 +40,6 @@ export async function createGithubReleaseCommand(argv, styles) {
40
40
  }
41
41
  if (outcome.skipped.length > 0) {
42
42
  const formatted = outcome.skipped.map((s) => `${s.tag} (${s.reason})`).join(', ');
43
- console.info(`Skipped ${outcome.skipped.length} tag(s) with no releasable content: ${formatted}.`);
43
+ console.info(`Skipped ${outcome.skipped.length} tag(s) without releasable content: ${formatted}.`);
44
44
  }
45
45
  }
@@ -5,8 +5,7 @@ export interface CreateTagsOptions {
5
5
  style: OutputStyle;
6
6
  }
7
7
  /**
8
- * Read tag names from the tags file produced by `prepare` and create annotated git tags.
9
- *
10
- * Returns the list of tag names that were created (or would be created in dry-run mode).
8
+ * Creates an annotated git tag for each name in the tags file written by `prepare`, then deletes the tags and summary
9
+ * files, and returns the tag names. In dry-run mode, it only lists the names.
11
10
  */
12
11
  export declare function createTags(options: CreateTagsOptions): string[];
@@ -1,23 +1,17 @@
1
- import { type PolicyViolationHandler } from './parseCommitMessage.js';
2
- import type { Commit, ReleaseType, VersionPatterns, WorkTypeConfig } from './types.js';
3
- /** Inputs to the unified release decision used by both pipelines. */
1
+ import type { ReleaseType } from './types.js';
2
+ /** Inputs to the release decision that every release path makes. */
4
3
  export interface DecideReleaseArgs {
5
- commits: readonly Commit[];
4
+ /** The bump that the window's changelog items call for; undefined when they call for none. */
5
+ naturalBump: ReleaseType | undefined;
6
+ /** The number of commits in the window, which selects the skip reason. */
7
+ commitCount: number;
6
8
  /** True when `--force` was passed; treats absence of a natural bump as patch. Defaults to false. */
7
9
  force?: boolean | undefined;
8
10
  /** Explicit `--bump=X` override. When set, the level is X regardless of the natural bump. */
9
11
  bumpOverride: ReleaseType | undefined;
10
- workTypes: Record<string, WorkTypeConfig>;
11
- versionPatterns: VersionPatterns;
12
- scopeAliases: Record<string, string> | undefined;
13
- /** Per-canonical-type breaking-policy lookup. When provided, drives `!` policy enforcement in `parseCommitMessage`. */
14
- breakingPolicies?: Record<string, 'forbidden' | 'optional' | 'required'>;
15
- /** Receives policy-violation notifications from `parseCommitMessage`. */
16
- onPolicyViolation?: PolicyViolationHandler;
17
12
  /**
18
- * Skip wordings to use when no commits exist (`noCommits`) or when commits exist but
19
- * none parse to a bump-worthy work type (`noBumpWorthy`). Both pipelines pass their
20
- * own pre-rendered wordings (with `{name}` / `since` substitutions already applied).
13
+ * Skip wordings to use when the window doesn't contain any commits (`noCommits`) or when it contains commits but none
14
+ * yields a changelog item (`noBumpWorthy`). Each path passes its own pre-rendered wordings.
21
15
  */
22
16
  skipReasons: {
23
17
  noCommits: string;
@@ -28,38 +22,14 @@ export interface DecideReleaseArgs {
28
22
  export type DecideReleaseResult = {
29
23
  outcome: 'release';
30
24
  releaseType: ReleaseType;
31
- parsedCommitCount: number;
32
- unparseableCommits: Commit[] | undefined;
33
25
  } | {
34
26
  outcome: 'skip';
35
27
  skipReason: string;
36
- parsedCommitCount: number;
37
- unparseableCommits: Commit[] | undefined;
38
28
  };
39
29
  /**
40
- * Apply the unified release-decision algorithm shared by both `releasePrepareProject`
41
- * and the per-workspace path in `releasePrepareMono`.
30
+ * Decides whether a window releases, and at which level.
42
31
  *
43
- * Algorithm:
44
- * shouldRelease = naturalBump !== undefined OR force === true
45
- * releaseLevel = bumpOverride ?? naturalBump ?? 'patch'
46
- *
47
- * "natural bump" is the raw output of `determineBumpType` over the parsed commits — it
48
- * is `undefined` when no commits exist or when none of the parsed commits map to a
49
- * bump-worthy work type. Note that this differs from `determineBumpFromCommits`, which
50
- * applies a patch floor whenever any commits exist; the patch floor is intentionally
51
- * not applied here so that `--bump=X` and `--force` remain orthogonal (the floor is
52
- * applied only when `--force` is set, by virtue of the `naturalBump ?? 'patch'`
53
- * fallback in the release-level expression above).
54
- *
55
- * `parsedCommitCount` and `unparseableCommits` are returned in both branches so callers
56
- * can surface diagnostic data uniformly regardless of skip/release outcome.
57
- *
58
- * Scope: this is the canonical algorithm for the orthogonal-flag model in the
59
- * monorepo path (`releasePrepareProject` + per-workspace `determineDirectBumps`). The
60
- * single-package executor (`releasePrepare`) deliberately retains
61
- * `determineBumpFromCommits` and the legacy patch-floor semantics; `prepareCommand`
62
- * rejects `--force` without `--bump` in single-package mode to surface that gap. A
63
- * future ticket can unify the single-package path; see #313's external plan.
32
+ * A window without a natural bump releases only under `--force`, so `--bump=X` chooses a level without triggering a
33
+ * release, and `--force` triggers one without choosing a level.
64
34
  */
65
35
  export declare function decideRelease(args: DecideReleaseArgs): DecideReleaseResult;
@@ -1,30 +1,8 @@
1
- import { determineBumpType } from "./determineBumpType.js";
2
- import { parseCommitMessage, } from "./parseCommitMessage.js";
3
1
  export function decideRelease(args) {
4
- const { commits, force = false, bumpOverride, workTypes, versionPatterns, scopeAliases, breakingPolicies, onPolicyViolation, skipReasons, } = args;
5
- const parseOptions = {
6
- ...(breakingPolicies !== undefined && { breakingPolicies }),
7
- ...(onPolicyViolation !== undefined && { onPolicyViolation }),
8
- };
9
- const parsedCommits = [];
10
- const unparseable = [];
11
- for (const commit of commits) {
12
- const parsed = parseCommitMessage(commit.message, commit.hash, workTypes, scopeAliases, parseOptions);
13
- if (parsed === undefined) {
14
- unparseable.push(commit);
15
- }
16
- else {
17
- parsedCommits.push(parsed);
18
- }
2
+ const { naturalBump, commitCount, force = false, bumpOverride, skipReasons } = args;
3
+ if (naturalBump === undefined && !force) {
4
+ const skipReason = commitCount === 0 ? skipReasons.noCommits : skipReasons.noBumpWorthy;
5
+ return { outcome: 'skip', skipReason };
19
6
  }
20
- const parsedCommitCount = parsedCommits.length;
21
- const unparseableCommits = unparseable.length > 0 ? unparseable : undefined;
22
- const naturalBump = determineBumpType(parsedCommits, workTypes, versionPatterns);
23
- const shouldRelease = naturalBump !== undefined || force;
24
- if (!shouldRelease) {
25
- const skipReason = commits.length === 0 ? skipReasons.noCommits : skipReasons.noBumpWorthy;
26
- return { outcome: 'skip', skipReason, parsedCommitCount, unparseableCommits };
27
- }
28
- const releaseType = bumpOverride ?? naturalBump ?? 'patch';
29
- return { outcome: 'release', releaseType, parsedCommitCount, unparseableCommits };
7
+ return { outcome: 'release', releaseType: bumpOverride ?? naturalBump ?? 'patch' };
30
8
  }
@@ -1,33 +1,14 @@
1
+ import { type BreakingPolicy } from '@williamthorsen/change-grammar';
1
2
  import type { ChangelogJsonConfig, ReleaseNotesConfig, VersionPatterns, WorkTypeConfig } from './types.js';
2
- /** Re-export the runtime taxonomy so consumers don't have to depend on `workTypesData.ts` directly. */
3
- export type { WorkTypesData } from './workTypesData.js';
4
- export { WORK_TYPES_DATA } from './workTypesData.js';
5
- /**
6
- * Compose the rendered section heading for a work-type entry as `${emoji} ${label}`.
7
- *
8
- * Single-sourced here so callers (DEFAULT_WORK_TYPES derivation, devOnlySections
9
- * derivation, cliff-template drift comparison) cannot disagree on the composition rule.
10
- */
3
+ /** Composes the rendered section heading of a work-type entry as `${emoji} ${label}`. */
11
4
  export declare function composeHeader(entry: {
12
5
  emoji: string;
13
6
  label: string;
14
7
  }): string;
15
- /**
16
- * Default work types ordered by canonical priority.
17
- *
18
- * Derived from `work-types.json` (the canonical SSOT, mirrored at runtime by
19
- * `workTypesData.ts`). To change the taxonomy, edit `work-types.json` and update
20
- * `workTypesData.ts` to match — a drift test enforces lockstep.
21
- */
8
+ /** Default work types, ordered by canonical priority. */
22
9
  export declare const DEFAULT_WORK_TYPES: Record<string, WorkTypeConfig>;
23
- /**
24
- * Per-canonical-type breaking-policy lookup derived from the canonical taxonomy.
25
- *
26
- * Pass this to `parseCommitMessage` (or an equivalent caller) so the parser knows which
27
- * types tolerate `!` and which trigger a policy-violation warning. Missing entries default
28
- * to `'optional'` to preserve back-compat for consumers that supply custom work-types.
29
- */
30
- export declare const DEFAULT_BREAKING_POLICIES: Record<string, 'forbidden' | 'optional' | 'required'>;
10
+ /** The breaking policy of each canonical work type, which decides whether a `!` on it is a policy violation. */
11
+ export declare const DEFAULT_BREAKING_POLICIES: Record<string, BreakingPolicy>;
31
12
  /** Default version bump patterns. */
32
13
  export declare const DEFAULT_VERSION_PATTERNS: VersionPatterns;
33
14
  /** Default configuration for structured changelog JSON generation. */
@@ -1,25 +1,27 @@
1
- import { WORK_TYPES_DATA } from "./workTypesData.js";
2
- export { WORK_TYPES_DATA } from "./workTypesData.js";
1
+ import { CANONICAL_TAXONOMY } from '@williamthorsen/change-grammar';
3
2
  export function composeHeader(entry) {
4
3
  return `${entry.emoji} ${entry.label}`;
5
4
  }
6
5
  const DEV_ONLY_TIERS = new Set(['internal', 'process']);
7
6
  function deriveDefaultWorkTypes() {
8
7
  const result = {};
9
- for (const entry of WORK_TYPES_DATA.types) {
8
+ for (const entry of CANONICAL_TAXONOMY.types) {
10
9
  const config = {
11
10
  header: composeHeader(entry),
12
11
  };
13
12
  if (entry.aliases.length > 0) {
14
13
  config.aliases = [...entry.aliases];
15
14
  }
15
+ if (entry.excludedFromChangelog === true) {
16
+ config.excludedFromChangelog = true;
17
+ }
16
18
  result[entry.key] = config;
17
19
  }
18
20
  return result;
19
21
  }
20
22
  function deriveDevOnlySections() {
21
23
  const sections = [];
22
- for (const entry of WORK_TYPES_DATA.types) {
24
+ for (const entry of CANONICAL_TAXONOMY.types) {
23
25
  if (!DEV_ONLY_TIERS.has(entry.tier)) {
24
26
  continue;
25
27
  }
@@ -31,7 +33,7 @@ function deriveDevOnlySections() {
31
33
  return sections;
32
34
  }
33
35
  export const DEFAULT_WORK_TYPES = deriveDefaultWorkTypes();
34
- export const DEFAULT_BREAKING_POLICIES = Object.fromEntries(WORK_TYPES_DATA.types.map((entry) => [entry.key, entry.breakingPolicy]));
36
+ export const DEFAULT_BREAKING_POLICIES = Object.fromEntries(CANONICAL_TAXONOMY.types.map((entry) => [entry.key, entry.breakingPolicy]));
35
37
  export const DEFAULT_VERSION_PATTERNS = {
36
38
  major: ['!'],
37
39
  minor: ['feat'],
@@ -1,7 +1,7 @@
1
1
  import type { ReleaseKitConfig } from './types.js';
2
2
  export type { LabelSpec, ReleaseKitConfig, RepoLabelsConfig } from './types.js';
3
3
  /**
4
- * Type-safe identity function for configuration files.
4
+ * Returns a configuration unchanged, typing it as a `ReleaseKitConfig`.
5
5
  *
6
6
  * Usage in `.config/release-kit.config.ts`:
7
7
  * ```ts
@@ -1,2 +1,2 @@
1
- /** Delete a file, silently ignoring the case where it does not exist. */
1
+ /** Deletes a file, ignoring one that does not exist. */
2
2
  export declare function deleteFileIfExists(filePath: string): void;
@@ -0,0 +1,20 @@
1
+ import type { ReleaseKitConfig, ReleaseNotesConfig } from './types.js';
2
+ export interface ResolvedReleaseNotesConfig {
3
+ releaseNotes: ReleaseNotesConfig;
4
+ changelogJsonOutputPath: string;
5
+ /** Section titles in priority order, derived from the merged workTypes record. */
6
+ sectionOrder: string[];
7
+ }
8
+ /**
9
+ * Returns the header of a merged `workTypes` record's `deps` entry, which titles the synthetic propagation section.
10
+ * Throws when the record lacks one, which `resolveWorkTypes` never produces.
11
+ */
12
+ export declare function deriveDependenciesHeader(workTypes: Record<string, {
13
+ header: string;
14
+ }>): string;
15
+ /** Derives the release-notes settings from a loaded config, falling back to the defaults for an absent one. */
16
+ export declare function deriveReleaseNotesConfig(config: ReleaseKitConfig | undefined): ResolvedReleaseNotesConfig;
17
+ /** Returns the section headers of a merged `workTypes` record, in declaration order. */
18
+ export declare function deriveSectionOrder(workTypes: Record<string, {
19
+ header: string;
20
+ }>): string[];
@@ -0,0 +1,21 @@
1
+ import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
2
+ import { resolveWorkTypes } from "./loadConfig.js";
3
+ export function deriveDependenciesHeader(workTypes) {
4
+ const header = workTypes['deps']?.header;
5
+ if (header === undefined) {
6
+ throw new Error("Resolved work types do not contain a 'deps' entry");
7
+ }
8
+ return header;
9
+ }
10
+ export function deriveReleaseNotesConfig(config) {
11
+ return {
12
+ releaseNotes: {
13
+ shouldInjectIntoReadme: config?.releaseNotes?.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme,
14
+ },
15
+ changelogJsonOutputPath: config?.changelogJson?.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
16
+ sectionOrder: deriveSectionOrder(resolveWorkTypes(config?.workTypes)),
17
+ };
18
+ }
19
+ export function deriveSectionOrder(workTypes) {
20
+ return Object.values(workTypes).map((entry) => entry.header);
21
+ }
@@ -2,10 +2,8 @@ import type { WorkspaceConfig } from './types.js';
2
2
  /**
3
3
  * Derives a workspace configuration from a workspace-relative path.
4
4
  *
5
- * Reads `package.json` at the workspace path to derive the tag identifier from the
6
- * package's `name` field (with any leading `@scope/` stripped). The `dir` field remains
7
- * the basename of the path — it is the stable internal identifier used for `--only`,
8
- * config overrides, and dependency-graph lookups. The `tagPrefix` is `${unscopedName}-v`,
9
- * so tags reflect the package identity rather than the directory layout.
5
+ * The tag prefix is `${unscopedName}-v`, from the `name` in the workspace's `package.json`, so that tags follow the
6
+ * package's identity rather than the directory layout. `dir` is the basename of the path: the stable internal
7
+ * identifier for `--only`, config overrides, and dependency-graph lookups.
10
8
  */
11
9
  export declare function deriveWorkspaceConfig(workspacePath: string): WorkspaceConfig;
@@ -1,8 +1,6 @@
1
1
  export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'yarn-berry';
2
2
  /**
3
- * Detect the repo's package manager by checking the `packageManager` field in root `package.json`,
4
- * then falling back to lockfile detection, then defaulting to `npm`.
5
- *
6
- * Yarn v2+ is returned as `'yarn-berry'` to distinguish it from Yarn Classic.
3
+ * Detects the repo's package manager from the `packageManager` field of the root `package.json`, then from lockfiles,
4
+ * and otherwise returns `npm`. Yarn v2+ is `'yarn-berry'`, to distinguish it from Yarn Classic.
7
5
  */
8
6
  export declare function detectPackageManager(): PackageManager;
@@ -14,12 +14,10 @@ export interface UndeclaredTagPrefix {
14
14
  suggestedDir: string;
15
15
  }
16
16
  /**
17
- * Scan local git tags for release-shaped tags whose prefix is not in the known set.
17
+ * Scans local git tags for release-shaped tags whose prefix is not in `knownPrefixes`, the union of derived and
18
+ * declared prefixes across all workspaces.
18
19
  *
19
- * Reads only the local tag list; callers expecting to see recently-fetched remote tags
20
- * should `git fetch --tags` first. Returns an empty array when the repo has no tags
21
- * or no candidate-shaped tags outside the known set.
22
- *
23
- * @param knownPrefixes - Union of derived and declared prefixes across all workspaces.
20
+ * Reads only the local tag list, so a remote tag that has not been fetched is not reported. Returns an empty array
21
+ * when the repo doesn't have any tags, or any candidate-shaped tags outside the known set.
24
22
  */
25
23
  export declare function detectUndeclaredTagPrefixes(knownPrefixes: readonly string[]): UndeclaredTagPrefix[];
@@ -42,7 +42,7 @@ export function detectUndeclaredTagPrefixes(knownPrefixes) {
42
42
  suggestedDir: stripTrailingTagMarker(prefix),
43
43
  });
44
44
  }
45
- return results.sort((a, b) => a.prefix.localeCompare(b.prefix));
45
+ return results.toSorted((a, b) => a.prefix.localeCompare(b.prefix));
46
46
  }
47
47
  function stripTrailingTagMarker(prefix) {
48
48
  return prefix.endsWith('-v') ? prefix.slice(0, -2) : prefix;
@@ -1,14 +1,14 @@
1
- import type { ParsedCommit, ReleaseType, VersionPatterns, WorkTypeConfig } from './types.js';
1
+ import type { ReleaseType, VersionPatterns, WorkTypeConfig } from './types.js';
2
+ /** The part of a changelog item or parsed commit that decides its bump: its resolved work type and breaking flag. */
3
+ export interface BumpSignal {
4
+ type: string;
5
+ breaking: boolean;
6
+ }
2
7
  /**
3
- * Determines the overall bump type from a set of parsed commits.
8
+ * Determines the overall bump type from a set of changes, returning undefined when none triggers a bump.
4
9
  *
5
- * Uses `versionPatterns` to decide which commit types trigger major/minor bumps.
6
- * The `'!'` sentinel in `versionPatterns.major` means "any breaking commit triggers major".
7
- * Any recognized commit type not listed in major or minor patterns defaults to patch.
8
- *
9
- * @param commits - The parsed commits to analyze.
10
- * @param workTypes - The work type configurations to verify recognized types.
11
- * @param versionPatterns - Rules for determining major and minor bumps.
12
- * @returns The determined release type, or undefined if no commits match a known work type.
10
+ * Uses `versionPatterns` to decide which types trigger major/minor bumps.
11
+ * The `'!'` sentinel in `versionPatterns.major` means "any breaking change triggers major".
12
+ * Any recognized type not listed in major or minor patterns defaults to patch.
13
13
  */
14
- export declare function determineBumpType(commits: readonly ParsedCommit[], workTypes: Record<string, WorkTypeConfig>, versionPatterns: VersionPatterns): ReleaseType | undefined;
14
+ export declare function determineBumpType(changes: readonly BumpSignal[], workTypes: Record<string, WorkTypeConfig>, versionPatterns: VersionPatterns): ReleaseType | undefined;
@@ -3,23 +3,22 @@ const RELEASE_PRIORITY = {
3
3
  minor: 2,
4
4
  patch: 1,
5
5
  };
6
- export function determineBumpType(commits, workTypes, versionPatterns) {
6
+ export function determineBumpType(changes, workTypes, versionPatterns) {
7
7
  const knownTypes = new Set(Object.keys(workTypes));
8
8
  let highestPriority = 0;
9
9
  let result;
10
- for (const commit of commits) {
11
- if (commit.breaking && versionPatterns.major.includes('!')) {
10
+ for (const change of changes) {
11
+ if (change.breaking && versionPatterns.major.includes('!')) {
12
12
  return 'major';
13
13
  }
14
- const commitType = commit.type;
15
- if (!knownTypes.has(commitType)) {
14
+ if (!knownTypes.has(change.type)) {
16
15
  continue;
17
16
  }
18
17
  let bump;
19
- if (versionPatterns.major.includes(commitType)) {
18
+ if (versionPatterns.major.includes(change.type)) {
20
19
  bump = 'major';
21
20
  }
22
- else if (versionPatterns.minor.includes(commitType)) {
21
+ else if (versionPatterns.minor.includes(change.type)) {
23
22
  bump = 'minor';
24
23
  }
25
24
  else {