@williamthorsen/release-kit 12.0.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.meta/changelog.json +212 -0
  2. package/.readyup/kits/default.js +18 -17
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +58 -0
  6. package/README.md +55 -48
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertCleanWorkingTree.d.ts +1 -5
  9. package/dist/esm/assertTaggedBaseline.d.ts +29 -0
  10. package/dist/esm/assertTaggedBaseline.js +41 -0
  11. package/dist/esm/attachChangelogDiagnostics.d.ts +1 -1
  12. package/dist/esm/attachChangelogDiagnostics.js +3 -0
  13. package/dist/esm/bin/release-kit.js +85 -22
  14. package/dist/esm/buildChangelogEntries.d.ts +23 -13
  15. package/dist/esm/buildChangelogEntries.js +36 -17
  16. package/dist/esm/buildDependencyGraph.d.ts +6 -6
  17. package/dist/esm/buildEmptyReleaseEntry.d.ts +1 -8
  18. package/dist/esm/buildReleaseSummary.d.ts +2 -8
  19. package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
  20. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  21. package/dist/esm/bumpVersion.d.ts +1 -8
  22. package/dist/esm/changelogJsonFile.d.ts +5 -7
  23. package/dist/esm/changelogJsonFile.js +2 -17
  24. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  25. package/dist/esm/changelogOverrides.d.ts +59 -98
  26. package/dist/esm/classifyChangelogCommit.d.ts +7 -9
  27. package/dist/esm/classifyChangelogCommit.js +3 -2
  28. package/dist/esm/commitCommand.d.ts +2 -4
  29. package/dist/esm/compareVersions.d.ts +10 -4
  30. package/dist/esm/compareVersions.js +16 -0
  31. package/dist/esm/configFlagSchema.d.ts +1 -1
  32. package/dist/esm/createGithubRelease.d.ts +7 -19
  33. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  34. package/dist/esm/createGithubReleaseCommand.js +9 -9
  35. package/dist/esm/createTags.d.ts +2 -3
  36. package/dist/esm/decideRelease.d.ts +2 -6
  37. package/dist/esm/defaults.d.ts +5 -24
  38. package/dist/esm/defaults.js +4 -5
  39. package/dist/esm/defineConfig.d.ts +1 -1
  40. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  41. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  42. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  43. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  44. package/dist/esm/detectPackageManager.d.ts +2 -4
  45. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  46. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  47. package/dist/esm/determineBumpType.d.ts +1 -1
  48. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  49. package/dist/esm/discoverWorkspaces.js +12 -12
  50. package/dist/esm/enterRepoRoot.d.ts +12 -0
  51. package/dist/esm/enterRepoRoot.js +16 -0
  52. package/dist/esm/enumerateReleaseWindows.d.ts +8 -8
  53. package/dist/esm/extractMigration.d.ts +6 -8
  54. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  55. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  56. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  57. package/dist/esm/formatExcludedSkip.js +3 -0
  58. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  59. package/dist/esm/generateChangelogs.d.ts +10 -5
  60. package/dist/esm/glyphs.d.ts +1 -1
  61. package/dist/esm/help/prepareHelp.d.ts +3 -4
  62. package/dist/esm/help/prepareHelp.js +4 -3
  63. package/dist/esm/init/checks.d.ts +3 -3
  64. package/dist/esm/init/checks.js +4 -1
  65. package/dist/esm/init/detectRepoType.d.ts +2 -3
  66. package/dist/esm/init/hasProvenance.d.ts +2 -0
  67. package/dist/esm/init/hasProvenance.js +3 -0
  68. package/dist/esm/init/initCommand.d.ts +3 -6
  69. package/dist/esm/init/initCommand.js +10 -10
  70. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  71. package/dist/esm/init/scaffold.d.ts +1 -1
  72. package/dist/esm/init/templates.d.ts +9 -11
  73. package/dist/esm/init/templates.js +12 -11
  74. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  75. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  76. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  77. package/dist/esm/injectSection.d.ts +3 -3
  78. package/dist/esm/loadConfig.d.ts +16 -14
  79. package/dist/esm/loadConfig.js +2 -2
  80. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  81. package/dist/esm/loadValidatedConfig.js +6 -1
  82. package/dist/esm/packsPath.d.ts +9 -9
  83. package/dist/esm/parseChangeRecordBlock.d.ts +2 -2
  84. package/dist/esm/parseCommitMessage.d.ts +17 -32
  85. package/dist/esm/parseCommitMessage.js +28 -37
  86. package/dist/esm/parseRequestedTags.d.ts +3 -3
  87. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  88. package/dist/esm/planVersionBump.d.ts +5 -3
  89. package/dist/esm/planVersionBump.js +3 -0
  90. package/dist/esm/prepareCommand.d.ts +4 -7
  91. package/dist/esm/prepareCommand.js +13 -6
  92. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  93. package/dist/esm/propagateBumps.d.ts +6 -7
  94. package/dist/esm/publish.d.ts +1 -1
  95. package/dist/esm/publishCommand.d.ts +4 -3
  96. package/dist/esm/publishCommand.js +8 -8
  97. package/dist/esm/pushCommand.d.ts +4 -3
  98. package/dist/esm/pushCommand.js +8 -2
  99. package/dist/esm/pushRelease.d.ts +2 -2
  100. package/dist/esm/readChangelogSections.d.ts +33 -0
  101. package/dist/esm/readChangelogSections.js +80 -0
  102. package/dist/esm/releaseFiles.d.ts +5 -7
  103. package/dist/esm/releasePlan.d.ts +11 -13
  104. package/dist/esm/releasePrepare.d.ts +23 -22
  105. package/dist/esm/releasePrepare.js +31 -11
  106. package/dist/esm/releasePrepareMono.d.ts +9 -7
  107. package/dist/esm/releasePrepareMono.js +85 -33
  108. package/dist/esm/releasePrepareProject.d.ts +19 -29
  109. package/dist/esm/releasePrepareProject.js +31 -12
  110. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  111. package/dist/esm/renderChangelogMarkdown.js +27 -2
  112. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  113. package/dist/esm/renderReleaseNotes.js +2 -6
  114. package/dist/esm/reportPrepare.d.ts +4 -5
  115. package/dist/esm/reportPrepare.js +33 -19
  116. package/dist/esm/resolveCommandTags.d.ts +15 -11
  117. package/dist/esm/resolveCommandTags.js +35 -5
  118. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  119. package/dist/esm/resolveConfigFlag.js +4 -0
  120. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  121. package/dist/esm/resolveReleaseTags.js +1 -1
  122. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  123. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  124. package/dist/esm/showTagPrefixesCommand.js +4 -4
  125. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  126. package/dist/esm/stripScope.d.ts +2 -2
  127. package/dist/esm/stripScope.js +3 -3
  128. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  129. package/dist/esm/sync-labels/generateCommand.js +11 -8
  130. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  131. package/dist/esm/sync-labels/initCommand.js +2 -2
  132. package/dist/esm/sync-labels/presets.d.ts +5 -4
  133. package/dist/esm/sync-labels/presets.js +9 -9
  134. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  135. package/dist/esm/sync-labels/resolveLabels.js +1 -1
  136. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  137. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  138. package/dist/esm/sync-labels/templates.d.ts +3 -3
  139. package/dist/esm/sync-labels/templates.js +3 -3
  140. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  141. package/dist/esm/sync-labels/typeLabels.js +38 -0
  142. package/dist/esm/sync-labels/types.d.ts +1 -1
  143. package/dist/esm/tagCommand.d.ts +1 -1
  144. package/dist/esm/typeGuards.d.ts +2 -2
  145. package/dist/esm/types.d.ts +116 -91
  146. package/dist/esm/types.js +1 -1
  147. package/dist/esm/validateConfig.d.ts +2 -11
  148. package/dist/esm/validateConfig.js +2 -2
  149. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  150. package/dist/esm/validateOverridesCommand.d.ts +17 -20
  151. package/dist/esm/validateOverridesCommand.js +1 -1
  152. package/docs/api.md +6 -6
  153. package/docs/changelogs.md +63 -40
  154. package/docs/configuration.md +29 -27
  155. package/docs/editorial-overrides.md +23 -23
  156. package/docs/labels.md +21 -19
  157. package/docs/project-releases.md +13 -13
  158. package/docs/publishing.md +5 -5
  159. package/docs/readiness-checks.md +3 -3
  160. package/docs/releasing.md +50 -25
  161. package/docs/work-types.md +41 -57
  162. package/package.json +7 -6
  163. package/presets/labels/common.yaml +1 -41
  164. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  165. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  166. package/dist/esm/workTypesData.d.ts +0 -50
  167. package/dist/esm/workTypesData.js +0 -145
  168. package/schemas/label-map.json +0 -24
