@williamthorsen/release-kit 12.0.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.meta/changelog.json +229 -0
  2. package/.readyup/kits/default.js +18 -17
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +64 -0
  6. package/README.md +27 -51
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertCleanWorkingTree.d.ts +1 -5
  9. package/dist/esm/assertTaggedBaseline.d.ts +29 -0
  10. package/dist/esm/assertTaggedBaseline.js +41 -0
  11. package/dist/esm/attachChangelogDiagnostics.d.ts +1 -1
  12. package/dist/esm/attachChangelogDiagnostics.js +3 -0
  13. package/dist/esm/bin/release-kit.js +85 -22
  14. package/dist/esm/buildChangelogEntries.d.ts +23 -13
  15. package/dist/esm/buildChangelogEntries.js +36 -17
  16. package/dist/esm/buildDependencyGraph.d.ts +6 -6
  17. package/dist/esm/buildEmptyReleaseEntry.d.ts +1 -8
  18. package/dist/esm/buildReleaseSummary.d.ts +2 -8
  19. package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
  20. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  21. package/dist/esm/bumpVersion.d.ts +1 -8
  22. package/dist/esm/changelogJsonFile.d.ts +5 -7
  23. package/dist/esm/changelogJsonFile.js +2 -17
  24. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  25. package/dist/esm/changelogOverrides.d.ts +59 -98
  26. package/dist/esm/classifyChangelogCommit.d.ts +7 -9
  27. package/dist/esm/classifyChangelogCommit.js +3 -2
  28. package/dist/esm/commitCommand.d.ts +2 -4
  29. package/dist/esm/compareVersions.d.ts +10 -4
  30. package/dist/esm/compareVersions.js +16 -0
  31. package/dist/esm/configFlagSchema.d.ts +1 -1
  32. package/dist/esm/createGithubRelease.d.ts +7 -19
  33. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  34. package/dist/esm/createGithubReleaseCommand.js +9 -9
  35. package/dist/esm/createTags.d.ts +2 -3
  36. package/dist/esm/decideRelease.d.ts +2 -6
  37. package/dist/esm/defaults.d.ts +5 -24
  38. package/dist/esm/defaults.js +4 -5
  39. package/dist/esm/defineConfig.d.ts +1 -1
  40. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  41. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  42. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  43. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  44. package/dist/esm/detectPackageManager.d.ts +2 -4
  45. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  46. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  47. package/dist/esm/determineBumpType.d.ts +1 -1
  48. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  49. package/dist/esm/discoverWorkspaces.js +12 -12
  50. package/dist/esm/enterRepoRoot.d.ts +12 -0
  51. package/dist/esm/enterRepoRoot.js +16 -0
  52. package/dist/esm/enumerateReleaseWindows.d.ts +8 -8
  53. package/dist/esm/extractMigration.d.ts +6 -8
  54. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  55. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  56. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  57. package/dist/esm/formatExcludedSkip.js +3 -0
  58. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  59. package/dist/esm/generateChangelogs.d.ts +10 -5
  60. package/dist/esm/glyphs.d.ts +1 -1
  61. package/dist/esm/help/prepareHelp.d.ts +3 -4
  62. package/dist/esm/help/prepareHelp.js +4 -3
  63. package/dist/esm/init/checks.d.ts +3 -3
  64. package/dist/esm/init/checks.js +4 -1
  65. package/dist/esm/init/detectRepoType.d.ts +2 -3
  66. package/dist/esm/init/hasProvenance.d.ts +2 -0
  67. package/dist/esm/init/hasProvenance.js +3 -0
  68. package/dist/esm/init/initCommand.d.ts +3 -6
  69. package/dist/esm/init/initCommand.js +10 -10
  70. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  71. package/dist/esm/init/scaffold.d.ts +1 -1
  72. package/dist/esm/init/templates.d.ts +9 -11
  73. package/dist/esm/init/templates.js +12 -11
  74. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  75. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  76. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  77. package/dist/esm/injectSection.d.ts +3 -3
  78. package/dist/esm/loadConfig.d.ts +16 -14
  79. package/dist/esm/loadConfig.js +2 -2
  80. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  81. package/dist/esm/loadValidatedConfig.js +6 -1
  82. package/dist/esm/packsPath.d.ts +9 -9
  83. package/dist/esm/parseChangeRecordBlock.d.ts +2 -2
  84. package/dist/esm/parseCommitMessage.d.ts +17 -32
  85. package/dist/esm/parseCommitMessage.js +28 -37
  86. package/dist/esm/parseRequestedTags.d.ts +3 -3
  87. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  88. package/dist/esm/planVersionBump.d.ts +5 -3
  89. package/dist/esm/planVersionBump.js +3 -0
  90. package/dist/esm/prepareCommand.d.ts +4 -7
  91. package/dist/esm/prepareCommand.js +13 -6
  92. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  93. package/dist/esm/propagateBumps.d.ts +6 -7
  94. package/dist/esm/publish.d.ts +1 -1
  95. package/dist/esm/publishCommand.d.ts +4 -3
  96. package/dist/esm/publishCommand.js +8 -8
  97. package/dist/esm/pushCommand.d.ts +4 -3
  98. package/dist/esm/pushCommand.js +8 -2
  99. package/dist/esm/pushRelease.d.ts +2 -2
  100. package/dist/esm/readChangelogSections.d.ts +33 -0
  101. package/dist/esm/readChangelogSections.js +80 -0
  102. package/dist/esm/releaseFiles.d.ts +5 -7
  103. package/dist/esm/releasePlan.d.ts +11 -13
  104. package/dist/esm/releasePrepare.d.ts +23 -22
  105. package/dist/esm/releasePrepare.js +31 -11
  106. package/dist/esm/releasePrepareMono.d.ts +9 -7
  107. package/dist/esm/releasePrepareMono.js +85 -33
  108. package/dist/esm/releasePrepareProject.d.ts +19 -29
  109. package/dist/esm/releasePrepareProject.js +31 -12
  110. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  111. package/dist/esm/renderChangelogMarkdown.js +27 -2
  112. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  113. package/dist/esm/renderReleaseNotes.js +2 -6
  114. package/dist/esm/reportPrepare.d.ts +4 -5
  115. package/dist/esm/reportPrepare.js +33 -19
  116. package/dist/esm/resolveCommandTags.d.ts +15 -11
  117. package/dist/esm/resolveCommandTags.js +35 -5
  118. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  119. package/dist/esm/resolveConfigFlag.js +4 -0
  120. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  121. package/dist/esm/resolveReleaseTags.js +1 -1
  122. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  123. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  124. package/dist/esm/showTagPrefixesCommand.js +4 -4
  125. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  126. package/dist/esm/stripScope.d.ts +2 -2
  127. package/dist/esm/stripScope.js +3 -3
  128. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  129. package/dist/esm/sync-labels/generateCommand.js +17 -9
  130. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  131. package/dist/esm/sync-labels/initCommand.js +2 -2
  132. package/dist/esm/sync-labels/presets.d.ts +5 -4
  133. package/dist/esm/sync-labels/presets.js +9 -9
  134. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  135. package/dist/esm/sync-labels/resolveLabels.js +2 -1
  136. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  137. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  138. package/dist/esm/sync-labels/templates.d.ts +3 -3
  139. package/dist/esm/sync-labels/templates.js +3 -3
  140. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  141. package/dist/esm/sync-labels/typeLabels.js +38 -0
  142. package/dist/esm/sync-labels/types.d.ts +4 -2
  143. package/dist/esm/tagCommand.d.ts +1 -1
  144. package/dist/esm/typeGuards.d.ts +2 -2
  145. package/dist/esm/types.d.ts +122 -93
  146. package/dist/esm/types.js +2 -1
  147. package/dist/esm/validateConfig.d.ts +2 -11
  148. package/dist/esm/validateConfig.js +2 -2
  149. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  150. package/dist/esm/validateOverridesCommand.d.ts +17 -20
  151. package/dist/esm/validateOverridesCommand.js +1 -1
  152. package/docs/api.md +6 -6
  153. package/docs/changelogs.md +63 -40
  154. package/docs/configuration.md +29 -27
  155. package/docs/editorial-overrides.md +23 -23
  156. package/docs/labels.md +37 -19
  157. package/docs/project-releases.md +13 -13
  158. package/docs/publishing.md +5 -5
  159. package/docs/readiness-checks.md +3 -3
  160. package/docs/releasing.md +50 -25
  161. package/docs/work-types.md +41 -57
  162. package/package.json +7 -6
  163. package/presets/labels/common.yaml +1 -41
  164. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  165. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  166. package/dist/esm/workTypesData.d.ts +0 -50
  167. package/dist/esm/workTypesData.js +0 -145
  168. package/schemas/label-map.json +0 -24
