@williamthorsen/release-kit 11.0.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.meta/changelog.json +274 -2
  2. package/.readyup/kits/default.js +18 -33
  3. package/.readyup/kits/npm-auto-publish.js +24 -23
  4. package/.readyup/manifest.json +16 -11
  5. package/CHANGELOG.md +98 -0
  6. package/README.md +57 -69
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertCleanWorkingTree.d.ts +1 -5
  9. package/dist/esm/assertTaggedBaseline.d.ts +29 -0
  10. package/dist/esm/assertTaggedBaseline.js +41 -0
  11. package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
  12. package/dist/esm/attachChangelogDiagnostics.js +14 -0
  13. package/dist/esm/bin/release-kit.js +88 -24
  14. package/dist/esm/buildChangelogEntries.d.ts +69 -17
  15. package/dist/esm/buildChangelogEntries.js +222 -120
  16. package/dist/esm/buildDependencyGraph.d.ts +6 -6
  17. package/dist/esm/buildEmptyReleaseEntry.d.ts +1 -8
  18. package/dist/esm/buildReleaseSummary.d.ts +2 -8
  19. package/dist/esm/buildReleaseSummary.js +2 -2
  20. package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
  21. package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
  22. package/dist/esm/bumpVersion.d.ts +1 -8
  23. package/dist/esm/changelogJsonFile.d.ts +5 -7
  24. package/dist/esm/changelogJsonFile.js +2 -17
  25. package/dist/esm/changelogJsonUtils.d.ts +7 -10
  26. package/dist/esm/changelogOverrides.d.ts +79 -100
  27. package/dist/esm/changelogOverrides.js +138 -58
  28. package/dist/esm/classifyChangelogCommit.d.ts +36 -0
  29. package/dist/esm/classifyChangelogCommit.js +34 -0
  30. package/dist/esm/commitCommand.d.ts +2 -4
  31. package/dist/esm/compareVersions.d.ts +10 -4
  32. package/dist/esm/compareVersions.js +16 -0
  33. package/dist/esm/configFlagSchema.d.ts +1 -1
  34. package/dist/esm/createGithubRelease.d.ts +7 -19
  35. package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
  36. package/dist/esm/createGithubReleaseCommand.js +9 -9
  37. package/dist/esm/createTags.d.ts +2 -3
  38. package/dist/esm/decideRelease.d.ts +11 -41
  39. package/dist/esm/decideRelease.js +5 -27
  40. package/dist/esm/defaults.d.ts +5 -24
  41. package/dist/esm/defaults.js +7 -5
  42. package/dist/esm/defineConfig.d.ts +1 -1
  43. package/dist/esm/deleteFileIfExists.d.ts +1 -1
  44. package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
  45. package/dist/esm/deriveReleaseNotesConfig.js +21 -0
  46. package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
  47. package/dist/esm/detectPackageManager.d.ts +2 -4
  48. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
  49. package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
  50. package/dist/esm/determineBumpType.d.ts +11 -11
  51. package/dist/esm/determineBumpType.js +6 -7
  52. package/dist/esm/discoverWorkspaces.d.ts +14 -15
  53. package/dist/esm/discoverWorkspaces.js +12 -12
  54. package/dist/esm/enterRepoRoot.d.ts +12 -0
  55. package/dist/esm/enterRepoRoot.js +16 -0
  56. package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
  57. package/dist/esm/enumerateReleaseWindows.js +124 -0
  58. package/dist/esm/extractMigration.d.ts +6 -8
  59. package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
  60. package/dist/esm/findUnroutedEntryScopes.js +57 -0
  61. package/dist/esm/formatExcludedSkip.d.ts +3 -0
  62. package/dist/esm/formatExcludedSkip.js +3 -0
  63. package/dist/esm/formatPrivateSkip.d.ts +1 -6
  64. package/dist/esm/generateChangelogs.d.ts +13 -26
  65. package/dist/esm/generateChangelogs.js +0 -14
  66. package/dist/esm/glyphs.d.ts +2 -2
  67. package/dist/esm/help/prepareHelp.d.ts +3 -4
  68. package/dist/esm/help/prepareHelp.js +7 -8
  69. package/dist/esm/index.d.ts +1 -1
  70. package/dist/esm/init/checks.d.ts +3 -3
  71. package/dist/esm/init/checks.js +4 -1
  72. package/dist/esm/init/detectRepoType.d.ts +2 -3
  73. package/dist/esm/init/hasProvenance.d.ts +2 -0
  74. package/dist/esm/init/hasProvenance.js +3 -0
  75. package/dist/esm/init/initCommand.d.ts +3 -6
  76. package/dist/esm/init/initCommand.js +11 -11
  77. package/dist/esm/init/parseJsonRecord.d.ts +1 -4
  78. package/dist/esm/init/scaffold.d.ts +1 -3
  79. package/dist/esm/init/scaffold.js +2 -30
  80. package/dist/esm/init/templates.d.ts +9 -11
  81. package/dist/esm/init/templates.js +12 -11
  82. package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
  83. package/dist/esm/init/updateTemplatesCommand.js +64 -0
  84. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
  85. package/dist/esm/injectSection.d.ts +3 -3
  86. package/dist/esm/loadConfig.d.ts +16 -14
  87. package/dist/esm/loadConfig.js +2 -5
  88. package/dist/esm/loadValidatedConfig.d.ts +16 -19
  89. package/dist/esm/loadValidatedConfig.js +6 -1
  90. package/dist/esm/packsPath.d.ts +9 -9
  91. package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
  92. package/dist/esm/parseChangeRecordBlock.js +134 -0
  93. package/dist/esm/parseCommitMessage.d.ts +42 -32
  94. package/dist/esm/parseCommitMessage.js +33 -41
  95. package/dist/esm/parseRequestedTags.d.ts +3 -3
  96. package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
  97. package/dist/esm/planVersionBump.d.ts +5 -3
  98. package/dist/esm/planVersionBump.js +3 -0
  99. package/dist/esm/prepareCommand.d.ts +4 -7
  100. package/dist/esm/prepareCommand.js +17 -18
  101. package/dist/esm/previewTagPrefixes.d.ts +8 -10
  102. package/dist/esm/propagateBumps.d.ts +6 -7
  103. package/dist/esm/publish.d.ts +1 -1
  104. package/dist/esm/publishCommand.d.ts +4 -3
  105. package/dist/esm/publishCommand.js +8 -8
  106. package/dist/esm/pushCommand.d.ts +4 -3
  107. package/dist/esm/pushCommand.js +8 -2
  108. package/dist/esm/pushRelease.d.ts +2 -2
  109. package/dist/esm/readChangelogSections.d.ts +33 -0
  110. package/dist/esm/readChangelogSections.js +80 -0
  111. package/dist/esm/releaseFiles.d.ts +5 -7
  112. package/dist/esm/releasePlan.d.ts +11 -13
  113. package/dist/esm/releasePrepare.d.ts +25 -25
  114. package/dist/esm/releasePrepare.js +80 -80
  115. package/dist/esm/releasePrepareMono.d.ts +9 -7
  116. package/dist/esm/releasePrepareMono.js +114 -99
  117. package/dist/esm/releasePrepareProject.d.ts +19 -29
  118. package/dist/esm/releasePrepareProject.js +46 -45
  119. package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
  120. package/dist/esm/renderChangelogMarkdown.js +27 -2
  121. package/dist/esm/renderReleaseNotes.d.ts +3 -11
  122. package/dist/esm/renderReleaseNotes.js +2 -6
  123. package/dist/esm/reportPrepare.d.ts +4 -5
  124. package/dist/esm/reportPrepare.js +67 -42
  125. package/dist/esm/resolveCommandTags.d.ts +15 -11
  126. package/dist/esm/resolveCommandTags.js +35 -5
  127. package/dist/esm/resolveConfigFlag.d.ts +7 -0
  128. package/dist/esm/resolveConfigFlag.js +4 -0
  129. package/dist/esm/resolveReleaseTags.d.ts +6 -8
  130. package/dist/esm/resolveReleaseTags.js +1 -1
  131. package/dist/esm/resolveStylesOrExit.d.ts +2 -2
  132. package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
  133. package/dist/esm/showTagPrefixesCommand.js +4 -4
  134. package/dist/esm/stripEmojiPrefix.d.ts +1 -0
  135. package/dist/esm/stripScope.d.ts +2 -2
  136. package/dist/esm/stripScope.js +3 -3
  137. package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
  138. package/dist/esm/sync-labels/generateCommand.js +11 -8
  139. package/dist/esm/sync-labels/initCommand.d.ts +6 -7
  140. package/dist/esm/sync-labels/initCommand.js +2 -2
  141. package/dist/esm/sync-labels/presets.d.ts +5 -4
  142. package/dist/esm/sync-labels/presets.js +9 -9
  143. package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
  144. package/dist/esm/sync-labels/resolveLabels.js +1 -1
  145. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
  146. package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
  147. package/dist/esm/sync-labels/templates.d.ts +3 -3
  148. package/dist/esm/sync-labels/templates.js +3 -3
  149. package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
  150. package/dist/esm/sync-labels/typeLabels.js +38 -0
  151. package/dist/esm/sync-labels/types.d.ts +1 -1
  152. package/dist/esm/tagCommand.d.ts +1 -1
  153. package/dist/esm/typeGuards.d.ts +2 -2
  154. package/dist/esm/types.d.ts +185 -116
  155. package/dist/esm/types.js +2 -2
  156. package/dist/esm/validateConfig.d.ts +2 -11
  157. package/dist/esm/validateConfig.js +5 -1
  158. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
  159. package/dist/esm/validateOverridesCommand.d.ts +19 -21
  160. package/dist/esm/validateOverridesCommand.js +16 -23
  161. package/docs/api.md +6 -6
  162. package/docs/changelogs.md +90 -28
  163. package/docs/configuration.md +30 -29
  164. package/docs/editorial-overrides.md +90 -31
  165. package/docs/labels.md +21 -19
  166. package/docs/project-releases.md +14 -14
  167. package/docs/publishing.md +5 -5
  168. package/docs/readiness-checks.md +3 -3
  169. package/docs/releasing.md +54 -24
  170. package/docs/work-types.md +46 -58
  171. package/package.json +7 -7
  172. package/presets/labels/common.yaml +1 -41
  173. package/schemas/changelog-overrides.json +39 -0
  174. package/cliff.toml.template +0 -65
  175. package/dist/esm/collectPolicyViolations.d.ts +0 -15
  176. package/dist/esm/collectPolicyViolations.js +0 -12
  177. package/dist/esm/determineBumpFromCommits.d.ts +0 -17
  178. package/dist/esm/determineBumpFromCommits.js +0 -28
  179. package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
  180. package/dist/esm/getCommitsSinceTarget.js +0 -66
  181. package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
  182. package/dist/esm/resolveCliffConfigPath.js +0 -20
  183. package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
  184. package/dist/esm/resolveReleaseNotesConfig.js +0 -29
  185. package/dist/esm/runGitCliff.d.ts +0 -38
  186. package/dist/esm/runGitCliff.js +0 -25
  187. package/dist/esm/workTypesData.d.ts +0 -50
  188. package/dist/esm/workTypesData.js +0 -145
  189. package/schemas/label-map.json +0 -24
