@williamthorsen/release-kit 10.9.0 → 12.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 +119 -2
- package/.readyup/kits/default.js +1 -17
- package/.readyup/kits/npm-auto-publish.js +42 -22
- package/.readyup/manifest.json +7 -7
- package/CHANGELOG.md +88 -0
- package/README.md +32 -32
- package/dist/esm/attachChangelogDiagnostics.d.ts +4 -0
- package/dist/esm/attachChangelogDiagnostics.js +11 -0
- package/dist/esm/bin/release-kit.js +34 -25
- package/dist/esm/buildChangelogEntries.d.ts +59 -17
- package/dist/esm/buildChangelogEntries.js +195 -111
- package/dist/esm/buildReleaseSummary.js +2 -2
- package/dist/esm/changelogOverrides.d.ts +31 -13
- package/dist/esm/changelogOverrides.js +138 -58
- package/dist/esm/classifyChangelogCommit.d.ts +38 -0
- package/dist/esm/classifyChangelogCommit.js +33 -0
- package/dist/esm/configFlagSchema.d.ts +12 -0
- package/dist/esm/configFlagSchema.js +3 -0
- package/dist/esm/createGithubReleaseCommand.js +6 -2
- package/dist/esm/decideRelease.d.ts +11 -37
- package/dist/esm/decideRelease.js +5 -27
- package/dist/esm/defaults.d.ts +1 -1
- package/dist/esm/defaults.js +3 -0
- package/dist/esm/determineBumpType.d.ts +11 -11
- package/dist/esm/determineBumpType.js +6 -7
- package/dist/esm/discoverWorkspaces.d.ts +53 -8
- package/dist/esm/discoverWorkspaces.js +57 -37
- package/dist/esm/enumerateReleaseWindows.d.ts +50 -0
- package/dist/esm/enumerateReleaseWindows.js +124 -0
- package/dist/esm/generateChangelogs.d.ts +5 -23
- package/dist/esm/generateChangelogs.js +0 -14
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +5 -6
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/init/initCommand.js +1 -1
- package/dist/esm/init/scaffold.d.ts +0 -2
- package/dist/esm/init/scaffold.js +2 -30
- package/dist/esm/loadConfig.d.ts +8 -5
- package/dist/esm/loadConfig.js +7 -7
- package/dist/esm/loadValidatedConfig.d.ts +46 -5
- package/dist/esm/loadValidatedConfig.js +32 -15
- package/dist/esm/parseChangeRecordBlock.d.ts +37 -0
- package/dist/esm/parseChangeRecordBlock.js +134 -0
- package/dist/esm/parseCommitMessage.d.ts +31 -6
- package/dist/esm/parseCommitMessage.js +6 -5
- package/dist/esm/prepareCommand.d.ts +5 -0
- package/dist/esm/prepareCommand.js +30 -47
- package/dist/esm/previewTagPrefixes.d.ts +9 -6
- package/dist/esm/previewTagPrefixes.js +12 -24
- package/dist/esm/publishCommand.d.ts +2 -2
- package/dist/esm/publishCommand.js +8 -2
- package/dist/esm/pushCommand.d.ts +1 -1
- package/dist/esm/pushCommand.js +2 -2
- package/dist/esm/releasePrepare.d.ts +7 -8
- package/dist/esm/releasePrepare.js +53 -73
- package/dist/esm/releasePrepareMono.js +48 -85
- package/dist/esm/releasePrepareProject.d.ts +1 -1
- package/dist/esm/releasePrepareProject.js +19 -37
- package/dist/esm/reportPrepare.js +45 -30
- package/dist/esm/resolveCommandTags.d.ts +3 -2
- package/dist/esm/resolveCommandTags.js +10 -6
- package/dist/esm/resolveReleaseNotesConfig.d.ts +9 -11
- package/dist/esm/resolveReleaseNotesConfig.js +9 -28
- package/dist/esm/showTagPrefixesCommand.d.ts +4 -2
- package/dist/esm/showTagPrefixesCommand.js +17 -3
- package/dist/esm/sync-labels/generateCommand.d.ts +16 -8
- package/dist/esm/sync-labels/generateCommand.js +12 -11
- package/dist/esm/sync-labels/initCommand.d.ts +9 -4
- package/dist/esm/sync-labels/initCommand.js +21 -15
- package/dist/esm/sync-labels/retiredConfig.d.ts +4 -2
- package/dist/esm/sync-labels/retiredConfig.js +2 -2
- package/dist/esm/types.d.ts +99 -55
- package/dist/esm/types.js +1 -1
- package/dist/esm/validateConfig.js +4 -0
- package/dist/esm/validateOverridesCommand.d.ts +16 -9
- package/dist/esm/validateOverridesCommand.js +39 -54
- package/docs/changelogs.md +64 -25
- package/docs/configuration.md +14 -3
- package/docs/editorial-overrides.md +75 -16
- package/docs/labels.md +2 -0
- package/docs/project-releases.md +4 -4
- package/docs/releasing.md +13 -8
- package/docs/work-types.md +13 -9
- package/package.json +5 -7
- package/schemas/changelog-overrides.json +39 -0
- package/cliff.toml.template +0 -65
- package/dist/esm/collectPolicyViolations.d.ts +0 -15
- package/dist/esm/collectPolicyViolations.js +0 -12
- package/dist/esm/determineBumpFromCommits.d.ts +0 -17
- package/dist/esm/determineBumpFromCommits.js +0 -28
- package/dist/esm/getCommitsSinceTarget.d.ts +0 -21
- package/dist/esm/getCommitsSinceTarget.js +0 -66
- package/dist/esm/resolveCliffConfigPath.d.ts +0 -11
- package/dist/esm/resolveCliffConfigPath.js +0 -20
- package/dist/esm/runGitCliff.d.ts +0 -38
- package/dist/esm/runGitCliff.js +0 -25
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { join as joinPath } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { attachChangelogDiagnostics } from "./attachChangelogDiagnostics.js";
|
|
3
|
+
import { readReleaseHistory, toChangelogEntries } from "./buildChangelogEntries.js";
|
|
3
4
|
import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
|
|
4
5
|
import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
|
|
5
6
|
import { applyChangelogOverrides } from "./changelogOverrides.js";
|
|
6
|
-
import { createPolicyViolationCollector } from "./collectPolicyViolations.js";
|
|
7
7
|
import { decideRelease } from "./decideRelease.js";
|
|
8
|
-
import {
|
|
9
|
-
import { buildTagPattern } from "./generateChangelogs.js";
|
|
10
|
-
import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
|
|
8
|
+
import { DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
11
9
|
import { planReleaseNotesPreviews } from "./planReleaseNotesPreviews.js";
|
|
12
10
|
import { planVersionBump } from "./planVersionBump.js";
|
|
13
11
|
import { renderChangelogMarkdown } from "./renderChangelogMarkdown.js";
|
|
@@ -23,54 +21,43 @@ export function releasePrepareProject(args) {
|
|
|
23
21
|
throw new Error('releasePrepareProject called without a configured project block');
|
|
24
22
|
}
|
|
25
23
|
const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const { tag, commits } = getCommitsSinceTarget([project.tagPrefix], contributingPaths);
|
|
24
|
+
const history = readReleaseHistory(config, { tagPrefixes: [project.tagPrefix], paths: project.paths });
|
|
25
|
+
const { commits, diagnostics, parsedCommitCount, unparseableCommits } = history.unreleased;
|
|
26
|
+
const tag = history.previousTag;
|
|
30
27
|
const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
|
|
31
|
-
const collector = createPolicyViolationCollector();
|
|
32
28
|
const decision = decideRelease({
|
|
33
|
-
|
|
29
|
+
naturalBump: history.unreleased.bump,
|
|
30
|
+
commitCount: commits.length,
|
|
34
31
|
force,
|
|
35
32
|
bumpOverride,
|
|
36
|
-
workTypes,
|
|
37
|
-
versionPatterns,
|
|
38
|
-
scopeAliases: config.scopeAliases,
|
|
39
|
-
breakingPolicies,
|
|
40
|
-
onPolicyViolation: collector.onPolicyViolation,
|
|
41
33
|
skipReasons: {
|
|
42
34
|
noCommits: `No commits ${since}. Pass --force to release at patch. Skipping.`,
|
|
43
35
|
noBumpWorthy: `No bump-worthy commits ${since}. Pass --force to release at patch (or --force --bump=X for a different level). Skipping.`,
|
|
44
36
|
},
|
|
45
37
|
});
|
|
46
|
-
const policyViolations = collector.violations.length > 0 ? collector.violations : undefined;
|
|
47
38
|
if (decision.outcome === 'skip') {
|
|
48
39
|
const skipped = {
|
|
49
40
|
status: 'skipped',
|
|
50
41
|
commitCount: commits.length,
|
|
51
|
-
parsedCommitCount
|
|
42
|
+
parsedCommitCount,
|
|
52
43
|
skipReason: decision.skipReason,
|
|
53
44
|
};
|
|
54
45
|
if (tag !== undefined) {
|
|
55
46
|
skipped.previousTag = tag;
|
|
56
47
|
}
|
|
57
|
-
if (
|
|
58
|
-
skipped.unparseableCommits =
|
|
59
|
-
}
|
|
60
|
-
if (policyViolations !== undefined) {
|
|
61
|
-
skipped.policyViolations = policyViolations;
|
|
48
|
+
if (unparseableCommits !== undefined) {
|
|
49
|
+
skipped.unparseableCommits = unparseableCommits;
|
|
62
50
|
}
|
|
51
|
+
attachChangelogDiagnostics(skipped, diagnostics);
|
|
63
52
|
return skipped;
|
|
64
53
|
}
|
|
65
|
-
const { releaseType
|
|
54
|
+
const { releaseType } = decision;
|
|
66
55
|
const bump = planVersionBump([ROOT_PACKAGE_FILE], releaseType);
|
|
67
56
|
writes.push(...bump.writes);
|
|
68
57
|
const newTag = `${project.tagPrefix}${bump.newVersion}`;
|
|
69
58
|
const changelogs = planProjectChangelogs({
|
|
70
59
|
config,
|
|
71
|
-
|
|
72
|
-
commits,
|
|
73
|
-
contributingPaths,
|
|
60
|
+
history,
|
|
74
61
|
newTag,
|
|
75
62
|
newVersion: bump.newVersion,
|
|
76
63
|
rootOverrides,
|
|
@@ -114,12 +101,10 @@ export function releasePrepareProject(args) {
|
|
|
114
101
|
if (unparseableCommits !== undefined) {
|
|
115
102
|
result.unparseableCommits = unparseableCommits;
|
|
116
103
|
}
|
|
117
|
-
if (policyViolations !== undefined) {
|
|
118
|
-
result.policyViolations = policyViolations;
|
|
119
|
-
}
|
|
120
104
|
if (bumpOverride !== undefined) {
|
|
121
105
|
result.bumpOverride = bumpOverride;
|
|
122
106
|
}
|
|
107
|
+
attachChangelogDiagnostics(result, diagnostics);
|
|
123
108
|
return result;
|
|
124
109
|
}
|
|
125
110
|
function resolveOptionalOverrideArgs(args) {
|
|
@@ -131,14 +116,11 @@ function resolveOptionalOverrideArgs(args) {
|
|
|
131
116
|
};
|
|
132
117
|
}
|
|
133
118
|
function planProjectChangelogs(args) {
|
|
134
|
-
const { config,
|
|
135
|
-
const isEmptyRange = commits.length === 0;
|
|
119
|
+
const { config, history, newTag, newVersion, rootOverrides, overrideWarnings, globalMatchedRootKeys } = args;
|
|
120
|
+
const isEmptyRange = history.unreleased.commits.length === 0;
|
|
136
121
|
const today = new Date().toISOString().slice(0, 10);
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
? [buildEmptyReleaseEntry(newVersion, today)]
|
|
140
|
-
: buildChangelogEntries(config, newTag, { tagPattern, includePaths: contributingPaths });
|
|
141
|
-
const applied = applyChangelogOverrides(newEntries, rootOverrides);
|
|
122
|
+
const builtEntries = isEmptyRange ? [buildEmptyReleaseEntry(newVersion, today)] : toChangelogEntries(history, newTag);
|
|
123
|
+
const applied = applyChangelogOverrides(builtEntries, rootOverrides);
|
|
142
124
|
if (applied.errors.length > 0) {
|
|
143
125
|
throw new Error(`Changelog override application failed:\n - ${applied.errors.join('\n - ')}`);
|
|
144
126
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, } from '@williamthorsen/nmr-core';
|
|
1
|
+
import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, truncateToWidth, } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { bold, dim, sectionHeader } from "./format.js";
|
|
3
3
|
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
4
|
-
|
|
5
|
-
const CLIFF_DRY_RUN_COMMAND = `npx ${GIT_CLIFF_NPX_ARGS.join(' ')} ...`;
|
|
4
|
+
const SUBJECT_COLUMN_BUDGET = 72;
|
|
6
5
|
export function reportPrepare(plan, options) {
|
|
7
6
|
const isMultiWorkspace = plan.workspaces.some((w) => w.name !== undefined) || plan.project !== undefined;
|
|
8
7
|
if (isMultiWorkspace) {
|
|
@@ -26,6 +25,7 @@ function formatSingleWorkspace(result, options) {
|
|
|
26
25
|
formatUnparseableWarning(lines, workspace, style);
|
|
27
26
|
formatPolicyViolations(lines, workspace.policyViolations, style);
|
|
28
27
|
if (workspace.status === 'skipped') {
|
|
28
|
+
formatChangeRecordWarnings(lines, workspace, style);
|
|
29
29
|
lines.push(formatStatusLine(style, 'skipped', workspace.skipReason));
|
|
30
30
|
formatWarnings(lines, result, style);
|
|
31
31
|
return lines.join('\n');
|
|
@@ -48,6 +48,7 @@ function formatSingleWorkspace(result, options) {
|
|
|
48
48
|
else if (workspace.releaseType !== undefined) {
|
|
49
49
|
lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType})`));
|
|
50
50
|
}
|
|
51
|
+
formatChangeRecordWarnings(lines, workspace, style);
|
|
51
52
|
formatBumpFiles(lines, workspace, dryRun);
|
|
52
53
|
lines.push(dim('Generating changelogs...'));
|
|
53
54
|
formatChangelogFiles(lines, workspace, dryRun);
|
|
@@ -86,6 +87,8 @@ function formatProjectSection(lines, project, dryRun, style) {
|
|
|
86
87
|
lines.push(dim(` Found ${project.commitCount} commits ${since}`));
|
|
87
88
|
formatPolicyViolations(lines, project.policyViolations, style, ' ');
|
|
88
89
|
if (project.status === 'skipped') {
|
|
90
|
+
formatUnparseableWarning(lines, project, style, ' ');
|
|
91
|
+
formatChangeRecordWarnings(lines, project, style, ' ');
|
|
89
92
|
lines.push(` ${formatStatusLine(style, 'skipped', project.skipReason)}`);
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
@@ -96,7 +99,8 @@ function formatProjectSection(lines, project, dryRun, style) {
|
|
|
96
99
|
if (project.bumpOverride !== undefined) {
|
|
97
100
|
lines.push(` Using bump override: ${project.bumpOverride}`);
|
|
98
101
|
}
|
|
99
|
-
|
|
102
|
+
formatUnparseableWarning(lines, project, style, ' ');
|
|
103
|
+
formatChangeRecordWarnings(lines, project, style, ' ');
|
|
100
104
|
lines.push(dim(` Bumping versions (${releaseType})...`), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${currentVersion} → ${bold(newVersion)} (${releaseType})`)}`);
|
|
101
105
|
for (const file of project.bumpedFiles) {
|
|
102
106
|
if (dryRun) {
|
|
@@ -109,7 +113,7 @@ function formatProjectSection(lines, project, dryRun, style) {
|
|
|
109
113
|
lines.push(dim(' Generating changelogs...'));
|
|
110
114
|
for (const file of project.changelogFiles) {
|
|
111
115
|
if (dryRun) {
|
|
112
|
-
lines.push(dim(` [dry-run] Would
|
|
116
|
+
lines.push(dim(` [dry-run] Would generate changelog: ${file}`));
|
|
113
117
|
}
|
|
114
118
|
else {
|
|
115
119
|
lines.push(dim(` Generating changelog: ${file}`));
|
|
@@ -118,21 +122,6 @@ function formatProjectSection(lines, project, dryRun, style) {
|
|
|
118
122
|
formatPreviewFiles(lines, project, dryRun, ' ');
|
|
119
123
|
lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
|
|
120
124
|
}
|
|
121
|
-
function formatProjectUnparseable(lines, project, style) {
|
|
122
|
-
const unparseable = project.unparseableCommits;
|
|
123
|
-
if (unparseable === undefined || unparseable.length === 0) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const count = unparseable.length;
|
|
127
|
-
const isPatchFloor = project.parsedCommitCount === 0;
|
|
128
|
-
const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
|
|
129
|
-
lines.push(` ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
|
|
130
|
-
for (const commit of unparseable) {
|
|
131
|
-
const shortHash = commit.hash.slice(0, 7);
|
|
132
|
-
const truncatedMessage = commit.message.length > 72 ? `${commit.message.slice(0, 69)}...` : commit.message;
|
|
133
|
-
lines.push(` · ${shortHash} ${truncatedMessage}`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
125
|
function formatWorkspaceSection(lines, workspace, dryRun, style) {
|
|
137
126
|
if (workspace.name !== undefined) {
|
|
138
127
|
lines.push(`\n${sectionHeader(workspace.name)}`);
|
|
@@ -140,6 +129,9 @@ function formatWorkspaceSection(lines, workspace, dryRun, style) {
|
|
|
140
129
|
const since = workspace.previousTag === undefined ? '(no previous release found)' : `since ${workspace.previousTag}`;
|
|
141
130
|
lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
|
|
142
131
|
if (workspace.status === 'skipped') {
|
|
132
|
+
formatUnparseableWarning(lines, workspace, style, ' ');
|
|
133
|
+
formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
|
|
134
|
+
formatChangeRecordWarnings(lines, workspace, style, ' ');
|
|
143
135
|
lines.push(` ${formatStatusLine(style, 'skipped', workspace.skipReason)}`);
|
|
144
136
|
return;
|
|
145
137
|
}
|
|
@@ -148,6 +140,7 @@ function formatWorkspaceSection(lines, workspace, dryRun, style) {
|
|
|
148
140
|
formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
|
|
149
141
|
formatUnparseableWarning(lines, workspace, style, ' ');
|
|
150
142
|
formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
|
|
143
|
+
formatChangeRecordWarnings(lines, workspace, style, ' ');
|
|
151
144
|
formatBumpLabels(lines, workspace, isPropagatedOnly);
|
|
152
145
|
formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style);
|
|
153
146
|
formatBumpFiles(lines, workspace, dryRun, ' ');
|
|
@@ -202,7 +195,7 @@ function formatBumpFiles(lines, workspace, dryRun, indent = '') {
|
|
|
202
195
|
function formatChangelogFiles(lines, workspace, dryRun, indent = '') {
|
|
203
196
|
for (const file of workspace.changelogFiles) {
|
|
204
197
|
if (dryRun) {
|
|
205
|
-
lines.push(dim(`${indent} [dry-run] Would
|
|
198
|
+
lines.push(dim(`${indent} [dry-run] Would generate changelog: ${file}`));
|
|
206
199
|
}
|
|
207
200
|
else {
|
|
208
201
|
lines.push(dim(`${indent} Generating changelog: ${file}`));
|
|
@@ -220,19 +213,17 @@ function formatPreviewFiles(lines, release, dryRun, indent = '') {
|
|
|
220
213
|
}
|
|
221
214
|
}
|
|
222
215
|
}
|
|
223
|
-
function formatUnparseableWarning(lines,
|
|
224
|
-
const unparseable =
|
|
216
|
+
function formatUnparseableWarning(lines, release, style, indent = '') {
|
|
217
|
+
const unparseable = release.unparseableCommits;
|
|
225
218
|
if (unparseable === undefined || unparseable.length === 0) {
|
|
226
219
|
return;
|
|
227
220
|
}
|
|
228
221
|
const count = unparseable.length;
|
|
229
|
-
|
|
230
|
-
const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
|
|
231
|
-
lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
|
|
222
|
+
lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed`)}`);
|
|
232
223
|
for (const commit of unparseable) {
|
|
233
224
|
const shortHash = commit.hash.slice(0, 7);
|
|
234
|
-
const
|
|
235
|
-
lines.push(`${indent} · ${shortHash} ${
|
|
225
|
+
const truncatedSubject = truncateSubject(commit.subject);
|
|
226
|
+
lines.push(`${indent} · ${shortHash} ${truncatedSubject}`);
|
|
236
227
|
}
|
|
237
228
|
}
|
|
238
229
|
function formatPolicyViolations(lines, violations, style, indent = '') {
|
|
@@ -244,8 +235,29 @@ function formatPolicyViolations(lines, violations, style, indent = '') {
|
|
|
244
235
|
for (const violation of violations) {
|
|
245
236
|
const shortHash = violation.commitHash.slice(0, 7);
|
|
246
237
|
const subject = violation.commitSubject;
|
|
247
|
-
const truncatedSubject = subject
|
|
248
|
-
|
|
238
|
+
const truncatedSubject = truncateSubject(subject);
|
|
239
|
+
const location = violation.surface === 'entry' ? `entry ${violation.entryPosition ?? '?'}` : `${violation.surface} surface`;
|
|
240
|
+
lines.push(`${indent} · ${shortHash} '${truncatedSubject}' — type '${violation.type}' at ${location}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function formatChangeRecordWarnings(lines, release, style, indent = '') {
|
|
244
|
+
const { malformedBlocks = [], undeclaredEntryTypes = [] } = release;
|
|
245
|
+
if (malformedBlocks.length > 0) {
|
|
246
|
+
const count = malformedBlocks.length;
|
|
247
|
+
const header = `${count} change-record block${count === 1 ? '' : 's'} could not be read (item taken from the title)`;
|
|
248
|
+
lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
|
|
249
|
+
for (const block of malformedBlocks) {
|
|
250
|
+
lines.push(`${indent} · ${block.commitHash.slice(0, 7)} '${truncateSubject(block.commitSubject)}' — ${block.reason}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (undeclaredEntryTypes.length > 0) {
|
|
254
|
+
const count = undeclaredEntryTypes.length;
|
|
255
|
+
const header = `${count} change-record entr${count === 1 ? 'y has an' : 'ies have'} undeclared type${count === 1 ? '' : 's'} (no item)`;
|
|
256
|
+
lines.push(`${indent} ${formatStatusLine(style, 'warning', `${header}:`)}`);
|
|
257
|
+
for (const entry of undeclaredEntryTypes) {
|
|
258
|
+
const shortHash = entry.commitHash.slice(0, 7);
|
|
259
|
+
lines.push(`${indent} · ${shortHash} '${truncateSubject(entry.commitSubject)}' — type '${entry.type}' at entry ${entry.entryPosition}`);
|
|
260
|
+
}
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
function formatPropagationSuffix(propagatedFrom) {
|
|
@@ -280,3 +292,6 @@ function formatFormatCommand(lines, result, options) {
|
|
|
280
292
|
const messageIndent = ' '.repeat(2 + measureGlyphColumn(STATUS_GLYPHS[options.style]) + 1);
|
|
281
293
|
lines.push(`\n ${formatStatusLine(options.style, 'warning', `Format command failed: ${result.formatCommand.command}`)}`, `${messageIndent}${options.formatError}`);
|
|
282
294
|
}
|
|
295
|
+
function truncateSubject(subject) {
|
|
296
|
+
return truncateToWidth(subject, { width: SUBJECT_COLUMN_BUDGET });
|
|
297
|
+
}
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
2
|
import type { ReleaseNotesConfig } from './types.js';
|
|
3
3
|
export interface ResolvedReleaseNotesConfig {
|
|
4
4
|
releaseNotes: ReleaseNotesConfig;
|
|
@@ -7,19 +7,17 @@ export interface ResolvedReleaseNotesConfig {
|
|
|
7
7
|
sectionOrder: string[];
|
|
8
8
|
}
|
|
9
9
|
export interface ResolveReleaseNotesConfigOptions {
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
* defaults. Use from CLI commands whose entire behavior depends on the resolved config (e.g.
|
|
13
|
-
* `create-github-release`), so a corrupt or unreadable config cannot silently send the command
|
|
14
|
-
* to the wrong changelog path.
|
|
15
|
-
*/
|
|
16
|
-
strictLoad?: boolean;
|
|
10
|
+
/** Config file to read, relative to the working directory. Defaults to `CONFIG_FILE_PATH`. */
|
|
11
|
+
configPath?: string;
|
|
17
12
|
}
|
|
18
13
|
/**
|
|
19
|
-
*
|
|
14
|
+
* Loads and validates the release-kit config, resolving the release-notes settings it carries.
|
|
20
15
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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.
|
|
23
21
|
*/
|
|
24
22
|
export declare function resolveReleaseNotesConfig(stderrStyle: OutputStyle, options?: ResolveReleaseNotesConfigOptions): Promise<ResolvedReleaseNotesConfig>;
|
|
25
23
|
/** Extract section headers in declaration order from a merged workTypes record. */
|
|
@@ -1,40 +1,21 @@
|
|
|
1
|
-
import { formatStatusLine, printError, 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 {
|
|
2
|
+
import { resolveWorkTypes } from "./loadConfig.js";
|
|
3
|
+
import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
|
|
6
4
|
export async function resolveReleaseNotesConfig(stderrStyle, 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}`);
|
|
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
|
-
printError(err, stderrStyle);
|
|
32
|
-
}
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
for (const warning of warnings) {
|
|
36
|
-
console.warn(` ${formatStatusLine(stderrStyle, 'warning', 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,
|
|
@@ -6,8 +6,10 @@ import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
|
6
6
|
* entries, followed by an "Undeclared tag prefixes" section when candidate-shaped tags
|
|
7
7
|
* exist outside the known set. Exits `0` on full derivation success and no collisions;
|
|
8
8
|
* `1` on any derivation failure or collision. Undeclared candidates do not affect the
|
|
9
|
-
* 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.
|
|
10
12
|
*
|
|
11
13
|
* @returns The exit code the caller should use.
|
|
12
14
|
*/
|
|
13
|
-
export declare function showTagPrefixesCommand(styles: StreamStyles): Promise<number>;
|
|
15
|
+
export declare function showTagPrefixesCommand(styles: StreamStyles, configPath?: string): Promise<number>;
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import { formatStatusLine } from '@williamthorsen/nmr-core';
|
|
1
|
+
import { formatStatusLine, reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
2
3
|
import { detectRepoType } from "./init/detectRepoType.js";
|
|
4
|
+
import { loadValidatedConfig, reportConfigProblem } from "./loadValidatedConfig.js";
|
|
3
5
|
import { previewTagPrefixes } from "./previewTagPrefixes.js";
|
|
4
|
-
export async function showTagPrefixesCommand(styles) {
|
|
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
|
+
}
|
|
5
12
|
if (detectRepoType() === 'single-package') {
|
|
6
13
|
process.stdout.write(renderSinglePackage());
|
|
7
14
|
return 0;
|
|
8
15
|
}
|
|
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
|
+
}
|
|
10
24
|
process.stdout.write(renderMonorepo(preview, styles.stdout));
|
|
11
25
|
return computeExitCode(preview);
|
|
12
26
|
}
|
|
@@ -3,27 +3,35 @@ import type { RepoLabelsConfig } from '../types.js';
|
|
|
3
3
|
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
|
-
/** Formats resolved labels as YAML with generated header including preset hashes. */
|
|
7
|
-
export declare function formatLabelsYaml(labels: LabelDefinition[], presetHashes: Map<string, string>): string;
|
|
8
6
|
/**
|
|
9
|
-
*
|
|
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.
|
|
10
15
|
*
|
|
11
16
|
* Reports an actionable error and returns `undefined` when the config file is missing,
|
|
12
|
-
* 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.
|
|
13
19
|
*/
|
|
14
|
-
export declare function loadRepoLabelsConfig(stderrStyle: OutputStyle): Promise<RepoLabelsConfig | undefined>;
|
|
20
|
+
export declare function loadRepoLabelsConfig(stderrStyle: OutputStyle, configPath?: string): Promise<RepoLabelsConfig | undefined>;
|
|
15
21
|
/** Options for the `sync-labels generate` subcommand. */
|
|
16
22
|
interface GenerateOptions {
|
|
17
23
|
/** Compare the regenerated content against the committed file instead of writing. */
|
|
18
24
|
check?: boolean;
|
|
25
|
+
/** Config file to read, relative to the working directory. Defaults to `CONFIG_FILE_PATH`. */
|
|
26
|
+
configPath?: string;
|
|
19
27
|
styles: StreamStyles;
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
30
|
* Runs the `sync-labels generate` command.
|
|
23
31
|
*
|
|
24
|
-
* Loads the `repoLabels` block from
|
|
25
|
-
*
|
|
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
|
|
26
34
|
* is stale, writing nothing. Returns 0 on success, 1 on failure.
|
|
27
35
|
*/
|
|
28
|
-
export declare function generateCommand({ check, styles }: GenerateOptions): Promise<number>;
|
|
36
|
+
export declare function generateCommand({ check, configPath, styles }: GenerateOptions): Promise<number>;
|
|
29
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(stderrStyle) {
|
|
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, styles }) {
|
|
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(styles.stderr);
|
|
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, styles }) {
|
|
|
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
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
2
2
|
/** Options for the `sync-labels init` subcommand. */
|
|
3
3
|
interface InitOptions {
|
|
4
|
+
/** Config file to read, relative to the working directory. Defaults to `CONFIG_FILE_PATH`. */
|
|
5
|
+
configPath?: string;
|
|
4
6
|
dryRun: boolean;
|
|
5
7
|
force: boolean;
|
|
6
8
|
styles: StreamStyles;
|
|
@@ -9,9 +11,12 @@ interface InitOptions {
|
|
|
9
11
|
* Run the `sync-labels init` subcommand.
|
|
10
12
|
*
|
|
11
13
|
* Discovers workspaces and retired packages, scaffolds the caller workflow, then seeds the
|
|
12
|
-
* `repoLabels` block: into a new
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
17
|
+
*
|
|
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`.
|
|
15
20
|
*/
|
|
16
|
-
export declare function syncLabelsInitCommand({ dryRun, force, styles }: InitOptions): Promise<number>;
|
|
21
|
+
export declare function syncLabelsInitCommand({ configPath, dryRun, force, styles }: InitOptions): Promise<number>;
|
|
17
22
|
export {};
|