@williamthorsen/release-kit 10.8.0 → 11.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.
- package/.meta/changelog.json +100 -3
- package/.readyup/kits/default.js +3 -3
- package/.readyup/kits/npm-auto-publish.js +43 -23
- package/.readyup/manifest.json +9 -9
- package/CHANGELOG.md +82 -3
- package/README.md +46 -11
- package/dist/esm/bin/release-kit.js +46 -120
- package/dist/esm/buildChangelogEntries.js +1 -0
- package/dist/esm/configFlagSchema.d.ts +12 -0
- package/dist/esm/configFlagSchema.js +3 -0
- package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
- package/dist/esm/createGithubReleaseCommand.js +9 -3
- package/dist/esm/createTags.d.ts +2 -0
- package/dist/esm/createTags.js +5 -4
- package/dist/esm/discoverWorkspaces.d.ts +53 -8
- package/dist/esm/discoverWorkspaces.js +57 -37
- package/dist/esm/format.d.ts +5 -5
- package/dist/esm/format.js +5 -4
- package/dist/esm/glyphs.d.ts +3 -0
- package/dist/esm/glyphs.js +6 -0
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +1 -0
- package/dist/esm/init/initCommand.d.ts +3 -1
- package/dist/esm/init/initCommand.js +15 -15
- package/dist/esm/loadConfig.d.ts +7 -4
- package/dist/esm/loadConfig.js +6 -3
- package/dist/esm/loadValidatedConfig.d.ts +47 -5
- package/dist/esm/loadValidatedConfig.js +32 -15
- package/dist/esm/prepareCommand.d.ts +7 -1
- package/dist/esm/prepareCommand.js +37 -45
- package/dist/esm/previewTagPrefixes.d.ts +9 -6
- package/dist/esm/previewTagPrefixes.js +12 -24
- package/dist/esm/publishCommand.d.ts +4 -3
- package/dist/esm/publishCommand.js +9 -3
- package/dist/esm/pushCommand.d.ts +1 -1
- package/dist/esm/pushCommand.js +2 -2
- package/dist/esm/reportPrepare.d.ts +2 -0
- package/dist/esm/reportPrepare.js +51 -41
- package/dist/esm/resolveCommandTags.d.ts +3 -2
- package/dist/esm/resolveCommandTags.js +10 -6
- package/dist/esm/resolveReleaseNotesConfig.d.ts +10 -11
- package/dist/esm/resolveReleaseNotesConfig.js +10 -29
- package/dist/esm/resolveStylesOrExit.d.ts +10 -0
- package/dist/esm/resolveStylesOrExit.js +10 -0
- package/dist/esm/showTagPrefixesCommand.d.ts +5 -2
- package/dist/esm/showTagPrefixesCommand.js +31 -11
- package/dist/esm/sync-labels/generateCommand.d.ts +18 -8
- package/dist/esm/sync-labels/generateCommand.js +12 -11
- package/dist/esm/sync-labels/initCommand.d.ts +11 -4
- package/dist/esm/sync-labels/initCommand.js +24 -18
- package/dist/esm/sync-labels/retiredConfig.d.ts +4 -2
- package/dist/esm/sync-labels/retiredConfig.js +2 -2
- package/dist/esm/tagCommand.d.ts +2 -1
- package/dist/esm/tagCommand.js +2 -2
- package/dist/esm/validateOverridesCommand.d.ts +13 -7
- package/dist/esm/validateOverridesCommand.js +28 -36
- package/docs/configuration.md +35 -1
- package/docs/labels.md +2 -0
- package/docs/work-types.md +11 -8
- package/package.json +13 -14
- package/presets/labels/common.yaml +2 -2
- package/dist/esm/checkWorkTypesDrift.d.ts +0 -35
- package/dist/esm/checkWorkTypesDrift.js +0 -117
- package/dist/esm/syncWorkTypes.d.ts +0 -28
- package/dist/esm/syncWorkTypes.js +0 -94
- package/dist/esm/work-types.json +0 -149
- package/dist/esm/work-types.schema.json +0 -110
- package/dist/esm/workTypesUtils.d.ts +0 -22
- package/dist/esm/workTypesUtils.js +0 -16
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, truncateToWidth, } from '@williamthorsen/nmr-core';
|
|
1
2
|
import { bold, dim, sectionHeader } from "./format.js";
|
|
3
|
+
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
2
4
|
import { GIT_CLIFF_NPX_ARGS } from "./runGitCliff.js";
|
|
3
5
|
const CLIFF_DRY_RUN_COMMAND = `npx ${GIT_CLIFF_NPX_ARGS.join(' ')} ...`;
|
|
6
|
+
const SUBJECT_COLUMN_BUDGET = 72;
|
|
4
7
|
export function reportPrepare(plan, options) {
|
|
5
8
|
const isMultiWorkspace = plan.workspaces.some((w) => w.name !== undefined) || plan.project !== undefined;
|
|
6
9
|
if (isMultiWorkspace) {
|
|
@@ -10,6 +13,7 @@ export function reportPrepare(plan, options) {
|
|
|
10
13
|
}
|
|
11
14
|
function formatSingleWorkspace(result, options) {
|
|
12
15
|
const dryRun = !options.applied;
|
|
16
|
+
const { style } = options;
|
|
13
17
|
const lines = [];
|
|
14
18
|
const workspace = result.workspaces[0];
|
|
15
19
|
if (workspace === undefined) {
|
|
@@ -20,11 +24,11 @@ function formatSingleWorkspace(result, options) {
|
|
|
20
24
|
if (workspace.parsedCommitCount !== undefined) {
|
|
21
25
|
lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
|
|
22
26
|
}
|
|
23
|
-
formatUnparseableWarning(lines, workspace);
|
|
24
|
-
formatPolicyViolations(lines, workspace.policyViolations);
|
|
27
|
+
formatUnparseableWarning(lines, workspace, style);
|
|
28
|
+
formatPolicyViolations(lines, workspace.policyViolations, style);
|
|
25
29
|
if (workspace.status === 'skipped') {
|
|
26
|
-
lines.push(
|
|
27
|
-
formatWarnings(lines, result);
|
|
30
|
+
lines.push(formatStatusLine(style, 'skipped', workspace.skipReason));
|
|
31
|
+
formatWarnings(lines, result, style);
|
|
28
32
|
return lines.join('\n');
|
|
29
33
|
}
|
|
30
34
|
if (workspace.setVersion !== undefined) {
|
|
@@ -40,49 +44,50 @@ function formatSingleWorkspace(result, options) {
|
|
|
40
44
|
lines.push(dim(`Bumping versions (version override)...`));
|
|
41
45
|
}
|
|
42
46
|
if (workspace.setVersion !== undefined) {
|
|
43
|
-
lines.push(
|
|
47
|
+
lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (version override)`));
|
|
44
48
|
}
|
|
45
49
|
else if (workspace.releaseType !== undefined) {
|
|
46
|
-
lines.push(
|
|
50
|
+
lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType})`));
|
|
47
51
|
}
|
|
48
52
|
formatBumpFiles(lines, workspace, dryRun);
|
|
49
53
|
lines.push(dim('Generating changelogs...'));
|
|
50
54
|
formatChangelogFiles(lines, workspace, dryRun);
|
|
51
55
|
formatPreviewFiles(lines, workspace, dryRun);
|
|
52
56
|
formatFormatCommand(lines, result, options);
|
|
53
|
-
formatWarnings(lines, result);
|
|
54
|
-
lines.push(
|
|
57
|
+
formatWarnings(lines, result, style);
|
|
58
|
+
lines.push(formatStatusLine(style, 'passed', 'Release preparation complete.'), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(workspace.tag))}`);
|
|
55
59
|
return lines.join('\n');
|
|
56
60
|
}
|
|
57
61
|
function formatMultiWorkspace(result, options) {
|
|
58
62
|
const dryRun = !options.applied;
|
|
63
|
+
const { style } = options;
|
|
59
64
|
const lines = [];
|
|
60
65
|
for (const workspace of result.workspaces) {
|
|
61
|
-
formatWorkspaceSection(lines, workspace, dryRun);
|
|
66
|
+
formatWorkspaceSection(lines, workspace, dryRun, style);
|
|
62
67
|
}
|
|
63
68
|
if (result.project !== undefined) {
|
|
64
|
-
formatProjectSection(lines, result.project, dryRun);
|
|
69
|
+
formatProjectSection(lines, result.project, dryRun, style);
|
|
65
70
|
}
|
|
66
71
|
formatFormatCommand(lines, result, options);
|
|
67
|
-
formatWarnings(lines, result);
|
|
72
|
+
formatWarnings(lines, result, style);
|
|
68
73
|
if (result.tags.length > 0) {
|
|
69
|
-
lines.push(`\n
|
|
74
|
+
lines.push(`\n${formatStatusLine(style, 'passed', 'Release preparation complete.')}`);
|
|
70
75
|
for (const tag of result.tags) {
|
|
71
|
-
lines.push(`
|
|
76
|
+
lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
else {
|
|
75
|
-
lines.push(`\n
|
|
80
|
+
lines.push(`\n${formatStatusLine(style, 'skipped', 'No workspaces had release-worthy changes.')}`);
|
|
76
81
|
}
|
|
77
82
|
return lines.join('\n');
|
|
78
83
|
}
|
|
79
|
-
function formatProjectSection(lines, project, dryRun) {
|
|
84
|
+
function formatProjectSection(lines, project, dryRun, style) {
|
|
80
85
|
lines.push(`\n${sectionHeader('project')}`);
|
|
81
86
|
const since = project.previousTag === undefined ? '(no previous release found)' : `since ${project.previousTag}`;
|
|
82
87
|
lines.push(dim(` Found ${project.commitCount} commits ${since}`));
|
|
83
|
-
formatPolicyViolations(lines, project.policyViolations, ' ');
|
|
88
|
+
formatPolicyViolations(lines, project.policyViolations, style, ' ');
|
|
84
89
|
if (project.status === 'skipped') {
|
|
85
|
-
lines.push(`
|
|
90
|
+
lines.push(` ${formatStatusLine(style, 'skipped', project.skipReason)}`);
|
|
86
91
|
return;
|
|
87
92
|
}
|
|
88
93
|
const { releaseType, currentVersion, newVersion, tag } = project;
|
|
@@ -92,8 +97,8 @@ function formatProjectSection(lines, project, dryRun) {
|
|
|
92
97
|
if (project.bumpOverride !== undefined) {
|
|
93
98
|
lines.push(` Using bump override: ${project.bumpOverride}`);
|
|
94
99
|
}
|
|
95
|
-
formatProjectUnparseable(lines, project);
|
|
96
|
-
lines.push(dim(` Bumping versions (${releaseType})...`), `
|
|
100
|
+
formatProjectUnparseable(lines, project, style);
|
|
101
|
+
lines.push(dim(` Bumping versions (${releaseType})...`), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${currentVersion} → ${bold(newVersion)} (${releaseType})`)}`);
|
|
97
102
|
for (const file of project.bumpedFiles) {
|
|
98
103
|
if (dryRun) {
|
|
99
104
|
lines.push(dim(` [dry-run] Would bump ${file}`));
|
|
@@ -112,9 +117,9 @@ function formatProjectSection(lines, project, dryRun) {
|
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
formatPreviewFiles(lines, project, dryRun, ' ');
|
|
115
|
-
lines.push(`
|
|
120
|
+
lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
|
|
116
121
|
}
|
|
117
|
-
function formatProjectUnparseable(lines, project) {
|
|
122
|
+
function formatProjectUnparseable(lines, project, style) {
|
|
118
123
|
const unparseable = project.unparseableCommits;
|
|
119
124
|
if (unparseable === undefined || unparseable.length === 0) {
|
|
120
125
|
return;
|
|
@@ -122,35 +127,35 @@ function formatProjectUnparseable(lines, project) {
|
|
|
122
127
|
const count = unparseable.length;
|
|
123
128
|
const isPatchFloor = project.parsedCommitCount === 0;
|
|
124
129
|
const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
|
|
125
|
-
lines.push(`
|
|
130
|
+
lines.push(` ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
|
|
126
131
|
for (const commit of unparseable) {
|
|
127
132
|
const shortHash = commit.hash.slice(0, 7);
|
|
128
|
-
const truncatedMessage = commit.message
|
|
133
|
+
const truncatedMessage = truncateSubject(commit.message);
|
|
129
134
|
lines.push(` · ${shortHash} ${truncatedMessage}`);
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
|
-
function formatWorkspaceSection(lines, workspace, dryRun) {
|
|
137
|
+
function formatWorkspaceSection(lines, workspace, dryRun, style) {
|
|
133
138
|
if (workspace.name !== undefined) {
|
|
134
139
|
lines.push(`\n${sectionHeader(workspace.name)}`);
|
|
135
140
|
}
|
|
136
141
|
const since = workspace.previousTag === undefined ? '(no previous release found)' : `since ${workspace.previousTag}`;
|
|
137
142
|
lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
|
|
138
143
|
if (workspace.status === 'skipped') {
|
|
139
|
-
lines.push(`
|
|
144
|
+
lines.push(` ${formatStatusLine(style, 'skipped', workspace.skipReason)}`);
|
|
140
145
|
return;
|
|
141
146
|
}
|
|
142
147
|
const { propagatedFrom } = workspace;
|
|
143
148
|
const isPropagatedOnly = propagatedFrom !== undefined && workspace.commitCount === 0;
|
|
144
149
|
formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
|
|
145
|
-
formatUnparseableWarning(lines, workspace, ' ');
|
|
146
|
-
formatPolicyViolations(lines, workspace.policyViolations, ' ');
|
|
150
|
+
formatUnparseableWarning(lines, workspace, style, ' ');
|
|
151
|
+
formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
|
|
147
152
|
formatBumpLabels(lines, workspace, isPropagatedOnly);
|
|
148
|
-
formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly);
|
|
153
|
+
formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style);
|
|
149
154
|
formatBumpFiles(lines, workspace, dryRun, ' ');
|
|
150
155
|
lines.push(dim(' Generating changelogs...'));
|
|
151
156
|
formatChangelogFiles(lines, workspace, dryRun, ' ');
|
|
152
157
|
formatPreviewFiles(lines, workspace, dryRun, ' ');
|
|
153
|
-
lines.push(`
|
|
158
|
+
lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(workspace.tag))}`);
|
|
154
159
|
}
|
|
155
160
|
function formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly) {
|
|
156
161
|
if (isPropagatedOnly && propagatedFrom !== undefined) {
|
|
@@ -175,13 +180,14 @@ function formatBumpLabels(lines, workspace, isPropagatedOnly) {
|
|
|
175
180
|
lines.push(dim(` Bumping versions (version override)...`));
|
|
176
181
|
}
|
|
177
182
|
}
|
|
178
|
-
function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly) {
|
|
183
|
+
function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style) {
|
|
184
|
+
const versions = `${workspace.currentVersion} → ${bold(workspace.newVersion)}`;
|
|
179
185
|
if (workspace.setVersion !== undefined) {
|
|
180
|
-
lines.push(`
|
|
186
|
+
lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (version override)`)}`);
|
|
181
187
|
}
|
|
182
188
|
else if (workspace.releaseType !== undefined) {
|
|
183
189
|
const suffix = isPropagatedOnly ? formatPropagationSuffix(propagatedFrom) : '';
|
|
184
|
-
lines.push(`
|
|
190
|
+
lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (${workspace.releaseType}${suffix})`)}`);
|
|
185
191
|
}
|
|
186
192
|
}
|
|
187
193
|
function formatBumpFiles(lines, workspace, dryRun, indent = '') {
|
|
@@ -215,7 +221,7 @@ function formatPreviewFiles(lines, release, dryRun, indent = '') {
|
|
|
215
221
|
}
|
|
216
222
|
}
|
|
217
223
|
}
|
|
218
|
-
function formatUnparseableWarning(lines, workspace, indent = '') {
|
|
224
|
+
function formatUnparseableWarning(lines, workspace, style, indent = '') {
|
|
219
225
|
const unparseable = workspace.unparseableCommits;
|
|
220
226
|
if (unparseable === undefined || unparseable.length === 0) {
|
|
221
227
|
return;
|
|
@@ -223,23 +229,23 @@ function formatUnparseableWarning(lines, workspace, indent = '') {
|
|
|
223
229
|
const count = unparseable.length;
|
|
224
230
|
const isPatchFloor = workspace.parsedCommitCount === 0;
|
|
225
231
|
const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
|
|
226
|
-
lines.push(`${indent}
|
|
232
|
+
lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
|
|
227
233
|
for (const commit of unparseable) {
|
|
228
234
|
const shortHash = commit.hash.slice(0, 7);
|
|
229
|
-
const truncatedMessage = commit.message
|
|
235
|
+
const truncatedMessage = truncateSubject(commit.message);
|
|
230
236
|
lines.push(`${indent} · ${shortHash} ${truncatedMessage}`);
|
|
231
237
|
}
|
|
232
238
|
}
|
|
233
|
-
function formatPolicyViolations(lines, violations, indent = '') {
|
|
239
|
+
function formatPolicyViolations(lines, violations, style, indent = '') {
|
|
234
240
|
if (violations === undefined || violations.length === 0) {
|
|
235
241
|
return;
|
|
236
242
|
}
|
|
237
243
|
const count = violations.length;
|
|
238
|
-
lines.push(`${indent}
|
|
244
|
+
lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} policy violation${count === 1 ? '' : 's'}:`)}`);
|
|
239
245
|
for (const violation of violations) {
|
|
240
246
|
const shortHash = violation.commitHash.slice(0, 7);
|
|
241
247
|
const subject = violation.commitSubject;
|
|
242
|
-
const truncatedSubject = subject
|
|
248
|
+
const truncatedSubject = truncateSubject(subject);
|
|
243
249
|
lines.push(`${indent} · ${shortHash} '${truncatedSubject}' — type '${violation.type}' at ${violation.surface} surface`);
|
|
244
250
|
}
|
|
245
251
|
}
|
|
@@ -250,14 +256,14 @@ function formatPropagationSuffix(propagatedFrom) {
|
|
|
250
256
|
const names = propagatedFrom.map((p) => p.packageName).join(', ');
|
|
251
257
|
return `, dependency: ${names}`;
|
|
252
258
|
}
|
|
253
|
-
function formatWarnings(lines, result) {
|
|
259
|
+
function formatWarnings(lines, result, style) {
|
|
254
260
|
const { warnings } = result;
|
|
255
261
|
if (warnings === undefined || warnings.length === 0) {
|
|
256
262
|
return;
|
|
257
263
|
}
|
|
258
264
|
lines.push('');
|
|
259
265
|
for (const warning of warnings) {
|
|
260
|
-
lines.push(
|
|
266
|
+
lines.push(formatStatusLine(style, 'warning', warning));
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
function formatFormatCommand(lines, result, options) {
|
|
@@ -272,5 +278,9 @@ function formatFormatCommand(lines, result, options) {
|
|
|
272
278
|
lines.push(dim(`\n Running format command: ${result.formatCommand.command}`));
|
|
273
279
|
return;
|
|
274
280
|
}
|
|
275
|
-
|
|
281
|
+
const messageIndent = ' '.repeat(2 + measureGlyphColumn(STATUS_GLYPHS[options.style]) + 1);
|
|
282
|
+
lines.push(`\n ${formatStatusLine(options.style, 'warning', `Format command failed: ${result.formatCommand.command}`)}`, `${messageIndent}${options.formatError}`);
|
|
283
|
+
}
|
|
284
|
+
function truncateSubject(subject) {
|
|
285
|
+
return truncateToWidth(subject, { width: SUBJECT_COLUMN_BUDGET });
|
|
276
286
|
}
|
|
@@ -3,7 +3,8 @@ import { type ResolvedTag } from './resolveReleaseTags.js';
|
|
|
3
3
|
* Discover workspaces, resolve release tags from HEAD, validate `--tags` names against the
|
|
4
4
|
* full resolved tag names (e.g., `nmr-core-v1.3.0`), and return the filtered tag list.
|
|
5
5
|
* Works in both single-package and monorepo modes. Exits with an error message on any validation
|
|
6
|
-
* failure — including `deriveWorkspaceConfig()` throws for workspaces missing a `package.json` `name` field
|
|
6
|
+
* failure — including `deriveWorkspaceConfig()` throws for workspaces missing a `package.json` `name` field,
|
|
7
|
+
* and a workspace declaring patterns that resolve to no package, which is not single-package mode.
|
|
7
8
|
*
|
|
8
9
|
* In both modes `deriveWorkspaceConfig` is called so `WorkspaceConfig.isPublishable` (read
|
|
9
10
|
* from `package.json#private`) propagates onto each `ResolvedTag`. Single-package mode now
|
|
@@ -11,4 +12,4 @@ import { type ResolvedTag } from './resolveReleaseTags.js';
|
|
|
11
12
|
* single-package mode skipping the derive step need a `name` field on `./package.json`,
|
|
12
13
|
* but in practice every npm/pnpm package already has one.
|
|
13
14
|
*/
|
|
14
|
-
export declare function resolveCommandTags(tags: string[] | undefined):
|
|
15
|
+
export declare function resolveCommandTags(tags: string[] | undefined): ResolvedTag[];
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
3
|
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
4
|
-
import { discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
4
|
+
import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
5
5
|
import { resolveReleaseTags } from "./resolveReleaseTags.js";
|
|
6
|
-
export
|
|
7
|
-
let
|
|
6
|
+
export function resolveCommandTags(tags) {
|
|
7
|
+
let workspace;
|
|
8
8
|
try {
|
|
9
|
-
|
|
9
|
+
workspace = discoverWorkspaces();
|
|
10
10
|
}
|
|
11
11
|
catch (error) {
|
|
12
12
|
reportError(`Failed to discover workspaces: ${describeError(error)}`);
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
+
if (workspace.kind === 'empty') {
|
|
16
|
+
reportError(`No workspace package to tag. ${describeEmptyWorkspace(workspace)}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
15
19
|
let workspaces;
|
|
16
20
|
let singleWorkspace;
|
|
17
21
|
try {
|
|
18
|
-
if (
|
|
22
|
+
if (workspace.kind === 'single-package') {
|
|
19
23
|
singleWorkspace = deriveWorkspaceConfig('.');
|
|
20
24
|
}
|
|
21
25
|
else {
|
|
22
|
-
workspaces =
|
|
26
|
+
workspaces = workspace.packageDirs.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
catch (error) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
import type { ReleaseNotesConfig } from './types.js';
|
|
2
3
|
export interface ResolvedReleaseNotesConfig {
|
|
3
4
|
releaseNotes: ReleaseNotesConfig;
|
|
@@ -6,21 +7,19 @@ export interface ResolvedReleaseNotesConfig {
|
|
|
6
7
|
sectionOrder: string[];
|
|
7
8
|
}
|
|
8
9
|
export interface ResolveReleaseNotesConfigOptions {
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
* defaults. Use from CLI commands whose entire behavior depends on the resolved config (e.g.
|
|
12
|
-
* `create-github-release`), so a corrupt or unreadable config cannot silently send the command
|
|
13
|
-
* to the wrong changelog path.
|
|
14
|
-
*/
|
|
15
|
-
strictLoad?: boolean;
|
|
10
|
+
/** Config file to read, relative to the working directory. Defaults to `CONFIG_FILE_PATH`. */
|
|
11
|
+
configPath?: string;
|
|
16
12
|
}
|
|
17
13
|
/**
|
|
18
|
-
*
|
|
14
|
+
* Loads and validates the release-kit config, resolving the release-notes settings it carries.
|
|
19
15
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* An unusable config reports to stderr and calls `process.exit(1)`, whether the file failed to load or failed
|
|
17
|
+
* validation, and whether it was named by the caller or the default one. Both callers are CLI commands whose
|
|
18
|
+
* whole behavior depends on the resolved config, so neither could proceed on derived defaults without silently
|
|
19
|
+
* dropping a configured `releaseNotes.shouldInjectIntoReadme`, `changelogJson.outputPath`, or `workTypes`.
|
|
20
|
+
* An absent default config is a supported state and resolves to the defaults.
|
|
22
21
|
*/
|
|
23
|
-
export declare function resolveReleaseNotesConfig(options?: ResolveReleaseNotesConfigOptions): Promise<ResolvedReleaseNotesConfig>;
|
|
22
|
+
export declare function resolveReleaseNotesConfig(stderrStyle: OutputStyle, options?: ResolveReleaseNotesConfigOptions): Promise<ResolvedReleaseNotesConfig>;
|
|
24
23
|
/** Extract section headers in declaration order from a merged workTypes record. */
|
|
25
24
|
export declare function deriveSectionOrder(workTypes: Record<string, {
|
|
26
25
|
header: string;
|
|
@@ -1,40 +1,21 @@
|
|
|
1
|
-
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
-
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
1
|
import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
export async function resolveReleaseNotesConfig(options = {}) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
const message = describeError(error);
|
|
14
|
-
if (strictLoad) {
|
|
15
|
-
reportError(`Failed to load config: ${message}`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
console.warn(`Warning: failed to load config; using defaults: ${message}`);
|
|
2
|
+
import { resolveWorkTypes } from "./loadConfig.js";
|
|
3
|
+
import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
|
|
4
|
+
export async function resolveReleaseNotesConfig(stderrStyle, options = {}) {
|
|
5
|
+
const result = await loadValidatedConfig(options.configPath);
|
|
6
|
+
if (result.status === 'invalid') {
|
|
7
|
+
reportConfigProblem(result.problem, stderrStyle);
|
|
8
|
+
process.exit(1);
|
|
19
9
|
}
|
|
20
|
-
if (
|
|
10
|
+
if (result.status === 'missing') {
|
|
21
11
|
return {
|
|
22
12
|
releaseNotes: { ...DEFAULT_RELEASE_NOTES_CONFIG },
|
|
23
13
|
changelogJsonOutputPath: DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
|
|
24
14
|
sectionOrder: deriveSectionOrder(resolveWorkTypes()),
|
|
25
15
|
};
|
|
26
16
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
process.stderr.write('Invalid config:\n');
|
|
30
|
-
for (const err of errors) {
|
|
31
|
-
process.stderr.write(` ❌ ${err}\n`);
|
|
32
|
-
}
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
for (const warning of warnings) {
|
|
36
|
-
console.warn(` ⚠️ ${warning}`);
|
|
37
|
-
}
|
|
17
|
+
reportConfigWarnings(result.warnings, stderrStyle);
|
|
18
|
+
const { config } = result;
|
|
38
19
|
return {
|
|
39
20
|
releaseNotes: {
|
|
40
21
|
shouldInjectIntoReadme: config.releaseNotes?.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
2
|
+
/** The environment variable holding the caller's standing style preference: `auto`, `plain`, or `rich`. */
|
|
3
|
+
export declare const OUTPUT_STYLE_ENV_VAR = "RELEASE_KIT_OUTPUT_STYLE";
|
|
4
|
+
export interface ResolveStylesOptions {
|
|
5
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
readonly stderrIsTty: boolean;
|
|
7
|
+
readonly stdoutIsTty: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Resolves the style of each output stream, exiting with a usage error when the variable names no setting. */
|
|
10
|
+
export declare function resolveStylesOrExit(options: ResolveStylesOptions): StreamStyles;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describeInvalidOutputStyle, reportError, resolveStreamStyles, } from '@williamthorsen/nmr-core';
|
|
2
|
+
export const OUTPUT_STYLE_ENV_VAR = 'RELEASE_KIT_OUTPUT_STYLE';
|
|
3
|
+
export function resolveStylesOrExit(options) {
|
|
4
|
+
const { invalid, styles } = resolveStreamStyles({ ...options, envVar: OUTPUT_STYLE_ENV_VAR });
|
|
5
|
+
if (invalid !== undefined) {
|
|
6
|
+
reportError(describeInvalidOutputStyle(invalid));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
return styles;
|
|
10
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
1
2
|
/**
|
|
2
3
|
* Orchestrate the CLI `show-tag-prefixes` command.
|
|
3
4
|
*
|
|
@@ -5,8 +6,10 @@
|
|
|
5
6
|
* entries, followed by an "Undeclared tag prefixes" section when candidate-shaped tags
|
|
6
7
|
* exist outside the known set. Exits `0` on full derivation success and no collisions;
|
|
7
8
|
* `1` on any derivation failure or collision. Undeclared candidates do not affect the
|
|
8
|
-
* exit code.
|
|
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.
|
|
9
12
|
*
|
|
10
13
|
* @returns The exit code the caller should use.
|
|
11
14
|
*/
|
|
12
|
-
export declare function showTagPrefixesCommand(): Promise<number>;
|
|
15
|
+
export declare function showTagPrefixesCommand(styles: StreamStyles, configPath?: string): Promise<number>;
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
import { formatStatusLine, reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
1
3
|
import { detectRepoType } from "./init/detectRepoType.js";
|
|
4
|
+
import { loadValidatedConfig, reportConfigProblem } from "./loadValidatedConfig.js";
|
|
2
5
|
import { previewTagPrefixes } from "./previewTagPrefixes.js";
|
|
3
|
-
export async function showTagPrefixesCommand() {
|
|
6
|
+
export async function showTagPrefixesCommand(styles, configPath) {
|
|
7
|
+
const result = await loadValidatedConfig(configPath);
|
|
8
|
+
if (result.status === 'invalid') {
|
|
9
|
+
reportConfigProblem(result.problem, styles.stderr);
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
4
12
|
if (detectRepoType() === 'single-package') {
|
|
5
13
|
process.stdout.write(renderSinglePackage());
|
|
6
14
|
return 0;
|
|
7
15
|
}
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
let preview;
|
|
17
|
+
try {
|
|
18
|
+
preview = previewTagPrefixes(result.status === 'ok' ? result.config : undefined);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
reportError(describeError(error));
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
process.stdout.write(renderMonorepo(preview, styles.stdout));
|
|
10
25
|
return computeExitCode(preview);
|
|
11
26
|
}
|
|
12
27
|
function renderSinglePackage() {
|
|
@@ -17,13 +32,13 @@ function renderSinglePackage() {
|
|
|
17
32
|
];
|
|
18
33
|
return lines.join('\n');
|
|
19
34
|
}
|
|
20
|
-
function renderMonorepo(preview) {
|
|
35
|
+
function renderMonorepo(preview, style) {
|
|
21
36
|
const lines = ['Workspace tag prefixes:', ''];
|
|
22
37
|
for (const row of preview.workspaces) {
|
|
23
|
-
lines.push(...renderWorkspaceRow(row));
|
|
38
|
+
lines.push(...renderWorkspaceRow(row, style));
|
|
24
39
|
}
|
|
25
40
|
if (preview.collisions.length > 0) {
|
|
26
|
-
lines.push('', ...preview.collisions.map((collision) =>
|
|
41
|
+
lines.push('', ...preview.collisions.map((collision) => formatStatusLine(style, 'failed', `tag prefix collision: '${collision.tagPrefix}' used by ${collision.workspacePaths.join(', ')}`)));
|
|
27
42
|
}
|
|
28
43
|
if (preview.undeclaredCandidates.length > 0) {
|
|
29
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.");
|
|
@@ -31,20 +46,25 @@ function renderMonorepo(preview) {
|
|
|
31
46
|
lines.push('');
|
|
32
47
|
return lines.join('\n');
|
|
33
48
|
}
|
|
34
|
-
function renderWorkspaceRow(row) {
|
|
49
|
+
function renderWorkspaceRow(row, style) {
|
|
35
50
|
const lines = [];
|
|
36
51
|
if (row.derivedPrefix === null) {
|
|
37
|
-
|
|
52
|
+
const failure = formatStatusLine(style, 'failed', `derivation failed: ${row.derivationError ?? 'unknown error'}`);
|
|
53
|
+
lines.push(` ${row.workspacePath} — ${failure}`);
|
|
38
54
|
return lines;
|
|
39
55
|
}
|
|
40
|
-
const statusMarker = row.derivedTagCount > 0
|
|
56
|
+
const statusMarker = row.derivedTagCount > 0
|
|
57
|
+
? formatStatusLine(style, 'passed', `${row.derivedTagCount} tags`)
|
|
58
|
+
: formatStatusLine(style, 'warning', 'no existing tags');
|
|
41
59
|
lines.push(` ${row.workspacePath} — derived prefix '${row.derivedPrefix}', ${statusMarker}`);
|
|
42
60
|
for (const entry of row.legacyEntries) {
|
|
43
61
|
if (entry.tagCount > 0) {
|
|
44
|
-
|
|
62
|
+
const recognized = `${entry.tagCount} legacy tags with '${entry.prefix}' prefix (recognized)`;
|
|
63
|
+
lines.push(` ${formatStatusLine(style, 'passed', recognized)}`);
|
|
45
64
|
}
|
|
46
65
|
else {
|
|
47
|
-
|
|
66
|
+
const tagless = `recorded legacy prefix '${entry.prefix}' has no tags`;
|
|
67
|
+
lines.push(` ${formatStatusLine(style, 'warning', tagless)}`);
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
70
|
return lines;
|
|
@@ -1,27 +1,37 @@
|
|
|
1
|
+
import { type OutputStyle, type StreamStyles } from '@williamthorsen/nmr-core';
|
|
1
2
|
import type { RepoLabelsConfig } from '../types.js';
|
|
2
3
|
import type { LabelDefinition } from './types.js';
|
|
3
4
|
/** Output path for the generated labels file. */
|
|
4
5
|
export declare const LABELS_OUTPUT_PATH = ".github/labels.yaml";
|
|
5
|
-
/** Formats resolved labels as YAML with generated header including preset hashes. */
|
|
6
|
-
export declare function formatLabelsYaml(labels: LabelDefinition[], presetHashes: Map<string, string>): string;
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
7
|
+
* Formats resolved labels as YAML with generated header including preset hashes.
|
|
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`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatLabelsYaml(labels: LabelDefinition[], presetHashes: Map<string, string>, configFilePath?: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Loads the config file and returns its `repoLabels` block.
|
|
9
15
|
*
|
|
10
16
|
* Reports an actionable error and returns `undefined` when the config file is missing,
|
|
11
|
-
* fails to load or validate, or carries no `repoLabels` block.
|
|
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.
|
|
12
19
|
*/
|
|
13
|
-
export declare function loadRepoLabelsConfig(): Promise<RepoLabelsConfig | undefined>;
|
|
20
|
+
export declare function loadRepoLabelsConfig(stderrStyle: OutputStyle, configPath?: string): Promise<RepoLabelsConfig | undefined>;
|
|
14
21
|
/** Options for the `sync-labels generate` subcommand. */
|
|
15
22
|
interface GenerateOptions {
|
|
16
23
|
/** Compare the regenerated content against the committed file instead of writing. */
|
|
17
24
|
check?: boolean;
|
|
25
|
+
/** Config file to read, relative to the working directory. Defaults to `CONFIG_FILE_PATH`. */
|
|
26
|
+
configPath?: string;
|
|
27
|
+
styles: StreamStyles;
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
30
|
* Runs the `sync-labels generate` command.
|
|
21
31
|
*
|
|
22
|
-
* Loads the `repoLabels` block from
|
|
23
|
-
*
|
|
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
|
|
24
34
|
* is stale, writing nothing. Returns 0 on success, 1 on failure.
|
|
25
35
|
*/
|
|
26
|
-
export declare function generateCommand({ check }
|
|
36
|
+
export declare function generateCommand({ check, configPath, styles }: GenerateOptions): Promise<number>;
|
|
27
37
|
export {};
|
|
@@ -4,13 +4,13 @@ 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
|
-
import { loadValidatedConfig } from "../loadValidatedConfig.js";
|
|
7
|
+
import { loadValidatedConfig, reportConfigProblem } from "../loadValidatedConfig.js";
|
|
8
8
|
import { hashPresetFile } 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
|
-
export function formatLabelsYaml(labels, presetHashes) {
|
|
13
|
-
const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${
|
|
12
|
+
export function formatLabelsYaml(labels, presetHashes, configFilePath = CONFIG_FILE_PATH) {
|
|
13
|
+
const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${configFilePath}`];
|
|
14
14
|
const sortedPresetHashes = [...presetHashes].sort(([a], [b]) => a.localeCompare(b));
|
|
15
15
|
for (const [name, hash] of sortedPresetHashes) {
|
|
16
16
|
headerLines.push(`# ${name} preset hash: ${hash}`);
|
|
@@ -19,26 +19,27 @@ export function formatLabelsYaml(labels, presetHashes) {
|
|
|
19
19
|
const yamlBody = stringify(declared, { singleQuote: true, lineWidth: 0 });
|
|
20
20
|
return headerLines.join('\n') + '\n' + yamlBody;
|
|
21
21
|
}
|
|
22
|
-
export async function loadRepoLabelsConfig() {
|
|
23
|
-
const result = await loadValidatedConfig();
|
|
22
|
+
export async function loadRepoLabelsConfig(stderrStyle, configPath) {
|
|
23
|
+
const result = await loadValidatedConfig(configPath);
|
|
24
24
|
if (result.status === 'missing') {
|
|
25
|
-
reportError(`No config file found at ${
|
|
25
|
+
reportError(`No config file found at ${result.configFilePath}. Run \`release-kit sync-labels init\` first.`);
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
28
|
if (result.status === 'invalid') {
|
|
29
|
+
reportConfigProblem(result.problem, stderrStyle);
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
31
32
|
if (result.config.repoLabels === undefined) {
|
|
32
|
-
reportError(`No \`repoLabels\` block found in ${
|
|
33
|
+
reportError(`No \`repoLabels\` block found in ${result.configFilePath}. Run \`release-kit sync-labels init\` to seed one.`);
|
|
33
34
|
return undefined;
|
|
34
35
|
}
|
|
35
36
|
return result.config.repoLabels;
|
|
36
37
|
}
|
|
37
|
-
export async function generateCommand({ check = false
|
|
38
|
-
if (checkRetiredSyncLabelsConfig()) {
|
|
38
|
+
export async function generateCommand({ check = false, configPath, styles }) {
|
|
39
|
+
if (checkRetiredSyncLabelsConfig(configPath)) {
|
|
39
40
|
return 1;
|
|
40
41
|
}
|
|
41
|
-
const repoLabels = await loadRepoLabelsConfig();
|
|
42
|
+
const repoLabels = await loadRepoLabelsConfig(styles.stderr, configPath);
|
|
42
43
|
if (repoLabels === undefined) {
|
|
43
44
|
return 1;
|
|
44
45
|
}
|
|
@@ -56,7 +57,7 @@ export async function generateCommand({ check = false } = {}) {
|
|
|
56
57
|
for (const presetName of presetNames) {
|
|
57
58
|
presetHashes.set(presetName, hashPresetFile(presetName));
|
|
58
59
|
}
|
|
59
|
-
const content = formatLabelsYaml(labels, presetHashes);
|
|
60
|
+
const content = formatLabelsYaml(labels, presetHashes, configPath);
|
|
60
61
|
if (check) {
|
|
61
62
|
return compareAgainstCommittedFile(content);
|
|
62
63
|
}
|