@@ -0,0 +1,4 @@
1
+ import path from 'node:path';
2
+ export function resolveConfigFlag(config, invocationDir) {
3
+ return config === undefined ? undefined : path.resolve(invocationDir, config);
4
+ }
@@ -4,10 +4,8 @@ export interface ResolvedTag {
4
4
  dir: string;
5
5
  workspacePath: string;
6
6
  /**
7
- * Whether the workspace this tag belongs to can be published to a registry. Propagated
8
- * from the matched `WorkspaceConfig.isPublishable` (monorepo) or from the single-package
9
- * workspace config (single-package mode). Consumed by `release-kit publish` to filter the
10
- * tag set; other commands ignore it.
7
+ * Whether the workspace to which this tag belongs can be published to a registry, copied from the matched
8
+ * workspace's `WorkspaceConfig.isPublishable`.
11
9
  */
12
10
  isPublishable: boolean;
13
11
  }
@@ -18,15 +16,15 @@ type ResolveReleaseTagsArgs = {
18
16
  singleWorkspace: WorkspaceConfig;
19
17
  };
20
18
  /**
21
- * Resolve release tags pointing at HEAD into publishable package descriptors.
19
+ * Resolves release tags pointing at HEAD into publishable package descriptors.
22
20
  *
23
- * Pass `{ workspaces }` for monorepo mode: each tag is matched against the workspace
21
+ * Pass `{ workspaces }` for monorepo mode: Each tag is matched against the workspace
24
22
  * whose `tagPrefix` it starts with, and that workspace's `isPublishable` propagates onto
25
23
  * the `ResolvedTag`. Because `tagPrefix` is derived from `deriveWorkspaceConfig()` (the
26
24
  * same source that produced the tag), encoding and decoding stay colocated.
27
25
  *
28
- * Pass `{ singleWorkspace }` for single-package mode: tags like `v1.2.3` are matched and
29
- * each carries the workspace's `isPublishable` bit derived from `./package.json#private`.
26
+ * Pass `{ singleWorkspace }` for single-package mode: Tags like `v1.2.3` are matched and
27
+ * each has the workspace's `isPublishable` bit derived from `./package.json#private`.
30
28
  *
31
29
  * The no-arg form is a test-isolation convenience for unit tests that exercise tag
32
30
  * matching without needing `isPublishable` propagation; resolved tags default to
@@ -30,7 +30,7 @@ function resolveSinglePackageTags(tags, singleWorkspace) {
30
30
  return matched.map((tag) => ({ tag, dir: '.', workspacePath: '.', isPublishable }));
31
31
  }
32
32
  function resolveMonorepoTags(tags, workspaces) {
33
- const sortedWorkspaces = [...workspaces].sort((a, b) => b.tagPrefix.length - a.tagPrefix.length);
33
+ const sortedWorkspaces = workspaces.toSorted((a, b) => b.tagPrefix.length - a.tagPrefix.length);
34
34
  const resolved = [];
35
35
  for (const tag of tags) {
36
36
  const match = findMatchingWorkspace(tag, sortedWorkspaces);
@@ -1,10 +1,10 @@
1
1
  import { type StreamStyles } from '@williamthorsen/nmr-core';
2
- /** The environment variable holding the caller's standing style preference: `auto`, `plain`, or `rich`. */
2
+ /** The environment variable that sets the caller's standing style preference: `auto`, `plain`, or `rich`. */
3
3
  export declare const OUTPUT_STYLE_ENV_VAR = "RELEASE_KIT_OUTPUT_STYLE";
