@williamthorsen/release-kit 18.1.1 → 18.3.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 (95) hide show
  1. package/.meta/changelog.json +150 -0
  2. package/.readyup/kits/default.js +596 -32
  3. package/.readyup/kits/npm-auto-publish.js +1 -1
  4. package/.readyup/manifest.json +55 -33
  5. package/CHANGELOG.md +44 -0
  6. package/README.md +19 -2
  7. package/dist/esm/assertCleanWorkingTree.d.ts +2 -1
  8. package/dist/esm/assertCleanWorkingTree.js +4 -8
  9. package/dist/esm/assertReleasesOnNpm.js +2 -9
  10. package/dist/esm/assertTarballsComplete.js +2 -9
  11. package/dist/esm/buildChangelogEntries.d.ts +5 -3
  12. package/dist/esm/buildChangelogEntries.js +9 -4
  13. package/dist/esm/buildChangelogItem.d.ts +1 -1
  14. package/dist/esm/buildDependencyGraph.js +2 -16
  15. package/dist/esm/buildReleasedResult.d.ts +15 -3
  16. package/dist/esm/buildReleasedResult.js +23 -2
  17. package/dist/esm/changelogJsonFile.d.ts +3 -2
  18. package/dist/esm/changelogJsonFile.js +14 -17
  19. package/dist/esm/changelogJsonUtils.d.ts +22 -2
  20. package/dist/esm/changelogJsonUtils.js +19 -10
  21. package/dist/esm/cli/maintenanceCommands.js +1 -1
  22. package/dist/esm/commitCommand.d.ts +2 -1
  23. package/dist/esm/commitCommand.js +8 -11
  24. package/dist/esm/configSchema.d.ts +3 -1
  25. package/dist/esm/configSchema.js +1 -0
  26. package/dist/esm/createGithubRelease.d.ts +13 -10
  27. package/dist/esm/createGithubRelease.js +9 -13
  28. package/dist/esm/createGithubReleaseCommand.d.ts +1 -1
  29. package/dist/esm/createGithubReleaseCommand.js +25 -3
  30. package/dist/esm/createTags.d.ts +2 -1
  31. package/dist/esm/createTags.js +12 -12
  32. package/dist/esm/decideDirectReleases.d.ts +2 -1
  33. package/dist/esm/decideDirectReleases.js +5 -5
  34. package/dist/esm/decideRelease.d.ts +1 -1
  35. package/dist/esm/defaults.d.ts +4 -2
  36. package/dist/esm/defaults.js +1 -0
  37. package/dist/esm/deriveDirectoryConfig.js +5 -4
  38. package/dist/esm/deriveWorkspaceConfig.js +2 -9
  39. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +2 -1
  40. package/dist/esm/detectUndeclaredTagPrefixes.js +4 -13
  41. package/dist/esm/determineBumpType.d.ts +2 -1
  42. package/dist/esm/determineBumpType.js +5 -0
  43. package/dist/esm/enumerateReleaseWindows.d.ts +3 -11
  44. package/dist/esm/enumerateReleaseWindows.js +17 -60
  45. package/dist/esm/findUnroutedEntryScopes.d.ts +1 -1
  46. package/dist/esm/git.d.ts +61 -0
  47. package/dist/esm/git.js +101 -0
  48. package/dist/esm/init/checks.d.ts +2 -1
  49. package/dist/esm/init/checks.js +5 -10
  50. package/dist/esm/init/eligibility.d.ts +12 -0
  51. package/dist/esm/init/eligibility.js +50 -0
  52. package/dist/esm/init/initCommand.d.ts +0 -2
  53. package/dist/esm/init/initCommand.js +4 -45
  54. package/dist/esm/init/managed-workflows.d.ts +10 -0
  55. package/dist/esm/init/managed-workflows.js +18 -0
  56. package/dist/esm/init/templates.js +2 -2
  57. package/dist/esm/init/updateTemplatesCommand.js +9 -42
  58. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +29 -7
  59. package/dist/esm/injectReleaseNotesIntoReadme.js +13 -21
  60. package/dist/esm/loadConfig.js +3 -16
  61. package/dist/esm/mergeConfig.d.ts +2 -1
  62. package/dist/esm/packFromCleanCheckout.js +3 -9
  63. package/dist/esm/planVersionBump.js +2 -16
  64. package/dist/esm/prepareCommand.js +2 -1
  65. package/dist/esm/prepareComposite.js +1 -1
  66. package/dist/esm/prepareReleaseTargets.d.ts +2 -1
  67. package/dist/esm/prepareReleaseTargets.js +3 -2
  68. package/dist/esm/prepareRunContext.d.ts +2 -0
  69. package/dist/esm/prepareTarget.d.ts +6 -4
  70. package/dist/esm/prepareTarget.js +9 -3
  71. package/dist/esm/previewTagPrefixes.d.ts +9 -2
  72. package/dist/esm/previewTagPrefixes.js +22 -22
  73. package/dist/esm/publishCommand.d.ts +1 -1
  74. package/dist/esm/publishCommand.js +27 -2
  75. package/dist/esm/pushRelease.d.ts +2 -1
  76. package/dist/esm/pushRelease.js +11 -11
  77. package/dist/esm/readJsonFile.d.ts +2 -0
  78. package/dist/esm/readJsonFile.js +17 -0
  79. package/dist/esm/reportPrepare.js +68 -114
  80. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  81. package/dist/esm/resolveReleaseTags.js +4 -12
  82. package/dist/esm/showTagPrefixesCommand.d.ts +6 -6
  83. package/dist/esm/showTagPrefixesCommand.js +94 -39
  84. package/dist/esm/sync-labels/generateCommand.js +0 -4
  85. package/dist/esm/sync-labels/initCommand.js +0 -4
  86. package/dist/esm/sync-labels/syncCommand.js +0 -4
  87. package/dist/esm/types.d.ts +8 -2
  88. package/docs/configuration.md +15 -6
  89. package/docs/labels.md +2 -2
  90. package/docs/releasing.md +3 -2
  91. package/docs/work-types.md +2 -0
  92. package/package.json +7 -7
  93. package/presets/labels/common.yaml +14 -0
  94. package/dist/esm/sync-labels/retiredConfig.d.ts +0 -11
  95. package/dist/esm/sync-labels/retiredConfig.js +0 -12