@@ -1,12 +1,11 @@
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
  };
@@ -22,7 +21,7 @@ function deriveDefaultWorkTypes() {
22
21
  }
23
22
  function deriveDevOnlySections() {
24
23
  const sections = [];
25
- for (const entry of WORK_TYPES_DATA.types) {
24
+ for (const entry of CANONICAL_TAXONOMY.types) {
26
25
  if (!DEV_ONLY_TIERS.has(entry.tier)) {
27
26
  continue;
28
27
  }
@@ -34,7 +33,7 @@ function deriveDevOnlySections() {
34
33
  return sections;
35
34
  }
36
35
  export const DEFAULT_WORK_TYPES = deriveDefaultWorkTypes();
37
- 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]));
38
37
  export const DEFAULT_VERSION_PATTERNS = {
39
38
  major: ['!'],
40
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;
@@ -5,7 +5,7 @@ export interface BumpSignal {
5
5
  breaking: boolean;
6
6
  }
7
7
  /**
8
- * Determines the overall bump type from a set of changes, returning undefined when none has a known work type.
8
+ * Determines the overall bump type from a set of changes, returning undefined when none triggers a bump.
9
9
  *
10
10
  * Uses `versionPatterns` to decide which types trigger major/minor bumps.
11
11
  * The `'!'` sentinel in `versionPatterns.major` means "any breaking change triggers major".
@@ -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
+ }
@@ -1,15 +1,15 @@
1
1
  /** A commit as git records it, before any parsing or filtering. */
2
2
  export interface RawCommit {
3
- /** The full 40-character hash, which `changelogOverrides` keys on. */
3
+ /** The full 40-character hash. */
4
4
  hash: string;
5
5
  /** The first line, with a wrapped subject folded onto one line by git. */
6
6
  subject: string;
7
- /** The message after its first blank line, empty when the commit carries no body. */
7
+ /** The message after its first blank line, empty when the commit does not have a body. */
8
8
  body: string;
9
9
  /** The raw commit message, subject and body together. */
10
10
  message: string;
11
11
  }
12
- /** The commits one release contains, with the version and date under which they shipped. */
12
+ /** The commits that one release contains, with the version and date under which they were released. */
13
13
  export interface ReleaseWindow {
14
14
  /** The tag name for a released window, the caller's unreleased-tag name for the newest one. */
15
15
  version: string;
@@ -32,19 +32,19 @@ export interface EnumerateReleaseWindowsOptions {
32
32
  /**
33
33
  * Splits the history reachable from HEAD into release windows, newest first.
34
34
  *
35
- * The newest window is always the unreleased one, empty when HEAD sits on a tag. Released
35
+ * The newest window is always the unreleased one, empty when HEAD is at a matching tag. Released
36
36
  * windows follow in descending order, one per reachable tag matching a prefix. A tag that
37
37
  * HEAD cannot reach is dropped.
38
38
  *
39
39
  * A commit belongs to the oldest matching tag that contains it, and to the unreleased window
40
- * when no matching tag does. Containment is read from the ancestry graph rather than from a
41
- * position in a linear walk, because a branch commit authored before a tag and merged after it
42
- * is not in that tag's release.
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
43
  *
44
44
  * Three git invocations serve any number of tags: `git rev-list --topo-order --parents` for the
45
45
  * ancestry graph, `git for-each-ref` for the tag names with their creation dates and target
46
46
  * commits, and one path-filtered `git log` for the commits themselves.
47
47
  *
48
- * @throws If `tagPrefixes` is empty, or if any git invocation fails.
48
+ * Throws if `tagPrefixes` is empty, or if any git invocation fails.
49
49
  */
50
50
  export declare function enumerateReleaseWindows(options: EnumerateReleaseWindowsOptions): ReleaseWindow[];
@@ -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,14 +1,19 @@
1
1
  import type { WorkspaceConfig } from './types.js';
2
+ /** Returns the workspace's derived tag prefix followed by each declared legacy-identity tag prefix. */
3
+ export declare function getAllTagPrefixes(workspace: WorkspaceConfig): string[];
2
4
  /**
3
- * Return the workspace's derived tag prefix followed by each declared legacy-identity tag
4
- * prefix. Shared by `releasePrepareMono` (the production prepare path) and
5
- * `validateOverridesCommand` so the two compute byte-equal per-workspace tag-prefix unions.
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.
6
7
  */
7
- export declare function getAllTagPrefixes(workspace: WorkspaceConfig): string[];
8
- /** Options for `buildChangelogEntries`: the tag prefixes and pathspecs that bound its release windows. */
9
8
  export interface GenerateChangelogOptions {
10
9
  /** Tag prefixes to match as a union; must contain at least one entry. */
11
10
  tagPrefixes: readonly string[];
12
11
  /** Git pathspecs restricting the history to commits that touch them; all paths when omitted. */
13
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;
14
19
  }
@@ -1,3 +1,3 @@
1
- /** Glyphs that name no status; a status marker comes from nmr-core's `STATUS_GLYPHS`. */
1
+ /** Glyphs that do not name a status; a status marker comes from nmr-core's `STATUS_GLYPHS`. */
2
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 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 no commits or no bump-worthy commits exist\n since the last tag. 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";
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,13 +4,14 @@ 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
10
  --bump=major|minor|patch Set the level of every release that happens; does not trigger one
10
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. Defaults to patch when --bump is not given;
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;
14
15
  use --bump=X for a different level.
15
16
  --no-git-checks, -n Skip the clean-working-tree check
16
17
  --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).
@@ -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 {};