4
4
  export interface ResolveStylesOptions {
5
5
  readonly env: Readonly<Record<string, string | undefined>>;
6
6
  readonly stderrIsTty: boolean;
7
7
  readonly stdoutIsTty: boolean;
8
8
  }
9
- /** Resolves the style of each output stream, exiting with a usage error when the variable names no setting. */
9
+ /** Resolves the style of each output stream, exiting with a usage error when the variable does not name a setting. */
10
10
  export declare function resolveStylesOrExit(options: ResolveStylesOptions): StreamStyles;
@@ -1,15 +1,12 @@
1
1
  import { type StreamStyles } from '@williamthorsen/nmr-core';
2
2
  /**
3
- * Orchestrate the CLI `show-tag-prefixes` command.
3
+ * Runs the CLI `show-tag-prefixes` command and returns its exit code.
4
4
  *
5
- * Prints a per-workspace table of derived prefixes, tag counts, and declared legacy
6
- * entries, followed by an "Undeclared tag prefixes" section when candidate-shaped tags
7
- * exist outside the known set. Exits `0` on full derivation success and no collisions;
8
- * `1` on any derivation failure or collision. Undeclared candidates do not affect the
9
- * exit code. A config that exists and either fails to load or fails validation reports to stderr and
10
- * exits `1` in both repo modes, rather than falling back to a default-config preview that would
11
- * understate the declared legacy prefixes. An absent default config previews against derived defaults.
12
- *
13
- * @returns The exit code the caller should use.
5
+ * Prints a per-workspace table of derived prefixes, tag counts, and declared legacy entries, followed by an
6
+ * "Undeclared tag prefixes" section when candidate-shaped tags exist outside the known set. Returns `1` on any
7
+ * derivation failure or collision and `0` otherwise; undeclared candidates do not affect the exit code. When a config
8
+ * exists and either fails to load or fails validation, the command reports it to stderr and returns `1` in both repo
9
+ * modes, because a default-config preview would understate the declared legacy prefixes. When the default config is
10
+ * absent, the command previews against derived defaults.
14
11
  */