@@ -39,7 +39,7 @@ export default defineConfig({
39
39
  });
40
40
  `;
41
41
  }
42
- export function publishWorkflow(repoType) {
42
+ export function publishWorkflow(repoType, { provenance = true } = {}) {
43
43
  const tagPattern = repoType === 'monorepo' ? "'*-v[0-9]*.[0-9]*.[0-9]*'" : "'v[0-9]*.[0-9]*.[0-9]*'";
44
44
  return `# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
45
45
  name: Publish
@@ -57,8 +57,7 @@ jobs:
57
57
  publish:
58
58
  uses: williamthorsen/node-monorepo-tools/.github/workflows/publish.reusable.yaml@workflow/publish-v1
59
59
  with:
60
- provenance: true
61
- tags: \${{ github.ref_name }}
60
+ ${provenance ? ' provenance: true\n' : ''} tags: \${{ github.ref_name }}
62
61
  `;
63
62
  }
64
63
  export function createGithubReleaseWorkflow(repoType) {
@@ -94,16 +93,17 @@ on:
94
93
  required: false
95
94
  type: string
96
95
  bump:
97
- description: 'Override version bump type (leave empty to auto-detect from commits)'
96
+ description: 'Version bump type (auto detects it from commits)'
98
97
  required: false
99
98
  type: choice
99
+ default: auto
100
100
  options:
101
- - ''
101
+ - auto
102
102
  - patch
103
103
  - minor
104
104
  - major
105
105
  force:
106
- description: 'Force a release even when no commits or no bump-worthy commits exist (defaults to patch; combine with --bump for a different level)'
106
+ description: 'Force a release even when there are not any commits, or any bump-worthy commits, since the last release (defaults to patch; combine with --bump for a different level)'
107
107
  required: false
108
108
  type: boolean
109
109
  default: false
@@ -117,7 +117,7 @@ jobs:
117
117
  uses: williamthorsen/node-monorepo-tools/.github/workflows/release.reusable.yaml@workflow/release-v1
118
118
  with:
119
119
  only: \${{ inputs.only }}
120
- bump: \${{ inputs.bump }}
120
+ bump: \${{ inputs.bump != 'auto' && inputs.bump || '' }}
121
121
  force: \${{ inputs.force }}
122
122
  `;
123
123
  }
@@ -128,16 +128,17 @@ on:
128
128
  workflow_dispatch:
129
129
  inputs:
130
130
  bump:
131
- description: 'Override version bump type (leave empty to auto-detect from commits)'
131
+ description: 'Version bump type (auto detects it from commits)'
132
132
  required: false
133
133
  type: choice
134
+ default: auto
134
135
  options:
135
- - ''
136
+ - auto
136
137
  - patch
137
138
  - minor
138
139
  - major
139
140
  force:
140
- description: 'Force a release even when no commits or no bump-worthy commits exist (defaults to patch; combine with --bump for a different level)'
141
+ description: 'Force a release even when there are not any commits, or any bump-worthy commits, since the last release (defaults to patch; combine with --bump for a different level)'
141
142
  required: false
142
143
  type: boolean
143
144
  default: false
@@ -150,7 +151,7 @@ jobs:
150
151
  release:
151
152
  uses: williamthorsen/node-monorepo-tools/.github/workflows/release.reusable.yaml@workflow/release-v1
152
153
  with:
153
- bump: \${{ inputs.bump }}
154
+ bump: \${{ inputs.bump != 'auto' && inputs.bump || '' }}
154
155
  force: \${{ inputs.force }}
155
156
  `;
156
157
  }
@@ -0,0 +1,13 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
2
+ interface UpdateTemplatesOptions {
3
+ dryRun: boolean;
4
+ styles: StreamStyles;
5
+ }
6
+ /**
7
+ * Runs the `release-kit update-templates` command and returns the process exit code.
8
+ *
9
+ * Brings each workflow that release-kit scaffolds up to date with the installed templates. The opt-in
10
+ * `sync-labels.yaml` is refreshed only when it exists, and config files are never read or written.
11
+ */
12
+ export declare function updateTemplatesCommand({ dryRun, styles }: UpdateTemplatesOptions): number;
13
+ export {};
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { printError, printStep, printSuccess, reportTemplateUpdate, updateManagedFile, } from '@williamthorsen/nmr-core';
3
+ import { describeError } from '@williamthorsen/toolbelt.errors';
4
+ import { syncLabelsWorkflow } from "../sync-labels/templates.js";
5
+ import { detectRepoType } from "./detectRepoType.js";
6
+ import { hasProvenance } from "./hasProvenance.js";
7
+ import { checkEligibility } from "./initCommand.js";
8
+ import { createGithubReleaseWorkflow, publishWorkflow, releaseWorkflow } from "./templates.js";
9
+ const PUBLISH_WORKFLOW_PATH = '.github/workflows/publish.yaml';
10
+ const SYNC_LABELS_WORKFLOW_PATH = '.github/workflows/sync-labels.yaml';
11
+ export function updateTemplatesCommand({ dryRun, styles }) {
12
+ if (dryRun) {
13
+ console.info('[dry-run mode]');
14
+ }
15
+ let eligible;
16
+ try {
17
+ eligible = checkEligibility(styles);
18
+ }
19
+ catch (error) {
20
+ printError(`Eligibility check failed: ${describeError(error)}`, styles.stderr);
21
+ return 1;
22
+ }
23
+ if (!eligible)
24
+ return 1;
25
+ printStep('Detecting repo type');
26
+ let repoType;
27
+ try {
28
+ repoType = detectRepoType();
29
+ }
30
+ catch (error) {
31
+ printError(`Failed to detect repo type: ${describeError(error)}`, styles.stderr);
32
+ return 1;
33
+ }
34
+ printSuccess(`Detected: ${repoType}`, styles.stdout);
35
+ printStep('Updating workflows');
36
+ let results;
37
+ try {
38
+ results = buildManagedFiles(repoType).map(([filePath, content]) => updateManagedFile(filePath, content, { dryRun }));
39
+ }
40
+ catch (error) {
41
+ printError(`Failed to render workflows: ${describeError(error)}`, styles.stderr);
42
+ return 1;
43
+ }
44
+ for (const result of results) {
45
+ reportTemplateUpdate(result, dryRun, styles);
46
+ }
47
+ return results.some((result) => result.outcome === 'failed') ? 1 : 0;
48
+ }
49
+ function buildManagedFiles(repoType) {
50
+ const files = [
51
+ ['.github/workflows/create-github-release.yaml', createGithubReleaseWorkflow(repoType)],
52
+ [PUBLISH_WORKFLOW_PATH, publishWorkflow(repoType, { provenance: readProvenanceSetting() })],
53
+ ['.github/workflows/release.yaml', releaseWorkflow(repoType)],
54
+ ];
55
+ if (existsSync(SYNC_LABELS_WORKFLOW_PATH)) {
56
+ files.push([SYNC_LABELS_WORKFLOW_PATH, syncLabelsWorkflow()]);
57
+ }
58
+ return files;
59
+ }
60
+ function readProvenanceSetting() {
61
+ if (!existsSync(PUBLISH_WORKFLOW_PATH))
62
+ return true;
63
+ return hasProvenance(readFileSync(PUBLISH_WORKFLOW_PATH, 'utf8'));
64
+ }
@@ -3,15 +3,11 @@ import type { ChangelogEntry } from './types.js';
3
3
  export interface RenderedInjectedReadme {
4
4
  /** The README with the release-notes section injected at the marker position. */
5
5
  injectedReadme: string;
6
- /**
7
- * The standalone release-notes markdown for the target version (trimmed), prefixed with a
8
- * labeled `## Release notes — v{version} ({date})` heading so the file is self-identifying.
9
- * The GitHub-release body is rendered separately (without this heading) by `createGithubRelease`.
10
- */
6
+ /** The standalone release-notes markdown for the target version, trimmed and led by a `## Release notes` heading. */
11
7
  releaseNotesMarkdown: string;
