@williamthorsen/release-kit 13.1.0 → 14.1.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 +118 -9
- package/.readyup/kits/default.js +4 -4
- package/.readyup/kits/npm-auto-publish.js +81 -41
- package/.readyup/manifest.json +19 -9
- package/CHANGELOG.md +38 -6
- package/README.md +14 -6
- package/dist/esm/assertReleasesOnNpm.d.ts +7 -0
- package/dist/esm/assertReleasesOnNpm.js +56 -0
- package/dist/esm/bin/release-kit.js +5 -2
- package/dist/esm/deriveWorkspaceConfig.js +2 -2
- package/dist/esm/enumerateReleaseWindows.d.ts +1 -1
- package/dist/esm/enumerateReleaseWindows.js +1 -3
- package/dist/esm/findTransitiveDependents.d.ts +3 -0
- package/dist/esm/findTransitiveDependents.js +20 -0
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +4 -2
- package/dist/esm/init/hasPublishablePackage.d.ts +10 -0
- package/dist/esm/init/hasPublishablePackage.js +28 -0
- package/dist/esm/init/initCommand.js +23 -13
- package/dist/esm/init/scaffold.d.ts +6 -1
- package/dist/esm/init/scaffold.js +10 -7
- package/dist/esm/init/templates.js +1 -1
- package/dist/esm/init/updateTemplatesCommand.d.ts +2 -1
- package/dist/esm/init/updateTemplatesCommand.js +12 -6
- package/dist/esm/isPublishableManifest.d.ts +2 -0
- package/dist/esm/isPublishableManifest.js +5 -0
- package/dist/esm/matchesTagPrefix.d.ts +2 -0
- package/dist/esm/matchesTagPrefix.js +4 -0
- package/dist/esm/npmRegistry.d.ts +35 -0
- package/dist/esm/npmRegistry.js +58 -0
- package/dist/esm/prepareCommand.d.ts +1 -0
- package/dist/esm/prepareCommand.js +52 -61
- package/dist/esm/previewTagPrefixes.js +2 -1
- package/dist/esm/releasePrepare.d.ts +2 -16
- package/dist/esm/releasePrepareMono.d.ts +17 -2
- package/dist/esm/releasePrepareMono.js +23 -11
- package/dist/esm/sync-labels/templates.js +1 -1
- package/docs/configuration.md +1 -1
- package/docs/labels.md +5 -3
- package/docs/project-releases.md +2 -2
- package/docs/readiness-checks.md +2 -2
- package/docs/releasing.md +20 -10
- package/package.json +8 -8
- package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +0 -37
- package/dist/esm/validateOnlyExcludesStrandedDependents.js +0 -115
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
4
|
+
import { isPublishableManifest } from "./isPublishableManifest.js";
|
|
4
5
|
import { isRecord } from "./typeGuards.js";
|
|
5
6
|
export function deriveWorkspaceConfig(workspacePath) {
|
|
6
7
|
const dir = basename(workspacePath);
|
|
@@ -17,8 +18,7 @@ export function deriveWorkspaceConfig(workspacePath) {
|
|
|
17
18
|
throw new Error(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
|
|
18
19
|
}
|
|
19
20
|
const unscopedName = stripNpmScope(name);
|
|
20
|
-
const
|
|
21
|
-
const isPublishable = privateField === undefined || privateField === false;
|
|
21
|
+
const isPublishable = isPublishableManifest(parsed);
|
|
22
22
|
return {
|
|
23
23
|
dir,
|
|
24
24
|
name,
|
|
@@ -22,7 +22,7 @@ export interface ReleaseWindow {
|
|
|
22
22
|
export interface EnumerateReleaseWindowsOptions {
|
|
23
23
|
/** Git pathspecs restricting the enumeration to commits that touch them. */
|
|
24
24
|
paths?: readonly string[];
|
|
25
|
-
/** Tag prefixes to match as a union; a matching tag name continues with a
|
|
25
|
+
/** Tag prefixes to match as a union; a matching tag name continues with a complete SemVer version. */
|
|
26
26
|
tagPrefixes: readonly string[];
|
|
27
27
|
/** The version that the unreleased window reports, such as the tag that the release being prepared will write. */
|
|
28
28
|
unreleasedTag: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
4
|
+
import { matchesTagPrefix } from "./matchesTagPrefix.js";
|
|
4
5
|
const FIELD_SEPARATOR = '\u{1F}';
|
|
5
6
|
const RECORD_SEPARATOR = '\u{1E}';
|
|
6
7
|
export function enumerateReleaseWindows(options) {
|
|
@@ -46,9 +47,6 @@ function claimAncestors(boundaries, parentsByHash) {
|
|
|
46
47
|
}
|
|
47
48
|
return windowIndexByHash;
|
|
48
49
|
}
|
|
49
|
-
function matchesTagPrefix(tagName, tagPrefixes) {
|
|
50
|
-
return tagPrefixes.some((prefix) => tagName.startsWith(prefix) && /^\d/.test(tagName.slice(prefix.length)));
|
|
51
|
-
}
|
|
52
50
|
function parseCommitRecord(record) {
|
|
53
51
|
const [hash, subject, message] = record.replace(/^\n+/, '').split(FIELD_SEPARATOR);
|
|
54
52
|
if (hash === undefined || subject === undefined || message === undefined || hash === '') {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DependencyGraph } from './buildDependencyGraph.js';
|
|
2
|
+
/** Returns the start workspace `dir`s together with the `dir` of every workspace that depends on one, transitively. */
|
|
3
|
+
export declare function findTransitiveDependents(graph: DependencyGraph, startDirs: Iterable<string>): Set<string>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function findTransitiveDependents(graph, startDirs) {
|
|
2
|
+
const found = new Set(startDirs);
|
|
3
|
+
const queue = [...found];
|
|
4
|
+
while (queue.length > 0) {
|
|
5
|
+
const dir = queue.shift();
|
|
6
|
+
if (dir === undefined)
|
|
7
|
+
break;
|
|
8
|
+
const packageName = graph.dirToPackageName.get(dir);
|
|
9
|
+
if (packageName === undefined)
|
|
10
|
+
continue;
|
|
11
|
+
const dependents = graph.dependentsOf.get(packageName) ?? [];
|
|
12
|
+
for (const dependent of dependents) {
|
|
13
|
+
if (found.has(dependent.dir))
|
|
14
|
+
continue;
|
|
15
|
+
found.add(dependent.dir);
|
|
16
|
+
queue.push(dependent.dir);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return found;
|
|
20
|
+
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Help text for `release-kit prepare`. The module does not have any import-time side effects, so that a test can
|
|
3
3
|
* import it without running the CLI.
|
|
4
4
|
*/
|
|
5
|
-
export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Config file, relative to the current directory\n (default: .config/release-kit.config.ts at the repo root)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Set the level of every release that happens; does not trigger one\n --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo\n mode
|
|
5
|
+
export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --config <path> Config file, relative to the current directory\n (default: .config/release-kit.config.ts at the repo root)\n --dry-run Run without modifying any files\n --bump=major|minor|patch Set the level of every release that happens; does not trigger one\n --set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo\n mode, name each workspace: --set-version=arrays@1.0.0[,strings@2.0.0].\n Rejected when a 'project' block is configured.\n --force Release even when the last tag is not followed by any commits,\n or by any bump-worthy ones. Defaults to patch when --bump is not given;\n use --bump=X for a different level.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Release only the named workspaces and their dependents (comma-separated,\n 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
6
|
/** Prints the prepare command's help text. */
|
|
7
7
|
export declare function showPrepareHelp(): void;
|
|
@@ -9,12 +9,14 @@ Options:
|
|
|
9
9
|
--dry-run Run without modifying any files
|
|
10
10
|
--bump=major|minor|patch Set the level of every release that happens; does not trigger one
|
|
11
11
|
--set-version=X.Y.Z Set an explicit version; bypasses the changelog-derived bump. In monorepo
|
|
12
|
-
mode
|
|
12
|
+
mode, name each workspace: --set-version=arrays@1.0.0[,strings@2.0.0].
|
|
13
|
+
Rejected when a 'project' block is configured.
|
|
13
14
|
--force Release even when the last tag is not followed by any commits,
|
|
14
15
|
or by any bump-worthy ones. Defaults to patch when --bump is not given;
|
|
15
16
|
use --bump=X for a different level.
|
|
16
17
|
--no-git-checks, -n Skip the clean-working-tree check
|
|
17
|
-
--only=name1,name2
|
|
18
|
+
--only=name1,name2 Release only the named workspaces and their dependents (comma-separated,
|
|
19
|
+
monorepo only).
|
|
18
20
|
When a 'project' block is configured, the project release is skipped.
|
|
19
21
|
--with-release-notes Also write per-workspace release-notes previews under <workspacePath>/docs/
|
|
20
22
|
(docs/README.v<version>.md and docs/RELEASE_NOTES.v<version>.md).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports whether the repo at `root` contains a package that can be published: a discovered workspace, or the root
|
|
3
|
+
* package of a single-package repo, whose `package.json` is not private.
|
|
4
|
+
*
|
|
5
|
+
* Returns `true` whenever the answer cannot be determined (an empty workspace, a failed discovery, or an unreadable
|
|
6
|
+
* manifest), so that the scaffolding commands keep the publish workflows in every repo that might need them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hasPublishablePackage(root?: string): boolean;
|
|
9
|
+
/** Formats the line that reports a workflow skipped because the repo does not contain any publishable package. */
|
|
10
|
+
export declare function formatPrivateWorkflowSkip(filePath: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { discoverWorkspaces } from "../discoverWorkspaces.js";
|
|
4
|
+
import { isPublishableManifest } from "../isPublishableManifest.js";
|
|
5
|
+
export function hasPublishablePackage(root = process.cwd()) {
|
|
6
|
+
let packageDirs;
|
|
7
|
+
try {
|
|
8
|
+
const workspace = discoverWorkspaces(root);
|
|
9
|
+
if (workspace.kind === 'empty')
|
|
10
|
+
return true;
|
|
11
|
+
packageDirs = workspace.kind === 'single-package' ? ['.'] : workspace.packageDirs;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return packageDirs.some((packageDir) => isPublishableAt(path.join(root, packageDir, 'package.json')));
|
|
17
|
+
}
|
|
18
|
+
export function formatPrivateWorkflowSkip(filePath) {
|
|
19
|
+
return `Skipping ${filePath}: every package is private`;
|
|
20
|
+
}
|
|
21
|
+
function isPublishableAt(manifestPath) {
|
|
22
|
+
try {
|
|
23
|
+
return isPublishableManifest(JSON.parse(readFileSync(manifestPath, 'utf8')));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { printError, printStep, printSuccess, reportWriteResult, } from '@williamthorsen/nmr-core';
|
|
1
|
+
import { printError, printSkip, printStep, printSuccess, reportWriteResult, } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
3
|
import { hasPackageJson, isGitRepo, usesPnpm } from "./checks.js";
|
|
4
4
|
import { detectRepoType } from "./detectRepoType.js";
|
|
5
|
-
import {
|
|
5
|
+
import { formatPrivateWorkflowSkip, hasPublishablePackage } from "./hasPublishablePackage.js";
|
|
6
|
+
import { PUBLISHING_WORKFLOW_PATHS, scaffoldFiles } from "./scaffold.js";
|
|
6
7
|
export function checkEligibility(styles) {
|
|
7
8
|
printStep('Checking eligibility');
|
|
8
9
|
if (!runRequiredCheck('Git repository detected', isGitRepo(), styles))
|
|
@@ -37,10 +38,16 @@ export function initCommand({ dryRun, force, styles, withConfig }) {
|
|
|
37
38
|
return 1;
|
|
38
39
|
}
|
|
39
40
|
printSuccess(`Detected: ${repoType}`, styles.stdout);
|
|
41
|
+
const publishable = hasPublishablePackage();
|
|
40
42
|
printStep('Scaffolding files');
|
|
43
|
+
if (!publishable) {
|
|
44
|
+
for (const filePath of PUBLISHING_WORKFLOW_PATHS) {
|
|
45
|
+
printSkip(formatPrivateWorkflowSkip(filePath), styles.stdout);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
41
48
|
let results;
|
|
42
49
|
try {
|
|
43
|
-
results = scaffoldFiles({ repoType, dryRun, overwrite: force, withConfig });
|
|
50
|
+
results = scaffoldFiles({ repoType, dryRun, overwrite: force, publishable, withConfig });
|
|
44
51
|
}
|
|
45
52
|
catch (error) {
|
|
46
53
|
printError(`Failed to scaffold files: ${describeError(error)}`, styles.stderr);
|
|
@@ -53,18 +60,21 @@ export function initCommand({ dryRun, force, styles, withConfig }) {
|
|
|
53
60
|
return 1;
|
|
54
61
|
}
|
|
55
62
|
printStep('Next steps');
|
|
56
|
-
|
|
57
|
-
? '1. (Optional) Customize .config/release-kit.config.ts.'
|
|
58
|
-
: '1. (Optional) Run again with --with-config to scaffold config files.';
|
|
59
|
-
console.info(`
|
|
60
|
-
${configHint}
|
|
61
|
-
2. If this is a private repo, remove provenance: true from .github/workflows/publish.yaml.
|
|
62
|
-
3. Test by running: npx @williamthorsen/release-kit prepare --dry-run
|
|
63
|
-
4. Commit the generated files.
|
|
64
|
-
5. Register each package as a trusted publisher on npmjs.com.
|
|
65
|
-
`);
|
|
63
|
+
console.info(`\n${formatNextSteps({ publishable, withConfig })}\n`);
|
|
66
64
|
return 0;
|
|
67
65
|
}
|
|
66
|
+
function formatNextSteps({ publishable, withConfig }) {
|
|
67
|
+
const steps = [
|
|
68
|
+
withConfig
|
|
69
|
+
? '(Optional) Customize .config/release-kit.config.ts.'
|
|
70
|
+
: '(Optional) Run again with --with-config to scaffold config files.',
|
|
71
|
+
...(publishable ? ['If this is a private repo, remove provenance: true from .github/workflows/publish.yaml.'] : []),
|
|
72
|
+
'Test by running: npx @williamthorsen/release-kit prepare --dry-run',
|
|
73
|
+
'Commit the generated files.',
|
|
74
|
+
...(publishable ? ['Register each package as a trusted publisher on npmjs.com.'] : []),
|
|
75
|
+
];
|
|
76
|
+
return steps.map((step, index) => ` ${String(index + 1)}. ${step}`).join('\n');
|
|
77
|
+
}
|
|
68
78
|
function runRequiredCheck(label, result, styles) {
|
|
69
79
|
if (result.ok) {
|
|
70
80
|
printSuccess(label, styles.stdout);
|
|
@@ -4,8 +4,13 @@ interface ScaffoldOptions {
|
|
|
4
4
|
repoType: RepoType;
|
|
5
5
|
dryRun: boolean;
|
|
6
6
|
overwrite: boolean;
|
|
7
|
+
publishable: boolean;
|
|
7
8
|
withConfig: boolean;
|
|
8
9
|
}
|
|
10
|
+
export declare const CREATE_GITHUB_RELEASE_WORKFLOW_PATH = ".github/workflows/create-github-release.yaml";
|
|
11
|
+
export declare const PUBLISH_WORKFLOW_PATH = ".github/workflows/publish.yaml";
|
|
12
|
+
/** The workflows that act only on publishable packages, which a repo whose packages are all private does not get. */
|
|
13
|
+
export declare const PUBLISHING_WORKFLOW_PATHS: readonly [".github/workflows/create-github-release.yaml", ".github/workflows/publish.yaml"];
|
|
9
14
|
/** Scaffolds release-kit files for the target repo, returning a result for each file attempted. */
|
|
10
|
-
export declare function scaffoldFiles({ repoType, dryRun, overwrite, withConfig }: ScaffoldOptions): WriteResult[];
|
|
15
|
+
export declare function scaffoldFiles({ repoType, dryRun, overwrite, publishable, withConfig, }: ScaffoldOptions): WriteResult[];
|
|
11
16
|
export {};
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { writeFileWithCheck } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { createGithubReleaseWorkflow, publishWorkflow, releaseConfigScript, releaseWorkflow } from "./templates.js";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export const CREATE_GITHUB_RELEASE_WORKFLOW_PATH = '.github/workflows/create-github-release.yaml';
|
|
4
|
+
export const PUBLISH_WORKFLOW_PATH = '.github/workflows/publish.yaml';
|
|
5
|
+
export const PUBLISHING_WORKFLOW_PATHS = [CREATE_GITHUB_RELEASE_WORKFLOW_PATH, PUBLISH_WORKFLOW_PATH];
|
|
6
|
+
export function scaffoldFiles({ repoType, dryRun, overwrite, publishable, withConfig, }) {
|
|
7
|
+
const results = [];
|
|
8
|
+
if (publishable) {
|
|
9
|
+
results.push(writeFileWithCheck(CREATE_GITHUB_RELEASE_WORKFLOW_PATH, createGithubReleaseWorkflow(repoType), {
|
|
6
10
|
dryRun,
|
|
7
11
|
overwrite,
|
|
8
|
-
}),
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
];
|
|
12
|
+
}), writeFileWithCheck(PUBLISH_WORKFLOW_PATH, publishWorkflow(repoType), { dryRun, overwrite }));
|
|
13
|
+
}
|
|
14
|
+
results.push(writeFileWithCheck('.github/workflows/release.yaml', releaseWorkflow(repoType), { dryRun, overwrite }));
|
|
12
15
|
if (withConfig) {
|
|
13
16
|
results.push(writeFileWithCheck('.config/release-kit.config.ts', releaseConfigScript(repoType), { dryRun, overwrite }));
|
|
14
17
|
}
|
|
@@ -89,7 +89,7 @@ on:
|
|
|
89
89
|
workflow_dispatch:
|
|
90
90
|
inputs:
|
|
91
91
|
only:
|
|
92
|
-
description: 'Workspaces to release (comma-separated, leave empty for all)'
|
|
92
|
+
description: 'Workspaces to release, with their dependents (comma-separated, leave empty for all)'
|
|
93
93
|
required: false
|
|
94
94
|
type: string
|
|
95
95
|
bump:
|
|
@@ -7,7 +7,8 @@ interface UpdateTemplatesOptions {
|
|
|
7
7
|
* Runs the `release-kit update-templates` command and returns the process exit code.
|
|
8
8
|
*
|
|
9
9
|
* Brings each workflow that release-kit scaffolds up to date with the installed templates. The opt-in
|
|
10
|
-
* `sync-labels.yaml` is refreshed only when it exists,
|
|
10
|
+
* `sync-labels.yaml` is refreshed only when it exists, as are the publishing workflows in a repo whose packages are
|
|
11
|
+
* all private. Config files are never read or written.
|
|
11
12
|
*/
|
|
12
13
|
export declare function updateTemplatesCommand({ dryRun, styles }: UpdateTemplatesOptions): number;
|
|
13
14
|
export {};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { printError, printStep, printSuccess, reportTemplateUpdate, updateManagedFile, } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { printError, printSkip, printStep, printSuccess, reportTemplateUpdate, updateManagedFile, } from '@williamthorsen/nmr-core';
|
|
3
3
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
4
|
import { syncLabelsWorkflow } from "../sync-labels/templates.js";
|
|
5
5
|
import { detectRepoType } from "./detectRepoType.js";
|
|
6
6
|
import { hasProvenance } from "./hasProvenance.js";
|
|
7
|
+
import { formatPrivateWorkflowSkip, hasPublishablePackage } from "./hasPublishablePackage.js";
|
|
7
8
|
import { checkEligibility } from "./initCommand.js";
|
|
9
|
+
import { CREATE_GITHUB_RELEASE_WORKFLOW_PATH, PUBLISH_WORKFLOW_PATH, PUBLISHING_WORKFLOW_PATHS } from "./scaffold.js";
|
|
8
10
|
import { createGithubReleaseWorkflow, publishWorkflow, releaseWorkflow } from "./templates.js";
|
|
9
|
-
const PUBLISH_WORKFLOW_PATH = '.github/workflows/publish.yaml';
|
|
10
11
|
const SYNC_LABELS_WORKFLOW_PATH = '.github/workflows/sync-labels.yaml';
|
|
11
12
|
export function updateTemplatesCommand({ dryRun, styles }) {
|
|
12
13
|
if (dryRun) {
|
|
@@ -35,7 +36,11 @@ export function updateTemplatesCommand({ dryRun, styles }) {
|
|
|
35
36
|
printStep('Updating workflows');
|
|
36
37
|
let results;
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
const { files, skipped } = buildManagedFiles(repoType, hasPublishablePackage());
|
|
40
|
+
for (const filePath of skipped) {
|
|
41
|
+
printSkip(formatPrivateWorkflowSkip(filePath), styles.stdout);
|
|
42
|
+
}
|
|
43
|
+
results = files.map(([filePath, content]) => updateManagedFile(filePath, content, { dryRun }));
|
|
39
44
|
}
|
|
40
45
|
catch (error) {
|
|
41
46
|
printError(`Failed to render workflows: ${describeError(error)}`, styles.stderr);
|
|
@@ -46,16 +51,17 @@ export function updateTemplatesCommand({ dryRun, styles }) {
|
|
|
46
51
|
}
|
|
47
52
|
return results.some((result) => result.outcome === 'failed') ? 1 : 0;
|
|
48
53
|
}
|
|
49
|
-
function buildManagedFiles(repoType) {
|
|
54
|
+
function buildManagedFiles(repoType, publishable) {
|
|
55
|
+
const skipped = publishable ? [] : PUBLISHING_WORKFLOW_PATHS.filter((filePath) => !existsSync(filePath));
|
|
50
56
|
const files = [
|
|
51
|
-
[
|
|
57
|
+
[CREATE_GITHUB_RELEASE_WORKFLOW_PATH, createGithubReleaseWorkflow(repoType)],
|
|
52
58
|
[PUBLISH_WORKFLOW_PATH, publishWorkflow(repoType, { provenance: readProvenanceSetting() })],
|
|
53
59
|
['.github/workflows/release.yaml', releaseWorkflow(repoType)],
|
|
54
60
|
];
|
|
55
61
|
if (existsSync(SYNC_LABELS_WORKFLOW_PATH)) {
|
|
56
62
|
files.push([SYNC_LABELS_WORKFLOW_PATH, syncLabelsWorkflow()]);
|
|
57
63
|
}
|
|
58
|
-
return files;
|
|
64
|
+
return { files: files.filter(([filePath]) => !skipped.includes(filePath)), skipped };
|
|
59
65
|
}
|
|
60
66
|
function readProvenanceSetting() {
|
|
61
67
|
if (!existsSync(PUBLISH_WORKFLOW_PATH))
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const SEMVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
2
|
+
export function matchesTagPrefix(tagName, tagPrefixes) {
|
|
3
|
+
return tagPrefixes.some((prefix) => tagName.startsWith(prefix) && SEMVER_VERSION_PATTERN.test(tagName.slice(prefix.length)));
|
|
4
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const AUTH_ERROR_CODES: ReadonlySet<string>;
|
|
2
|
+
export declare const UNREACHABLE_ERROR_CODES: ReadonlySet<string>;
|
|
3
|
+
/**
|
|
4
|
+
* Classifies the outcome of `npm view <package> version --json`.
|
|
5
|
+
*
|
|
6
|
+
* Only `E404` means that the registry doesn't know the package; any other failure leaves the question open.
|
|
7
|
+
*/
|
|
8
|
+
export declare function classifyNpmPackageLookup(result: NpmCommandResult): NpmPackageLookup;
|
|
9
|
+
/** Looks up whether a package exists on the npm registry, querying `registry` when one is given. */
|
|
10
|
+
export declare function lookUpNpmPackage(packageName: string, registry?: string): NpmPackageLookup;
|
|
11
|
+
export interface NpmCommandResult {
|
|
12
|
+
exitOk: boolean;
|
|
13
|
+
stdout: string;
|
|
14
|
+
}
|
|
15
|
+
export interface NpmErrorPayload {
|
|
16
|
+
code: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
}
|
|
19
|
+
export type NpmPackageLookup = {
|
|
20
|
+
status: 'published';
|
|
21
|
+
} | {
|
|
22
|
+
status: 'unpublished';
|
|
23
|
+
} | {
|
|
24
|
+
status: 'unverifiable';
|
|
25
|
+
detail: string;
|
|
26
|
+
};
|
|
27
|
+
/** Reads npm's `--json` error envelope, which npm writes to stdout on both the success and failure paths. */
|
|
28
|
+
export declare function readNpmError(stdout: string): NpmErrorPayload | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Runs npm with the given arguments, capturing stdout whether or not the command exits zero.
|
|
31
|
+
*
|
|
32
|
+
* The arguments are expected to include `--json`: npm writes its machine-readable error envelope to stdout,
|
|
33
|
+
* which `execFileSync` attaches to the thrown error instead of returning it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runNpmJson(args: readonly string[]): NpmCommandResult;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { isRecord } from "./typeGuards.js";
|
|
3
|
+
export const AUTH_ERROR_CODES = new Set(['E401', 'ENEEDAUTH']);
|
|
4
|
+
export const UNREACHABLE_ERROR_CODES = new Set([
|
|
5
|
+
'EAI_AGAIN',
|
|
6
|
+
'ECONNREFUSED',
|
|
7
|
+
'ECONNRESET',
|
|
8
|
+
'ENETUNREACH',
|
|
9
|
+
'ENOTFOUND',
|
|
10
|
+
'ERR_SOCKET_TIMEOUT',
|
|
11
|
+
'ETIMEDOUT',
|
|
12
|
+
]);
|
|
13
|
+
export function classifyNpmPackageLookup(result) {
|
|
14
|
+
if (result.exitOk) {
|
|
15
|
+
return { status: 'published' };
|
|
16
|
+
}
|
|
17
|
+
const error = readNpmError(result.stdout);
|
|
18
|
+
if (error === undefined) {
|
|
19
|
+
return { status: 'unverifiable', detail: 'The npm registry query failed without a readable error payload' };
|
|
20
|
+
}
|
|
21
|
+
if (error.code === 'E404') {
|
|
22
|
+
return { status: 'unpublished' };
|
|
23
|
+
}
|
|
24
|
+
if (AUTH_ERROR_CODES.has(error.code)) {
|
|
25
|
+
return { status: 'unverifiable', detail: `The npm registry rejected the session (${error.code})` };
|
|
26
|
+
}
|
|
27
|
+
if (UNREACHABLE_ERROR_CODES.has(error.code)) {
|
|
28
|
+
return { status: 'unverifiable', detail: `Cannot reach the npm registry (${error.code})` };
|
|
29
|
+
}
|
|
30
|
+
return { status: 'unverifiable', detail: `The npm registry query failed (${error.code}): ${error.summary}` };
|
|
31
|
+
}
|
|
32
|
+
export function lookUpNpmPackage(packageName, registry) {
|
|
33
|
+
const args = ['view', packageName, 'version', '--json', ...(registry === undefined ? [] : ['--registry', registry])];
|
|
34
|
+
return classifyNpmPackageLookup(runNpmJson(args));
|
|
35
|
+
}
|
|
36
|
+
export function readNpmError(stdout) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(stdout);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!isRecord(parsed) || !isRecord(parsed['error']) || typeof parsed['error']['code'] !== 'string') {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const { code, summary } = parsed['error'];
|
|
48
|
+
return { code, summary: typeof summary === 'string' ? summary : '' };
|
|
49
|
+
}
|
|
50
|
+
export function runNpmJson(args) {
|
|
51
|
+
try {
|
|
52
|
+
return { exitOk: true, stdout: execFileSync('npm', args, { encoding: 'utf8', stdio: 'pipe' }) };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const stdout = isRecord(error) && typeof error['stdout'] === 'string' ? error['stdout'] : '';
|
|
56
|
+
return { exitOk: false, stdout };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -43,6 +43,7 @@ export declare function parseArgs(argv: string[]): {
|
|
|
43
43
|
noGitChecks: boolean;
|
|
44
44
|
bumpOverride: ReleaseType | undefined;
|
|
45
45
|
only: string[] | undefined;
|
|
46
|
+
/** The raw `--set-version` value, whose accepted form depends on the mode. */
|
|
46
47
|
setVersion: string | undefined;
|
|
47
48
|
withReleaseNotes: boolean;
|
|
48
49
|
};
|
|
@@ -2,12 +2,10 @@ import { execSync } from 'node:child_process';
|
|
|
2
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 {
|
|
6
|
-
import { buildDependencyGraph } from "./buildDependencyGraph.js";
|
|
5
|
+
import { assertReleasesOnNpm } from "./assertReleasesOnNpm.js";
|
|
7
6
|
import { configFlagSchema } from "./configFlagSchema.js";
|
|
8
7
|
import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
9
8
|
import { dim } from "./format.js";
|
|
10
|
-
import { getAllTagPrefixes } from "./generateChangelogs.js";
|
|
11
9
|
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
12
10
|
import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
|
|
13
11
|
import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
|
|
@@ -17,7 +15,6 @@ import { releasePrepare } from "./releasePrepare.js";
|
|
|
17
15
|
import { releasePrepareMono } from "./releasePrepareMono.js";
|
|
18
16
|
import { reportPrepare } from "./reportPrepare.js";
|
|
19
17
|
import { resolveConfigFlag } from "./resolveConfigFlag.js";
|
|
20
|
-
import { validateOnlyExcludesStrandedDependents } from "./validateOnlyExcludesStrandedDependents.js";
|
|
21
18
|
const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
|
|
22
19
|
const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
23
20
|
function isReleaseType(value) {
|
|
@@ -47,26 +44,10 @@ export function parseArgs(argv) {
|
|
|
47
44
|
}
|
|
48
45
|
bumpOverride = flags.bump;
|
|
49
46
|
}
|
|
50
|
-
let setVersion;
|
|
51
|
-
if (flags.setVersion !== undefined) {
|
|
52
|
-
if (!CANONICAL_SEMVER_PATTERN.test(flags.setVersion)) {
|
|
53
|
-
reportError(`Invalid --set-version value "${flags.setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
setVersion = flags.setVersion;
|
|
57
|
-
}
|
|
58
47
|
let only;
|
|
59
48
|
if (flags.only !== undefined) {
|
|
60
49
|
only = flags.only.split(',');
|
|
61
50
|
}
|
|
62
|
-
if (setVersion !== undefined && bumpOverride !== undefined) {
|
|
63
|
-
reportError('--set-version cannot be combined with --bump');
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
if (setVersion !== undefined && flags.force) {
|
|
67
|
-
reportError('--set-version cannot be combined with --force');
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
51
|
return {
|
|
71
52
|
configPath: flags.config,
|
|
72
53
|
dryRun: flags.dryRun,
|
|
@@ -74,7 +55,7 @@ export function parseArgs(argv) {
|
|
|
74
55
|
noGitChecks: flags.noGitChecks,
|
|
75
56
|
bumpOverride,
|
|
76
57
|
only,
|
|
77
|
-
setVersion,
|
|
58
|
+
setVersion: flags.setVersion,
|
|
78
59
|
withReleaseNotes: flags.withReleaseNotes,
|
|
79
60
|
};
|
|
80
61
|
}
|
|
@@ -83,7 +64,6 @@ export async function prepareCommand(argv, styles, invocationDir) {
|
|
|
83
64
|
const options = {
|
|
84
65
|
force,
|
|
85
66
|
...(bumpOverride !== undefined && { bumpOverride }),
|
|
86
|
-
...(setVersion !== undefined && { setVersion }),
|
|
87
67
|
...(withReleaseNotes && { withReleaseNotes: true }),
|
|
88
68
|
};
|
|
89
69
|
if (dryRun) {
|
|
@@ -121,19 +101,27 @@ export async function prepareCommand(argv, styles, invocationDir) {
|
|
|
121
101
|
process.exit(1);
|
|
122
102
|
}
|
|
123
103
|
if (workspace.kind === 'single-package') {
|
|
124
|
-
runSinglePackageMode(userConfig, options, only, dryRun, styles.stdout);
|
|
104
|
+
runSinglePackageMode(userConfig, options, only, setVersion, dryRun, styles.stdout);
|
|
125
105
|
}
|
|
126
106
|
else {
|
|
127
107
|
runMonorepoMode(workspace.packageDirs, userConfig, options, only, setVersion, dryRun, styles.stdout);
|
|
128
108
|
}
|
|
129
109
|
}
|
|
130
|
-
function runSinglePackageMode(userConfig, options, only, dryRun, style) {
|
|
110
|
+
function runSinglePackageMode(userConfig, options, only, setVersion, dryRun, style) {
|
|
131
111
|
if (only !== undefined) {
|
|
132
112
|
reportError('--only is only supported for monorepo configurations');
|
|
133
113
|
process.exit(1);
|
|
134
114
|
}
|
|
115
|
+
if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
|
|
116
|
+
reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
135
119
|
const config = mergeSinglePackageConfig(userConfig);
|
|
136
|
-
runAndReport(() =>
|
|
120
|
+
runAndReport(() => {
|
|
121
|
+
const plan = releasePrepare(config, { ...options, ...(setVersion !== undefined && { setVersion }) });
|
|
122
|
+
assertReleasesOnNpm(plan.workspaces.some((result) => result.status === 'released') ? ['.'] : []);
|
|
123
|
+
return plan;
|
|
124
|
+
}, dryRun, style);
|
|
137
125
|
}
|
|
138
126
|
function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun, style) {
|
|
139
127
|
let config;
|
|
@@ -147,53 +135,56 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
147
135
|
}
|
|
148
136
|
if (setVersion !== undefined && config.project !== undefined) {
|
|
149
137
|
reportError('--set-version cannot be combined with a project release. ' +
|
|
150
|
-
|
|
138
|
+
"--set-version sets individual workspaces' versions; a project release rolls up every " +
|
|
151
139
|
'contributing workspace. To use --set-version, run on a config without a `project` block.');
|
|
152
140
|
process.exit(1);
|
|
153
141
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
|
|
162
|
-
process.exit(1);
|
|
142
|
+
const knownNames = config.workspaces.map((w) => w.dir);
|
|
143
|
+
const onlyNames = only ?? [];
|
|
144
|
+
for (const name of onlyNames) {
|
|
145
|
+
if (knownNames.includes(name)) {
|
|
146
|
+
continue;
|
|
163
147
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
148
|
+
reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const monorepoOptions = {
|
|
152
|
+
...options,
|
|
153
|
+
...(only !== undefined && { only }),
|
|
154
|
+
...(setVersion !== undefined && { setVersions: parseSetVersions(setVersion, knownNames) }),
|
|
155
|
+
};
|
|
156
|
+
runAndReport(() => {
|
|
157
|
+
const plan = releasePrepareMono(config, monorepoOptions);
|
|
158
|
+
assertReleasesOnNpm(findReleasedWorkspacePaths(plan, config.workspaces));
|
|
159
|
+
return plan;
|
|
160
|
+
}, dryRun, style);
|
|
161
|
+
}
|
|
162
|
+
function findReleasedWorkspacePaths(plan, workspaces) {
|
|
163
|
+
const releasedDirs = new Set(plan.workspaces.flatMap((result) => result.status === 'released' && result.name !== undefined ? [result.name] : []));
|
|
164
|
+
return workspaces.filter((workspace) => releasedDirs.has(workspace.dir)).map((workspace) => workspace.workspacePath);
|
|
165
|
+
}
|
|
166
|
+
function parseSetVersions(value, knownNames) {
|
|
167
|
+
const setVersions = new Map();
|
|
168
|
+
for (const entry of value.split(',')) {
|
|
169
|
+
const separatorIndex = entry.lastIndexOf('@');
|
|
170
|
+
const name = entry.slice(0, Math.max(0, separatorIndex));
|
|
171
|
+
const version = entry.slice(separatorIndex + 1);
|
|
172
|
+
if (separatorIndex <= 0 || !CANONICAL_SEMVER_PATTERN.test(version)) {
|
|
173
|
+
reportError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <workspace>@N.N.N ` +
|
|
174
|
+
'(canonical semver, no pre-release suffix), e.g. --set-version=arrays@1.0.0.');
|
|
177
175
|
process.exit(1);
|
|
178
176
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
if (setVersion !== undefined) {
|
|
183
|
-
if (only === undefined) {
|
|
184
|
-
reportError('--set-version requires --only in monorepo mode');
|
|
177
|
+
if (!knownNames.includes(name)) {
|
|
178
|
+
reportError(`Unknown workspace "${name}" in --set-version. Known workspaces: ${knownNames.join(', ')}`);
|
|
185
179
|
process.exit(1);
|
|
186
180
|
}
|
|
187
|
-
if (
|
|
188
|
-
reportError(`--set-version
|
|
181
|
+
if (setVersions.has(name)) {
|
|
182
|
+
reportError(`--set-version names workspace "${name}" more than once`);
|
|
189
183
|
process.exit(1);
|
|
190
184
|
}
|
|
185
|
+
setVersions.set(name, version);
|
|
191
186
|
}
|
|
192
|
-
|
|
193
|
-
...options,
|
|
194
|
-
...(only !== undefined && { only }),
|
|
195
|
-
...(configuredWorkspaceDirs !== undefined && { configuredWorkspaceDirs }),
|
|
196
|
-
}), dryRun, style);
|
|
187
|
+
return setVersions;
|
|
197
188
|
}
|
|
198
189
|
function runAndReport(computePlan, dryRun, style) {
|
|
199
190
|
let plan;
|