15
12
  export declare function showTagPrefixesCommand(styles: StreamStyles, configPath?: string): Promise<number>;
@@ -41,7 +41,7 @@ function renderMonorepo(preview, style) {
41
41
  lines.push('', ...preview.collisions.map((collision) => formatStatusLine(style, 'failed', `tag prefix collision: '${collision.tagPrefix}' used by ${collision.workspacePaths.join(', ')}`)));
42
42
  }
43
43
  if (preview.undeclaredCandidates.length > 0) {
44
- lines.push('', 'Undeclared tag prefixes:', '', ...preview.undeclaredCandidates.map((candidate) => ` '${candidate.prefix}' — ${candidate.tagCount} tags (e.g., ${candidate.exampleTags.join(', ')})`), '', 'Suggested config snippet (adjust `dir` to match your workspace if the guess is wrong, and replace the `name` placeholder with the legacy npm name):', '', renderSuggestedSnippet(preview.undeclaredCandidates), '', "If the suggested `dir` does not match your workspace, adjust before pasting. Each legacy identity requires a `name` — replace the `TODO-fill-in-legacy-npm-name` placeholder with the package's prior npm name.");
44
+ lines.push('', 'Undeclared tag prefixes:', '', ...preview.undeclaredCandidates.map((candidate) => ` '${candidate.prefix}': ${candidate.tagCount} tags (e.g., ${candidate.exampleTags.join(', ')})`), '', 'Suggested config snippet (adjust `dir` to match the workspace if the guess is wrong, and replace the `name` placeholder with the legacy npm name):', '', renderSuggestedSnippet(preview.undeclaredCandidates), '', "If the suggested `dir` does not match the workspace, adjust it before pasting. Each legacy identity requires a `name`: Replace the `TODO-fill-in-legacy-npm-name` placeholder with the package's prior npm name.");
45
45
  }
46
46
  lines.push('');
47
47
  return lines.join('\n');