12
8
  }
13
9
  /**
14
- * Discriminated reason no release notes were rendered for a tag.
10
+ * Discriminated reason why release notes were not rendered for a tag.
15
11
  *
16
12
  * Shares the vocabulary of `CreateReleaseSkipReason` in `createGithubRelease.ts`, which classifies
17
13
  * the same two conditions on the GitHub-release path.
@@ -30,11 +26,11 @@ export type RenderInjectedReadmeResult = {
30
26
  * Renders a README with release notes injected at the marker position, and the standalone
31
27
  * release-notes markdown, from an already-loaded README string and an in-memory entry set.
32
28
  *
33
- * This is the pure rendering core: it reads nothing, writes nothing, and reports nothing, so a
34
- * caller holding entries a release has computed but not yet written gets the same result as one
35
- * rendering from a saved file, and each caller words a skip for its own output.
29
+ * This is the pure rendering core: It reads nothing, writes nothing, and reports nothing, so a
30
+ * caller that has entries that a release has computed but not yet written gets the same result as
31
+ * one rendering from a saved file, and each caller words a skip for its own output.
36
32
  *
37
- * Carries the version on a skip because both callers name it in their message and only this
33
+ * Returns the version on a skip because both callers name it in their message and only this
38
34
  * function has derived it from the tag.
39
35
  */
40
36
  export declare function renderInjectedReadmeFromEntries(readme: string, entries: readonly ChangelogEntry[], tag: string, sectionOrder?: string[]): RenderInjectedReadmeResult;
