@williamthorsen/release-kit 14.0.0 → 14.2.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 +173 -0
- package/.readyup/kits/default.js +64 -7
- package/.readyup/kits/npm-auto-publish.js +127 -75
- package/.readyup/manifest.json +22 -7
- package/CHANGELOG.md +47 -0
- package/README.md +18 -4
- package/dist/esm/assertReleasesOnNpm.d.ts +7 -0
- package/dist/esm/assertReleasesOnNpm.js +56 -0
- package/dist/esm/assertTaggedBaseline.d.ts +2 -2
- package/dist/esm/attachChangelogDiagnostics.d.ts +2 -2
- package/dist/esm/bin/release-kit.js +10 -6
- package/dist/esm/buildChangelogEntries.d.ts +3 -3
- package/dist/esm/buildChangelogEntries.js +3 -3
- package/dist/esm/buildDependencyGraph.d.ts +11 -11
- package/dist/esm/buildDependencyGraph.js +13 -13
- package/dist/esm/buildReleaseSummary.d.ts +3 -3
- package/dist/esm/buildReleaseSummary.js +4 -12
- package/dist/esm/changelogArchive.d.ts +14 -0
- package/dist/esm/changelogArchive.js +21 -0
- package/dist/esm/changelogJsonFile.d.ts +2 -2
- package/dist/esm/changelogOverrides.d.ts +9 -8
- package/dist/esm/changelogOverrides.js +3 -6
- package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
- package/dist/esm/createGithubReleaseCommand.js +7 -4
- package/dist/esm/deriveDirectoryConfig.d.ts +13 -0
- package/dist/esm/deriveDirectoryConfig.js +59 -0
- package/dist/esm/deriveWorkspaceConfig.js +2 -3
- package/dist/esm/discoverWorkspaces.d.ts +3 -3
- package/dist/esm/enumerateReleaseWindows.d.ts +1 -1
- package/dist/esm/enumerateReleaseWindows.js +1 -3
- package/dist/esm/findTransitiveDependents.js +3 -3
- package/dist/esm/findUnroutedEntryScopes.d.ts +2 -2
- package/dist/esm/formatDirectoryPublishSkip.d.ts +3 -0
- package/dist/esm/formatDirectoryPublishSkip.js +3 -0
- package/dist/esm/formatExcludedSkip.d.ts +5 -2
- package/dist/esm/formatExcludedSkip.js +2 -2
- package/dist/esm/formatProjectPublishSkip.d.ts +3 -0
- package/dist/esm/formatProjectPublishSkip.js +3 -0
- package/dist/esm/generateChangelogs.d.ts +7 -7
- package/dist/esm/generateChangelogs.js +2 -2
- package/dist/esm/help/prepareHelp.d.ts +1 -1
- package/dist/esm/help/prepareHelp.js +7 -5
- 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/loadConfig.d.ts +12 -12
- package/dist/esm/loadConfig.js +79 -30
- 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 -1
- package/dist/esm/prepareCommand.js +50 -36
- package/dist/esm/prepareTarget.d.ts +103 -0
- package/dist/esm/prepareTarget.js +274 -0
- package/dist/esm/previewTagPrefixes.d.ts +5 -3
- package/dist/esm/previewTagPrefixes.js +60 -14
- package/dist/esm/propagateBumps.js +1 -1
- package/dist/esm/publishCommand.js +13 -1
- package/dist/esm/readChangelogSections.d.ts +6 -4
- package/dist/esm/readChangelogSections.js +18 -4
- package/dist/esm/releasePlan.d.ts +4 -2
- package/dist/esm/releasePlan.js +34 -12
- package/dist/esm/releasePrepareTargets.d.ts +47 -0
- package/dist/esm/releasePrepareTargets.js +335 -0
- package/dist/esm/renderChangelogMarkdown.d.ts +23 -0
- package/dist/esm/renderChangelogMarkdown.js +47 -11
- package/dist/esm/reportPrepare.d.ts +2 -3
- package/dist/esm/reportPrepare.js +24 -12
- package/dist/esm/resolveCommandTags.d.ts +3 -1
- package/dist/esm/resolveCommandTags.js +15 -22
- package/dist/esm/resolveReleaseDeclaration.d.ts +45 -0
- package/dist/esm/resolveReleaseDeclaration.js +153 -0
- package/dist/esm/resolveReleaseTags.d.ts +14 -3
- package/dist/esm/resolveReleaseTags.js +36 -15
- package/dist/esm/resolveReleaseTargets.d.ts +29 -0
- package/dist/esm/resolveReleaseTargets.js +131 -0
- package/dist/esm/sync-labels/generateCommand.js +2 -1
- package/dist/esm/sync-labels/resolveLabels.d.ts +4 -3
- package/dist/esm/sync-labels/resolveLabels.js +30 -1
- package/dist/esm/sync-labels/templates.js +1 -1
- package/dist/esm/sync-labels/types.d.ts +2 -0
- package/dist/esm/types.d.ts +251 -167
- package/dist/esm/types.js +22 -0
- package/dist/esm/validateConfig.js +80 -21
- package/dist/esm/validateOverridesCommand.d.ts +1 -1
- package/dist/esm/validateOverridesCommand.js +17 -28
- package/docs/api.md +25 -58
- package/docs/changelogs.md +26 -6
- package/docs/configuration.md +80 -17
- package/docs/labels.md +30 -5
- package/docs/project-releases.md +42 -2
- package/docs/publishing.md +3 -3
- package/docs/releasing.md +12 -4
- package/docs/work-types.md +1 -1
- package/package.json +6 -6
- package/presets/labels/common.yaml +11 -0
- package/dist/esm/releasePrepare.d.ts +0 -36
- package/dist/esm/releasePrepare.js +0 -230
- package/dist/esm/releasePrepareMono.d.ts +0 -29
- package/dist/esm/releasePrepareMono.js +0 -503
- package/dist/esm/releasePrepareProject.d.ts +0 -47
- package/dist/esm/releasePrepareProject.js +0 -164
package/dist/esm/loadConfig.js
CHANGED
|
@@ -11,7 +11,9 @@ import path from 'node:path';
|
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
|
|
13
13
|
import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_PROJECT_TAG_PREFIX, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from "./defaults.js";
|
|
14
|
+
import { deriveDirectoryConfig } from "./deriveDirectoryConfig.js";
|
|
14
15
|
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
16
|
+
import { getTargetName, normalizeWorkspacePath, resolveReleaseDeclaration, } from "./resolveReleaseDeclaration.js";
|
|
15
17
|
import { isRecord } from "./typeGuards.js";
|
|
16
18
|
export const ROOT_PACKAGE_JSON_PATH = 'package.json';
|
|
17
19
|
export function readRootPackageVersion() {
|
|
@@ -55,33 +57,25 @@ export async function loadConfig(configPath) {
|
|
|
55
57
|
return resolved;
|
|
56
58
|
}
|
|
57
59
|
export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
|
|
58
|
-
|
|
59
|
-
assertUniqueTagPrefixes(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
})
|
|
67
|
-
.map((w) => {
|
|
68
|
-
const override = overrides.get(w.dir);
|
|
69
|
-
if (override?.legacyIdentities === undefined) {
|
|
70
|
-
return w;
|
|
71
|
-
}
|
|
72
|
-
assertLegacyIdentityDoesNotMatchCurrent(w.dir, w.name, w.tagPrefix, override.legacyIdentities);
|
|
73
|
-
return { ...w, legacyIdentities: override.legacyIdentities.map((identity) => ({ ...identity })) };
|
|
74
|
-
});
|
|
60
|
+
const discovered = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
|
|
61
|
+
assertUniqueTagPrefixes(discovered);
|
|
62
|
+
const { retained: workspaces, excluded, directories: declaredDirectories, composite, } = resolveReleaseDeclaration(discovered, userConfig);
|
|
63
|
+
const directories = declaredDirectories.map((declared) => deriveDirectoryConfig(declared));
|
|
64
|
+
for (const workspace of workspaces) {
|
|
65
|
+
if (workspace.legacyIdentities !== undefined) {
|
|
66
|
+
assertLegacyIdentityDoesNotMatchCurrent(workspace.dir, workspace.name, workspace.tagPrefix, workspace.legacyIdentities);
|
|
67
|
+
}
|
|
75
68
|
}
|
|
76
69
|
if (userConfig?.retiredPackages !== undefined) {
|
|
77
70
|
assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
|
|
78
71
|
}
|
|
79
|
-
const project = resolveProjectConfig(
|
|
72
|
+
const project = resolveProjectConfig(composite, rootPackage, workspaces);
|
|
80
73
|
const workTypes = resolveWorkTypes(userConfig?.workTypes);
|
|
81
74
|
const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
|
|
82
75
|
const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
|
|
83
76
|
const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
|
|
84
|
-
|
|
77
|
+
assertNoSharedVersionFiles(workspaces, directories, project);
|
|
78
|
+
assertNoTagPrefixCollisions(workspaces, directories, userConfig?.retiredPackages, project);
|
|
85
79
|
const result = {
|
|
86
80
|
workspaces,
|
|
87
81
|
workTypes,
|
|
@@ -89,6 +83,12 @@ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
|
|
|
89
83
|
changelogJson,
|
|
90
84
|
releaseNotes,
|
|
91
85
|
};
|
|
86
|
+
if (excluded.length > 0) {
|
|
87
|
+
result.excludedWorkspaces = excluded;
|
|
88
|
+
}
|
|
89
|
+
if (directories.length > 0) {
|
|
90
|
+
result.directories = directories;
|
|
91
|
+
}
|
|
92
92
|
if (project !== undefined) {
|
|
93
93
|
result.project = project;
|
|
94
94
|
}
|
|
@@ -96,6 +96,9 @@ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
|
|
|
96
96
|
return result;
|
|
97
97
|
}
|
|
98
98
|
function applyOptionalPassthroughFields(result, userConfig) {
|
|
99
|
+
if (userConfig?.changelogMarkdown !== undefined) {
|
|
100
|
+
result.changelogMarkdown = userConfig.changelogMarkdown;
|
|
101
|
+
}
|
|
99
102
|
if (userConfig?.formatCommand !== undefined) {
|
|
100
103
|
result.formatCommand = userConfig.formatCommand;
|
|
101
104
|
}
|
|
@@ -110,14 +113,14 @@ export function mergeSinglePackageConfig(userConfig) {
|
|
|
110
113
|
if (userConfig?.project !== undefined) {
|
|
111
114
|
throw new Error('project block is not supported in single-package mode');
|
|
112
115
|
}
|
|
116
|
+
if (userConfig?.releases !== undefined) {
|
|
117
|
+
throw new Error('releases block is not supported in single-package mode');
|
|
118
|
+
}
|
|
113
119
|
const workTypes = resolveWorkTypes(userConfig?.workTypes);
|
|
114
120
|
const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
|
|
115
121
|
const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
|
|
116
122
|
const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
|
|
117
123
|
const result = {
|
|
118
|
-
tagPrefix: 'v',
|
|
119
|
-
packageFiles: ['package.json'],
|
|
120
|
-
changelogPaths: ['.'],
|
|
121
124
|
workTypes,
|
|
122
125
|
versionPatterns,
|
|
123
126
|
changelogJson,
|
|
@@ -161,22 +164,52 @@ function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages
|
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
|
-
function resolveProjectConfig(
|
|
165
|
-
if (
|
|
167
|
+
function resolveProjectConfig(composite, rootPackage, workspaces) {
|
|
168
|
+
if (composite === undefined) {
|
|
166
169
|
return undefined;
|
|
167
170
|
}
|
|
171
|
+
const subject = composite.name === undefined ? 'project block' : `composite release '${composite.name}'`;
|
|
168
172
|
if (rootPackage === undefined || !rootPackage.exists) {
|
|
169
|
-
throw new Error(
|
|
173
|
+
throw new Error(`${subject} requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
|
|
170
174
|
}
|
|
171
175
|
if (rootPackage.version === undefined) {
|
|
172
|
-
throw new Error(
|
|
176
|
+
throw new Error(`${subject} requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to the root package.json`);
|
|
173
177
|
}
|
|
174
178
|
return {
|
|
175
|
-
paths:
|
|
176
|
-
tagPrefix:
|
|
179
|
+
paths: composite.paths ?? workspaces.flatMap((workspace) => workspace.paths),
|
|
180
|
+
tagPrefix: composite.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX,
|
|
181
|
+
...(composite.name !== undefined && { name: composite.name }),
|
|
182
|
+
...(composite.members !== undefined && { members: composite.members }),
|
|
177
183
|
};
|
|
178
184
|
}
|
|
179
|
-
function
|
|
185
|
+
function assertNoSharedVersionFiles(workspaces, directories, project) {
|
|
186
|
+
const owners = [
|
|
187
|
+
...directories.map((directory) => ({
|
|
188
|
+
label: `directory release '${directory.name}'`,
|
|
189
|
+
versionFiles: [directory.versionFile],
|
|
190
|
+
})),
|
|
191
|
+
...workspaces.map((workspace) => ({
|
|
192
|
+
label: `workspace '${getTargetName(workspace)}'`,
|
|
193
|
+
versionFiles: workspace.packageFiles.map(normalizeWorkspacePath),
|
|
194
|
+
})),
|
|
195
|
+
...(project === undefined
|
|
196
|
+
? []
|
|
197
|
+
: [
|
|
198
|
+
{
|
|
199
|
+
label: project.name === undefined ? 'the project release' : `composite release '${project.name}'`,
|
|
200
|
+
versionFiles: [ROOT_PACKAGE_JSON_PATH],
|
|
201
|
+
},
|
|
202
|
+
]),
|
|
203
|
+
];
|
|
204
|
+
for (const [index, directory] of directories.entries()) {
|
|
205
|
+
const peer = owners.slice(index + 1).find((owner) => owner.versionFiles.includes(directory.versionFile));
|
|
206
|
+
if (peer !== undefined) {
|
|
207
|
+
throw new Error(`directory release '${directory.name}' and ${peer.label} share the version file '${directory.versionFile}', ` +
|
|
208
|
+
"so each release would overwrite the other's version and changelog; declare only one of them");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function assertNoTagPrefixCollisions(workspaces, directories, retiredPackages, project) {
|
|
180
213
|
const sources = [];
|
|
181
214
|
for (const workspace of workspaces) {
|
|
182
215
|
const owner = `ws:${workspace.dir}`;
|
|
@@ -190,6 +223,18 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
|
|
|
190
223
|
});
|
|
191
224
|
}
|
|
192
225
|
}
|
|
226
|
+
for (const directory of directories) {
|
|
227
|
+
const owner = `dir:${directory.path}`;
|
|
228
|
+
sources.push({ prefix: directory.tagPrefix, label: `directory release '${directory.name}'`, owner });
|
|
229
|
+
const legacyIdentities = directory.legacyIdentities ?? [];
|
|
230
|
+
for (const identity of legacyIdentities) {
|
|
231
|
+
sources.push({
|
|
232
|
+
prefix: identity.tagPrefix,
|
|
233
|
+
label: `directory release '${directory.name}' legacyIdentities entry (name='${identity.name}')`,
|
|
234
|
+
owner,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
193
238
|
const retiredEntries = (retiredPackages ?? []).entries();
|
|
194
239
|
for (const [index, retired] of retiredEntries) {
|
|
195
240
|
sources.push({
|
|
@@ -199,7 +244,11 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
|
|
|
199
244
|
});
|
|
200
245
|
}
|
|
201
246
|
if (project !== undefined) {
|
|
202
|
-
sources.push({
|
|
247
|
+
sources.push({
|
|
248
|
+
prefix: project.tagPrefix,
|
|
249
|
+
label: project.name === undefined ? 'project' : `composite release '${project.name}'`,
|
|
250
|
+
owner: 'project',
|
|
251
|
+
});
|
|
203
252
|
}
|
|
204
253
|
for (let i = 0; i < sources.length; i++) {
|
|
205
254
|
for (let j = i + 1; j < sources.length; j++) {
|
|
@@ -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
|
+
}
|
|
@@ -49,7 +49,7 @@ export declare function parseArgs(argv: string[]): {
|
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
51
51
|
* Orchestrates the CLI `prepare` command: checks the working tree, loads and validates the config, discovers the
|
|
52
|
-
* workspaces, and prepares the release
|
|
52
|
+
* workspaces, and prepares the release over the targets of the repo shape.
|
|
53
53
|
*
|
|
54
54
|
* A relative `--config` resolves against `invocationDir`.
|
|
55
55
|
*/
|
|
@@ -2,18 +2,18 @@ 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 { assertReleasesOnNpm } from "./assertReleasesOnNpm.js";
|
|
5
6
|
import { configFlagSchema } from "./configFlagSchema.js";
|
|
6
7
|
import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
7
8
|
import { dim } from "./format.js";
|
|
8
9
|
import { RELEASE_GLYPHS } from "./glyphs.js";
|
|
9
|
-
import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
|
|
10
10
|
import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
|
|
11
11
|
import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
|
|
12
12
|
import { applyReleasePlan } from "./releasePlan.js";
|
|
13
|
-
import {
|
|
14
|
-
import { releasePrepareMono } from "./releasePrepareMono.js";
|
|
13
|
+
import { releasePrepareTargets } from "./releasePrepareTargets.js";
|
|
15
14
|
import { reportPrepare } from "./reportPrepare.js";
|
|
16
15
|
import { resolveConfigFlag } from "./resolveConfigFlag.js";
|
|
16
|
+
import { isSinglePackageTargets, PROJECT_TARGET_ID, resolveReleaseTargets, SINGLE_PACKAGE_TARGET_ID, } from "./resolveReleaseTargets.js";
|
|
17
17
|
const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
|
|
18
18
|
const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
19
19
|
function isReleaseType(value) {
|
|
@@ -99,56 +99,70 @@ export async function prepareCommand(argv, styles, invocationDir) {
|
|
|
99
99
|
reportError(`No workspace package to release. ${describeEmptyWorkspace(workspace)}`);
|
|
100
100
|
process.exit(1);
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
102
|
+
const config = resolveTargetsOrExit(userConfig, workspace, only, setVersion);
|
|
103
|
+
const targetOptions = {
|
|
104
|
+
...options,
|
|
105
|
+
...(only !== undefined && { only }),
|
|
106
|
+
...(setVersion !== undefined && { setVersions: resolveSetVersions(setVersion, config.targets) }),
|
|
107
|
+
};
|
|
108
|
+
runAndReport(() => {
|
|
109
|
+
const plan = releasePrepareTargets(config, targetOptions);
|
|
110
|
+
assertReleasesOnNpm(findReleasedPackagePaths(plan, config.targets));
|
|
111
|
+
return plan;
|
|
112
|
+
}, dryRun, styles.stdout);
|
|
108
113
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
|
|
115
|
-
reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
const config = mergeSinglePackageConfig(userConfig);
|
|
119
|
-
runAndReport(() => releasePrepare(config, { ...options, ...(setVersion !== undefined && { setVersion }) }), dryRun, style);
|
|
114
|
+
function findReleasedPackagePaths(plan, targets) {
|
|
115
|
+
const releasedIds = new Set(plan.targets.flatMap((result) => result.status === 'released' && result.kind === 'package' ? [result.targetId] : []));
|
|
116
|
+
return targets.flatMap((target) => target.kind === 'package' && releasedIds.has(target.id) ? [target.packagePath] : []);
|
|
120
117
|
}
|
|
121
|
-
function
|
|
118
|
+
function resolveTargetsOrExit(userConfig, discovery, only, setVersion) {
|
|
119
|
+
if (discovery.kind === 'single-package') {
|
|
120
|
+
if (only !== undefined) {
|
|
121
|
+
reportError('--only is only supported for monorepo configurations');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
|
|
125
|
+
reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
return resolveReleaseTargets(userConfig, discovery);
|
|
129
|
+
}
|
|
122
130
|
let config;
|
|
123
131
|
try {
|
|
124
|
-
|
|
125
|
-
config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
|
|
132
|
+
config = resolveReleaseTargets(userConfig, discovery);
|
|
126
133
|
}
|
|
127
134
|
catch (error) {
|
|
128
135
|
reportError(`Failed to resolve workspaces: ${describeError(error)}`);
|
|
129
136
|
process.exit(1);
|
|
130
137
|
}
|
|
131
|
-
if (setVersion !== undefined && config.
|
|
138
|
+
if (setVersion !== undefined && config.targets.some((target) => target.kind === 'composite')) {
|
|
132
139
|
reportError('--set-version cannot be combined with a project release. ' +
|
|
133
140
|
"--set-version sets individual workspaces' versions; a project release rolls up every " +
|
|
134
|
-
'contributing workspace. To use --set-version, run on a config without a `project` block.');
|
|
141
|
+
'contributing workspace. To use --set-version, run on a config without a `project` block or a `releases` composite.');
|
|
135
142
|
process.exit(1);
|
|
136
143
|
}
|
|
137
|
-
const knownNames = config.
|
|
144
|
+
const knownNames = listSelectableIds(config.targets);
|
|
138
145
|
const onlyNames = only ?? [];
|
|
139
146
|
for (const name of onlyNames) {
|
|
140
147
|
if (knownNames.includes(name)) {
|
|
141
148
|
continue;
|
|
142
149
|
}
|
|
143
|
-
reportError(`Unknown
|
|
150
|
+
reportError(`Unknown release target "${name}". Known targets: ${knownNames.join(', ')}`);
|
|
144
151
|
process.exit(1);
|
|
145
152
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
return config;
|
|
154
|
+
}
|
|
155
|
+
function resolveSetVersions(setVersion, targets) {
|
|
156
|
+
if (isSinglePackageTargets(targets)) {
|
|
157
|
+
return new Map([[SINGLE_PACKAGE_TARGET_ID, setVersion]]);
|
|
158
|
+
}
|
|
159
|
+
return parseSetVersions(setVersion, listMemberIds(targets));
|
|
160
|
+
}
|
|
161
|
+
function listSelectableIds(targets) {
|
|
162
|
+
return targets.flatMap((target) => (target.id === PROJECT_TARGET_ID ? [] : [target.id]));
|
|
163
|
+
}
|
|
164
|
+
function listMemberIds(targets) {
|
|
165
|
+
return targets.flatMap((target) => (target.kind === 'composite' ? [] : [target.id]));
|
|
152
166
|
}
|
|
153
167
|
function parseSetVersions(value, knownNames) {
|
|
154
168
|
const setVersions = new Map();
|
|
@@ -157,16 +171,16 @@ function parseSetVersions(value, knownNames) {
|
|
|
157
171
|
const name = entry.slice(0, Math.max(0, separatorIndex));
|
|
158
172
|
const version = entry.slice(separatorIndex + 1);
|
|
159
173
|
if (separatorIndex <= 0 || !CANONICAL_SEMVER_PATTERN.test(version)) {
|
|
160
|
-
reportError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <
|
|
174
|
+
reportError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <target>@N.N.N ` +
|
|
161
175
|
'(canonical semver, no pre-release suffix), e.g. --set-version=arrays@1.0.0.');
|
|
162
176
|
process.exit(1);
|
|
163
177
|
}
|
|
164
178
|
if (!knownNames.includes(name)) {
|
|
165
|
-
reportError(`Unknown
|
|
179
|
+
reportError(`Unknown release target "${name}" in --set-version. Known targets: ${knownNames.join(', ')}`);
|
|
166
180
|
process.exit(1);
|
|
167
181
|
}
|
|
168
182
|
if (setVersions.has(name)) {
|
|
169
|
-
reportError(`--set-version names
|
|
183
|
+
reportError(`--set-version names target "${name}" more than once`);
|
|
170
184
|
process.exit(1);
|
|
171
185
|
}
|
|
172
186
|
setVersions.set(name, version);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type UntaggedBaseline } from './assertTaggedBaseline.js';
|
|
2
|
+
import { type ReleaseHistory } from './buildChangelogEntries.js';
|
|
3
|
+
import { type OverrideContext } from './changelogOverrides.js';
|
|
4
|
+
import { type GenerateChangelogOptions } from './generateChangelogs.js';
|
|
5
|
+
import type { PlannedWrite } from './releasePlan.js';
|
|
6
|
+
import type { DirectoryTarget, PropagationSource, ReleasedTargetResult, ReleaseSettings, ReleaseTarget, ReleaseType, TargetPrepareResult } from './types.js';
|
|
7
|
+
/** The shared settings that the prepare step reads, which do not include the release-notes settings. */
|
|
8
|
+
export type TargetPrepareSettings = Omit<ReleaseSettings, 'releaseNotes'>;
|
|
9
|
+
/** One read of a target's release windows, and the untagged baseline that it found, if any. */
|
|
10
|
+
export interface TargetReading {
|
|
11
|
+
history: ReleaseHistory;
|
|
12
|
+
untagged: UntaggedBaseline | undefined;
|
|
13
|
+
}
|
|
14
|
+
/** Outcome of {@link decideTargetRelease}: a release at a decided level or at an explicit version, or a skip. */
|
|
15
|
+
export type TargetDecision = {
|
|
16
|
+
outcome: 'release';
|
|
17
|
+
releaseType: ReleaseType;
|
|
18
|
+
bumpOverride: ReleaseType | undefined;
|
|
19
|
+
setVersion: undefined;
|
|
20
|
+
} | {
|
|
21
|
+
outcome: 'release';
|
|
22
|
+
releaseType: undefined;
|
|
23
|
+
bumpOverride: undefined;
|
|
24
|
+
setVersion: string;
|
|
25
|
+
} | {
|
|
26
|
+
outcome: 'skip';
|
|
27
|
+
skipReason: string;
|
|
28
|
+
};
|
|
29
|
+
/** The release level that a target takes, whether its own commits or a dependency's bump decided it. */
|
|
30
|
+
export interface TargetRelease {
|
|
31
|
+
releaseType: ReleaseType;
|
|
32
|
+
/** Dependencies whose bumps this release records; present for a propagated or mixed release. */
|
|
33
|
+
propagatedFrom?: PropagationSource[];
|
|
34
|
+
}
|
|
35
|
+
/** Inputs to {@link planTargetRelease}. */
|
|
36
|
+
export interface PlanTargetReleaseArgs {
|
|
37
|
+
target: ReleaseTarget;
|
|
38
|
+
settings: TargetPrepareSettings;
|
|
39
|
+
/** The level of the release and, for a propagated release, the dependencies that set it. */
|
|
40
|
+
release: TargetRelease;
|
|
41
|
+
/**
|
|
42
|
+
* The decision behind a direct release; undefined for a release that propagation alone set, whose own window called
|
|
43
|
+
* for none.
|
|
44
|
+
*/
|
|
45
|
+
decision: Extract<TargetDecision, {
|
|
46
|
+
outcome: 'release';
|
|
47
|
+
}> | undefined;
|
|
48
|
+
/** The window that the target's read produced. */
|
|
49
|
+
history: ReleaseHistory;
|
|
50
|
+
overrideContext: OverrideContext;
|
|
51
|
+
/** Section titles in priority order. */
|
|
52
|
+
sectionOrder: string[];
|
|
53
|
+
/** Header of the section under which a propagation-only entry lists its dependencies. */
|
|
54
|
+
dependenciesHeader: string;
|
|
55
|
+
/** True when release-notes previews are requested and `changelogJson.enabled` is true. */
|
|
56
|
+
shouldPlanPreviews: boolean;
|
|
57
|
+
/** The release date, as `YYYY-MM-DD`. */
|
|
58
|
+
today: string;
|
|
59
|
+
}
|
|
60
|
+
/** A target's planned release: its result and the tag, writes, and modified files that the run collects. */
|
|
61
|
+
export interface PlannedTargetRelease {
|
|
62
|
+
result: ReleasedTargetResult;
|
|
63
|
+
tag: string;
|
|
64
|
+
writes: PlannedWrite[];
|
|
65
|
+
/** Archive files of the target's `CHANGELOG.md` that the release removes. */
|
|
66
|
+
removals: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Files that the format command covers: the version files, the `CHANGELOG.md` and its archive files, and any
|
|
69
|
+
* changelog JSON.
|
|
70
|
+
*/
|
|
71
|
+
modifiedFiles: string[];
|
|
72
|
+
/** Warnings raised while planning the release-notes previews. */
|
|
73
|
+
warnings: string[];
|
|
74
|
+
}
|
|
75
|
+
/** Reads a target's release windows under its tag prefixes and commit window, and finds its untagged baseline. */
|
|
76
|
+
export declare function readTargetHistory(target: ReleaseTarget, settings: TargetPrepareSettings): TargetReading;
|
|
77
|
+
/**
|
|
78
|
+
* Returns the bounds of a target's history read: its current and legacy tag prefixes, its commit window, and, for a
|
|
79
|
+
* workspace or a directory other than the repo root, the scope to which change-record entries route, which is its name.
|
|
80
|
+
*/
|
|
81
|
+
export declare function describeHistoryWindow(target: ReleaseTarget): GenerateChangelogOptions;
|
|
82
|
+
/**
|
|
83
|
+
* Decides a target's direct release from its window: `setVersion` releases at that version, and otherwise the
|
|
84
|
+
* window's bump, `force`, and `bumpOverride` decide as {@link decideRelease} states.
|
|
85
|
+
*/
|
|
86
|
+
export declare function decideTargetRelease(target: ReleaseTarget, history: ReleaseHistory, options: {
|
|
87
|
+
force: boolean;
|
|
88
|
+
bumpOverride: ReleaseType | undefined;
|
|
89
|
+
setVersion: string | undefined;
|
|
90
|
+
}): TargetDecision;
|
|
91
|
+
/**
|
|
92
|
+
* Plans a target's release: the version bump, the changelogs, and the release-notes previews, and builds its result.
|
|
93
|
+
*
|
|
94
|
+
* The changelog entries come from the target's release windows, with the synthetic "Forced version bump." entry for a
|
|
95
|
+
* window that doesn't yield any item, or, for a release that propagation alone set, from one synthetic entry listing
|
|
96
|
+
* the dependencies. Overrides apply before `changelog.json` and `CHANGELOG.md` render from the merged set, and
|
|
97
|
+
* `CHANGELOG.md` keeps the existing sections whose versions the merged set lacks.
|
|
98
|
+
*/
|
|
99
|
+
export declare function planTargetRelease(args: PlanTargetReleaseArgs): PlannedTargetRelease;
|
|
100
|
+
/** Builds the result of a target that skips, from the window that its read produced. */
|
|
101
|
+
export declare function buildSkippedTargetResult(target: ReleaseTarget, history: ReleaseHistory, skipReason: string): TargetPrepareResult;
|
|
102
|
+
/** Reports whether a directory target is the repo root, which reads the whole repo and renders every entry. */
|
|
103
|
+
export declare function isRepoRoot(target: DirectoryTarget): boolean;
|