@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
package/dist/esm/loadConfig.d.ts
CHANGED
|
@@ -15,11 +15,14 @@ export declare function readRootPackageVersion(): {
|
|
|
15
15
|
/** The path where the consumer-facing config file is expected. */
|
|
16
16
|
export declare const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
|
|
17
17
|
/**
|
|
18
|
-
* Loads the config file
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
18
|
+
* Loads the config file, returning the raw config object.
|
|
19
|
+
*
|
|
20
|
+
* `configPath` is resolved against the working directory and defaults to `CONFIG_FILE_PATH`. An absent default
|
|
21
|
+
* path returns `undefined`, because a repo that declares no config is a supported state; an absent named path
|
|
22
|
+
* throws, because the caller asked for that file by name. Also throws when the file exists but cannot be
|
|
23
|
+
* imported, or exports neither a default nor a named `config`.
|
|
21
24
|
*/
|
|
22
|
-
export declare function loadConfig(): Promise<unknown>;
|
|
25
|
+
export declare function loadConfig(configPath?: string): Promise<unknown>;
|
|
23
26
|
/**
|
|
24
27
|
* Information about the root `package.json` passed into `mergeMonorepoConfig` when a
|
|
25
28
|
* `project` block is configured. The async I/O lives in `readRootPackageVersion`; this
|
|
@@ -37,7 +40,7 @@ export interface RootPackageInfo {
|
|
|
37
40
|
* removes the workspace; unlisted packages keep defaults.
|
|
38
41
|
* - `workTypes`: shallow merge — consumer entries override or add to defaults by key.
|
|
39
42
|
* - `versionPatterns`: consumer value replaces defaults entirely.
|
|
40
|
-
* - `formatCommand`, `
|
|
43
|
+
* - `formatCommand`, `scopeAliases`: consumer value wins.
|
|
41
44
|
* - `project`: present iff `userConfig.project` is declared. Resolves `tagPrefix` to
|
|
42
45
|
* `DEFAULT_PROJECT_TAG_PREFIX` and `paths` to the union of the retained workspaces' `paths`
|
|
43
46
|
* when omitted. Requires `rootPackage` to be passed and to contain a valid `version` field;
|
package/dist/esm/loadConfig.js
CHANGED
|
@@ -39,10 +39,13 @@ export function readRootPackageVersion() {
|
|
|
39
39
|
return { exists: true, version: typeof parsed['version'] === 'string' ? parsed['version'] : undefined };
|
|
40
40
|
}
|
|
41
41
|
export const CONFIG_FILE_PATH = '.config/release-kit.config.ts';
|
|
42
|
-
export async function loadConfig() {
|
|
43
|
-
const absoluteConfigPath = path.resolve(process.cwd(), CONFIG_FILE_PATH);
|
|
42
|
+
export async function loadConfig(configPath) {
|
|
43
|
+
const absoluteConfigPath = path.resolve(process.cwd(), configPath ?? CONFIG_FILE_PATH);
|
|
44
44
|
if (!existsSync(absoluteConfigPath)) {
|
|
45
|
-
|
|
45
|
+
if (configPath === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Config file not found: ${absoluteConfigPath}`);
|
|
46
49
|
}
|
|
47
50
|
const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(absoluteConfigPath).href));
|
|
48
51
|
const resolved = isRecord(imported) ? (imported['default'] ?? imported['config']) : undefined;
|
|
@@ -96,9 +99,6 @@ function applyOptionalPassthroughFields(result, userConfig) {
|
|
|
96
99
|
if (userConfig?.formatCommand !== undefined) {
|
|
97
100
|
result.formatCommand = userConfig.formatCommand;
|
|
98
101
|
}
|
|
99
|
-
if (userConfig?.cliffConfigPath !== undefined) {
|
|
100
|
-
result.cliffConfigPath = userConfig.cliffConfigPath;
|
|
101
|
-
}
|
|
102
102
|
if (userConfig?.scopeAliases !== undefined) {
|
|
103
103
|
result.scopeAliases = userConfig.scopeAliases;
|
|
104
104
|
}
|
|
@@ -212,7 +212,7 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
|
|
|
212
212
|
if (isPrefixCollision(a.prefix, b.prefix)) {
|
|
213
213
|
throw new Error(`Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). ` +
|
|
214
214
|
'One prefix is identical to or a strict prefix of the other; ' +
|
|
215
|
-
'
|
|
215
|
+
'a tag under the longer prefix can then match the shorter one, whose releases would absorb it.');
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
}
|
|
@@ -1,19 +1,60 @@
|
|
|
1
1
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
2
|
import type { ReleaseKitConfig } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Why a config file could not be used: the import threw, or the contents failed the schema.
|
|
5
|
+
*
|
|
6
|
+
* The distinction exists so that a caller can render the two differently. Every caller aborts on either.
|
|
7
|
+
*/
|
|
8
|
+
export type ConfigProblem = {
|
|
9
|
+
kind: 'load';
|
|
10
|
+
message: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'validation';
|
|
13
|
+
errors: string[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Outcome of loading and validating the consumer config file. `missing` is not an error
|
|
17
|
+
* here — whether an absent config file is one is the caller's call.
|
|
18
|
+
*
|
|
19
|
+
* `configFilePath` names the file the run read: the caller's `configPath`, or `CONFIG_FILE_PATH` when none was
|
|
20
|
+
* given. It stays relative to the working directory, so a message or a generated artifact that splices it reads
|
|
21
|
+
* the same on every machine.
|
|
6
22
|
*/
|
|
7
23
|
export type LoadValidatedConfigResult = {
|
|
8
24
|
status: 'invalid';
|
|
25
|
+
configFilePath: string;
|
|
26
|
+
problem: ConfigProblem;
|
|
9
27
|
} | {
|
|
10
28
|
status: 'missing';
|
|
29
|
+
configFilePath: string;
|
|
11
30
|
} | {
|
|
12
31
|
status: 'ok';
|
|
13
32
|
config: ReleaseKitConfig;
|
|
33
|
+
configFilePath: string;
|
|
34
|
+
warnings: string[];
|
|
14
35
|
};
|
|
15
36
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
37
|
+
* Loads the consumer config file and validates it against the shared schema, returning the outcome.
|
|
38
|
+
*
|
|
39
|
+
* Writes nothing to any stream: the caller picks the sink, which is what lets a command that must stay silent
|
|
40
|
+
* reuse this loader. `reportConfigProblem` and `reportConfigWarnings` render the stderr form.
|
|
41
|
+
*
|
|
42
|
+
* `configPath` defaults to `CONFIG_FILE_PATH`. A named path that does not exist is a load failure, so it reports
|
|
43
|
+
* as `invalid` rather than `missing`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadValidatedConfig(configPath?: string): Promise<LoadValidatedConfigResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Loads and validates the config, reporting the problem and exiting 1 when it is unusable, so that a command
|
|
48
|
+
* whose later work may never reach a loader still fails on a config that does.
|
|
49
|
+
*
|
|
50
|
+
* An absent default config returns cleanly, which is what keeps a repo that declares no config a supported state.
|
|
51
|
+
* Reports problems but never warnings: the command's own later load stays the single place that emits those, so
|
|
52
|
+
* a warning is not printed twice.
|
|
53
|
+
*
|
|
54
|
+
* The second load costs nothing: `import()` caches by URL and `validateConfig` is pure.
|
|
18
55
|
*/
|
|
19
|
-
export declare function
|
|
56
|
+
export declare function assertConfigUsable(stderrStyle: OutputStyle, configPath?: string): Promise<void>;
|
|
57
|
+
/** Writes a config problem to stderr in the form every CLI command uses. */
|
|
58
|
+
export declare function reportConfigProblem(problem: ConfigProblem, stderrStyle: OutputStyle): void;
|
|
59
|
+
/** Writes validation warnings to stderr as indented status lines. */
|
|
60
|
+
export declare function reportConfigWarnings(warnings: readonly string[], stderrStyle: OutputStyle): void;
|
|
@@ -1,27 +1,44 @@
|
|
|
1
|
-
import { printError, reportError } from '@williamthorsen/nmr-core';
|
|
1
|
+
import { formatStatusLine, printError, reportError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
|
-
import { loadConfig } from "./loadConfig.js";
|
|
3
|
+
import { CONFIG_FILE_PATH, loadConfig } from "./loadConfig.js";
|
|
4
4
|
import { validateConfig } from "./validateConfig.js";
|
|
5
|
-
export async function loadValidatedConfig(
|
|
5
|
+
export async function loadValidatedConfig(configPath) {
|
|
6
|
+
const configFilePath = configPath ?? CONFIG_FILE_PATH;
|
|
6
7
|
let raw;
|
|
7
8
|
try {
|
|
8
|
-
raw = await loadConfig();
|
|
9
|
+
raw = await loadConfig(configPath);
|
|
9
10
|
}
|
|
10
11
|
catch (error) {
|
|
11
|
-
|
|
12
|
-
reportError(`Failed to load config: ${message}`);
|
|
13
|
-
return { status: 'invalid' };
|
|
12
|
+
return { status: 'invalid', configFilePath, problem: { kind: 'load', message: describeError(error) } };
|
|
14
13
|
}
|
|
15
14
|
if (raw === undefined) {
|
|
16
|
-
return { status: 'missing' };
|
|
15
|
+
return { status: 'missing', configFilePath };
|
|
17
16
|
}
|
|
18
|
-
const { config, errors } = validateConfig(raw);
|
|
17
|
+
const { config, errors, warnings } = validateConfig(raw);
|
|
19
18
|
if (errors.length > 0) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
return { status: 'invalid', configFilePath, problem: { kind: 'validation', errors } };
|
|
20
|
+
}
|
|
21
|
+
return { status: 'ok', config, configFilePath, warnings };
|
|
22
|
+
}
|
|
23
|
+
export async function assertConfigUsable(stderrStyle, configPath) {
|
|
24
|
+
const result = await loadValidatedConfig(configPath);
|
|
25
|
+
if (result.status === 'invalid') {
|
|
26
|
+
reportConfigProblem(result.problem, stderrStyle);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function reportConfigProblem(problem, stderrStyle) {
|
|
31
|
+
if (problem.kind === 'load') {
|
|
32
|
+
reportError(`Failed to load config: ${problem.message}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
process.stderr.write('Invalid config:\n');
|
|
36
|
+
for (const error of problem.errors) {
|
|
37
|
+
printError(error, stderrStyle);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function reportConfigWarnings(warnings, stderrStyle) {
|
|
41
|
+
for (const warning of warnings) {
|
|
42
|
+
console.warn(` ${formatStatusLine(stderrStyle, 'warning', warning)}`);
|
|
25
43
|
}
|
|
26
|
-
return { status: 'ok', config };
|
|
27
44
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the last `change-record` block in a commit message into the change entries that it records.
|
|
3
|
+
*
|
|
4
|
+
* The block's grammar is codeassembly's `change-record.md`: a fence whose info string is exactly `change-record`,
|
|
5
|
+
* containing a YAML mapping. A merge commit's block carries `entries` plus the top-level `pr_number` and `ticket_ref`.
|
|
6
|
+
* A key that the grammar does not declare is ignored, and a declared key whose value is null reads as absent. Any
|
|
7
|
+
* defect makes the whole block malformed; no entry is salvaged from a defective list.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseChangeRecordBlock(message: string): ChangeRecordBlockReading;
|
|
10
|
+
/**
|
|
11
|
+
* Removes every `change-record` block from a message, fences included. A block that never closes runs to the end of
|
|
12
|
+
* the message, as a Markdown renderer reads it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function stripChangeRecordBlocks(message: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* What a commit's last `change-record` block reads as: absent, malformed with the defect named, or the entries that
|
|
17
|
+
* it records with the merge's pull-request number and ticket reference when the block declares them.
|
|
18
|
+
*/
|
|
19
|
+
export type ChangeRecordBlockReading = {
|
|
20
|
+
kind: 'absent';
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'malformed';
|
|
23
|
+
reason: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'read';
|
|
26
|
+
entries: ChangeRecordEntry[];
|
|
27
|
+
prNumber?: number;
|
|
28
|
+
ticketRef?: string;
|
|
29
|
+
};
|
|
30
|
+
/** One outcome of a change, as the block records it. */
|
|
31
|
+
export interface ChangeRecordEntry {
|
|
32
|
+
breaking: boolean;
|
|
33
|
+
migration?: string;
|
|
34
|
+
scopes: string[];
|
|
35
|
+
text: string;
|
|
36
|
+
type: string;
|
|
37
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { parseDocument } from 'yaml';
|
|
2
|
+
import { isRecord } from "./typeGuards.js";
|
|
3
|
+
export function parseChangeRecordBlock(message) {
|
|
4
|
+
const lines = message.split(/\r?\n/);
|
|
5
|
+
const fence = findFences(lines).at(-1);
|
|
6
|
+
if (fence === undefined) {
|
|
7
|
+
return { kind: 'absent' };
|
|
8
|
+
}
|
|
9
|
+
if (fence.close === undefined) {
|
|
10
|
+
return { kind: 'malformed', reason: 'the block opens but never closes' };
|
|
11
|
+
}
|
|
12
|
+
const document = parseDocument(lines.slice(fence.open + 1, fence.close).join('\n'));
|
|
13
|
+
const [error] = document.errors;
|
|
14
|
+
if (error !== undefined) {
|
|
15
|
+
const detail = (error.message.split('\n', 1)[0] ?? '').replace(/:$/, '');
|
|
16
|
+
return { kind: 'malformed', reason: `the payload is not valid YAML: ${detail}` };
|
|
17
|
+
}
|
|
18
|
+
return readPayload(document.toJS());
|
|
19
|
+
}
|
|
20
|
+
export function stripChangeRecordBlocks(message) {
|
|
21
|
+
const lines = message.split(/\r?\n/);
|
|
22
|
+
const fences = findFences(lines);
|
|
23
|
+
return lines
|
|
24
|
+
.filter((_line, index) => fences.every((fence) => index < fence.open || index > (fence.close ?? lines.length)))
|
|
25
|
+
.join('\n');
|
|
26
|
+
}
|
|
27
|
+
const FENCE = '```';
|
|
28
|
+
function findFences(lines) {
|
|
29
|
+
const fences = [];
|
|
30
|
+
let open;
|
|
31
|
+
for (const [index, line] of lines.entries()) {
|
|
32
|
+
if (open === undefined) {
|
|
33
|
+
if (line.trim() === `${FENCE}${INFO_STRING}`) {
|
|
34
|
+
open = index;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (line.trim() === FENCE) {
|
|
38
|
+
fences.push({ close: index, open });
|
|
39
|
+
open = undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (open !== undefined) {
|
|
43
|
+
fences.push({ open });
|
|
44
|
+
}
|
|
45
|
+
return fences;
|
|
46
|
+
}
|
|
47
|
+
const INFO_STRING = 'change-record';
|
|
48
|
+
function readEntry(item, index) {
|
|
49
|
+
const at = `entries[${index}]`;
|
|
50
|
+
if (!isRecord(item)) {
|
|
51
|
+
return { reason: `\`${at}\` is not a mapping` };
|
|
52
|
+
}
|
|
53
|
+
const { breaking, migration, scopes } = item;
|
|
54
|
+
if (breaking !== undefined && breaking !== null && typeof breaking !== 'boolean') {
|
|
55
|
+
return { reason: `\`${at}.breaking\` is not a boolean` };
|
|
56
|
+
}
|
|
57
|
+
if (migration !== undefined && migration !== null && typeof migration !== 'string') {
|
|
58
|
+
return { reason: `\`${at}.migration\` is not a string` };
|
|
59
|
+
}
|
|
60
|
+
if (scopes !== undefined && scopes !== null && !Array.isArray(scopes)) {
|
|
61
|
+
return { reason: `\`${at}.scopes\` is not a list` };
|
|
62
|
+
}
|
|
63
|
+
const declaredScopes = scopes ?? [];
|
|
64
|
+
const readScopes = [];
|
|
65
|
+
for (const [position, scope] of declaredScopes.entries()) {
|
|
66
|
+
if (typeof scope !== 'string') {
|
|
67
|
+
return { reason: `\`${at}.scopes[${position}]\` is not a string` };
|
|
68
|
+
}
|
|
69
|
+
readScopes.push(scope.trim());
|
|
70
|
+
}
|
|
71
|
+
const type = readRequiredString(item, at, 'type');
|
|
72
|
+
if ('reason' in type) {
|
|
73
|
+
return type;
|
|
74
|
+
}
|
|
75
|
+
const text = readRequiredString(item, at, 'text');
|
|
76
|
+
if ('reason' in text) {
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
const trimmedMigration = migration?.trim();
|
|
80
|
+
return {
|
|
81
|
+
entry: {
|
|
82
|
+
breaking: breaking === true,
|
|
83
|
+
...(trimmedMigration !== undefined && trimmedMigration !== '' && { migration: trimmedMigration }),
|
|
84
|
+
scopes: readScopes,
|
|
85
|
+
text: text.value,
|
|
86
|
+
type: type.value,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function readPayload(payload) {
|
|
91
|
+
if (!isRecord(payload)) {
|
|
92
|
+
return { kind: 'malformed', reason: 'the payload is not a mapping' };
|
|
93
|
+
}
|
|
94
|
+
const { entries, pr_number: prNumber, ticket_ref: ticketRef } = payload;
|
|
95
|
+
if (prNumber !== undefined &&
|
|
96
|
+
prNumber !== null &&
|
|
97
|
+
(typeof prNumber !== 'number' || !Number.isSafeInteger(prNumber) || prNumber <= 0)) {
|
|
98
|
+
return { kind: 'malformed', reason: '`pr_number` is not a positive integer' };
|
|
99
|
+
}
|
|
100
|
+
if (ticketRef !== undefined && ticketRef !== null && typeof ticketRef !== 'string') {
|
|
101
|
+
return { kind: 'malformed', reason: '`ticket_ref` is not a string' };
|
|
102
|
+
}
|
|
103
|
+
const readEntries = [];
|
|
104
|
+
if (entries !== undefined && entries !== null) {
|
|
105
|
+
if (!Array.isArray(entries)) {
|
|
106
|
+
return { kind: 'malformed', reason: '`entries` is not a list' };
|
|
107
|
+
}
|
|
108
|
+
for (const [index, item] of entries.entries()) {
|
|
109
|
+
const read = readEntry(item, index);
|
|
110
|
+
if ('reason' in read) {
|
|
111
|
+
return { kind: 'malformed', reason: read.reason };
|
|
112
|
+
}
|
|
113
|
+
readEntries.push(read.entry);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const trimmedTicketRef = ticketRef?.trim();
|
|
117
|
+
return {
|
|
118
|
+
kind: 'read',
|
|
119
|
+
entries: readEntries,
|
|
120
|
+
...(typeof prNumber === 'number' && { prNumber }),
|
|
121
|
+
...(trimmedTicketRef !== undefined && trimmedTicketRef !== '' && { ticketRef: trimmedTicketRef }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function readRequiredString(item, at, name) {
|
|
125
|
+
const value = item[name];
|
|
126
|
+
if (value === undefined || value === null) {
|
|
127
|
+
return { reason: `\`${at}.${name}\` is missing` };
|
|
128
|
+
}
|
|
129
|
+
if (typeof value !== 'string') {
|
|
130
|
+
return { reason: `\`${at}.${name}\` is not a string` };
|
|
131
|
+
}
|
|
132
|
+
const trimmed = value.trim();
|
|
133
|
+
return trimmed === '' ? { reason: `\`${at}.${name}\` is blank` } : { value: trimmed };
|
|
134
|
+
}
|
|
@@ -6,18 +6,20 @@ export declare const COMMIT_PREPROCESSOR_PATTERNS: readonly RegExp[];
|
|
|
6
6
|
*
|
|
7
7
|
* Deliberately permissive: `*` is the sanctioned structural scope for changes spanning every
|
|
8
8
|
* workspace, so the atom admits any run of non-pipe characters rather than word characters.
|
|
9
|
-
* `
|
|
10
|
-
* `cliffConfigAlignment.unit.test.ts` asserts that parity — exported so neither side can drift.
|
|
9
|
+
* Exported because `buildChangelogEntries.ts` builds its breaking-marker pattern from the same atom.
|
|
11
10
|
*/
|
|
12
11
|
export declare const PIPE_SCOPE_SOURCE = "[^|]+";
|
|
13
|
-
/**
|
|
14
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Surface where a breaking-policy violation was detected: a subject's `!`, a `BREAKING CHANGE:` footer, or a
|
|
14
|
+
* change-record entry's `breaking`.
|
|
15
|
+
*/
|
|
16
|
+
export type PolicyViolationSurface = 'prefix' | 'body' | 'entry';
|
|
15
17
|
/**
|
|
16
18
|
* Callback invoked when `parseCommitMessage` detects a `!`-policy violation.
|
|
17
19
|
*
|
|
18
20
|
* The parser warns and continues on a violation, so legacy log entries don't block releases.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* `readReleaseHistory` collects the invocations for the unreleased window, and the release
|
|
22
|
+
* report lists them.
|
|
21
23
|
*/
|
|
22
24
|
export type PolicyViolationHandler = (commit: Commit, type: string, surface: PolicyViolationSurface) => void;
|
|
23
25
|
/** Optional configuration for `parseCommitMessage` beyond the core inputs. */
|
|
@@ -49,3 +51,26 @@ export interface ParseCommitMessageOptions {
|
|
|
49
51
|
* same warning path.
|
|
50
52
|
*/
|
|
51
53
|
export declare function parseCommitMessage(message: string, hash: string, workTypes: Record<string, WorkTypeConfig>, scopeAliases?: Record<string, string>, options?: ParseCommitMessageOptions): ParsedCommit | undefined;
|
|
54
|
+
/** Inputs for {@link evaluateBreakingPolicy}. */
|
|
55
|
+
export interface BreakingPolicyInputs {
|
|
56
|
+
commit: Commit;
|
|
57
|
+
resolvedType: string;
|
|
58
|
+
/** Whether the marker that `prefixSurface` names is present. */
|
|
59
|
+
hasPrefixBreaking: boolean;
|
|
60
|
+
hasFooterBreaking: boolean;
|
|
61
|
+
policy: 'forbidden' | 'optional' | 'required';
|
|
62
|
+
onPolicyViolation: PolicyViolationHandler | undefined;
|
|
63
|
+
/** The surface reported for a violation of the marker; `'prefix'` when omitted. */
|
|
64
|
+
prefixSurface?: PolicyViolationSurface;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Applies the `!`-policy rules and returns the effective `breaking` flag.
|
|
68
|
+
*
|
|
69
|
+
* A violation invokes `onPolicyViolation` and drops the marker (`breaking: false`); the
|
|
70
|
+
* commit still parses.
|
|
71
|
+
*/
|
|
72
|
+
export declare function evaluateBreakingPolicy(inputs: BreakingPolicyInputs): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Resolves a raw type string to its canonical type name using the record keys and aliases.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveType(rawType: string, workTypes: Record<string, WorkTypeConfig>): string | undefined;
|
|
@@ -20,7 +20,7 @@ export function parseCommitMessage(message, hash, workTypes, scopeAliases, optio
|
|
|
20
20
|
if (resolvedType === undefined) {
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
-
const commit = { message, hash };
|
|
23
|
+
const commit = { message, subject: firstLine, hash };
|
|
24
24
|
const breaking = evaluateBreakingPolicy({
|
|
25
25
|
commit,
|
|
26
26
|
resolvedType,
|
|
@@ -40,11 +40,12 @@ export function parseCommitMessage(message, hash, workTypes, scopeAliases, optio
|
|
|
40
40
|
...(resolvedScope !== undefined && { scope: resolvedScope }),
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
function evaluateBreakingPolicy(inputs) {
|
|
43
|
+
export function evaluateBreakingPolicy(inputs) {
|
|
44
44
|
const { commit, resolvedType, hasPrefixBreaking, hasFooterBreaking, policy, onPolicyViolation } = inputs;
|
|
45
|
+
const prefixSurface = inputs.prefixSurface ?? 'prefix';
|
|
45
46
|
if (policy === 'forbidden') {
|
|
46
47
|
if (hasPrefixBreaking) {
|
|
47
|
-
onPolicyViolation?.(commit, resolvedType,
|
|
48
|
+
onPolicyViolation?.(commit, resolvedType, prefixSurface);
|
|
48
49
|
}
|
|
49
50
|
if (hasFooterBreaking) {
|
|
50
51
|
onPolicyViolation?.(commit, resolvedType, 'body');
|
|
@@ -53,14 +54,14 @@ function evaluateBreakingPolicy(inputs) {
|
|
|
53
54
|
}
|
|
54
55
|
if (policy === 'required') {
|
|
55
56
|
if (!hasPrefixBreaking) {
|
|
56
|
-
onPolicyViolation?.(commit, resolvedType,
|
|
57
|
+
onPolicyViolation?.(commit, resolvedType, prefixSurface);
|
|
57
58
|
return false;
|
|
58
59
|
}
|
|
59
60
|
return true;
|
|
60
61
|
}
|
|
61
62
|
return hasPrefixBreaking || hasFooterBreaking;
|
|
62
63
|
}
|
|
63
|
-
function resolveType(rawType, workTypes) {
|
|
64
|
+
export function resolveType(rawType, workTypes) {
|
|
64
65
|
const lowered = rawType.toLowerCase();
|
|
65
66
|
for (const [key, config] of Object.entries(workTypes)) {
|
|
66
67
|
if (key === lowered) {
|
|
@@ -30,9 +30,14 @@ export declare const prepareFlagSchema: {
|
|
|
30
30
|
long: string;
|
|
31
31
|
type: "boolean";
|
|
32
32
|
};
|
|
33
|
+
config: {
|
|
34
|
+
long: string;
|
|
35
|
+
type: "string";
|
|
36
|
+
};
|
|
33
37
|
};
|
|
34
38
|
/** Parses CLI arguments into structured options. Prints a usage error and exits on invalid input. */
|
|
35
39
|
export declare function parseArgs(argv: string[]): {
|
|
40
|
+
configPath: string | undefined;
|
|
36
41
|
dryRun: boolean;
|
|
37
42
|
force: boolean;
|
|
38
43
|
noGitChecks: boolean;
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { formatGlyphLine,
|
|
2
|
+
import { formatGlyphLine, parseArgsOrExit, reportError, } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
4
|
import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
|
|
5
|
+
import { readReleaseHistory } from "./buildChangelogEntries.js";
|
|
5
6
|
import { buildDependencyGraph } from "./buildDependencyGraph.js";
|
|
6
|
-
import {
|
|
7
|
+
import { configFlagSchema } from "./configFlagSchema.js";
|
|
8
|
+
import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
7
9
|
import { dim } from "./format.js";
|
|
8
|
-
import {
|
|
10
|
+
import { getAllTagPrefixes } from "./generateChangelogs.js";
|
|
9
11
|
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
10
|
-
import {
|
|
12
|
+
import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
|
|
13
|
+
import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
|
|
11
14
|
import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
|
|
12
15
|
import { applyReleasePlan } from "./releasePlan.js";
|
|
13
16
|
import { releasePrepare } from "./releasePrepare.js";
|
|
14
17
|
import { releasePrepareMono } from "./releasePrepareMono.js";
|
|
15
18
|
import { reportPrepare } from "./reportPrepare.js";
|
|
16
|
-
import { validateConfig } from "./validateConfig.js";
|
|
17
19
|
import { validateOnlyExcludesStrandedDependents } from "./validateOnlyExcludesStrandedDependents.js";
|
|
18
20
|
const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
|
|
19
21
|
const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
@@ -21,6 +23,7 @@ function isReleaseType(value) {
|
|
|
21
23
|
return VALID_BUMP_TYPES.includes(value);
|
|
22
24
|
}
|
|
23
25
|
export const prepareFlagSchema = {
|
|
26
|
+
...configFlagSchema,
|
|
24
27
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
25
28
|
force: { long: '--force', type: 'boolean' },
|
|
26
29
|
noGitChecks: {
|
|
@@ -64,6 +67,7 @@ export function parseArgs(argv) {
|
|
|
64
67
|
process.exit(1);
|
|
65
68
|
}
|
|
66
69
|
return {
|
|
70
|
+
configPath: flags.config,
|
|
67
71
|
dryRun: flags.dryRun,
|
|
68
72
|
force: flags.force,
|
|
69
73
|
noGitChecks: flags.noGitChecks,
|
|
@@ -74,7 +78,7 @@ export function parseArgs(argv) {
|
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
export async function prepareCommand(argv, styles) {
|
|
77
|
-
const { dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv);
|
|
81
|
+
const { configPath, dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv);
|
|
78
82
|
const options = {
|
|
79
83
|
force,
|
|
80
84
|
...(bumpOverride !== undefined && { bumpOverride }),
|
|
@@ -93,20 +97,33 @@ export async function prepareCommand(argv, styles) {
|
|
|
93
97
|
process.exit(1);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
100
|
+
const configResult = await loadValidatedConfig(configPath);
|
|
101
|
+
if (configResult.status === 'invalid') {
|
|
102
|
+
reportConfigProblem(configResult.problem, styles.stderr);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
let userConfig;
|
|
106
|
+
if (configResult.status === 'ok') {
|
|
107
|
+
reportConfigWarnings(configResult.warnings, styles.stderr);
|
|
108
|
+
userConfig = configResult.config;
|
|
109
|
+
}
|
|
110
|
+
let workspace;
|
|
98
111
|
try {
|
|
99
|
-
|
|
112
|
+
workspace = discoverWorkspaces();
|
|
100
113
|
}
|
|
101
114
|
catch (error) {
|
|
102
115
|
reportError(`Failed to discover workspaces: ${describeError(error)}`);
|
|
103
116
|
process.exit(1);
|
|
104
117
|
}
|
|
105
|
-
if (
|
|
118
|
+
if (workspace.kind === 'empty') {
|
|
119
|
+
reportError(`No workspace package to release. ${describeEmptyWorkspace(workspace)}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (workspace.kind === 'single-package') {
|
|
106
123
|
runSinglePackageMode(userConfig, options, only, dryRun, styles.stdout);
|
|
107
124
|
}
|
|
108
125
|
else {
|
|
109
|
-
runMonorepoMode(
|
|
126
|
+
runMonorepoMode(workspace.packageDirs, userConfig, options, only, setVersion, dryRun, styles.stdout);
|
|
110
127
|
}
|
|
111
128
|
}
|
|
112
129
|
function runSinglePackageMode(userConfig, options, only, dryRun, style) {
|
|
@@ -114,11 +131,6 @@ function runSinglePackageMode(userConfig, options, only, dryRun, style) {
|
|
|
114
131
|
reportError('--only is only supported for monorepo configurations');
|
|
115
132
|
process.exit(1);
|
|
116
133
|
}
|
|
117
|
-
if (options.force && options.bumpOverride === undefined) {
|
|
118
|
-
reportError('--force without --bump is only supported for monorepo configurations. ' +
|
|
119
|
-
'Use --bump=major|minor|patch to set the level for a single-package release.');
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
134
|
const config = mergeSinglePackageConfig(userConfig);
|
|
123
135
|
runAndReport(() => releasePrepare(config, options), dryRun, style);
|
|
124
136
|
}
|
|
@@ -149,12 +161,8 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
149
161
|
}
|
|
150
162
|
const graph = buildDependencyGraph(config.workspaces);
|
|
151
163
|
const violations = validateOnlyExcludesStrandedDependents(config.workspaces, only, graph, (workspace) => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
...(workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? []),
|
|
155
|
-
];
|
|
156
|
-
const result = getCommitsSinceTarget(tagPrefixes, workspace.paths);
|
|
157
|
-
return { has: result.commits.length > 0, tag: result.tag };
|
|
164
|
+
const history = readReleaseHistory(config, { tagPrefixes: getAllTagPrefixes(workspace), paths: workspace.paths });
|
|
165
|
+
return { has: history.unreleased.commits.length > 0, tag: history.previousTag };
|
|
158
166
|
});
|
|
159
167
|
if (violations !== undefined) {
|
|
160
168
|
reportError('--only excludes packages with changes that would be stranded by the release.');
|
|
@@ -180,31 +188,6 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
180
188
|
}
|
|
181
189
|
runAndReport(() => releasePrepareMono(config, { ...options, ...(only !== undefined && { only }) }), dryRun, style);
|
|
182
190
|
}
|
|
183
|
-
async function loadAndValidateConfig(stderrStyle) {
|
|
184
|
-
let rawConfig;
|
|
185
|
-
try {
|
|
186
|
-
rawConfig = await loadConfig();
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
reportError(`Failed to load config: ${describeError(error)}`);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
|
-
if (rawConfig === undefined) {
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
const { config, errors, warnings } = validateConfig(rawConfig);
|
|
196
|
-
if (errors.length > 0) {
|
|
197
|
-
process.stderr.write('Invalid config:\n');
|
|
198
|
-
for (const err of errors) {
|
|
199
|
-
printError(err, stderrStyle);
|
|
200
|
-
}
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
for (const warning of warnings) {
|
|
204
|
-
console.warn(` ${formatStatusLine(stderrStyle, 'warning', warning)}`);
|
|
205
|
-
}
|
|
206
|
-
return config;
|
|
207
|
-
}
|
|
208
191
|
function runAndReport(computePlan, dryRun, style) {
|
|
209
192
|
let plan;
|
|
210
193
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type UndeclaredTagPrefix } from './detectUndeclaredTagPrefixes.js';
|
|
2
|
+
import type { ReleaseKitConfig } from './types.js';
|
|
2
3
|
/** One workspace's preview row in the tag-prefix preview. */
|
|
3
4
|
export interface TagPrefixPreviewRow {
|
|
4
5
|
/** Workspace-relative directory path (e.g., `packages/core`). */
|
|
@@ -41,10 +42,12 @@ export interface TagPrefixPreview {
|
|
|
41
42
|
/**
|
|
42
43
|
* Build a structured preview of tag-prefix state for every discovered workspace.
|
|
43
44
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* Derives each workspace's tag prefix via `deriveWorkspaceConfig()`, recording the derivation error on failure
|
|
46
|
+
* rather than aborting. A repo releasing as a single package previews no row; one declaring patterns that
|
|
47
|
+
* resolve to no package throws, because the manifest is then what needs repairing and a clean table would
|
|
48
|
+
* hide that.
|
|
49
|
+
* Reads the already-validated `config` to surface declared legacy prefixes per workspace, scans local
|
|
50
|
+
* git tags for undeclared candidate prefixes via `detectUndeclaredTagPrefixes`, and reports collisions
|
|
51
|
+
* across successfully-derived prefixes.
|
|
49
52
|
*/
|
|
50
|
-
export declare function previewTagPrefixes():
|
|
53
|
+
export declare function previewTagPrefixes(config?: ReleaseKitConfig): TagPrefixPreview;
|