@@ -42,16 +38,15 @@ export declare function renderInjectedReadmeFromEntries(readme: string, entries:
42
38
  * Path-taking wrapper over {@link renderInjectedReadmeFromEntries} for callers rendering from a
43
39
  * saved `changelog.json`, such as the publish-time injection flow.
44
40
  *
45
- * Adds the two skip conditions that only a file can present: the changelog is missing, or it does
46
- * not parse. Warns on every skip, its own and the renderer's alike, because the publish path has
47
- * no other channel for the message.
41
+ * Adds the two skip conditions that only a file can present: The changelog is missing, or it does
42
+ * not parse. Warns on every skip, its own and the renderer's alike, because the publish path does
43
+ * not have any other channel for the message.
48
44
  */
49
45
  export declare function renderInjectedReadme(readme: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): RenderedInjectedReadme | undefined;
50
46
  /**
51
- * Inject release notes into a README and return the original content for restoration.
52
- *
53
- * Returns the original README content, or `undefined` if injection was skipped.
47
+ * Injects release notes into a README file, returning the original content for restoration, or `undefined` when
48
+ * injection was skipped.
54
49
  */
55
50
  export declare function injectReleaseNotesIntoReadme(readmePath: string, changelogJsonPath: string, tag: string, sectionOrder?: string[]): string | undefined;
56
- /** Find the README file in a workspace directory. */
51
+ /** Returns the path of a workspace's `README.md`, or `undefined` when it has none. */
57
52
  export declare function resolveReadmePath(workspacePath: string): string | undefined;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Replace or insert a delimited section in file content.
2
+ * Replaces or inserts a delimited section in file content.
3
3
  *
4
- * When markers exist, replaces the content between them. When absent, prepends the section
5
- * (with markers) at the top of the content. The function is pure — no file I/O.
4
+ * When both markers exist in order, replaces the content between them. Otherwise, prepends the
5
+ * section (with markers) at the top of the content.
6
6
  */
7
7
  export declare function injectSection(content: string, key: string, injection: string): string;
@@ -2,11 +2,12 @@ import type { MonorepoReleaseConfig, ReleaseConfig, ReleaseKitConfig, WorkTypeCo
2
2
  /** Path of the root `package.json` consulted when validating a `project` block. */
3
3
  export declare const ROOT_PACKAGE_JSON_PATH = "package.json";
4
4
  /**
5
- * Read the root `package.json` and return its `version` field.
5
+ * Reads the root `package.json` and returns its `version` field.
6
6
  *
7
7
  * Returns `{ exists: false }` when the file is missing, `{ exists: true, version: undefined }`
8
- * when the file exists but has no `version` field, and `{ exists: true, version }` when both
9
- * are present. The caller (`mergeMonorepoConfig`) decides whether the situation is an error.
8
+ * when the file exists but does not have a `version` field, and `{ exists: true, version }`
9
+ * when both are present; `mergeMonorepoConfig` decides whether the situation is an error.
10
+ * Throws when the file cannot be read or parsed.
10
11
  */
11
12
  export declare function readRootPackageVersion(): {
12
13
  exists: boolean;
@@ -17,16 +18,17 @@ export declare const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
17
18
  /**
18
19
  * Loads the config file, returning the raw config object.
19
20
  *
20
- * `configPath` is resolved against the working directory and defaults to `CONFIG_FILE_PATH`. An absent default
21
- * path returns `undefined`, because a repo that declares no config is a supported state; an absent named path
21
+ * `configPath` is resolved against the working directory, which the CLI sets to the repo root, and defaults to
22
+ * `CONFIG_FILE_PATH`. The CLI resolves a `--config` value to an absolute path before passing it. An absent default
23
+ * path returns `undefined`, because a repo that does not declare a config is a supported state; an absent named path
22
24
  * throws, because the caller asked for that file by name. Also throws when the file exists but cannot be
23
25
  * imported, or exports neither a default nor a named `config`.
24
26
  */
25
27
  export declare function loadConfig(configPath?: string): Promise<unknown>;
26
28
  /**
27
29
  * Information about the root `package.json` passed into `mergeMonorepoConfig` when a
28
- * `project` block is configured. The async I/O lives in `readRootPackageVersion`; this
29
- * function stays a pure transformation.
30
+ * `project` block is configured. `readRootPackageVersion` performs the read, so that
31
+ * `mergeMonorepoConfig` does not perform any I/O.
30
32
  */
31
33
  export interface RootPackageInfo {
32
34
  exists: boolean;
@@ -36,12 +38,12 @@ export interface RootPackageInfo {
36
38
  * Resolves a final monorepo config from discovered workspaces and an optional user config overlay.
37
39
  *
38
40
  * Merging rules:
39
- * - `workspaces`: match overlay entries by `dir` against discovered list; `shouldExclude: true`
41
+ * - `workspaces`: Match overlay entries by `dir` against discovered list; `shouldExclude: true`
40
42
  * removes the workspace; unlisted packages keep defaults.
41
- * - `workTypes`: shallow merge — consumer entries override or add to defaults by key.
42
- * - `versionPatterns`: consumer value replaces defaults entirely.
43
- * - `formatCommand`, `scopeAliases`: consumer value wins.
44
- * - `project`: present iff `userConfig.project` is declared. Resolves `tagPrefix` to
43
+ * - `workTypes`: Shallow merge; consumer entries override or add to defaults by key.
44
+ * - `versionPatterns`: Consumer value replaces defaults entirely.
45
+ * - Pass-through fields (see `applyOptionalPassthroughFields`): Consumer value wins.
46
+ * - `project`: Present iff `userConfig.project` is declared. Resolves `tagPrefix` to
45
47
  * `DEFAULT_PROJECT_TAG_PREFIX` and `paths` to the union of the retained workspaces' `paths`
46
48
  * when omitted. Requires `rootPackage` to be passed and to contain a valid `version` field;
47
49
  * throws otherwise. The resolved prefix is included in the strict-prefix collision check
@@ -51,13 +53,13 @@ export declare function mergeMonorepoConfig(discoveredPaths: string[], userConfi
51
53
  /**
52
54
  * Resolves a final single-package config from an optional user config overlay.
53
55
  *
54
- * Rejects a configured `project` block: project-level releases are a monorepo-only feature,
56
+ * Rejects a configured `project` block: Project-level releases are a monorepo-only feature,
55
57
  * since the implicit "all non-excluded workspaces contribute" rule is meaningless in a
56
58
  * single-package repo.
57
59
  */
58
60
  export declare function mergeSinglePackageConfig(userConfig: ReleaseKitConfig | undefined): ReleaseConfig;
59
61
  /**
60
- * Merge consumer work-type overrides onto `DEFAULT_WORK_TYPES`.
62
+ * Merges consumer work-type overrides onto `DEFAULT_WORK_TYPES`.
61
63
  *
62
64
  * Preserves the declaration order of defaults; net-new consumer keys append at the end.
63
65
  */
@@ -169,7 +169,7 @@ function resolveProjectConfig(userProject, rootPackage, workspaces) {
169
169
  throw new Error(`project block requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
170
170
  }
171
171
  if (rootPackage.version === undefined) {
172
- throw new Error(`project block requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to your root package.json`);
172
+ throw new Error(`project block requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to the root package.json`);
173
173
  }
174
174
  return {
175
175
  paths: userProject.paths ?? workspaces.flatMap((workspace) => workspace.paths),
@@ -212,7 +212,7 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
212
212
  if (isPrefixCollision(a.prefix, b.prefix)) {
213
213
  throw new Error(`Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). ` +
214
214
  'One prefix is identical to or a strict prefix of the other; ' +
215
- 'a tag under the longer prefix can then match the shorter one, whose releases would absorb it.');
215
+ 'a tag under the longer prefix can then match the shorter one and be counted among its releases.');
216
216
  }
217
217
  }
218
218
  }
@@ -1,7 +1,7 @@
1
1
  import { type OutputStyle } from '@williamthorsen/nmr-core';
2
2
  import type { ReleaseKitConfig } from './types.js';
3
3
  /**
4
- * Why a config file could not be used: the import threw, or the contents failed the schema.
4
+ * Why a config file could not be used: The import threw, or the contents failed the schema.
5
5
  *
6
6
  * The distinction exists so that a caller can render the two differently. Every caller aborts on either.
7
7
  */
@@ -13,12 +13,12 @@ export type ConfigProblem = {
13
13
  errors: string[];
14
14
  };
15
15
  /**
16
- * Outcome of loading and validating the consumer config file. `missing` is not an error
17
- * here — whether an absent config file is one is the caller's call.
16
+ * Outcome of loading and validating the consumer config file. `missing` is not an error here; the caller decides
17
+ * whether an absent config file is one.
18
18
  *
19
- * `configFilePath` names the file the run read: the caller's `configPath`, or `CONFIG_FILE_PATH` when none was
20
- * given. It stays relative to the working directory, so a message or a generated artifact that splices it reads
21
- * the same on every machine.
19
+ * `configFilePath` names the file that the run read: the caller's `configPath`, or `CONFIG_FILE_PATH` when none was
20
+ * given. The default stays relative to the working directory, so a message or a generated artifact that includes it
21
+ * reads the same on every machine.
22
22
  */
23
23
  export type LoadValidatedConfigResult = {
24
24
  status: 'invalid';
@@ -36,25 +36,22 @@ export type LoadValidatedConfigResult = {
36
36
  /**
37
37
  * Loads the consumer config file and validates it against the shared schema, returning the outcome.
38
38
  *
39
- * Writes nothing to any stream: the caller picks the sink, which is what lets a command that must stay silent
40
- * reuse this loader. `reportConfigProblem` and `reportConfigWarnings` render the stderr form.
39
+ * Writes nothing to any stream: The caller picks the sink, which lets a command that must stay silent reuse this
40
+ * loader. `reportConfigProblem` and `reportConfigWarnings` render the stderr form.
41
41
  *
42
- * `configPath` defaults to `CONFIG_FILE_PATH`. A named path that does not exist is a load failure, so it reports
43
- * as `invalid` rather than `missing`.
42
+ * `configPath` defaults to `CONFIG_FILE_PATH`. A named path that does not exist is a load failure, so this function
43
+ * reports it as `invalid` rather than `missing`.
44
44
  */
45
45
  export declare function loadValidatedConfig(configPath?: string): Promise<LoadValidatedConfigResult>;
46
46
  /**
47
- * Loads and validates the config, reporting the problem and exiting 1 when it is unusable, so that a command
48
- * whose later work may never reach a loader still fails on a config that does.
47
+ * Loads and validates the config for a CLI command, returning it, or `undefined` when the default config file does
48
+ * not exist.
49
49
  *
50
- * An absent default config returns cleanly, which is what keeps a repo that declares no config a supported state.
51
- * Reports problems but never warnings: the command's own later load stays the single place that emits those, so
52
- * a warning is not printed twice.
53
- *
54
- * The second load costs nothing: `import()` caches by URL and `validateConfig` is pure.
50
+ * Reports an unusable config to stderr and exits 1, whether the file failed to load or failed validation, and
51
+ * whether it was named by the caller or the default one. Validation warnings print to stderr.
55
52
  */
56
- export declare function assertConfigUsable(stderrStyle: OutputStyle, configPath?: string): Promise<void>;
57
- /** Writes a config problem to stderr in the form every CLI command uses. */
53
+ export declare function loadUsableConfig(stderrStyle: OutputStyle, configPath?: string): Promise<ReleaseKitConfig | undefined>;
54
+ /** Writes a config problem to stderr in the form used by every CLI command. */
58
55
  export declare function reportConfigProblem(problem: ConfigProblem, stderrStyle: OutputStyle): void;
59
56
  /** Writes validation warnings to stderr as indented status lines. */
60
57
  export declare function reportConfigWarnings(warnings: readonly string[], stderrStyle: OutputStyle): void;
@@ -20,12 +20,17 @@ export async function loadValidatedConfig(configPath) {
20
20
  }
21
21
  return { status: 'ok', config, configFilePath, warnings };
22
22
  }
23
- export async function assertConfigUsable(stderrStyle, configPath) {
23
+ export async function loadUsableConfig(stderrStyle, configPath) {
24
24
  const result = await loadValidatedConfig(configPath);
25
25
  if (result.status === 'invalid') {
26
26
  reportConfigProblem(result.problem, stderrStyle);
27
27
  process.exit(1);
28
28
  }
29
+ if (result.status === 'missing') {
30
+ return undefined;
31
+ }
32
+ reportConfigWarnings(result.warnings, stderrStyle);
33
+ return result.config;
29
34
  }
30
35
  export function reportConfigProblem(problem, stderrStyle) {
31
36
  if (problem.kind === 'load') {
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Reports whether npm would place `path` in the published tarball, given a package.json `files` value.
3
3
  *
4
- * An absent or malformed `files` value packs everything, so it reports `true`; an empty array packs nothing
5
- * beyond the files npm always includes, so it reports `false`. An entry naming an ancestor directory covers
6
- * everything beneath it, which is how `files: ["dist/*"]` ships `dist/esm/index.js`, so every ancestor prefix
7
- * of `path` is tested rather than `path` alone.
4
+ * npm packs everything when the `files` value is absent or malformed, and `packsPath` reports `true`; for an empty
5
+ * array, npm packs only the files that it always includes, and `packsPath` reports `false`. An entry naming an
6
+ * ancestor directory covers everything beneath it, which is how `files: ["dist/*"]` includes `dist/esm/index.js`, so
7
+ * `packsPath` tests every ancestor prefix of `path` rather than `path` alone.
8
8
  *
9
- * Two approximations of npm's packing rules remain, both failing toward a silent pass: a negation entry
10
- * (`!dist/*.map`) never counts toward inclusion, because gitignore-style ordered negation is not modeled; and a
11
- * repo controlling its tarball through `.npmignore` alone declares no `files` field, so it reports `true`
12
- * without the ignore file being read. For a check that warns about an omission, a missed report costs less than
13
- * a false one.
9
+ * Two approximations of npm's packing rules remain, both failing toward a silent pass: A negation entry
10
+ * (`!dist/*.map`) never counts toward inclusion, because gitignore-style ordered negation is not modeled; and for a
11
+ * repo controlling its tarball through `.npmignore` alone, which does not declare a `files` field, `packsPath`
12
+ * reports `true` without reading the ignore file. For a check that warns about an omission, a missed report does
13
+ * less harm than a false one.
14
14
  */
15
15
  export declare function packsPath(filesField: unknown, path: string): boolean;
@@ -2,9 +2,9 @@
2
2
  * Reads the last `change-record` block in a commit message into the change entries that it records.
3
3
  *
4
4
  * The block's grammar is codeassembly's `change-record.md`: a fence whose info string is exactly `change-record`,
5
- * containing a YAML mapping. A merge commit's block carries `entries` plus the top-level `pr_number` and `ticket_ref`.
5
+ * containing a YAML mapping. A merge commit's block contains `entries` plus the top-level `pr_number` and `ticket_ref`.
6
6
  * A key that the grammar does not declare is ignored, and a declared key whose value is null reads as absent. Any
7
- * defect makes the whole block malformed; no entry is salvaged from a defective list.
7
+ * defect makes the whole block malformed; `parseChangeRecordBlock` does not salvage any entry from a defective list.
8
8
  */
9
9
  export declare function parseChangeRecordBlock(message: string): ChangeRecordBlockReading;
10
10
  /**
@@ -1,14 +1,5 @@
1
+ import { type BreakingPolicy, type ChangeRecord } from '@williamthorsen/change-grammar';
1
2
  import type { Commit, ParsedCommit, WorkTypeConfig } from './types.js';
2
- /** Regex patterns stripped from the start of commit messages before parsing. */
3
- export declare const COMMIT_PREPROCESSOR_PATTERNS: readonly RegExp[];
4
- /**
5
- * Regex source for the pipe-prefixed scope atom (the `web` in `web|feat: ...`).
6
- *
7
- * Deliberately permissive: `*` is the sanctioned structural scope for changes spanning every
8
- * workspace, so the atom admits any run of non-pipe characters rather than word characters.
9
- * Exported because `buildChangelogEntries.ts` builds its breaking-marker pattern from the same atom.
10
- */
11
- export declare const PIPE_SCOPE_SOURCE = "[^|]+";
12
3
  /**
13
4
  * Surface where a breaking-policy violation was detected: a subject's `!`, a `BREAKING CHANGE:` footer, or a
14
5
  * change-record entry's `breaking`.
@@ -17,40 +8,34 @@ export type PolicyViolationSurface = 'prefix' | 'body' | 'entry';
17
8
  /**
18
9
  * Callback invoked when `parseCommitMessage` detects a `!`-policy violation.
19
10
  *
20
- * The parser warns and continues on a violation, so legacy log entries don't block releases.
21
- * `readReleaseHistory` collects the invocations for the unreleased window, and the release
22
- * report lists them.
11
+ * Parsing continues after a violation so that an older commit in the log does not block a release.
23
12
  */
24
13
  export type PolicyViolationHandler = (commit: Commit, type: string, surface: PolicyViolationSurface) => void;
25
14
  /** Optional configuration for `parseCommitMessage` beyond the core inputs. */
26
15
  export interface ParseCommitMessageOptions {
27
- /**
28
- * Per-type breaking-policy lookup keyed by canonical type name. Missing entries are
29
- * treated as `'optional'` for backward compatibility with consumers that have not
30
- * supplied policies.
31
- */
32
- breakingPolicies?: Record<string, 'forbidden' | 'optional' | 'required'>;
16
+ /** Per-type breaking-policy lookup keyed by canonical type name; a type without an entry is `'optional'`. */
17
+ breakingPolicies?: Record<string, BreakingPolicy>;
33
18
  /** Receives policy-violation notifications. See {@link PolicyViolationHandler}. */
34
19
  onPolicyViolation?: PolicyViolationHandler;
35
20
  }
36
21
  /**
37
- * Parse a commit message into structured metadata.
22
+ * Parses a commit message into structured metadata.
38
23
  *
39
- * Supports three formats:
40
- * - `type: description`
41
- * - `scope|type: description` (pipe-prefixed scope)
42
- * - `type(scope): description` (conventional commit parenthesized scope)
24
+ * Reads the first line through change-grammar's `pipedScope` template (`scope|type: description`, the scope optional),
25
+ * then its `conventionalCommits` template (`type(scope): description`), and takes the first match. Types and aliases
26
+ * resolve against `workTypes`, ignoring case; scope aliases resolve when a `scopeAliases` map is provided. Detects
27
+ * breaking changes via the subject's `!` or `BREAKING CHANGE:` in the message.
43
28
  *
44
- * Resolves aliases (e.g., `feature` → `feat`) using the provided work-type configs and the
45
- * canonical aliases list. Resolves scope aliases when a `scopeAliases` map is provided.
46
- * Detects breaking changes via `type!:` or `BREAKING CHANGE:` in the message.
47
- *
48
- * `!`-policy enforcement is **release-time tolerant**: when the resolved type's policy
29
+ * `!`-policy enforcement is **release-time tolerant**: When the resolved type's policy
49
30
  * forbids `!`, the marker is dropped from the parse (`breaking: false`) and
50
- * `onPolicyViolation` is invoked. When the policy requires `!`, a bare type triggers the
51
- * same warning path.
31
+ * `onPolicyViolation` is invoked.
52
32
  */
53
33
  export declare function parseCommitMessage(message: string, hash: string, workTypes: Record<string, WorkTypeConfig>, scopeAliases?: Record<string, string>, options?: ParseCommitMessageOptions): ParsedCommit | undefined;
34
+ /**
35
+ * Reads a commit subject into a change record through the templates that release-kit accepts, before any breaking
36
+ * policy applies. The engine strips ticket prefixes.
37
+ */
38
+ export declare function parseSubject(subject: string, workTypes: Record<string, WorkTypeConfig>): ChangeRecord | undefined;
54
39
  /** Inputs for {@link evaluateBreakingPolicy}. */
55
40
  export interface BreakingPolicyInputs {
56
41
  commit: Commit;
@@ -58,7 +43,7 @@ export interface BreakingPolicyInputs {
58
43
  /** Whether the marker that `prefixSurface` names is present. */
59
44
  hasPrefixBreaking: boolean;
60
45
  hasFooterBreaking: boolean;
61
- policy: 'forbidden' | 'optional' | 'required';
46
+ policy: BreakingPolicy;
62
47
  onPolicyViolation: PolicyViolationHandler | undefined;
63
48
  /** The surface reported for a violation of the marker; `'prefix'` when omitted. */
64
49
  prefixSurface?: PolicyViolationSurface;
@@ -1,45 +1,44 @@
1
- export const COMMIT_PREPROCESSOR_PATTERNS = [/^##\s+/, /^#\d+([.-]\d+)?\s+/, /^[A-Z]+-\d+\s+/];
2
- export const PIPE_SCOPE_SOURCE = '[^|]+';
3
- const COMMIT_SUBJECT_PATTERN = new RegExp(String.raw `^(?:(${PIPE_SCOPE_SOURCE})\|)?(\w+)(?:\(([^)]+)\))?(!)?:\s*(.*)$`);
1
+ import { compileTemplate, parse, TEMPLATE_CATALOGUE, } from '@williamthorsen/change-grammar';
2
+ const SUBJECT_TEMPLATES = [
3
+ compileTemplate(TEMPLATE_CATALOGUE.pipedScope),
4
+ compileTemplate(TEMPLATE_CATALOGUE.conventionalCommits),
5
+ ];
4
6
  export function parseCommitMessage(message, hash, workTypes, scopeAliases, options) {
5
7
  const firstLine = message.split('\n', 1)[0] ?? '';
6
- const stripped = stripTicketPrefix(firstLine);
7
- const match = stripped.match(COMMIT_SUBJECT_PATTERN);
8
- if (!match) {
9
- return undefined;
10
- }
11
- const pipeScope = match[1];
12
- const rawType = match[2];
13
- const parenthesizedScope = match[3];
14
- const breakingMarker = match[4];
15
- const description = match[5];
16
- if (rawType === undefined || description === undefined) {
17
- return undefined;
18
- }
19
- const resolvedType = resolveType(rawType, workTypes);
20
- if (resolvedType === undefined) {
8
+ const record = parseSubject(firstLine, workTypes);
9
+ if (record?.type === undefined || record.title === undefined) {
21
10
  return undefined;
22
11
  }
23
12
  const commit = { message, subject: firstLine, hash };
24
13
  const breaking = evaluateBreakingPolicy({
25
14
  commit,
26
- resolvedType,
27
- hasPrefixBreaking: breakingMarker === '!',
15
+ resolvedType: record.type,
16
+ hasPrefixBreaking: record.breaking === true,
28
17
  hasFooterBreaking: message.includes('BREAKING CHANGE:'),
29
- policy: options?.breakingPolicies?.[resolvedType] ?? 'optional',
18
+ policy: options?.breakingPolicies?.[record.type] ?? 'optional',
30
19
  onPolicyViolation: options?.onPolicyViolation,
31
20
  });
32
- const rawScope = pipeScope ?? parenthesizedScope;
21
+ const rawScope = record.scope;
33
22
  const resolvedScope = rawScope !== undefined && scopeAliases !== undefined ? (scopeAliases[rawScope] ?? rawScope) : rawScope;
34
23
  return {
35
24
  message,
36
25
  hash,
37
- type: resolvedType,
38
- description,
26
+ type: record.type,
27
+ description: record.title,
39
28
  breaking,
40
29
  ...(resolvedScope !== undefined && { scope: resolvedScope }),
41
30
  };
42
31
  }
32
+ export function parseSubject(subject, workTypes) {
33
+ const taxonomy = toTaxonomy(workTypes);
34
+ for (const nodes of SUBJECT_TEMPLATES) {
35
+ const record = parse(nodes, subject, taxonomy);
36
+ if (record !== undefined) {
37
+ return record;
38
+ }
39
+ }
40
+ return undefined;
41
+ }
43
42
  export function evaluateBreakingPolicy(inputs) {
44
43
  const { commit, resolvedType, hasPrefixBreaking, hasFooterBreaking, policy, onPolicyViolation } = inputs;
45
44
  const prefixSurface = inputs.prefixSurface ?? 'prefix';
@@ -52,13 +51,6 @@ export function evaluateBreakingPolicy(inputs) {
52
51
  }
53
52
  return false;
54
53
  }
55
- if (policy === 'required') {
56
- if (!hasPrefixBreaking) {
57
- onPolicyViolation?.(commit, resolvedType, prefixSurface);
58
- return false;
59
- }
60
- return true;
61
- }
62
54
  return hasPrefixBreaking || hasFooterBreaking;
63
55
  }
64
56
  export function resolveType(rawType, workTypes) {
@@ -77,10 +69,9 @@ export function resolveType(rawType, workTypes) {
77
69
  }
78
70
  return undefined;
79
71
  }
80
- function stripTicketPrefix(message) {
81
- let result = message;
82
- for (const pattern of COMMIT_PREPROCESSOR_PATTERNS) {
83
- result = result.replace(pattern, '');
84
- }
85
- return result;
72
+ function toTaxonomy(workTypes) {
73
+ return {
74
+ tiers: [],
75
+ types: Object.entries(workTypes).map(([key, config]) => ({ aliases: config.aliases ?? [], key, tier: '' })),
76
+ };
86
77
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Parse the comma-separated `--tags` flag value into a list of requested tag names.
2
+ * Parses the comma-separated `--tags` flag value into a list of requested tag names.
3
3
  *
4
4
  * Empty segments (from `--tags=`, leading/trailing commas, or `--tags=,,`) are dropped. When the
5
- * resulting list is empty, returns `undefined` so the caller treats it as "no filter" — the same
6
- * behavior as omitting `--tags` entirely.
5
+ * resulting list is empty, returns `undefined`, and the caller does not filter the tags, as when
6
+ * `--tags` is omitted.
7
7
  */
8
8
  export declare function parseRequestedTags(flagValue: string | undefined): string[] | undefined;