@williamthorsen/release-kit 10.9.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 +57 -0
- package/.readyup/kits/default.js +1 -1
- package/.readyup/kits/npm-auto-publish.js +42 -22
- package/.readyup/manifest.json +5 -5
- package/CHANGELOG.md +48 -0
- package/README.md +37 -18
- package/dist/esm/bin/release-kit.js +30 -22
- 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.js +6 -2
- package/dist/esm/discoverWorkspaces.d.ts +53 -8
- package/dist/esm/discoverWorkspaces.js +57 -37
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +1 -0
- package/dist/esm/loadConfig.d.ts +7 -4
- package/dist/esm/loadConfig.js +6 -3
- package/dist/esm/loadValidatedConfig.d.ts +46 -5
- package/dist/esm/loadValidatedConfig.js +32 -15
- package/dist/esm/prepareCommand.d.ts +5 -0
- package/dist/esm/prepareCommand.js +26 -35
- 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/reportPrepare.js +8 -4
- 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/validateOverridesCommand.d.ts +10 -4
- package/dist/esm/validateOverridesCommand.js +23 -31
- package/docs/configuration.md +12 -0
- package/docs/labels.md +2 -0
- package/package.json +5 -6
|
@@ -1,13 +1,58 @@
|
|
|
1
|
+
import { type EmptyWorkspaceCause } from '@williamthorsen/nmr-core/workspace';
|
|
2
|
+
export type { EmptyWorkspaceCause } from '@williamthorsen/nmr-core/workspace';
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* What release-kit's workspace discovery produced: the repo releases as a single package, it resolves to a
|
|
5
|
+
* set of package directories, or it declares a workspace that resolves to none and the cause says which
|
|
6
|
+
* condition emptied it.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
8
|
+
* Held apart from nmr-core's `WorkspaceResolution`, whose `packageDirs` are absolute, because these are
|
|
9
|
+
* relative to the monorepo root. The two carry incompatible path contracts, and a distinct `kind` for the
|
|
10
|
+
* single-package case keeps either from standing in for the other.
|
|
11
|
+
*/
|
|
12
|
+
export type WorkspaceDiscovery = {
|
|
13
|
+
kind: 'empty';
|
|
14
|
+
cause: FailingWorkspaceCause;
|
|
15
|
+
patterns: string[];
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'packages';
|
|
18
|
+
packageDirs: string[];
|
|
19
|
+
patterns: string[];
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'single-package';
|
|
22
|
+
};
|
|
23
|
+
/** A workspace that resolved to no package directory, which every release-kit command treats as a failure. */
|
|
24
|
+
export type EmptyWorkspace = Extract<WorkspaceDiscovery, {
|
|
25
|
+
kind: 'empty';
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* The empty-workspace causes that reach a command as a failure.
|
|
29
|
+
*
|
|
30
|
+
* `no-packages-list` is absent because a manifest declaring nothing releases as a single package, so no
|
|
31
|
+
* command ever composes a message for it.
|
|
32
|
+
*/
|
|
33
|
+
export type FailingWorkspaceCause = Exclude<EmptyWorkspaceCause, 'no-packages-list'>;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the sentence naming which of the five conditions left a workspace holding no package directory, and
|
|
36
|
+
* the remedy for that one. Each quotes the `packages` list the manifest declares, apart from the two that
|
|
37
|
+
* reach the matcher with no list to quote.
|
|
38
|
+
*
|
|
39
|
+
* The `package.json` requirement is stated under `no-package` because it is a divergence from pnpm, which
|
|
40
|
+
* recognizes two further manifests, and the reader of a workspace that pnpm resolves has no way to infer it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function describeEmptyWorkspace(workspace: EmptyWorkspace): string;
|
|
43
|
+
/**
|
|
44
|
+
* Reads `pnpm-workspace.yaml` at `monorepoRoot` and resolves its `packages` patterns, applying pnpm's
|
|
45
|
+
* semantics — `!`-prefixed exclusions included.
|
|
6
46
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
47
|
+
* A directory holding no manifest, and one whose manifest declares no `packages` list, both release as a
|
|
48
|
+
* single package: pnpm resolves each to the root package alone, and a workspace file kept for `catalog:`,
|
|
49
|
+
* `overrides:`, or `onlyBuiltDependencies:` is the second of them. Every other empty resolution is a failure,
|
|
50
|
+
* whether the reader declared patterns that matched nothing or a `packages` value the resolver could not read
|
|
51
|
+
* as patterns at all.
|
|
10
52
|
*
|
|
11
|
-
*
|
|
53
|
+
* `packageDirs` come back relative to `monorepoRoot` as POSIX paths, because that is what every consumer
|
|
54
|
+
* needs: `WorkspaceConfig.paths` feeds `git log -- <paths>`, and `packageFiles` and `changelogPaths` are read
|
|
55
|
+
* from disk relative to the working directory. The workspace root itself relativizes to `.`, the path
|
|
56
|
+
* `deriveWorkspaceConfig` reads a root package's manifest through.
|
|
12
57
|
*/
|
|
13
|
-
export declare function discoverWorkspaces():
|
|
58
|
+
export declare function discoverWorkspaces(monorepoRoot?: string): WorkspaceDiscovery;
|
|
@@ -1,42 +1,62 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { resolveWorkspace } from '@williamthorsen/nmr-core/workspace';
|
|
3
|
+
export function describeEmptyWorkspace(workspace) {
|
|
4
|
+
const declaredClause = describeDeclaredPatterns(workspace.patterns);
|
|
5
|
+
switch (workspace.cause) {
|
|
6
|
+
case 'all-excluded':
|
|
7
|
+
return (`pnpm-workspace.yaml ${declaredClause}, whose \`!\` entries exclude every directory matched by the ` +
|
|
8
|
+
'positive patterns. Drop or narrow the exclusion.');
|
|
9
|
+
case 'no-package':
|
|
10
|
+
return (`pnpm-workspace.yaml ${declaredClause}, and the matcher found no directory holding a \`package.json\`. ` +
|
|
11
|
+
'Add one to the directory that should be a package, or declare a pattern reaching a directory that ' +
|
|
12
|
+
'holds one.');
|
|
13
|
+
case 'no-pattern':
|
|
14
|
+
return (`pnpm-workspace.yaml ${declaredClause}, so no pattern reaches the matcher. Declare a positive pattern ` +
|
|
15
|
+
'such as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string where ' +
|
|
16
|
+
'it stands bare.');
|
|
17
|
+
case 'unreadable-manifest':
|
|
18
|
+
return ('pnpm-workspace.yaml holds no valid YAML, so nothing it declares reaches the matcher. Repair the ' +
|
|
19
|
+
'syntax error and run the command again. An unterminated quoted string and a mis-indented entry are ' +
|
|
20
|
+
'the usual ones.');
|
|
21
|
+
case 'unreadable-packages':
|
|
22
|
+
return ('pnpm-workspace.yaml declares a `packages` value that is not a list of strings, so nothing it ' +
|
|
23
|
+
'declares reaches the matcher. Make it a list whose every entry is a quoted pattern; a bare value ' +
|
|
24
|
+
'and an entry YAML read as a number or a map are the usual ones.');
|
|
25
|
+
default: {
|
|
26
|
+
const unhandledCause = workspace.cause;
|
|
27
|
+
throw new Error(`Unhandled empty-workspace cause: ${String(unhandledCause)}`);
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
}
|
|
31
|
+
export function discoverWorkspaces(monorepoRoot = process.cwd()) {
|
|
32
|
+
const resolution = resolveWorkspace(monorepoRoot);
|
|
33
|
+
if (resolution.kind === 'not-a-workspace') {
|
|
34
|
+
return { kind: 'single-package' };
|
|
27
35
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
36
|
+
if (resolution.kind === 'empty') {
|
|
37
|
+
const { cause, patterns } = resolution;
|
|
38
|
+
return cause === 'no-packages-list' ? { kind: 'single-package' } : { cause, kind: 'empty', patterns };
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
return {
|
|
41
|
+
...resolution,
|
|
42
|
+
packageDirs: resolution.packageDirs.map((packageDir) => relativizeToPosix(monorepoRoot, packageDir)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function describeDeclaredPatterns(patterns) {
|
|
46
|
+
const quotablePatterns = patterns.filter((pattern) => pattern.trim() !== '');
|
|
47
|
+
const emptiedCount = patterns.length - quotablePatterns.length;
|
|
48
|
+
if (emptiedCount === 0) {
|
|
49
|
+
return `declares ${renderQuotedList(patterns)}`;
|
|
40
50
|
}
|
|
41
|
-
|
|
51
|
+
const emptiedClause = `${String(emptiedCount)} ${emptiedCount === 1 ? 'entry' : 'entries'} that YAML left empty`;
|
|
52
|
+
return quotablePatterns.length === 0
|
|
53
|
+
? `declares ${emptiedClause}`
|
|
54
|
+
: `declares ${renderQuotedList(quotablePatterns)}, beside ${emptiedClause}`;
|
|
55
|
+
}
|
|
56
|
+
function relativizeToPosix(monorepoRoot, packageDir) {
|
|
57
|
+
const relativeDir = path.relative(monorepoRoot, packageDir);
|
|
58
|
+
return relativeDir === '' ? '.' : relativeDir.split(path.sep).join('/');
|
|
59
|
+
}
|
|
60
|
+
function renderQuotedList(patterns) {
|
|
61
|
+
return patterns.map((pattern) => `\`${pattern}\``).join(', ');
|
|
42
62
|
}
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* dispatcher and asserted by the help drift guard test; kept free of import-time side
|
|
4
4
|
* effects so both can load it.
|
|
5
5
|
*/
|
|
6
|
-
export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. In monorepo\n mode it requires --only, and is rejected when a 'project' block is configured.\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).\n When a 'project' block is configured, the project release is skipped.\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
|
|
6
|
+
export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Path to config file (default: .config/release-kit.config.ts)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. In monorepo\n mode it requires --only, and is rejected when a 'project' block is configured.\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only).\n When a 'project' block is configured, the project release is skipped.\n --with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/\n (docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
|
|
7
7
|
/** Prints the prepare command's help text. */
|
|
8
8
|
export declare function showPrepareHelp(): void;
|
|
@@ -4,6 +4,7 @@ Usage: release-kit prepare [options]
|
|
|
4
4
|
Run release preparation with automatic workspace discovery.
|
|
5
5
|
|
|
6
6
|
Options:
|
|
7
|
+
--config <path> Path to config file (default: .config/release-kit.config.ts)
|
|
7
8
|
--dry-run Run without modifying any files
|
|
8
9
|
--bump=major|minor|patch Override the bump type for all workspaces
|
|
9
10
|
--set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. In monorepo
|
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
|
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;
|
|
@@ -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
|
}
|
|
@@ -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,20 @@
|
|
|
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
5
|
import { buildDependencyGraph } from "./buildDependencyGraph.js";
|
|
6
|
-
import {
|
|
6
|
+
import { configFlagSchema } from "./configFlagSchema.js";
|
|
7
|
+
import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
7
8
|
import { dim } from "./format.js";
|
|
8
9
|
import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
|
|
9
10
|
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
10
|
-
import {
|
|
11
|
+
import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
|
|
12
|
+
import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
|
|
11
13
|
import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
|
|
12
14
|
import { applyReleasePlan } from "./releasePlan.js";
|
|
13
15
|
import { releasePrepare } from "./releasePrepare.js";
|
|
14
16
|
import { releasePrepareMono } from "./releasePrepareMono.js";
|
|
15
17
|
import { reportPrepare } from "./reportPrepare.js";
|
|
16
|
-
import { validateConfig } from "./validateConfig.js";
|
|
17
18
|
import { validateOnlyExcludesStrandedDependents } from "./validateOnlyExcludesStrandedDependents.js";
|
|
18
19
|
const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
|
|
19
20
|
const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
@@ -21,6 +22,7 @@ function isReleaseType(value) {
|
|
|
21
22
|
return VALID_BUMP_TYPES.includes(value);
|
|
22
23
|
}
|
|
23
24
|
export const prepareFlagSchema = {
|
|
25
|
+
...configFlagSchema,
|
|
24
26
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
25
27
|
force: { long: '--force', type: 'boolean' },
|
|
26
28
|
noGitChecks: {
|
|
@@ -64,6 +66,7 @@ export function parseArgs(argv) {
|
|
|
64
66
|
process.exit(1);
|
|
65
67
|
}
|
|
66
68
|
return {
|
|
69
|
+
configPath: flags.config,
|
|
67
70
|
dryRun: flags.dryRun,
|
|
68
71
|
force: flags.force,
|
|
69
72
|
noGitChecks: flags.noGitChecks,
|
|
@@ -74,7 +77,7 @@ export function parseArgs(argv) {
|
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
79
|
export async function prepareCommand(argv, styles) {
|
|
77
|
-
const { dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv);
|
|
80
|
+
const { configPath, dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv);
|
|
78
81
|
const options = {
|
|
79
82
|
force,
|
|
80
83
|
...(bumpOverride !== undefined && { bumpOverride }),
|
|
@@ -93,20 +96,33 @@ export async function prepareCommand(argv, styles) {
|
|
|
93
96
|
process.exit(1);
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
99
|
+
const configResult = await loadValidatedConfig(configPath);
|
|
100
|
+
if (configResult.status === 'invalid') {
|
|
101
|
+
reportConfigProblem(configResult.problem, styles.stderr);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
let userConfig;
|
|
105
|
+
if (configResult.status === 'ok') {
|
|
106
|
+
reportConfigWarnings(configResult.warnings, styles.stderr);
|
|
107
|
+
userConfig = configResult.config;
|
|
108
|
+
}
|
|
109
|
+
let workspace;
|
|
98
110
|
try {
|
|
99
|
-
|
|
111
|
+
workspace = discoverWorkspaces();
|
|
100
112
|
}
|
|
101
113
|
catch (error) {
|
|
102
114
|
reportError(`Failed to discover workspaces: ${describeError(error)}`);
|
|
103
115
|
process.exit(1);
|
|
104
116
|
}
|
|
105
|
-
if (
|
|
117
|
+
if (workspace.kind === 'empty') {
|
|
118
|
+
reportError(`No workspace package to release. ${describeEmptyWorkspace(workspace)}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
if (workspace.kind === 'single-package') {
|
|
106
122
|
runSinglePackageMode(userConfig, options, only, dryRun, styles.stdout);
|
|
107
123
|
}
|
|
108
124
|
else {
|
|
109
|
-
runMonorepoMode(
|
|
125
|
+
runMonorepoMode(workspace.packageDirs, userConfig, options, only, setVersion, dryRun, styles.stdout);
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
function runSinglePackageMode(userConfig, options, only, dryRun, style) {
|
|
@@ -180,31 +196,6 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
180
196
|
}
|
|
181
197
|
runAndReport(() => releasePrepareMono(config, { ...options, ...(only !== undefined && { only }) }), dryRun, style);
|
|
182
198
|
}
|
|
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
199
|
function runAndReport(computePlan, dryRun, style) {
|
|
209
200
|
let plan;
|
|
210
201
|
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;
|
|
@@ -4,38 +4,26 @@ import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
|
|
|
4
4
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
5
|
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
6
6
|
import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
|
|
7
|
-
import { discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
7
|
+
import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
8
|
+
export function previewTagPrefixes(config) {
|
|
9
|
+
const workspace = discoverWorkspaces();
|
|
10
|
+
if (workspace.kind === 'empty') {
|
|
11
|
+
throw new Error(`No workspace package to preview. ${describeEmptyWorkspace(workspace)}`);
|
|
12
|
+
}
|
|
13
|
+
const workspacePaths = workspace.kind === 'packages' ? workspace.packageDirs : [];
|
|
14
|
+
const overridesByDir = buildOverrideMap(config);
|
|
14
15
|
const workspaces = Array.from(workspacePaths, (workspacePath) => buildPreviewRow(workspacePath, overridesByDir));
|
|
15
|
-
const retiredPackages = buildRetiredPreviewEntries(
|
|
16
|
+
const retiredPackages = buildRetiredPreviewEntries(config?.retiredPackages ?? []);
|
|
16
17
|
const collisions = detectCollisions(workspaces);
|
|
17
18
|
const knownPrefixes = collectKnownPrefixes(workspaces, retiredPackages);
|
|
18
19
|
const undeclaredCandidates = detectUndeclaredTagPrefixes(knownPrefixes);
|
|
19
20
|
return { workspaces, collisions, undeclaredCandidates, retiredPackages };
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
let raw;
|
|
23
|
-
try {
|
|
24
|
-
raw = await loadConfig();
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
if (raw === undefined)
|
|
30
|
-
return undefined;
|
|
31
|
-
const { config, errors } = validateConfig(raw);
|
|
32
|
-
return errors.length === 0 ? config : undefined;
|
|
33
|
-
}
|
|
34
|
-
function buildOverrideMap(userConfig) {
|
|
22
|
+
function buildOverrideMap(config) {
|
|
35
23
|
const map = new Map();
|
|
36
|
-
if (
|
|
24
|
+
if (config?.workspaces === undefined)
|
|
37
25
|
return map;
|
|
38
|
-
for (const entry of
|
|
26
|
+
for (const entry of config.workspaces) {
|
|
39
27
|
if (entry.legacyIdentities !== undefined) {
|
|
40
28
|
map.set(entry.dir, entry.legacyIdentities);
|
|
41
29
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
2
2
|
/**
|
|
3
|
-
* Orchestrate the CLI `publish` command: parse flags, discover workspaces, resolve tags
|
|
4
|
-
* detect the package manager, validate `--tags`, and publish each tag with inject/restore lifecycle.
|
|
3
|
+
* Orchestrate the CLI `publish` command: parse flags, open a named config, discover workspaces, resolve tags
|
|
4
|
+
* from HEAD, detect the package manager, validate `--tags`, and publish each tag with inject/restore lifecycle.
|
|
5
5
|
*/
|
|
6
6
|
export declare function publishCommand(argv: string[], styles: StreamStyles): Promise<void>;
|
|
@@ -3,14 +3,17 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
|
|
4
4
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
5
|
import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
|
|
6
|
+
import { configFlagSchema } from "./configFlagSchema.js";
|
|
6
7
|
import { detectPackageManager } from "./detectPackageManager.js";
|
|
7
8
|
import { formatPrivateSkip } from "./formatPrivateSkip.js";
|
|
8
9
|
import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
|
|
10
|
+
import { assertConfigUsable } from "./loadValidatedConfig.js";
|
|
9
11
|
import { parseRequestedTags } from "./parseRequestedTags.js";
|
|
10
12
|
import { publishPackage } from "./publish.js";
|
|
11
13
|
import { resolveCommandTags } from "./resolveCommandTags.js";
|
|
12
14
|
import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
|
|
13
15
|
const publishFlagSchema = {
|
|
16
|
+
...configFlagSchema,
|
|
14
17
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
15
18
|
noGitChecks: { long: '--no-git-checks', type: 'boolean' },
|
|
16
19
|
provenance: { long: '--provenance', type: 'boolean' },
|
|
@@ -19,6 +22,7 @@ const publishFlagSchema = {
|
|
|
19
22
|
export async function publishCommand(argv, styles) {
|
|
20
23
|
const parsed = parseArgsOrExit(argv, publishFlagSchema);
|
|
21
24
|
const { dryRun, noGitChecks, provenance } = parsed.flags;
|
|
25
|
+
await assertConfigUsable(styles.stderr, parsed.flags.config);
|
|
22
26
|
if (!dryRun && !noGitChecks) {
|
|
23
27
|
try {
|
|
24
28
|
assertCleanWorkingTree();
|
|
@@ -29,7 +33,7 @@ export async function publishCommand(argv, styles) {
|
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
const requestedTags = parseRequestedTags(parsed.flags.tags);
|
|
32
|
-
const resolvedTags =
|
|
36
|
+
const resolvedTags = resolveCommandTags(requestedTags);
|
|
33
37
|
if (resolvedTags.length === 0) {
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
@@ -39,7 +43,9 @@ export async function publishCommand(argv, styles) {
|
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
const packageManager = detectPackageManager();
|
|
42
|
-
const { releaseNotes, changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig(styles.stderr
|
|
46
|
+
const { releaseNotes, changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig(styles.stderr, {
|
|
47
|
+
...(parsed.flags.config !== undefined && { configPath: parsed.flags.config }),
|
|
48
|
+
});
|
|
43
49
|
const shouldInject = releaseNotes.shouldInjectIntoReadme;
|
|
44
50
|
console.info(dryRun ? '[dry-run] Would publish:' : 'Publishing:');
|
|
45
51
|
for (const { tag, workspacePath } of publishableTags) {
|