@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
@@ -5,8 +5,8 @@ export type { EmptyWorkspaceCause } from '@williamthorsen/nmr-core/workspace';
5
5
  * set of package directories, or it declares a workspace that resolves to none and the cause says which
6
6
  * condition emptied it.
7
7
  *
8
- * Held apart from nmr-core's `WorkspaceResolution`, whose `packageDirs` are absolute, because these are
9
- * relative to the monorepo root. The two carry incompatible path contracts, and a distinct `kind` for the
8
+ * Kept separate from nmr-core's `WorkspaceResolution`, whose `packageDirs` are absolute, because these are
9
+ * relative to the monorepo root. The two have incompatible path contracts, and a distinct `kind` for the
10
10
  * single-package case keeps either from standing in for the other.
11
11
  */
12
12
  export type WorkspaceDiscovery = {
@@ -20,39 +20,38 @@ export type WorkspaceDiscovery = {
20
20
  } | {
21
21
  kind: 'single-package';
22
22
  };
23
- /** A workspace that resolved to no package directory, which every release-kit command treats as a failure. */
23
+ /** A workspace that did not resolve to any package directory, which every release-kit command treats as a failure. */
24
24
  export type EmptyWorkspace = Extract<WorkspaceDiscovery, {
25
25
  kind: 'empty';
26
26
  }>;
27
27
  /**
28
- * The empty-workspace causes that reach a command as a failure.
28
+ * The empty-workspace causes that every release-kit command treats as a failure.
29
29
  *
30
- * `no-packages-list` is absent because a manifest declaring nothing releases as a single package, so no
31
- * command ever composes a message for it.
30
+ * `no-packages-list` is absent because a manifest declaring nothing releases as a single package, so
31
+ * release-kit's commands never compose a message for it.
32
32
  */
33
33
  export type FailingWorkspaceCause = Exclude<EmptyWorkspaceCause, 'no-packages-list'>;
34
34
  /**
35
- * Returns the sentence naming which of the five conditions left a workspace holding no package directory, and
36
- * the remedy for that one. Each quotes the `packages` list the manifest declares, apart from the two that
37
- * reach the matcher with no list to quote.
35
+ * Returns the sentence naming which condition left a workspace without a package directory, and the remedy for
36
+ * it. A message quotes the manifest's `packages` list whenever the manifest declares one that the resolver read.
38
37
  *
39
38
  * The `package.json` requirement is stated under `no-package` because it is a divergence from pnpm, which
40
- * recognizes two further manifests, and the reader of a workspace that pnpm resolves has no way to infer it.
39
+ * recognizes two further manifests, and the reader of a workspace that pnpm resolves cannot infer it.
41
40
  */
42
41
  export declare function describeEmptyWorkspace(workspace: EmptyWorkspace): string;
43
42
  /**
44
43
  * Reads `pnpm-workspace.yaml` at `monorepoRoot` and resolves its `packages` patterns, applying pnpm's
45
- * semantics — `!`-prefixed exclusions included.
44
+ * semantics, `!`-prefixed exclusions included.
46
45
  *
47
- * A directory holding no manifest, and one whose manifest declares no `packages` list, both release as a
46
+ * A directory without a manifest, and one whose manifest does not declare a `packages` list, both release as a
48
47
  * single package: pnpm resolves each to the root package alone, and a workspace file kept for `catalog:`,
49
48
  * `overrides:`, or `onlyBuiltDependencies:` is the second of them. Every other empty resolution is a failure,
50
- * whether the reader declared patterns that matched nothing or a `packages` value the resolver could not read
49
+ * whether the manifest declares patterns that match nothing or a `packages` value that the resolver cannot read
51
50
  * as patterns at all.
52
51
  *
53
52
  * `packageDirs` come back relative to `monorepoRoot` as POSIX paths, because that is what every consumer
54
53
  * needs: `WorkspaceConfig.paths` feeds `git log -- <paths>`, and `packageFiles` and `changelogPaths` are read
55
- * from disk relative to the working directory. The workspace root itself relativizes to `.`, the path
56
- * `deriveWorkspaceConfig` reads a root package's manifest through.
54
+ * from disk relative to the working directory. The workspace root itself relativizes to `.`, the path through
55
+ * which `deriveWorkspaceConfig` reads a root package's manifest.
57
56
  */
58
57
  export declare function discoverWorkspaces(monorepoRoot?: string): WorkspaceDiscovery;
@@ -7,21 +7,21 @@ export function describeEmptyWorkspace(workspace) {
7
7
  return (`pnpm-workspace.yaml ${declaredClause}, whose \`!\` entries exclude every directory matched by the ` +
8
8
  'positive patterns. Drop or narrow the exclusion.');
9
9
  case 'no-package':
10
- return (`pnpm-workspace.yaml ${declaredClause}, and the matcher found no directory holding a \`package.json\`. ` +
11
- 'Add one to the directory that should be a package, or declare a pattern reaching a directory that ' +
12
- 'holds one.');
10
+ return (`pnpm-workspace.yaml ${declaredClause}, and the matcher did not find any directory containing a ` +
11
+ '`package.json`. Add one to the directory that should be a package, or declare a pattern matching a ' +
12
+ 'directory that contains one.');
13
13
  case 'no-pattern':
14
- return (`pnpm-workspace.yaml ${declaredClause}, so no pattern reaches the matcher. Declare a positive pattern ` +
15
- 'such as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string where ' +
16
- 'it stands bare.');
14
+ return (`pnpm-workspace.yaml ${declaredClause}, so the matcher does not receive any pattern. Declare a positive ` +
15
+ 'pattern such as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string ' +
16
+ 'when it is unquoted.');
17
17
  case 'unreadable-manifest':
18
- return ('pnpm-workspace.yaml holds no valid YAML, so nothing it declares reaches the matcher. Repair the ' +
19
- 'syntax error and run the command again. An unterminated quoted string and a mis-indented entry are ' +
20
- 'the usual ones.');
18
+ return ('pnpm-workspace.yaml does not contain valid YAML, so the matcher does not receive anything that it ' +
19
+ 'declares. Repair the syntax error and run the command again. An unterminated quoted string and a ' +
20
+ 'mis-indented entry are the usual ones.');
21
21
  case 'unreadable-packages':
22
- return ('pnpm-workspace.yaml declares a `packages` value that is not a list of strings, so nothing it ' +
23
- 'declares reaches the matcher. Make it a list whose every entry is a quoted pattern; a bare value ' +
24
- 'and an entry YAML read as a number or a map are the usual ones.');
22
+ return ('pnpm-workspace.yaml declares a `packages` value that is not a list of strings, so the matcher does ' +
23
+ 'not receive anything that it declares. Make it a list whose every entry is a quoted pattern; a bare ' +
24
+ 'value and an entry that YAML read as a number or a map are the usual ones.');
25
25
  default: {
26
26
  const unhandledCause = workspace.cause;
27
27
  throw new Error(`Unhandled empty-workspace cause: ${String(unhandledCause)}`);
@@ -0,0 +1,12 @@
1
+ /** The directory from which `release-kit` was invoked, and the repo root in which it now runs. */
2
+ export interface RepoLocation {
3
+ invocationDir: string;
4
+ root: string;
5
+ }
6
+ /**
7
+ * Locates the repo root from `startDir` and makes it the process working directory.
8
+ *
9
+ * The root is the nearest ancestor containing `pnpm-workspace.yaml`, or else `startDir` itself when it contains a
10
+ * `package.json`. A single-package repo is therefore found only from its own root. Throws when neither applies.
11
+ */
12
+ export declare function enterRepoRoot(startDir?: string): RepoLocation;
@@ -0,0 +1,16 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { findMonorepoRoot } from '@williamthorsen/nmr-core/workspace';
4
+ export function enterRepoRoot(startDir = process.cwd()) {
5
+ const invocationDir = path.resolve(startDir);
6
+ const root = findMonorepoRoot(invocationDir) ?? findSinglePackageRoot(invocationDir);
7
+ if (root === undefined) {
8
+ throw new Error(`No repo root found from ${invocationDir}: expected a pnpm-workspace.yaml in it or any parent directory, ` +
9
+ 'or a package.json in it. Run release-kit from inside a monorepo or at the root of a single-package repo.');
10
+ }
11
+ process.chdir(root);
12
+ return { invocationDir, root };
13
+ }
14
+ function findSinglePackageRoot(dir) {
15
+ return existsSync(path.join(dir, 'package.json')) ? dir : undefined;
16
+ }
@@ -0,0 +1,50 @@
1
+ /** A commit as git records it, before any parsing or filtering. */
2
+ export interface RawCommit {
3
+ /** The full 40-character hash. */
4
+ hash: string;
5
+ /** The first line, with a wrapped subject folded onto one line by git. */
6
+ subject: string;
7
+ /** The message after its first blank line, empty when the commit does not have a body. */
8
+ body: string;
9
+ /** The raw commit message, subject and body together. */
10
+ message: string;
11
+ }
12
+ /** The commits that one release contains, with the version and date under which they were released. */
13
+ export interface ReleaseWindow {
14
+ /** The tag name for a released window, the caller's unreleased-tag name for the newest one. */
15
+ version: string;
16
+ /** Unix seconds: the tag's creation date for a released window, the clock for the unreleased one. */
17
+ timestamp: number;
18
+ /** Oldest first. */
19
+ commits: RawCommit[];
20
+ }
21
+ /** Inputs for {@link enumerateReleaseWindows}. */
22
+ export interface EnumerateReleaseWindowsOptions {
23
+ /** Git pathspecs restricting the enumeration to commits that touch them. */
24
+ paths?: readonly string[];
25
+ /** Tag prefixes to match as a union; a matching tag name continues with a digit. */
26
+ tagPrefixes: readonly string[];
27
+ /** The version that the unreleased window reports, such as the tag that the release being prepared will write. */
28
+ unreleasedTag: string;
29
+ /** Returns the current time in epoch milliseconds; dates the unreleased window. */
30
+ now?: () => number;
31
+ }
32
+ /**
33
+ * Splits the history reachable from HEAD into release windows, newest first.
34
+ *
35
+ * The newest window is always the unreleased one, empty when HEAD is at a matching tag. Released
36
+ * windows follow in descending order, one per reachable tag matching a prefix. A tag that
37
+ * HEAD cannot reach is dropped.
38
+ *
39
+ * A commit belongs to the oldest matching tag that contains it, and to the unreleased window
40
+ * when it is not contained by any matching tag. Containment is read from the ancestry graph
41
+ * rather than from a position in a linear walk, because a branch commit authored before a tag
42
+ * and merged after it is not in that tag's release.
43
+ *
44
+ * Three git invocations serve any number of tags: `git rev-list --topo-order --parents` for the
45
+ * ancestry graph, `git for-each-ref` for the tag names with their creation dates and target
46
+ * commits, and one path-filtered `git log` for the commits themselves.
47
+ *
48
+ * Throws if `tagPrefixes` is empty, or if any git invocation fails.
49
+ */
50
+ export declare function enumerateReleaseWindows(options: EnumerateReleaseWindowsOptions): ReleaseWindow[];
@@ -0,0 +1,124 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
3
+ import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
4
+ const FIELD_SEPARATOR = '\u{1F}';
5
+ const RECORD_SEPARATOR = '\u{1E}';
6
+ export function enumerateReleaseWindows(options) {
7
+ const { now = Date.now, paths, tagPrefixes, unreleasedTag } = options;
8
+ if (tagPrefixes.length === 0) {
9
+ throw new Error('enumerateReleaseWindows: tagPrefixes must contain at least one entry');
10
+ }
11
+ const unreleasedWindow = {
12
+ version: unreleasedTag,
13
+ timestamp: Math.floor(now() / 1_000),
14
+ commits: [],
15
+ };
16
+ const ancestry = readAncestry();
17
+ if (ancestry.parentsByHash.size === 0) {
18
+ return [unreleasedWindow];
19
+ }
20
+ const boundaries = readTagBoundaries(tagPrefixes, ancestry.positionByHash);
21
+ const releasedWindows = boundaries.map((boundary) => ({
22
+ version: boundary.tag,
23
+ timestamp: boundary.timestamp,
24
+ commits: [],
25
+ }));
26
+ const windowIndexByHash = claimAncestors(boundaries, ancestry.parentsByHash);
27
+ for (const commit of readCommits(paths).toReversed()) {
28
+ const windowIndex = windowIndexByHash.get(commit.hash);
29
+ const window = windowIndex === undefined ? unreleasedWindow : (releasedWindows[windowIndex] ?? unreleasedWindow);
30
+ window.commits.push(commit);
31
+ }
32
+ return [unreleasedWindow, ...releasedWindows.toReversed()];
33
+ }
34
+ function claimAncestors(boundaries, parentsByHash) {
35
+ const windowIndexByHash = new Map();
36
+ for (const [windowIndex, boundary] of boundaries.entries()) {
37
+ const pending = [boundary.hash];
38
+ while (pending.length > 0) {
39
+ const hash = pending.pop();
40
+ if (hash === undefined || windowIndexByHash.has(hash)) {
41
+ continue;
42
+ }
43
+ windowIndexByHash.set(hash, windowIndex);
44
+ pending.push(...(parentsByHash.get(hash) ?? []));
45
+ }
46
+ }
47
+ return windowIndexByHash;
48
+ }
49
+ function matchesTagPrefix(tagName, tagPrefixes) {
50
+ return tagPrefixes.some((prefix) => tagName.startsWith(prefix) && /^\d/.test(tagName.slice(prefix.length)));
51
+ }
52
+ function parseCommitRecord(record) {
53
+ const [hash, subject, message] = record.replace(/^\n+/, '').split(FIELD_SEPARATOR);
54
+ if (hash === undefined || subject === undefined || message === undefined || hash === '') {
55
+ return undefined;
56
+ }
57
+ const trimmedMessage = message.replace(/\s+$/, '');
58
+ const bodyStart = trimmedMessage.indexOf('\n\n');
59
+ const body = bodyStart === -1 ? '' : trimmedMessage.slice(bodyStart + 2);
60
+ return { hash, subject, body, message: trimmedMessage };
61
+ }
62
+ function readAncestry() {
63
+ const args = ['rev-list', '--topo-order', '--parents', '--ignore-missing', 'HEAD'];
64
+ const output = runGit(args, `'git rev-list' for HEAD`);
65
+ const ancestry = { parentsByHash: new Map(), positionByHash: new Map() };
66
+ for (const [position, line] of output.split('\n').entries()) {
67
+ const [hash, ...parents] = line.split(' ');
68
+ if (hash === undefined || hash === '') {
69
+ continue;
70
+ }
71
+ ancestry.parentsByHash.set(hash, parents);
72
+ ancestry.positionByHash.set(hash, position);
73
+ }
74
+ return ancestry;
75
+ }
76
+ function readCommits(paths) {
77
+ const format = `%H${FIELD_SEPARATOR}%s${FIELD_SEPARATOR}%B${RECORD_SEPARATOR}`;
78
+ const args = ['log', 'HEAD', `--pretty=format:${format}`];
79
+ if (paths !== undefined && paths.length > 0) {
80
+ args.push('--', ...paths);
81
+ }
82
+ const output = runGit(args, `'git log' for HEAD`);
83
+ const commits = [];
84
+ for (const record of output.split(RECORD_SEPARATOR)) {
85
+ const commit = parseCommitRecord(record);
86
+ if (commit !== undefined) {
87
+ commits.push(commit);
88
+ }
89
+ }
90
+ return commits;
91
+ }
92
+ function readTagBoundaries(tagPrefixes, positionByHash) {
93
+ const fields = ['%(refname:strip=2)', '%(creatordate:unix)', '%(objectname)', '%(*objectname)'];
94
+ const args = ['for-each-ref', `--format=${fields.join('%1f')}`, 'refs/tags'];
95
+ const output = runGit(args, `'git for-each-ref' for refs/tags`);
96
+ const boundaries = [];
97
+ for (const line of output.split('\n')) {
98
+ const [tag, creatorDate, objectName, dereferencedName] = line.split(FIELD_SEPARATOR);
99
+ if (tag === undefined || creatorDate === undefined || objectName === undefined) {
100
+ continue;
101
+ }
102
+ if (!matchesTagPrefix(tag, tagPrefixes)) {
103
+ continue;
104
+ }
105
+ const hash = dereferencedName || objectName;
106
+ const position = positionByHash.get(hash);
107
+ if (position !== undefined) {
108
+ boundaries.push({ hash, position, tag, timestamp: Number(creatorDate) });
109
+ }
110
+ }
111
+ return boundaries.toSorted((left, right) => right.position - left.position);
112
+ }
113
+ function runGit(args, description) {
114
+ try {
115
+ return execFileSync('git', args, {
116
+ encoding: 'utf8',
117
+ maxBuffer: GIT_OUTPUT_LIMIT,
118
+ stdio: ['pipe', 'pipe', 'pipe'],
119
+ }).trim();
120
+ }
121
+ catch (error) {
122
+ throw chainError(`Failed to run ${description}`, error);
123
+ }
124
+ }
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * Extracts the instruction from a commit body's `Migration:` paragraph.
3
3
  *
4
- * The first paragraph opening with the label wins: the lede doctrine specifies one labeled
5
- * paragraph per body, so a second is ignored rather than appended. The match is anchored and
6
- * case-sensitive, so `migration:` and `**Migration:**` yield nothing; the doctrine calls the
7
- * label literal, and tolerating variants would let the form drift.
4
+ * The commit convention defines one literal `Migration:` label per body, so the first paragraph
5
+ * opening with it is read and a second is ignored. Because the match is anchored and
6
+ * case-sensitive, `migration:` and `**Migration:**` yield nothing.
8
7
  *
9
- * Newlines inside the paragraph are preserved. The first character is capitalized, which is
10
- * inert for the imperative the doctrine requires and repairs the lowercase continuation of a
11
- * paragraph written before it. Returns `undefined` when no paragraph carries the label, or when
12
- * the label is followed by nothing.
8
+ * Newlines inside the paragraph are preserved. The first character is capitalized, which repairs
9
+ * an instruction that continues the label in lowercase. Returns `undefined` when the body does not
10
+ * contain a paragraph opening with the label, or when the label is followed by nothing.
13
11
  */
14
12
  export declare function extractMigration(body: string | undefined): string | undefined;
@@ -0,0 +1,16 @@
1
+ import type { RawCommit } from './enumerateReleaseWindows.js';
2
+ import type { UnroutedEntryScope } from './types.js';
3
+ /** A workspace's `dir` with the commits of its unreleased window. */
4
+ export interface WorkspaceWindow {
5
+ commits: readonly RawCommit[];
6
+ dir: string;
7
+ }
8
+ /**
9
+ * Finds the change-record entry scopes of the unreleased windows that do not route their entry to any workspace whose
10
+ * window contains the commit, and returns the findings keyed by the `dir` of every workspace whose window contains it.
11
+ *
12
+ * A scope resolves through `scopeAliases` first. It is not reported when it resolves to `*`, to a workspace whose
13
+ * window contains the commit, or to a `configuredDirs` workspace absent from `windows`, which the run did not read.
14
+ * `root` is not reported unless a configured workspace has that `dir`, in which case it matches as that workspace.
15
+ */
16
+ export declare function findUnroutedEntryScopes(windows: readonly WorkspaceWindow[], configuredDirs: readonly string[], scopeAliases: Readonly<Record<string, string>>): Map<string, UnroutedEntryScope[]>;
@@ -0,0 +1,57 @@
1
+ import { isNonChangeSubject } from "./classifyChangelogCommit.js";
2
+ import { parseChangeRecordBlock } from "./parseChangeRecordBlock.js";
3
+ export function findUnroutedEntryScopes(windows, configuredDirs, scopeAliases) {
4
+ const readDirs = new Set(windows.map((window) => window.dir));
5
+ const unreadDirs = new Set(configuredDirs.filter((dir) => !readDirs.has(dir)));
6
+ const isRootReserved = !configuredDirs.includes('root');
7
+ const findings = new Map();
8
+ for (const { commit, windowDirs } of indexCommits(windows).values()) {
9
+ for (const finding of findCommitUnroutedScopes(commit, windowDirs, unreadDirs, isRootReserved, scopeAliases)) {
10
+ for (const dir of windowDirs) {
11
+ const list = findings.get(dir) ?? [];
12
+ list.push(finding);
13
+ findings.set(dir, list);
14
+ }
15
+ }
16
+ }
17
+ return findings;
18
+ }
19
+ function findCommitUnroutedScopes(commit, windowDirs, unreadDirs, isRootReserved, scopeAliases) {
20
+ if (isNonChangeSubject(commit.subject)) {
21
+ return [];
22
+ }
23
+ const reading = parseChangeRecordBlock(commit.message);
24
+ if (reading.kind !== 'read') {
25
+ return [];
26
+ }
27
+ const findings = [];
28
+ for (const [index, entry] of reading.entries.entries()) {
29
+ const scopes = new Set(entry.scopes);
30
+ for (const scope of scopes) {
31
+ const resolved = scopeAliases[scope] ?? scope;
32
+ const isRouted = resolved === '*' ||
33
+ windowDirs.has(resolved) ||
34
+ unreadDirs.has(resolved) ||
35
+ (resolved === 'root' && isRootReserved);
36
+ if (!isRouted) {
37
+ findings.push({ commitHash: commit.hash, commitSubject: commit.subject, entryPosition: index + 1, scope });
38
+ }
39
+ }
40
+ }
41
+ return findings;
42
+ }
43
+ function indexCommits(windows) {
44
+ const index = new Map();
45
+ for (const window of windows) {
46
+ for (const commit of window.commits) {
47
+ const indexed = index.get(commit.hash);
48
+ if (indexed === undefined) {
49
+ index.set(commit.hash, { commit, windowDirs: new Set([window.dir]) });
50
+ }
51
+ else {
52
+ indexed.windowDirs.add(window.dir);
53
+ }
54
+ }
55
+ }
56
+ return index;
57
+ }
@@ -0,0 +1,3 @@
1
+ import type { ResolvedTag } from './resolveReleaseTags.js';
2
+ /** Formats the warning emitted when a release tag's workspace is excluded by config and therefore skipped. */
3
+ export declare function formatExcludedSkip(resolvedTag: Pick<ResolvedTag, 'tag' | 'workspacePath'>): string;
@@ -0,0 +1,3 @@
1
+ export function formatExcludedSkip(resolvedTag) {
2
+ return `Skipping ${resolvedTag.tag} (${resolvedTag.workspacePath}): excluded by config (shouldExclude: true).`;
3
+ }
@@ -1,8 +1,3 @@
1
1
  import type { ResolvedTag } from './resolveReleaseTags.js';
2
- /**
3
- * Format the warning emitted when a release tag's workspace is private and therefore skipped.
4
- *
5
- * Shared by `release-kit publish` and `release-kit create-github-release` so the skip wording
6
- * is defined once and stays identical across both commands.
7
- */
2
+ /** Formats the warning emitted when a release tag's workspace is private and therefore skipped. */
8
3
  export declare function formatPrivateSkip(resolvedTag: Pick<ResolvedTag, 'tag' | 'workspacePath'>): string;
@@ -1,32 +1,19 @@
1
1
  import type { WorkspaceConfig } from './types.js';
2
- /**
3
- * Build a git-cliff tag pattern from one or more tag prefixes.
4
- *
5
- * Single-prefix input preserves the historical `<prefix>[0-9].*` shape. Multi-prefix input
6
- * returns `(prefix1|prefix2|...)[0-9].*` so git-cliff's regex-based `--tag-pattern` accepts
7
- * tags under any listed prefix. Each prefix is regex-escaped so metacharacters in operator-
8
- * supplied legacy prefixes cannot alter the pattern's meaning.
9
- *
10
- * @param tagPrefixes - Tag prefixes to match as a union (must contain at least one entry).
11
- */
12
- export declare function buildTagPattern(tagPrefixes: readonly string[]): string;
13
- /**
14
- * Return the workspace's derived tag prefix followed by each declared legacy-identity tag
15
- * prefix. Shared by `releasePrepareMono` (the production prepare path) and
16
- * `validateOverridesCommand` so the two compute byte-equal per-workspace tag-prefix unions.
17
- */
2
+ /** Returns the workspace's derived tag prefix followed by each declared legacy-identity tag prefix. */
18
3
  export declare function getAllTagPrefixes(workspace: WorkspaceConfig): string[];
19
4
  /**
20
- * Options for `buildChangelogEntries`-driven flows that consume git-cliff output.
21
- *
22
- * The fields here mirror the subset of git-cliff CLI options that the in-package pipeline
23
- * still passes through (tag-pattern union, include-path filtering for monorepo workspaces).
24
- * `generateChangelog` and `generateChangelogs` (the prior cliff `--output` invokers) are
25
- * removed; markdown rendering is now handled by `renderChangelogMarkdown.ts`.
5
+ * Options for `readReleaseHistory`: the tag prefixes and pathspecs that bound its release windows, and the workspace
6
+ * to which it routes change-record entries.
26
7
  */
27
8
  export interface GenerateChangelogOptions {
28
- /** Tag pattern to match release tags (passed as --tag-pattern to git-cliff). */
29
- tagPattern?: string;
30
- /** Paths to include in the changelog (passed as --include-path to git-cliff). */
31
- includePaths?: string[];
9
+ /** Tag prefixes to match as a union; must contain at least one entry. */
10
+ tagPrefixes: readonly string[];
11
+ /** Git pathspecs restricting the history to commits that touch them; all paths when omitted. */
12
+ paths?: readonly string[];
13
+ /**
14
+ * The `dir` of the workspace being read. When set, the read includes a change-record entry only when its scopes,
15
+ * after `scopeAliases` resolution, name this `dir`, are empty, or contain `*`; when omitted, the read includes every
16
+ * entry.
17
+ */
18
+ workspaceDir?: string;
32
19
  }
@@ -1,17 +1,3 @@
1
- export function buildTagPattern(tagPrefixes) {
2
- if (tagPrefixes.length === 0) {
3
- throw new Error('buildTagPattern: tagPrefixes must contain at least one entry');
4
- }
5
- if (tagPrefixes.length === 1) {
6
- const single = tagPrefixes[0] ?? '';
7
- return `${single}[0-9].*`;
8
- }
9
- const escaped = tagPrefixes.map(escapeRegex);
10
- return `(${escaped.join('|')})[0-9].*`;
11
- }
12
1
  export function getAllTagPrefixes(workspace) {
13
2
  return [workspace.tagPrefix, ...(workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? [])];
14
3
  }
15
- function escapeRegex(value) {
16
- return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
17
- }
@@ -1,3 +1,3 @@
1
- /** Glyphs that name no status; a status marker comes from nmr-core's `STATUS_GLYPHS`. */
2
- export declare const RELEASE_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"bump" | "dryRun" | "tag", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
1
+ /** Glyphs that do not name a status; a status marker comes from nmr-core's `STATUS_GLYPHS`. */
2
+ export declare const RELEASE_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"tag" | "bump" | "dryRun", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
3
3
  export type ReleaseGlyphName = keyof (typeof RELEASE_GLYPHS)['rich'];
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Single source of truth for the `release-kit prepare` help text. Imported by the bin
3
- * dispatcher and asserted by the help drift guard test; kept free of import-time side
4
- * effects so both can load it.
2
+ * Help text for `release-kit prepare`. The module does not have any import-time side effects, so that a test can
3
+ * import it without running the CLI.
5
4
  */
6
- export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Path to config file (default: .config/release-kit.config.ts)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. In monorepo\n mode it requires --only, and is rejected when a 'project' block is configured.\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).\n When a 'project' block is configured, the project release is skipped.\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
5
+ export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Config file, relative to the current directory\n (default: .config/release-kit.config.ts at the repo root)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Set the level of every release that happens; does not trigger one\n --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo\n mode it requires --only, and is rejected when a 'project' block is configured.\n --force Release even when the last tag is not followed by any commits,\n or by any bump-worthy ones. Defaults to patch when --bump is not given;\n use --bump=X for a different level.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).\n When a 'project' block is configured, the project release is skipped.\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
7
6
  /** Prints the prepare command's help text. */
8
7
  export declare function showPrepareHelp(): void;
@@ -4,16 +4,15 @@ Usage: release-kit prepare [options]
4
4
  Run release preparation with automatic workspace discovery.
5
5
 
6
6
  Options:
7
- --config <path> Path to config file (default: .config/release-kit.config.ts)
7
+ --config <path> Config file, relative to the current directory
8
+ (default: .config/release-kit.config.ts at the repo root)
8
9
  --dry-run Run without modifying any files
9
- --bump=major|minor|patch Override the bump type for all workspaces
10
- --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. In monorepo
10
+ --bump=major|minor|patch Set the level of every release that happens; does not trigger one
11
+ --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo
11
12
  mode it requires --only, and is rejected when a 'project' block is configured.
12
- --force Release even when no commits or no bump-worthy commits exist
13
- since the last tag. In monorepo and project mode, defaults to
14
- patch when --bump is not given; use --bump=X for a different
15
- level. In single-package mode, a bare --force is rejected —
16
- pass --bump=major|minor|patch.
13
+ --force Release even when the last tag is not followed by any commits,
14
+ or by any bump-worthy ones. Defaults to patch when --bump is not given;
15
+ use --bump=X for a different level.
17
16
  --no-git-checks, -n Skip the clean-working-tree check
18
17
  --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).