@@ -19,42 +19,12 @@ function formatSingleWorkspace(result, options) {
19
19
  if (workspace === undefined || workspace.kind === 'composite') {
20
20
  return '';
21
21
  }
22
- const sinceLabel = workspace.previousTag === undefined ? 'the beginning' : workspace.previousTag;
23
- lines.push(dim(`Found ${workspace.commitCount} commits since ${sinceLabel}`));
24
- if (workspace.parsedCommitCount !== undefined) {
25
- lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
26
- }
27
- formatUnparseableWarning(lines, workspace, style);
28
- formatPolicyViolations(lines, workspace.policyViolations, style);
22
+ const sinceLabel = `since ${workspace.previousTag ?? 'the beginning'}`;
23
+ formatTargetSection(lines, workspace, { indent: '', sinceLabel }, isDryRun, style);
29
24
  if (workspace.status === 'skipped') {
30
- formatChangeRecordWarnings(lines, workspace, style);
31
- lines.push(formatStatusLine(style, 'skipped', workspace.skipReason));
32
25
  formatWarnings(lines, result, style);
33
26
  return lines.join('\n');
34
27
  }
35
- if (workspace.versionOverride !== undefined) {
36
- lines.push(` Using version override: ${workspace.versionOverride}`);
37
- }
38
- else if (workspace.bumpOverride !== undefined) {
39
- lines.push(` Using bump override: ${workspace.bumpOverride}`);
40
- }
41
- if (workspace.releaseType !== undefined) {
42
- lines.push(dim(`Bumping versions (${workspace.releaseType})...`));
43
- }
44
- else if (workspace.versionOverride !== undefined) {
45
- lines.push(dim(`Bumping versions (version override)...`));
46
- }
47
- if (workspace.versionOverride !== undefined) {
48
- lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (version override)`));
49
- }
50
- else if (workspace.releaseType !== undefined) {
51
- lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType})`));
52
- }
53
- formatChangeRecordWarnings(lines, workspace, style);
54
- formatBumpFiles(lines, workspace, isDryRun);
55
- lines.push(dim('Generating changelogs...'));
56
- formatChangelogFiles(lines, workspace, isDryRun, style);
57
- formatPreviewFiles(lines, workspace, isDryRun);
58
28
  formatFormatCommand(lines, result, options);
59
29
  formatWarnings(lines, result, style);
60
30
  lines.push(formatStatusLine(style, 'passed', 'Release preparation complete.'), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(workspace.tag))}`);
@@ -65,11 +35,14 @@ function formatMultiWorkspace(result, options) {
65
35
  const { style } = options;
66
36
  const lines = [];
67
37
  for (const target of result.targets) {
68
- if (target.kind === 'composite') {
69
- formatProjectSection(lines, target, isDryRun, style);
38
+ const title = getSectionTitle(target);
39
+ if (title !== undefined) {
40
+ lines.push(`\n${renderSectionHeader(title)}`);
70
41
  }
71
- else {
72
- formatWorkspaceSection(lines, target, isDryRun, style);
42
+ const sinceLabel = target.previousTag === undefined ? '(no previous release found)' : `since ${target.previousTag}`;
43
+ formatTargetSection(lines, target, { indent: ' ', sinceLabel }, isDryRun, style);
44
+ if (target.status === 'released') {
45
+ lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(target.tag))}`);
73
46
  }
74
47
  }
75
48
  formatFormatCommand(lines, result, options);
@@ -85,98 +58,79 @@ function formatMultiWorkspace(result, options) {
85
58
  }
86
59
  return lines.join('\n');
87
60
  }