@@ -50,20 +50,20 @@ function renderWorkspaceRow(row, style) {
50
50
  const lines = [];
51
51
  if (row.derivedPrefix === null) {
52
52
  const failure = formatStatusLine(style, 'failed', `derivation failed: ${row.derivationError ?? 'unknown error'}`);
53
- lines.push(` ${row.workspacePath} — ${failure}`);
53
+ lines.push(` ${row.workspacePath}: ${failure}`);
54
54
  return lines;
55
55
  }
56
56
  const statusMarker = row.derivedTagCount > 0
57
57
  ? formatStatusLine(style, 'passed', `${row.derivedTagCount} tags`)
58
58
  : formatStatusLine(style, 'warning', 'no existing tags');
59
- lines.push(` ${row.workspacePath} — derived prefix '${row.derivedPrefix}', ${statusMarker}`);
59
+ lines.push(` ${row.workspacePath}: derived prefix '${row.derivedPrefix}', ${statusMarker}`);
60
60
  for (const entry of row.legacyEntries) {
61
61
  if (entry.tagCount > 0) {
62
62
  const recognized = `${entry.tagCount} legacy tags with '${entry.prefix}' prefix (recognized)`;
63
63
  lines.push(` ${formatStatusLine(style, 'passed', recognized)}`);
64
64
  }
65
65
  else {
66
- const tagless = `recorded legacy prefix '${entry.prefix}' has no tags`;
66
+ const tagless = `recorded legacy prefix '${entry.prefix}' does not have any tags`;
67
67
  lines.push(` ${formatStatusLine(style, 'warning', tagless)}`);
68
68
  }
69
69
  }
@@ -1 +1,2 @@
1
+ /** Removes a leading emoji and one space from a section title, returning the title unchanged when it has none. */
1
2
  export declare function stripEmojiPrefix(value: string): string;
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Strip scope indicators from a raw commit message.
2
+ * Strips scope indicators from a raw commit message.
3
3
  *
4
4
  * Handles `scope|type: desc`, `type(scope): desc`, and messages with
5
5
  * ticket prefixes (e.g., `#72 release-kit|fix: ...`). Returns the
6
- * message unchanged when no scope is detected.
6
+ * message unchanged when it does not detect a scope.
7
7
  */
8
8
  export declare function stripScope(message: string): string;
@@ -1,9 +1,9 @@
1
- import { COMMIT_PREPROCESSOR_PATTERNS, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
2
- const PIPE_SCOPE_PATTERN = new RegExp(String.raw `^${PIPE_SCOPE_SOURCE}\|(.*)$`);
1
+ import { TICKET_PREFIX_PATTERNS } from '@williamthorsen/change-grammar';
2
+ const PIPE_SCOPE_PATTERN = /^[^|]+\|(.*)$/;
3
3
  export function stripScope(message) {
4
4
  let ticketPrefix = '';
5
5
  let remainder = message;
6
- for (const pattern of COMMIT_PREPROCESSOR_PATTERNS) {
6
+ for (const pattern of TICKET_PREFIX_PATTERNS) {
7
7
  const match = remainder.match(pattern);
8
8
  if (match) {
9
9
  ticketPrefix += match[0];
@@ -4,34 +4,35 @@ import type { LabelDefinition } from './types.js';
4
4
  /** Output path for the generated labels file. */
5
5
  export declare const LABELS_OUTPUT_PATH = ".github/labels.yaml";
6
6
  /**
7
- * Formats resolved labels as YAML with generated header including preset hashes.
7
+ * Formats resolved labels as YAML under a generated header that includes the preset hashes.
8
8
  *
9
- * `configFilePath` is recorded in the header so that a reader of the generated file can find the config it came
10
- * from; it defaults to `CONFIG_FILE_PATH`.
9
+ * The header records `configFilePath` so that a reader of the generated file can find the config from which it came.
11
10
  */
12
11
  export declare function formatLabelsYaml(labels: LabelDefinition[], presetHashes: Map<string, string>, configFilePath?: string): string;
13
12
  /**
14
13
  * Loads the config file and returns its `repoLabels` block.
15
14
  *
16
15
  * Reports an actionable error and returns `undefined` when the config file is missing,
17
- * fails to load or validate, or carries no `repoLabels` block. Every message names the file
18
- * this run read, so that `--config` never points the reader at the default path.
16
+ * fails to load or validate, or lacks a `repoLabels` block. Every message names the file
17
+ * that this run read, so that `--config` never points the reader at the default path.
19
18
  */
20
19
  export declare function loadRepoLabelsConfig(stderrStyle: OutputStyle, configPath?: string): Promise<RepoLabelsConfig | undefined>;
21
20
  /** Options for the `sync-labels generate` subcommand. */
22
21
  interface GenerateOptions {
23
- /** Compare the regenerated content against the committed file instead of writing. */
22
+ /** Compares the regenerated content against the committed file instead of writing. */
24
23
  check?: boolean;
25
24
  /** Config file to read, relative to the working directory. Defaults to `CONFIG_FILE_PATH`. */
26
25
  configPath?: string;
27
26
  styles: StreamStyles;
28
27
  }
29
28
  /**
30
- * Runs the `sync-labels generate` command.
29
+ * Runs the `sync-labels generate` command and returns the process exit code.
31
30
  *
32
- * Loads the `repoLabels` block from the config file, resolves presets and label adjustments, and writes
33
- * `.github/labels.yaml` -- or, with `check`, regenerates in memory and reports whether the committed file
34
- * is stale, writing nothing. Returns 0 on success, 1 on failure.
31
+ * Writes `.github/labels.yaml` from the config's `repoLabels` block, or, with `check`, reports whether the committed
32
+ * file is stale and writes nothing.
33
+ *
34
+ * The header names the config by its POSIX path relative to the working directory, the repo root, so that the file
35
+ * does not depend on the invocation directory or on the machine on which the command ran.
35
36
  */
36
37
  export declare function generateCommand({ check, configPath, styles }: GenerateOptions): Promise<number>;
37
38
  export {};
@@ -1,17 +1,17 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname } from 'node:path';
2
+ import path from 'node:path';
3
3
  import { reportError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { stringify } from 'yaml';
6
6
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
7
7
  import { loadValidatedConfig, reportConfigProblem } from "../loadValidatedConfig.js";
8
- import { hashPresetFile } from "./presets.js";
8
+ import { hashPreset } from "./presets.js";
9
9
  import { resolveLabels } from "./resolveLabels.js";
10
10
  import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
11
11
  export const LABELS_OUTPUT_PATH = '.github/labels.yaml';
12
12
  export function formatLabelsYaml(labels, presetHashes, configFilePath = CONFIG_FILE_PATH) {
13
13
  const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${configFilePath}`];
14
- const sortedPresetHashes = [...presetHashes].sort(([a], [b]) => a.localeCompare(b));
14
+ const sortedPresetHashes = [...presetHashes].toSorted(([a], [b]) => a.localeCompare(b));
15
15
  for (const [name, hash] of sortedPresetHashes) {
16
16
  headerLines.push(`# ${name} preset hash: ${hash}`);
17
17
  }
@@ -22,7 +22,7 @@ export function formatLabelsYaml(labels, presetHashes, configFilePath = CONFIG_F
22
22
  export async function loadRepoLabelsConfig(stderrStyle, configPath) {
23
23
  const result = await loadValidatedConfig(configPath);
24
24
  if (result.status === 'missing') {
25
- reportError(`No config file found at ${result.configFilePath}. Run \`release-kit sync-labels init\` first.`);
25
+ reportError(`Config file not found at ${result.configFilePath}. Run \`release-kit sync-labels init\` first.`);
26
26
  return undefined;
27
27
  }
28
28
  if (result.status === 'invalid') {
@@ -30,7 +30,7 @@ export async function loadRepoLabelsConfig(stderrStyle, configPath) {
30
30
  return undefined;
31
31
  }
32
32
  if (result.config.repoLabels === undefined) {
33
- reportError(`No \`repoLabels\` block found in ${result.configFilePath}. Run \`release-kit sync-labels init\` to seed one.`);
33
+ reportError(`The \`repoLabels\` block is missing from ${result.configFilePath}. Run \`release-kit sync-labels init\` to seed one.`);
34
34
  return undefined;
35
35
  }
36
36
  return result.config.repoLabels;
@@ -55,14 +55,14 @@ export async function generateCommand({ check = false, configPath, styles }) {
55
55
  const presetHashes = new Map();
56
56
  const presetNames = repoLabels.extends ?? [];
57
57
  for (const presetName of presetNames) {
58
- presetHashes.set(presetName, hashPresetFile(presetName));
58
+ presetHashes.set(presetName, hashPreset(presetName));
59
59
  }
60
- const content = formatLabelsYaml(labels, presetHashes, configPath);
60
+ const content = formatLabelsYaml(labels, presetHashes, toRootRelativePosixPath(configPath ?? CONFIG_FILE_PATH));
61
61
  if (check) {
62
62
  return compareAgainstCommittedFile(content);
63
63
  }
64
64
  try {
65
- mkdirSync(dirname(LABELS_OUTPUT_PATH), { recursive: true });
65
+ mkdirSync(path.dirname(LABELS_OUTPUT_PATH), { recursive: true });
66
66
  writeFileSync(LABELS_OUTPUT_PATH, content, 'utf8');
67
67
  }
68
68
  catch (error) {
@@ -89,3 +89,6 @@ function compareAgainstCommittedFile(expected) {
89
89
  console.info(`${LABELS_OUTPUT_PATH} is up to date.`);
90
90
  return 0;
91
91
  }
92
+ function toRootRelativePosixPath(filePath) {
93
+ return path.relative(process.cwd(), path.resolve(filePath)).split(path.sep).join('/');
94
+ }
@@ -8,15 +8,14 @@ interface InitOptions {
8
8
  styles: StreamStyles;
9
9
  }
10
10
  /**
11
- * Run the `sync-labels init` subcommand.
11
+ * Runs the `sync-labels init` subcommand and returns the process exit code.
12
12
  *
13
- * Discovers workspaces and retired packages, scaffolds the caller workflow, then seeds the
14
- * `repoLabels` block: into a new config file when none exists, or printed to stdout for manual
15
- * paste when the file is already there — an existing, hand-authored config is never rewritten.
16
- * Returns 0 on success, 1 on failure.
13
+ * Scaffolds the caller workflow and seeds the `repoLabels` block with scope labels for the workspaces and retired
14
+ * packages. The block goes into a new config file when none exists; an existing config is never rewritten, so the
15
+ * command prints the block for manual paste instead.
17
16
  *
18
- * `configPath` names the config to read. Naming one that does not exist fails the command, so the
19
- * scaffolding branch is reached only under the default path and always writes to `CONFIG_FILE_PATH`.
17
+ * `configPath` names the config to read. Because the command fails when the named config does not exist, it
18
+ * reaches the scaffolding branch only under the default path, and that branch always writes to `CONFIG_FILE_PATH`.
20
19
  */
21
20
  export declare function syncLabelsInitCommand({ configPath, dryRun, force, styles }: InitOptions): Promise<number>;
22
21
  export {};
@@ -57,7 +57,7 @@ export async function syncLabelsInitCommand({ configPath, dryRun, force, styles
57
57
  1. Paste the block above into ${configFilePath}.
58
58
  2. Run \`release-kit sync-labels generate\` to produce ${LABELS_OUTPUT_PATH}.
59
59
  3. Commit the changes.
60
- 4. Run \`release-kit sync-labels sync\` to apply labels to your GitHub repo.
60
+ 4. Run \`release-kit sync-labels sync\` to apply labels to the GitHub repo.
61
61
  `);
62
62
  return 0;
63
63
  }
@@ -88,7 +88,7 @@ export async function syncLabelsInitCommand({ configPath, dryRun, force, styles
88
88
  - ${LABELS_OUTPUT_PATH}
89
89
  2. Customize the \`repoLabels\` block in ${CONFIG_FILE_PATH} as needed, then re-run \`release-kit sync-labels generate\`.
90
90
  3. Commit the generated files.
91
- 4. Run \`release-kit sync-labels sync\` to apply labels to your GitHub repo.
91
+ 4. Run \`release-kit sync-labels sync\` to apply labels to the GitHub repo.
92
92
  `);
93
93
  return 0;
94
94
  }
@@ -1,9 +1,10 @@
1
1
  import type { LabelDefinition } from './types.js';
2
- /** Computes SHA-256 hex digest of a preset file's raw content. Throws if the preset does not exist. */
3
- export declare function hashPresetFile(presetName: string): string;
2
+ /** Computes the SHA-256 hex digest of a preset's resolved labels. Throws as `loadPreset` does. */
3
+ export declare function hashPreset(presetName: string): string;
4
4
  /**
5
- * Loads a named preset from the bundled YAML files.
5
+ * Loads a named preset from the bundled YAML files. The `common` preset also defines the label of every work type in
6
+ * change-grammar's taxonomy, ahead of the labels in its file.
6
7
  *
7
- * Returns the parsed label definitions. Throws if the preset does not exist or has invalid content.
8
+ * Throws if the preset does not exist or has invalid content.
8
9
  */
9
10
  export declare function loadPreset(presetName: string): LabelDefinition[];
@@ -1,22 +1,22 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
+ import { CANONICAL_TAXONOMY } from '@williamthorsen/change-grammar';
4
5
  import { findPackageRoot } from '@williamthorsen/nmr-core';
5
6
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
6
7
  import { parse } from 'yaml';
7
8
  import { isRecord } from "../typeGuards.js";
8
- export function hashPresetFile(presetName) {
9
- const presetPath = resolvePresetPath(presetName);
10
- if (!existsSync(presetPath)) {
11
- throw new Error(`Unknown preset "${presetName}". No file found at ${presetPath}`);
12
- }
13
- const content = readFileSync(presetPath, 'utf8');
14
- return createHash('sha256').update(content).digest('hex');
9
+ import { deriveTypeLabels } from "./typeLabels.js";
10
+ const TYPE_LABELS_PRESET = 'common';
11
+ export function hashPreset(presetName) {
12
+ return createHash('sha256')
13
+ .update(JSON.stringify(loadPreset(presetName)))
14
+ .digest('hex');
15
15
  }
16
16
  export function loadPreset(presetName) {
17
17
  const presetPath = resolvePresetPath(presetName);
18
18
  if (!existsSync(presetPath)) {
19
- throw new Error(`Unknown preset "${presetName}". No file found at ${presetPath}`);
19
+ throw new Error(`Unknown preset "${presetName}". The file ${presetPath} does not exist`);
20
20
  }
21
21
  let content;
22
22
  try {
@@ -42,7 +42,7 @@ export function loadPreset(presetName) {
42
42
  }
43
43
  labels.push({ name, color, ...(description !== undefined && { description }) });
44
44
  }
45
- return labels;
45
+ return presetName === TYPE_LABELS_PRESET ? [...deriveTypeLabels(CANONICAL_TAXONOMY), ...labels] : labels;
46
46
  }
47
47
  function resolvePresetPath(presetName) {
48
48
  const root = findPackageRoot(import.meta.url);
@@ -3,13 +3,13 @@ import type { LabelDefinition } from './types.js';
3
3
  /**
4
4
  * Resolves the repository's label set from a `repoLabels` config block.
5
5
  *
6
- * Resolution is an ordered fold with last-writer-wins: presets are loaded in `extends` order (a later preset replaces
6
+ * Resolution is an ordered fold with last-writer-wins: Presets are loaded in `extends` order (a later preset replaces
7
7
  * an earlier preset's label of the same name), then the `labels` record is applied: An entry adds a label, replaces
8
- * one an earlier layer defined, or removes it (`null`). Names match case-insensitively, as GitHub matches them, and the
9
- * replacing layer's spelling becomes the label's name. Replacement is wholesale, so an entry omitting `description`
10
- * drops the one an earlier layer supplied rather than inheriting it.
11
- * Throws on two misstatements that would otherwise leave no trace in the output diff: keys in the `labels` record that
12
- * differ only in case, all but one of which the fold would discard, and a dangling `null`, a removal naming a label that
13
- * no preset defined.
8
+ * one that an earlier layer defined, or removes it (`null`). Names match case-insensitively, as GitHub matches them,
9
+ * and the replacing layer's spelling becomes the label's name. Replacement is wholesale, so an entry omitting
10
+ * `description` drops the one that an earlier layer supplied rather than inheriting it.
11
+ * Throws on two misstatements that the output diff would otherwise not show: keys in the `labels` record that differ
12
+ * only in case, all but one of which the fold would discard, and a dangling `null`, a removal naming a label that the
13
+ * `extends` presets did not define.
14
14
  */
15
15
  export declare function resolveLabels(config: RepoLabelsConfig): LabelDefinition[];
@@ -17,7 +17,7 @@ export function resolveLabels(config) {
17
17
  const key = toLabelKey(name);
18
18
  if (spec === null) {
19
19
  if (!resolved.has(key)) {
20
- throw new Error(`Label '${name}' is set to null (remove), but no preset in 'extends' defines it. Fix the name or delete the entry.`);
20
+ throw new Error(`Label '${name}' is set to null (remove), but the presets in 'extends' don't define it. Fix the name or delete the entry.`);
21
21
  }
22
22
  resolved.delete(key);
23
23
  }
@@ -1,12 +1,11 @@
1
1
  /** Path of the retired standalone sync-labels config file. */
2
2
  export declare const RETIRED_SYNC_LABELS_CONFIG_PATH = ".config/sync-labels.config.ts";
3
3
  /**
4
- * Report an actionable migration error when the retired standalone config file is present.
4
+ * Reports an actionable migration error when the retired standalone config file is present.
5
5
  *
6
6
  * Returns `true` when the file exists, in which case the caller must abort. The check is
7
- * unconditional on config content: file presence alone triggers it, so a half-migrated
8
- * repo cannot silently lose its custom labels. `configPath` names the config to move the labels
9
- * into; it defaults to `CONFIG_FILE_PATH`, which `sync-labels sync` relies on because it takes
10
- * no `--config`.
7
+ * unconditional on config content: File presence alone triggers it, so a half-migrated
8
+ * repo cannot silently lose its custom labels. `configPath` names the config into which the
9
+ * message directs the labels.
11
10
  */
12
11
  export declare function checkRetiredSyncLabelsConfig(configPath?: string): boolean;
@@ -1,7 +1,4 @@
1
1
  /**
2
- * Run the `sync-labels sync` subcommand.
3
- *
4
- * Validates prerequisites (gh CLI, workflow file), then triggers the sync-labels workflow
5
- * via `gh workflow run`. Returns 0 on success, 1 on failure.
2
+ * Runs the `sync-labels sync` subcommand, which triggers the sync-labels workflow, and returns the process exit code.
6
3
  */
7
4
  export declare function syncLabelsCommand(): number;
@@ -5,8 +5,8 @@ export declare function syncLabelsWorkflow(): string;
5
5
  * Generates scope labels from workspace paths and retired-package names.
6
6
  *
7
7
  * Workspace labels are named after the path basename; retired-package labels after the unscoped package name.
8
- * None carries a description: GitHub renders a described label taller and more prominently than a bare one, and
9
- * the scope labels are meant to read as a compact group.
8
+ * None has a description: GitHub renders a described label taller and more prominently than a bare one, and
9
+ * the scope labels are meant to appear as a compact group.
10
10
  * Describing only the retired scopes would single them out for that prominence.
11
11
  */
12
12
  export declare function buildScopeLabels(workspacePaths: string[], retiredNames?: string[]): LabelDefinition[];
@@ -15,5 +15,5 @@ export declare function buildScopeLabels(workspacePaths: string[], retiredNames?
15
15
  * `.config/release-kit.config.ts`.
16
16
  */
17
17
  export declare function renderRepoLabelsBlock(scopeLabels: LabelDefinition[]): string;
18
- /** Generates a new `.config/release-kit.config.ts` carrying the `repoLabels` block. */
18
+ /** Generates a new `.config/release-kit.config.ts` containing the `repoLabels` block. */
19
19
  export declare function repoLabelsConfigScript(scopeLabels: LabelDefinition[]): string;
@@ -6,7 +6,7 @@ name: Sync labels
6
6
  on:
7
7
  workflow_dispatch:
8
8
 
9
- # Apply on merge, so that a regenerated labels file cannot sit unapplied.
9
+ # Apply on merge, so that the repository's labels match each merged version of the labels file.
10
10
  push:
11
11
  paths:
12
12
  - .github/labels.yaml
@@ -19,8 +19,8 @@ on:
19
19
  # Permissions are fixed when a run is created and cannot vary by trigger within a job,
20
20
  # so applying and previewing are separate jobs.
21
21
  jobs:
22
- # The push arm applies on the default branch alone. A \`branches:\` filter cannot name the default branch,
23
- # so that the gate lives on the job and this file stays identical across repos.
22
+ # The push arm applies on the default branch alone. The job's \`if:\` names that branch through the event payload,
23
+ # which keeps this file identical across repos; a \`branches:\` filter would have to name it literally.
24
24
  sync:
25
25
  if: github.event_name != 'pull_request' && (github.event_name != 'push' || github.ref_name == github.event.repository.default_branch)
26
26
  permissions:
@@ -0,0 +1,14 @@
1
+ import type { CanonicalWorkTypeEntry } from '@williamthorsen/change-grammar';
2
+ import type { LabelDefinition } from './types.js';
3
+ /**
4
+ * Builds one label per work type, named by the type's tracker label. A type without a presentation row gets its
5
+ * tier's default color and does not get a description.
6
+ */
7
+ export declare function deriveTypeLabels(taxonomy: {
8
+ types: readonly Pick<CanonicalWorkTypeEntry, 'key' | 'tier' | 'trackerLabel'>[];
9
+ }): LabelDefinition[];
10
+ /** Color and description of each work type's label, keyed by work-type key. Descriptions fit GitHub's 100 characters. */
11
+ export declare const TYPE_LABEL_PRESENTATION: Readonly<Record<string, {
12
+ color: string;
13
+ description: string;
14
+ }>>;
@@ -0,0 +1,38 @@
1
+ const FALLBACK_COLOR = 'ededed';
2
+ const TIER_DEFAULT_COLORS = {
3
+ internal: '1d76db',
4
+ process: 'edc287',
5
+ public: '0075ca',
6
+ };
7
+ export function deriveTypeLabels(taxonomy) {
8
+ return taxonomy.types.map(({ key, tier, trackerLabel }) => {
9
+ const presentation = TYPE_LABEL_PRESENTATION[key];
10
+ if (presentation === undefined) {
11
+ return { name: trackerLabel, color: TIER_DEFAULT_COLORS[tier] ?? FALLBACK_COLOR };
12
+ }
13
+ return { name: trackerLabel, ...presentation };
14
+ });
15
+ }
16
+ export const TYPE_LABEL_PRESENTATION = {
17
+ ai: {
18
+ color: 'a2eeef',
19
+ description: 'Guidance on working in this repo, such as its AGENTS.md or a repo-local skill, or its agent config',
20
+ },
21
+ ci: { color: 'edc287', description: 'CI/CD tooling & configuration' },
22
+ deprecate: { color: 'fbca04', description: 'Marks functionality for future removal' },
23
+ deps: { color: 'edc287', description: 'Change to dependencies' },
24
+ docs: {
25
+ color: 'a2eeef',
26
+ description: 'Documentation for human readers, such as a README, a guide, or a code comment',
27
+ },
28
+ drop: { color: 'd93f0b', description: 'Removes functionality that consumers depend on' },
29
+ feat: { color: '0075ca', description: 'Added or improved external functionality' },
30
+ fix: { color: 'd73a4a', description: 'Fixes a bug' },
31
+ fmt: { color: 'edc287', description: 'Formatting only, with no change to content' },
32
+ internal: { color: '1d76db', description: 'Internal change without external impact' },
33
+ perf: { color: '0075ca', description: 'Improves performance without changing behavior' },
34
+ refactor: { color: 'edc287', description: 'Improvement to code without change in functionality' },
35
+ sec: { color: 'b60205', description: 'Security vulnerability or hardening' },
36
+ tests: { color: 'edc287', description: 'Tests' },
37
+ tooling: { color: 'edc287', description: 'Development tools' },
38
+ };
@@ -4,6 +4,6 @@ export interface LabelDefinition {
4
4
  name: string;
5
5
  /** Hex color code without the leading `#`. */
6
6
  color: string;
7
- /** Short description shown in the GitHub UI. Absent when the label carries none. */
7
+ /** Short description shown in the GitHub UI. Absent when the label has none. */
8
8
  description?: string;
9
9
  }
@@ -1,3 +1,3 @@
1
1
  import { type StreamStyles } from '@williamthorsen/nmr-core';
2
- /** Orchestrate the CLI `tag` command: parse flags and delegate to `createTags`. */
2
+ /** Runs the CLI `tag` command: parses flags and delegates to `createTags`. */
3
3
  export declare function tagCommand(argv: string[], styles: StreamStyles): void;
@@ -1,4 +1,4 @@
1
- /** Check whether a value is a plain object (non-null, non-array). */
1
+ /** Checks whether a value is a plain object (non-null, non-array). */
2
2
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
- /** Narrow an `unknown` value to `unknown[]` via `Array.isArray`, avoiding `any[]`. */
3
+ /** Narrows an `unknown` value to `unknown[]` via `Array.isArray`, avoiding `any[]`. */
4
4
  export declare function isUnknownArray(value: unknown): value is unknown[];