19
18
  When a 'project' block is configured, the project release is skipped.
@@ -1,2 +1,2 @@
1
- export { type ChangelogOverrideScope, validateAllChangelogOverrides, type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult, } from './changelogOverrides.js';
1
+ export { type ChangelogOverrideScope, type OverrideTargetItem, validateAllChangelogOverrides, type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult, } from './changelogOverrides.js';
2
2
  export { formatValidateOverridesResult, type ValidateOverridesCommandResult } from './validateOverridesCommand.js';
@@ -3,9 +3,9 @@ export interface CheckResult {
3
3
  ok: boolean;
4
4
  message?: string;
5
5
  }
6
- /** Verify the current directory is inside a git repository. */
6
+ /** Verifies that the current directory is inside a git repository. */
7
7
  export declare function isGitRepo(): CheckResult;
8
- /** Verify that package.json exists in the current directory. */
8
+ /** Verifies that package.json exists in the current directory. */
9
9
  export declare function hasPackageJson(): CheckResult;
10
- /** Verify that the project uses pnpm (has pnpm-lock.yaml or packageManager field). */
10
+ /** Verifies that the project uses pnpm, signaled by pnpm-lock.yaml or a pnpm `packageManager` field. */
11
11
  export declare function usesPnpm(): CheckResult;