88
- function formatProjectSection(lines, project, isDryRun, style) {
89
- lines.push(`\n${renderSectionHeader(project.targetId === PROJECT_TARGET_ID ? 'project' : project.targetId)}`);
90
- const sinceLabel = project.previousTag === undefined ? '(no previous release found)' : `since ${project.previousTag}`;
91
- lines.push(dim(` Found ${project.commitCount} commits ${sinceLabel}`));
92
- formatPolicyViolations(lines, project.policyViolations, style, ' ');
93
- if (project.status === 'skipped') {
94
- formatUnparseableWarning(lines, project, style, ' ');
95
- formatChangeRecordWarnings(lines, project, style, ' ');
96
- lines.push(` ${formatStatusLine(style, 'skipped', project.skipReason)}`);
97
- return;
98
- }
99
- const { releaseType, currentVersion, newVersion, tag } = project;
100
- if (project.parsedCommitCount > 0) {
101
- lines.push(dim(` Parsed ${project.parsedCommitCount} typed commits`));
61
+ function getSectionTitle(target) {
62
+ if (target.kind === 'composite') {
63
+ return target.targetId === PROJECT_TARGET_ID ? 'project' : target.targetId;
102
64
  }
103
- if (project.bumpOverride !== undefined) {
104
- lines.push(` Using bump override: ${project.bumpOverride}`);
105
- }
106
- formatUnparseableWarning(lines, project, style, ' ');
107
- formatChangeRecordWarnings(lines, project, style, ' ');
108
- lines.push(dim(` Bumping versions (${releaseType})...`), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${currentVersion} → ${bold(newVersion)} (${releaseType})`)}`);
109
- for (const file of project.bumpedFiles) {
110
- if (isDryRun) {
111
- lines.push(dim(` [dry-run] Would bump ${file}`));
112
- }
113
- else {
114
- lines.push(dim(` Bumped ${file}`));
115
- }
116
- }
117
- lines.push(dim(' Generating changelogs...'));
118
- formatChangelogFiles(lines, project, isDryRun, style, ' ');
119
- formatPreviewFiles(lines, project, isDryRun, ' ');
120
- lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
65
+ return target.name;
121
66
  }
122
- function formatWorkspaceSection(lines, workspace, isDryRun, style) {
123
- if (workspace.name !== undefined) {
124
- lines.push(`\n${renderSectionHeader(workspace.name)}`);
125
- }
126
- const sinceLabel = workspace.previousTag === undefined ? '(no previous release found)' : `since ${workspace.previousTag}`;
127
- lines.push(dim(` Found ${workspace.commitCount} commits ${sinceLabel}`));
128
- if (workspace.status === 'skipped') {
129
- formatUnparseableWarning(lines, workspace, style, ' ');
130
- formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
131
- formatChangeRecordWarnings(lines, workspace, style, ' ');
132
- lines.push(` ${formatStatusLine(style, 'skipped', workspace.skipReason)}`);
67
+ function formatTargetSection(lines, target, layout, isDryRun, style) {
68
+ const { indent, sinceLabel } = layout;
69
+ lines.push(dim(`${indent}Found ${target.commitCount} commits ${sinceLabel}`));
70
+ if (target.status === 'skipped') {
71
+ formatTargetWarnings(lines, target, style, indent);
72
+ lines.push(`${indent}${formatStatusLine(style, 'skipped', target.skipReason)}`);
133
73
  return;
134
74
  }
135
- const propagatedFrom = workspace.kind === 'package' ? workspace.propagatedFrom : undefined;
136
- const isPropagatedOnly = workspace.kind === 'package' && workspace.propagatedOnly === true;
137
- formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
138
- formatUnparseableWarning(lines, workspace, style, ' ');
139
- formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
140
- formatChangeRecordWarnings(lines, workspace, style, ' ');
141
- formatBumpLabels(lines, workspace, isPropagatedOnly);
142
- formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style);
143
- formatBumpFiles(lines, workspace, isDryRun, ' ');
144
- lines.push(dim(' Generating changelogs...'));
145
- formatChangelogFiles(lines, workspace, isDryRun, style, ' ');
146
- formatPreviewFiles(lines, workspace, isDryRun, ' ');
147
- lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(workspace.tag))}`);
75
+ const origin = describeBumpOrigin(target);
76
+ formatCommitSummary(lines, target, origin);
77
+ formatTargetWarnings(lines, target, style, indent);
78
+ formatBumpLabels(lines, target, origin, indent);
79
+ formatVersionLine(lines, target, origin, indent, style);
80
+ formatBumpFiles(lines, target, isDryRun, indent);
81
+ lines.push(dim(`${indent}Generating changelogs...`));
82
+ formatChangelogFiles(lines, target, isDryRun, style, indent);
83
+ formatPreviewFiles(lines, target, isDryRun, indent);
84
+ }
85
+ function formatTargetWarnings(lines, target, style, indent) {
86
+ formatUnparseableWarning(lines, target, style, indent);
87
+ formatPolicyViolations(lines, target.policyViolations, style, indent);
88
+ formatChangeRecordWarnings(lines, target, style, indent);
89
+ }
90
+ function describeBumpOrigin(target) {
91
+ if (target.kind === 'composite') {
92
+ return { propagatedFrom: undefined, isPropagatedOnly: false, versionOverride: undefined };
93
+ }
94
+ if (target.kind === 'directory') {
95
+ return { propagatedFrom: undefined, isPropagatedOnly: false, versionOverride: target.versionOverride };
96
+ }
97
+ return {
98
+ propagatedFrom: target.propagatedFrom,
99
+ isPropagatedOnly: target.propagatedOnly === true,
100
+ versionOverride: target.versionOverride,
101
+ };
148
102
  }
149
- function formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly) {
150
- if (isPropagatedOnly && propagatedFrom !== undefined) {
151
- const dependencyNames = propagatedFrom.map((p) => p.packageName).join(', ');
103
+ function formatCommitSummary(lines, target, origin) {
104
+ if (origin.isPropagatedOnly && origin.propagatedFrom !== undefined) {
105
+ const dependencyNames = origin.propagatedFrom.map((p) => p.packageName).join(', ');
152
106
  lines.push(dim(` Bumped via dependency: ${dependencyNames}`));
153
107
  }
154
- else if (workspace.parsedCommitCount !== undefined && workspace.parsedCommitCount > 0) {
155
- lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
108
+ else if (target.parsedCommitCount !== undefined && target.parsedCommitCount > 0) {
109
+ lines.push(dim(` Parsed ${target.parsedCommitCount} typed commits`));
156
110
  }
157
111
  }
158
- function formatBumpLabels(lines, workspace, isPropagatedOnly) {
159
- if (workspace.versionOverride !== undefined) {
160
- lines.push(` Using version override: ${workspace.versionOverride}`);
112
+ function formatBumpLabels(lines, target, origin, indent) {
113
+ if (origin.versionOverride !== undefined) {
114
+ lines.push(` Using version override: ${origin.versionOverride}`);
161
115
  }
162
- else if (workspace.bumpOverride !== undefined && !isPropagatedOnly) {
163
- lines.push(` Using bump override: ${workspace.bumpOverride}`);
116
+ else if (target.bumpOverride !== undefined && !origin.isPropagatedOnly) {
117
+ lines.push(` Using bump override: ${target.bumpOverride}`);
164
118
  }
165
- if (workspace.releaseType !== undefined) {
166
- lines.push(dim(` Bumping versions (${workspace.releaseType})...`));
119
+ if (target.releaseType !== undefined) {
120
+ lines.push(dim(`${indent}Bumping versions (${target.releaseType})...`));
167
121
  }
168
- else if (workspace.versionOverride !== undefined) {
169
- lines.push(dim(` Bumping versions (version override)...`));
122
+ else if (origin.versionOverride !== undefined) {
123
+ lines.push(dim(`${indent}Bumping versions (version override)...`));
170
124
  }
171
125
  }
172
- function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style) {
173
- const versions = `${workspace.currentVersion} → ${bold(workspace.newVersion)}`;
174
- if (workspace.versionOverride !== undefined) {
175
- lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (version override)`)}`);
176
- }
177
- else if (workspace.releaseType !== undefined) {
178
- const suffix = isPropagatedOnly ? formatPropagationSuffix(propagatedFrom) : '';
179
- lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (${workspace.releaseType}${suffix})`)}`);
126
+ function formatVersionLine(lines, target, origin, indent, style) {
127
+ const versions = `${target.currentVersion} → ${bold(target.newVersion)}`;
128
+ if (origin.versionOverride !== undefined) {
129
+ lines.push(`${indent}${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (version override)`)}`);
130
+ }
131
+ else if (target.releaseType !== undefined) {
132
+ const suffix = origin.isPropagatedOnly ? formatPropagationSuffix(origin.propagatedFrom) : '';
133
+ lines.push(`${indent}${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (${target.releaseType}${suffix})`)}`);
180
134
  }
181
135
  }
182
136
  function formatPropagationSuffix(propagatedFrom) {
@@ -1,3 +1,4 @@
1
+ import { type Git } from './git.js';
1
2
  import type { DirectoryConfig, ResolvedProjectConfig, WorkspaceConfig } from './types.js';
2
3
  export interface ResolvedTag {
3
4
  tag: string;
@@ -41,5 +42,5 @@ type ResolveReleaseTagsArgs = {
41
42
  * matching without needing `isPublishable` propagation; resolved tags default to
42
43
  * `isPublishable: true`. Production callers should pass one of the named forms.
43
44
  */
44
- export declare function resolveReleaseTags(args?: ResolveReleaseTagsArgs): ResolvedTag[];
45
+ export declare function resolveReleaseTags(args?: ResolveReleaseTagsArgs, git?: Git): ResolvedTag[];
45
46
  export {};
@@ -1,25 +1,17 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { GIT_OUTPUT_LIMIT_BYTES } from '@williamthorsen/nmr-core';
1
+ import { realGit } from "./git.js";
3
2
  import { getTargetName } from "./resolveReleaseDeclaration.js";
4
3
  import { PROJECT_TARGET_ID } from "./resolveReleaseTargets.js";
5
4
  import { asUserError } from "./user-error-chaining.js";
6
5
  const VERSION_PATTERN = /^v\d+\.\d+\.\d+/;
7
6
  const SEMVER_SUFFIX_PATTERN = /^\d+\.\d+\.\d+/;
8
- export function resolveReleaseTags(args) {
9
- let output;
7
+ export function resolveReleaseTags(args, git = realGit) {
8
+ let tags;
10
9
  try {
11
- output = execFileSync('git', ['tag', '--points-at', 'HEAD'], {
12
- encoding: 'utf8',
13
- maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
14
- });
10
+ tags = git.listTagsAtHead();
15
11
  }
16
12
  catch (error) {
17
13
  throw asUserError(error);
18
14
  }
19
- const tags = output
20
- .split('\n')
21
- .map((line) => line.trim())
22
- .filter((line) => line.length > 0);
23
15
  if (args === undefined) {
24
16
  return resolveSinglePackageTags(tags);
25
17
  }
@@ -2,11 +2,11 @@ import { type StreamStyles } from '@williamthorsen/nmr-core';
2
2
  /**
3
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 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.
5
+ * Prints one block per target, with its derived prefix and declared legacy identities and their tag counts, then the
6
+ * declared retired packages, the candidate-shaped tags outside the known set, and a footer on creating the first tag
7
+ * when a target does not have any. Returns `1` on any derivation failure or collision and `0` otherwise; undeclared
8
+ * candidates do not affect the exit code. When a config exists and either fails to load or fails validation, the
9
+ * command reports it to stderr and returns `1` in both repo modes, because a default-config preview would understate
10
+ * the declared legacy prefixes. When the default config is absent, the command previews against derived defaults.
11
11
  */
12
12
  export declare function showTagPrefixesCommand(styles: StreamStyles, configPath?: string): Promise<number>;
@@ -2,7 +2,7 @@ import { formatStatusLine, isAnticipatedFailure, reportError, } from '@williamth
2
2
  import { BIN } from "./bin.js";
3
3
  import { detectRepoType } from "./init/detectRepoType.js";
4
4
  import { loadValidatedConfig, reportConfigProblem } from "./loadValidatedConfig.js";
5
- import { previewTagPrefixes } from "./previewTagPrefixes.js";
5
+ import { previewTagPrefixes, } from "./previewTagPrefixes.js";
6
6
  export async function showTagPrefixesCommand(styles, configPath) {
7
7
  const result = await loadValidatedConfig(configPath);
8
8
  if (result.status === 'invalid') {
@@ -27,59 +27,114 @@ export async function showTagPrefixesCommand(styles, configPath) {
27
27
  process.stdout.write(renderMonorepo(preview, styles.stdout));
28
28
  return computeExitCode(preview);
29
29
  }
30
- function renderSinglePackage() {
31
- const lines = [
32
- 'Workspace Derived prefix Status',
33
- '. v (single-package mode)',
34
- '',
35
- ];
36
- return lines.join('\n');
30
+ const LABEL_WIDTH = 'legacy identity'.length;
31
+ const FIRST_TAG_FOOTER = 'A workspace marked "no derived tags yet" has not been released under its derived prefix. `release-kit tag` creates its first tag from the tags file that `release-kit prepare` writes.';
32
+ function computeExitCode(preview) {
33
+ const hasDerivationFailure = preview.workspaces.some((row) => row.derivedPrefix === null);
34
+ const hasCollision = preview.collisions.length > 0;
35
+ return hasDerivationFailure || hasCollision ? 1 : 0;
36
+ }
37
+ function formatPrefixColumns(columns, prefix, tagCount, note) {
38
+ const cells = `${prefix.padEnd(columns.prefixWidth)} ${String(tagCount).padStart(columns.countDigits)} tags`;
39
+ return note === undefined ? cells : `${cells} ${note}`;
40
+ }
41
+ function formatRow(label, body) {
42
+ return ` ${label.padEnd(LABEL_WIDTH)} ${body}`;
43
+ }
44
+ function measureColumns(preview) {
45
+ const entries = preview.retiredPackages.map((entry) => ({
46
+ prefix: entry.tagPrefix,
47
+ tagCount: entry.tagCount,
48
+ }));
49
+ for (const row of preview.workspaces) {
50
+ if (row.derivedPrefix !== null)
51
+ entries.push({ prefix: row.derivedPrefix, tagCount: row.derivedTagCount });
52
+ entries.push(...row.legacyEntries);
53
+ }
54
+ return {
55
+ prefixWidth: Math.max(0, ...entries.map((entry) => entry.prefix.length)),
56
+ countDigits: Math.max(1, ...entries.map((entry) => String(entry.tagCount).length)),
57
+ };
37
58
  }
38
59
  function renderMonorepo(preview, style) {
39
- const lines = ['Workspace tag prefixes:', ''];
60
+ const columns = measureColumns(preview);
61
+ const lines = ['Workspace tag prefixes'];
40
62
  for (const row of preview.workspaces) {
41
- lines.push(...renderWorkspaceRow(row, style));
63
+ lines.push('', ...renderTargetBlock(row, preview.collisions, columns, style));
42
64
  }
43
- if (preview.collisions.length > 0) {
44
- lines.push('', ...preview.collisions.map((collision) => formatStatusLine(style, 'failed', `tag prefix collision: '${collision.tagPrefix}' used by ${collision.workspacePaths.join(', ')}`)));
65
+ if (preview.retiredPackages.length > 0) {
66
+ lines.push('', 'Retired packages', '', ...renderRetiredLines(preview.retiredPackages, columns, style));
45
67
  }
46
- if (preview.undeclaredCandidates.length > 0) {
47
- 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.");
68
+ lines.push('', 'Undeclared tag prefixes', '', ...renderUndeclaredSection(preview.undeclaredCandidates, style));
69
+ if (preview.workspaces.some((row) => row.derivedPrefix !== null && row.derivedTagCount === 0)) {
70
+ lines.push('', formatStatusLine(style, 'info', FIRST_TAG_FOOTER));
48
71
  }
49
72
  lines.push('');
50
73
  return lines.join('\n');
51
74
  }
52
- function renderWorkspaceRow(row, style) {
53
- const lines = [];
75
+ function renderRetiredLines(retiredPackages, columns, style) {
76
+ return retiredPackages.map((entry) => {
77
+ const identity = entry.successor === undefined ? entry.name : `${entry.name}, successor: ${entry.successor}`;
78
+ const status = entry.tagCount > 0 ? 'passed' : 'warning';
79
+ return ` ${formatStatusLine(style, status, formatPrefixColumns(columns, entry.tagPrefix, entry.tagCount, identity))}`;
80
+ });
81
+ }
82
+ function renderSinglePackage() {
83
+ const lines = [
84
+ 'Workspace tag prefixes',
85
+ '',
86
+ '. (single-package mode)',
87
+ formatRow('derived prefix', 'v'),
88
+ '',
89
+ ];
90
+ return lines.join('\n');
91
+ }
92
+ function renderSuggestedSnippet(candidates) {
93
+ const entries = candidates
94
+ .map((candidate) => ` { dir: '${candidate.suggestedDir}', legacyIdentities: [{ name: 'TODO-fill-in-legacy-npm-name', tagPrefix: '${candidate.prefix}' }] },`)
95
+ .join('\n');
96
+ return ` workspaces: [\n${entries}\n ],`;
97
+ }
98
+ function renderTargetBlock(row, collisions, columns, style) {
99
+ const lines = [row.name === null ? row.workspacePath : `${row.workspacePath} (${row.name})`];
54
100
  if (row.derivedPrefix === null) {
55
101
  const failure = formatStatusLine(style, 'failed', `derivation failed: ${row.derivationError ?? 'unknown error'}`);
56
- lines.push(` ${row.workspacePath}: ${failure}`);
102
+ lines.push(formatRow('derived prefix', failure));
57
103
  return lines;
58
104
  }
59
- const statusMarker = row.derivedTagCount > 0
60
- ? formatStatusLine(style, 'passed', `${row.derivedTagCount} tags`)
61
- : formatStatusLine(style, 'warning', 'no existing tags');
62
- lines.push(` ${row.workspacePath}: derived prefix '${row.derivedPrefix}', ${statusMarker}`);
105
+ const collidingPaths = collisions
106
+ .filter((collision) => collision.tagPrefix === row.derivedPrefix)
107
+ .flatMap((collision) => collision.workspacePaths)
108
+ .filter((path) => path !== row.workspacePath);
109
+ let derivedStatus = 'passed';
110
+ let derivedNote;
111
+ if (collidingPaths.length > 0) {
112
+ derivedStatus = 'failed';
113
+ derivedNote = `collides with ${collidingPaths.join(', ')}`;
114
+ }
115
+ else if (row.derivedTagCount === 0) {
116
+ derivedStatus = 'warning';
117
+ derivedNote = 'no derived tags yet';
118
+ }
119
+ const derivedCells = formatPrefixColumns(columns, row.derivedPrefix, row.derivedTagCount, derivedNote);
120
+ lines.push(formatRow('derived prefix', formatStatusLine(style, derivedStatus, derivedCells)));
63
121
  for (const entry of row.legacyEntries) {
64
- if (entry.tagCount > 0) {
65
- const recognizedMessage = `${entry.tagCount} legacy tags with '${entry.prefix}' prefix (recognized)`;
66
- lines.push(` ${formatStatusLine(style, 'passed', recognizedMessage)}`);
67
- }
68
- else {
69
- const taglessMessage = `recorded legacy prefix '${entry.prefix}' does not have any tags`;
70
- lines.push(` ${formatStatusLine(style, 'warning', taglessMessage)}`);
71
- }
122
+ const status = entry.tagCount > 0 ? 'passed' : 'warning';
123
+ const legacyCells = formatPrefixColumns(columns, entry.prefix, entry.tagCount, entry.name);
124
+ lines.push(formatRow('legacy identity', formatStatusLine(style, status, legacyCells)));
72
125
  }
73
126
  return lines;
74
127
  }
75
- function renderSuggestedSnippet(candidates) {
76
- const entries = candidates
77
- .map((candidate) => ` { dir: '${candidate.suggestedDir}', legacyIdentities: [{ name: 'TODO-fill-in-legacy-npm-name', tagPrefix: '${candidate.prefix}' }] },`)
78
- .join('\n');
79
- return ` workspaces: [\n${entries}\n ],`;
80
- }
81
- function computeExitCode(preview) {
82
- const hasDerivationFailure = preview.workspaces.some((row) => row.derivedPrefix === null);
83
- const hasCollision = preview.collisions.length > 0;
84
- return hasDerivationFailure || hasCollision ? 1 : 0;
128
+ function renderUndeclaredSection(candidates, style) {
129
+ if (candidates.length === 0)
130
+ return [' (none)'];
131
+ return [
132
+ ...candidates.map((candidate) => ` ${formatStatusLine(style, 'warning', `'${candidate.prefix}': ${candidate.tagCount} tags (e.g., ${candidate.exampleTags.join(', ')})`)}`),
133
+ '',
134
+ 'Suggested config snippet (adjust `dir` to match the workspace if the guess is wrong, and replace the `name` placeholder with the legacy npm name):',
135
+ '',
136
+ renderSuggestedSnippet(candidates),
137
+ '',
138
+ "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.",
139
+ ];
85
140
  }
@@ -8,7 +8,6 @@ import { CONFIG_FILE_PATH } from "../loadConfig.js";
8
8
  import { loadValidatedConfig, reportConfigProblem } from "../loadValidatedConfig.js";
9
9
  import { hashPreset } from "./presets.js";
10
10
  import { resolveLabels } from "./resolveLabels.js";
11
- import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
12
11
  export const LABELS_OUTPUT_PATH = '.github/labels.yaml';
13
12
  export function formatLabelsYaml(labels, presetHashes, configFilePath = CONFIG_FILE_PATH) {
14
13
  const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${configFilePath}`];