@@ -15,7 +15,10 @@ export function hasPackageJson() {
15
15
  if (existsSync('package.json')) {
16
16
  return { ok: true };
17
17
  }
18
- return { ok: false, message: 'No package.json found. Run `npm init` or `pnpm init` first.' };
18
+ return {
19
+ ok: false,
20
+ message: 'The current directory does not contain a package.json. Run `npm init` or `pnpm init` first.',
21
+ };
19
22
  }
20
23
  export function usesPnpm() {
21
24
  if (existsSync('pnpm-lock.yaml')) {
@@ -1,9 +1,8 @@
1
1
  /** Repo layout type. */
2
2
  export type RepoType = 'monorepo' | 'single-package';
3
3
  /**
4
- * Detect whether the current directory is a monorepo or a single-package repo.
4
+ * Detects whether the current directory is a monorepo or a single-package repo.
5
5
  *
6
- * Returns `'monorepo'` if `pnpm-workspace.yaml` exists or `package.json` has a `workspaces` field.
7
- * Otherwise returns `'single-package'`.
6
+ * A monorepo has `pnpm-workspace.yaml` or a `workspaces` array in `package.json`.
8
7
  */
9
8
  export declare function detectRepoType(): RepoType;
@@ -0,0 +1,2 @@
1
+ /** Checks whether a publish workflow enables provenance on an uncommented line. */
2
+ export declare function hasProvenance(workflowContent: string): boolean;
@@ -0,0 +1,3 @@
1
+ export function hasProvenance(workflowContent) {
2
+ return /^[^#]*provenance:\s*['"]?true['"]?/im.test(workflowContent);
3
+ }
@@ -5,11 +5,8 @@ interface InitOptions {
5
5
  styles: StreamStyles;
6
6
  withConfig: boolean;
7
7
  }
8
- /**
9
- * Run the `release-kit init` command.
10
- *
11
- * Checks eligibility, detects repo type, scaffolds files, and prints next steps.
12
- * Returns the process exit code (0 for success, 1 for failure).
13
- */
8
+ /** Runs the eligibility checks, stopping at the first failure, and reports whether all passed. */
9
+ export declare function checkEligibility(styles: StreamStyles): boolean;
10
+ /** Runs the `release-kit init` command and returns the process exit code. */
14
11
  export declare function initCommand({ dryRun, force, styles, withConfig }: InitOptions): number;
15
12
  export {};