@@ -43,9 +42,6 @@ export async function loadRepoLabelsConfig(stderrStyle, configPath) {
43
42
  return result.config.repoLabels;
44
43
  }
45
44
  export async function generateCommand({ shouldCheck = false, configPath, styles }) {
46
- if (checkRetiredSyncLabelsConfig(configPath)) {
47
- return 1;
48
- }
49
45
  const repoLabels = await loadRepoLabelsConfig(styles.stderr, configPath);
50
46
  if (repoLabels === undefined) {
51
47
  return 1;
@@ -5,14 +5,10 @@ import { describeEmptyWorkspace, discoverWorkspaces } from "../discoverWorkspace
5
5
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
6
6
  import { loadValidatedConfig, reportConfigProblem } from "../loadValidatedConfig.js";
7
7
  import { generateCommand, LABELS_OUTPUT_PATH } from "./generateCommand.js";
8
- import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
9
8
  import { buildScopeLabels, readSyncLabelsWorkflowTemplate, renderRepoLabelsBlock, renderRepoLabelsConfigScript, } from "./templates.js";
10
9
  const WORKFLOW_PATH = '.github/workflows/sync-labels.yaml';
11
10
  export async function syncLabelsInitCommand({ configPath, isDryRun, shouldForce, styles, }) {
12
11
  const configFilePath = configPath ?? CONFIG_FILE_PATH;
13
- if (checkRetiredSyncLabelsConfig(configPath)) {
14
- return 1;
15
- }
16
12
  if (isDryRun) {
17
13
  console.info('[dry-run mode]');
18
14
  }
@@ -3,7 +3,6 @@ import { existsSync } from 'node:fs';
3
3
  import { GIT_OUTPUT_LIMIT_BYTES, reportError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { BIN } from "../bin.js";
6
- import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
7
6
  const WORKFLOW_FILE = '.github/workflows/sync-labels.yaml';
8
7
  function checkGhAvailable() {
9
8
  try {
@@ -15,9 +14,6 @@ function checkGhAvailable() {
15
14
  }
16
15
  }
17
16
  export function syncLabelsCommand() {
18
- if (checkRetiredSyncLabelsConfig()) {
19
- return 1;
20
- }
21
17
  if (!checkGhAvailable()) {
22
18
  reportError(BIN, 'The `gh` CLI is not installed or not in PATH. Install it from https://cli.github.com/');
23
19
  return 1;
@@ -355,8 +355,9 @@ export interface WorkTypeConfig {
355
355
  excludedFromChangelog?: boolean | undefined;
356
356
  }
357
357
  /**
358
- * Defines which commit types trigger major or minor version bumps.
359
- * Any recognized commit type not listed defaults to a patch bump.
358
+ * Defines which commit types trigger major or minor version bumps, and which trigger none.
359
+ * Any recognized commit type not listed in `major`, `minor`, or `none` (the default list when omitted) defaults to a
360
+ * patch bump.
360
361
  * The sentinel `'!'` in `major` means "any breaking commit triggers a major bump".
361
362
  */
362
363
  export interface VersionPatterns {
@@ -364,6 +365,11 @@ export interface VersionPatterns {
364
365
  major: string[];
365
366
  /** Commit types that trigger a minor bump. */
366
367
  minor: string[];
368
+ /**
369
+ * Commit types that do not trigger a bump unless breaking. Defaults to `DEFAULT_VERSION_PATTERNS.none` when omitted;
370
+ * `[]` gives every unlisted type a patch bump.
371
+ */
372
+ none?: string[] | undefined;
367
373
  }
368
374
  /** A raw commit from the git log. */
369
375
  export interface Commit {
@@ -22,7 +22,7 @@ The two paths differ in what an absent file means. An absent default path means
22
22
  | `workspaces` | `WorkspaceOverride[]` | Override or exclude discovered workspaces (matched by `dir`) |
23
23
  | `devExportConditions` | `string[]` | Export conditions that resolve only inside the repo, such as one that points at unpublished source; the [tarball check](releasing.md#packages-whose-tarball-would-lack-a-manifest-file) does not require their targets in the tarball. `source` is exempt without being listed |
24
24
  | `formatCommand` | `string` | Shell command to run after changelog generation; modified file paths are appended as arguments |
25
- | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major/minor bumps |
25
+ | `versionPatterns` | `VersionPatterns` | Rules for which commit types trigger major, minor, or no bumps |
26
26
  | `scopeAliases` | `Record<string, string>` | Maps shorthand scope names to canonical names in commit titles and in change-record entry `scopes`, which [route items to workspaces](changelogs.md#routing-to-workspaces). A renamed workspace directory or [release target](#releases) needs an entry from its old name to its new one |
27
27
  | `workTypes` | `Record<string, WorkTypeConfig>` | Work type definitions, merged with defaults by key |
28
28
  | `breakingPolicies` | `Record<string, 'forbidden' \| 'optional'>` | Per-type `!`-policy lookup. Defaults to `DEFAULT_BREAKING_POLICIES`. Replaces the default entirely when provided. Set to `{}` to disable enforcement |
@@ -143,7 +143,7 @@ Validation rules:
143
143
  - Full-tuple `(name, tagPrefix)` duplicates within `retiredPackages` are rejected.
144
144
  - Two entries sharing the same `tagPrefix` but different `name`s are accepted: Such a pair documents a package renamed within the repo before being retired.
145
145
 
146
- `show-tag-prefixes` currently does not render a dedicated "Retired packages" section (deferred). Declaring a retired entry is verifiable by confirming that its `tagPrefix` stops appearing under "Undeclared tag prefixes" in the `show-tag-prefixes` output.
146
+ `show-tag-prefixes` lists each retired entry under "Retired packages", with its tag count and successor.
147
147
 
148
148
  ## Tag prefix collisions
149
149
 
@@ -160,26 +160,35 @@ The rule is enforced at config load; the resulting error identifies both collidi
160
160
 
161
161
  ## `release-kit show-tag-prefixes`
162
162
 
163
- Prints a per-workspace table of derived tag prefixes, tag counts, and declared legacy prefixes, with a row for each directory target. Also reports any release-shaped tag prefix in the repo that is neither a derived prefix nor declared via `legacyIdentities`, along with a copy-pasteable `workspaces: [...]` config snippet. The snippet uses a `TODO-fill-in-legacy-npm-name` placeholder for each identity's `name`; replace it with the package's prior npm name before pasting.
163
+ Prints one block per workspace and directory target, titled with its path and its package name or `releases` key, with its derived tag prefix and each declared legacy identity, their tag counts, and a status. Prefixes and counts are aligned in columns across the whole output. A "Retired packages" section follows when the config declares `retiredPackages`. An "Undeclared tag prefixes" section always follows: It reports any release-shaped tag prefix in the repo that is neither derived nor declared, along with a copy-pasteable `workspaces: [...]` config snippet, or `(none)`. The snippet uses a `TODO-fill-in-legacy-npm-name` placeholder for each identity's `name`; replace it with the package's prior npm name before pasting. When a derived prefix does not have any tags yet, a closing note explains that `release-kit tag` creates the first tag from the tags file that `release-kit prepare` writes.
164
164
 
165
165
  Exits `0` when every workspace derives a prefix and the workspaces' prefixes don't collide; exits `1` on any derivation failure or collision. Undeclared candidates do not affect the exit code; the `legacy tag prefixes are declared` readyup check reports them as a warning.
166
166
 
167
167
  When a config file exists and either fails to load or fails schema validation, the command reports the failure on stderr and exits `1` without printing a preview, because a config that cannot be read leaves the declared-versus-undeclared verdict wrong in the one dimension on which the command exists to report. That holds on the default path and under `--config`, and in single-package mode as well as monorepo mode. An absent default config is a supported state and still previews against derived defaults.
168
168
 
169
- In single-package mode, prints a single row with `workspacePath = .` and `derivedPrefix = v`; legacy entries and undeclared-candidate scanning are not applicable.
169
+ In single-package mode, prints a single block for `.` with the derived prefix `v`; legacy entries and undeclared-candidate scanning are not applicable.
170
170
 
171
171
  ## `VersionPatterns`
172
172
 
173
- Defines which work types trigger major or minor bumps. `prepare` applies it to each changelog item of the unreleased window and takes the highest level. An item of a type not listed bumps patch.
173
+ Defines which work types trigger major or minor bumps, and which trigger none. `prepare` applies it to each changelog item of the unreleased window and takes the highest level. An item of a type that `none` lists doesn't bump, and an item of any other type not listed bumps patch. A window whose items don't call for any bump doesn't release unless `--force` is passed, and its items wait in the unreleased window for the next release.
174
174
 
175
175
  ```typescript
176
176
  interface VersionPatterns {
177
177
  major: string[]; // Patterns triggering a major bump ('!' = any breaking change)
178
178
  minor: string[]; // Commit types triggering a minor bump
179
+ none?: string[]; // Commit types triggering no bump unless breaking
179
180
  }
180
181
  ```
181
182
 
182
- Default: `{ major: ['!'], minor: ['feat'] }`
183
+ Default: `{ major: ['!'], minor: ['feat'], none: ['ai', 'ci', 'docs', 'tests', 'tooling'] }`
184
+
185
+ The default `none` types don't change the code that a consumer runs. `refactor`, `internal`, and `deps` bump patch, because they change shipped code or installed dependencies. A `docs` change to a file that the package publishes reaches the registry with the next release.
186
+
187
+ A config that sets `versionPatterns` replaces `major` and `minor`, but an omitted `none` keeps the default list. To give every type not listed in `major` or `minor` a patch bump, as release-kit did before `none` existed, set `none: []`:
188
+
189
+ ```typescript
190
+ versionPatterns: { major: ['!'], minor: ['feat'], none: [] },
191
+ ```
183
192
 
184
193
  ## Output style
185
194
 
package/docs/labels.md CHANGED
@@ -10,8 +10,6 @@ Manages GitHub label definitions via the `repoLabels` block of `.config/release-
10
10
 
11
11
  `init` and `generate` accept `--config <path>` to read a config elsewhere; `sync` reads none and rejects the flag. Under `--config`, `init` reads the named file and prints the block for manual paste, and fails when that file does not exist rather than scaffolding one, so its write target stays `.config/release-kit.config.ts`. See [Config file location](configuration.md#config-file-location).
12
12
 
13
- Every `sync-labels` subcommand refuses to run while the retired `.config/sync-labels.config.ts` is present, so that custom labels cannot be silently dropped mid-migration, and the refusal names what to move and where.
14
-
15
13
  ### Label configuration
16
14
 
17
15
  ```typescript
@@ -40,6 +38,8 @@ The bundled file's `confidence:` family grades how advisable a recommendation is
40
38
 
41
39
  The `urgency:` family states how soon an item needs attention, apart from how valuable it is, which the `value:` family states. Its labels replace the earlier `priority:` family and name the old labels in `aliases`, so a repository that has `priority:` labels has them renamed in place by the first sync after it regenerates `.github/labels.yaml` (see [Renaming a label](#renaming-a-label)).
42
40
 
41
+ The `effort:` family states an item's size in points, apart from its urgency and value: one label for each value of the scale 1, 2, 3, 5, 8, and 13, from `effort:1` to `effort:13`.
42
+
43
43
  The `status:` family marks where an item stands in its work: `status:in-progress` once work on it has started, `status:blocked` while it waits on another item, and `status:on-hold` while it is deliberately paused.
44
44
 
45
45
  Names match case-insensitively, as GitHub matches them: `Bug` in the `labels` record replaces a preset's `bug`, and `Bug: null` removes it. A label takes the spelling of the last layer to write it.