@williamthorsen/release-kit 4.8.0 → 5.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/CHANGELOG.md +134 -4
- package/README.md +404 -40
- package/cliff.toml.template +2 -1
- package/dist/esm/.cache +1 -1
- package/dist/esm/assertCleanWorkingTree.js +1 -1
- package/dist/esm/bin/release-kit.js +45 -14
- package/dist/esm/buildChangelogEntries.d.ts +3 -0
- package/dist/esm/{generateChangelogJson.js → buildChangelogEntries.js} +40 -77
- package/dist/esm/buildDependencyGraph.d.ts +4 -3
- package/dist/esm/buildDependencyGraph.js +18 -11
- package/dist/esm/buildReleaseSummary.js +12 -4
- package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
- package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
- package/dist/esm/bumpAllVersions.d.ts +1 -0
- package/dist/esm/bumpAllVersions.js +16 -2
- package/dist/esm/bumpVersion.js +3 -0
- package/dist/esm/changelogJsonFile.d.ts +4 -0
- package/dist/esm/changelogJsonFile.js +68 -0
- package/dist/esm/commitCommand.js +1 -1
- package/dist/esm/compareVersions.d.ts +1 -0
- package/dist/esm/compareVersions.js +27 -0
- package/dist/esm/createGithubRelease.d.ts +6 -2
- package/dist/esm/createGithubRelease.js +17 -17
- package/dist/esm/createGithubReleaseCommand.d.ts +1 -0
- package/dist/esm/createGithubReleaseCommand.js +41 -0
- package/dist/esm/decideRelease.d.ts +25 -0
- package/dist/esm/decideRelease.js +28 -0
- package/dist/esm/defaults.d.ts +1 -0
- package/dist/esm/defaults.js +7 -3
- package/dist/esm/deriveWorkspaceConfig.d.ts +2 -0
- package/dist/esm/deriveWorkspaceConfig.js +37 -0
- package/dist/esm/detectUndeclaredTagPrefixes.d.ts +7 -0
- package/dist/esm/detectUndeclaredTagPrefixes.js +46 -0
- package/dist/esm/generateChangelogs.d.ts +1 -1
- package/dist/esm/generateChangelogs.js +14 -3
- package/dist/esm/getCommitsSinceTarget.d.ts +1 -1
- package/dist/esm/getCommitsSinceTarget.js +8 -4
- package/dist/esm/index.d.ts +2 -39
- package/dist/esm/index.js +0 -75
- package/dist/esm/init/initCommand.js +1 -1
- package/dist/esm/init/scaffold.d.ts +1 -1
- package/dist/esm/init/scaffold.js +8 -5
- package/dist/esm/init/templates.d.ts +1 -0
- package/dist/esm/init/templates.js +35 -5
- package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
- package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
- package/dist/esm/loadConfig.d.ts +12 -2
- package/dist/esm/loadConfig.js +161 -14
- package/dist/esm/parseRequestedTags.d.ts +1 -0
- package/dist/esm/parseRequestedTags.js +10 -0
- package/dist/esm/prepareCommand.d.ts +3 -1
- package/dist/esm/prepareCommand.js +121 -31
- package/dist/esm/previewTagPrefixes.d.ts +30 -0
- package/dist/esm/previewTagPrefixes.js +120 -0
- package/dist/esm/propagateBumps.d.ts +1 -0
- package/dist/esm/propagateBumps.js +1 -1
- package/dist/esm/publish.d.ts +0 -1
- package/dist/esm/publish.js +3 -3
- package/dist/esm/publishCommand.js +18 -14
- package/dist/esm/pushCommand.js +5 -4
- package/dist/esm/readCurrentVersion.d.ts +1 -0
- package/dist/esm/readCurrentVersion.js +21 -0
- package/dist/esm/releasePrepare.d.ts +2 -0
- package/dist/esm/releasePrepare.js +140 -54
- package/dist/esm/releasePrepareMono.js +312 -143
- package/dist/esm/releasePrepareProject.d.ts +9 -0
- package/dist/esm/releasePrepareProject.js +109 -0
- package/dist/esm/renderReleaseNotes.d.ts +1 -0
- package/dist/esm/renderReleaseNotes.js +29 -2
- package/dist/esm/reportPrepare.js +146 -73
- package/dist/esm/resolveCliffConfigPath.js +1 -1
- package/dist/esm/resolveCommandTags.d.ts +1 -1
- package/dist/esm/resolveCommandTags.js +17 -13
- package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
- package/dist/esm/resolveReleaseNotesConfig.js +17 -7
- package/dist/esm/resolveReleaseTags.d.ts +2 -1
- package/dist/esm/resolveReleaseTags.js +19 -14
- package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
- package/dist/esm/showTagPrefixesCommand.js +84 -0
- package/dist/esm/sync-labels/initCommand.js +1 -1
- package/dist/esm/sync-labels/presets.js +1 -1
- package/dist/esm/tagCommand.js +1 -1
- package/dist/esm/types.d.ts +77 -19
- package/dist/esm/validateConfig.js +205 -36
- package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
- package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
- package/dist/esm/writeReleaseNotesPreviews.js +65 -0
- package/package.json +5 -2
- package/presets/labels/common.yaml +9 -6
- package/schemas/label-map.json +24 -0
- package/dist/esm/component.d.ts +0 -2
- package/dist/esm/component.js +0 -14
- package/dist/esm/findPackageRoot.d.ts +0 -1
- package/dist/esm/findPackageRoot.js +0 -17
- package/dist/esm/generateChangelogJson.d.ts +0 -7
- package/dist/esm/githubReleaseCommand.d.ts +0 -1
- package/dist/esm/githubReleaseCommand.js +0 -35
package/dist/esm/loadConfig.js
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { component } from "./component.js";
|
|
4
3
|
import {
|
|
5
4
|
DEFAULT_CHANGELOG_JSON_CONFIG,
|
|
5
|
+
DEFAULT_PROJECT_TAG_PREFIX,
|
|
6
6
|
DEFAULT_RELEASE_NOTES_CONFIG,
|
|
7
7
|
DEFAULT_VERSION_PATTERNS,
|
|
8
8
|
DEFAULT_WORK_TYPES
|
|
9
9
|
} from "./defaults.js";
|
|
10
|
+
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
10
11
|
import { isRecord } from "./typeGuards.js";
|
|
12
|
+
const ROOT_PACKAGE_JSON_PATH = "package.json";
|
|
13
|
+
function readRootPackageVersion() {
|
|
14
|
+
const absolutePath = path.resolve(process.cwd(), ROOT_PACKAGE_JSON_PATH);
|
|
15
|
+
if (!existsSync(absolutePath)) {
|
|
16
|
+
return { exists: false, version: void 0 };
|
|
17
|
+
}
|
|
18
|
+
let contents;
|
|
19
|
+
try {
|
|
20
|
+
contents = readFileSync(absolutePath, "utf8");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Failed to read root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(contents);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (!isRecord(parsed)) {
|
|
35
|
+
return { exists: true, version: void 0 };
|
|
36
|
+
}
|
|
37
|
+
return { exists: true, version: typeof parsed.version === "string" ? parsed.version : void 0 };
|
|
38
|
+
}
|
|
11
39
|
const CONFIG_FILE_PATH = ".config/release-kit.config.ts";
|
|
12
40
|
async function loadConfig() {
|
|
13
41
|
const absoluteConfigPath = path.resolve(process.cwd(), CONFIG_FILE_PATH);
|
|
@@ -28,26 +56,42 @@ async function loadConfig() {
|
|
|
28
56
|
}
|
|
29
57
|
return resolved;
|
|
30
58
|
}
|
|
31
|
-
function mergeMonorepoConfig(discoveredPaths, userConfig) {
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
|
|
60
|
+
let workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
|
|
61
|
+
assertUniqueTagPrefixes(workspaces);
|
|
62
|
+
if (userConfig?.workspaces !== void 0) {
|
|
63
|
+
const overrides = new Map(userConfig.workspaces.map((w) => [w.dir, w]));
|
|
64
|
+
workspaces = workspaces.filter((w) => {
|
|
65
|
+
const override = overrides.get(w.dir);
|
|
37
66
|
return override?.shouldExclude !== true;
|
|
67
|
+
}).map((w) => {
|
|
68
|
+
const override = overrides.get(w.dir);
|
|
69
|
+
if (override?.legacyIdentities === void 0) {
|
|
70
|
+
return w;
|
|
71
|
+
}
|
|
72
|
+
assertLegacyIdentityDoesNotMatchCurrent(w.dir, w.name, w.tagPrefix, override.legacyIdentities);
|
|
73
|
+
return { ...w, legacyIdentities: override.legacyIdentities.map((identity) => ({ ...identity })) };
|
|
38
74
|
});
|
|
39
75
|
}
|
|
40
|
-
|
|
76
|
+
if (userConfig?.retiredPackages !== void 0) {
|
|
77
|
+
assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
|
|
78
|
+
}
|
|
79
|
+
const project = resolveProjectConfig(userConfig?.project, rootPackage);
|
|
80
|
+
const workTypes = resolveWorkTypes(userConfig?.workTypes);
|
|
41
81
|
const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
|
|
42
82
|
const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
|
|
43
83
|
const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
|
|
84
|
+
assertNoTagPrefixCollisions(workspaces, userConfig?.retiredPackages, project);
|
|
44
85
|
const result = {
|
|
45
|
-
|
|
86
|
+
workspaces,
|
|
46
87
|
workTypes,
|
|
47
88
|
versionPatterns,
|
|
48
89
|
changelogJson,
|
|
49
90
|
releaseNotes
|
|
50
91
|
};
|
|
92
|
+
if (project !== void 0) {
|
|
93
|
+
result.project = project;
|
|
94
|
+
}
|
|
51
95
|
const formatCommand = userConfig?.formatCommand;
|
|
52
96
|
if (formatCommand !== void 0) {
|
|
53
97
|
result.formatCommand = formatCommand;
|
|
@@ -63,7 +107,10 @@ function mergeMonorepoConfig(discoveredPaths, userConfig) {
|
|
|
63
107
|
return result;
|
|
64
108
|
}
|
|
65
109
|
function mergeSinglePackageConfig(userConfig) {
|
|
66
|
-
|
|
110
|
+
if (userConfig?.project !== void 0) {
|
|
111
|
+
throw new Error("project block is not supported in single-package mode");
|
|
112
|
+
}
|
|
113
|
+
const workTypes = resolveWorkTypes(userConfig?.workTypes);
|
|
67
114
|
const versionPatterns = userConfig?.versionPatterns === void 0 ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
|
|
68
115
|
const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
|
|
69
116
|
const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
|
|
@@ -90,6 +137,9 @@ function mergeSinglePackageConfig(userConfig) {
|
|
|
90
137
|
}
|
|
91
138
|
return result;
|
|
92
139
|
}
|
|
140
|
+
function resolveWorkTypes(userWorkTypes) {
|
|
141
|
+
return userWorkTypes === void 0 ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userWorkTypes };
|
|
142
|
+
}
|
|
93
143
|
function mergeChangelogJsonConfig(partial) {
|
|
94
144
|
if (partial === void 0) {
|
|
95
145
|
return { ...DEFAULT_CHANGELOG_JSON_CONFIG };
|
|
@@ -100,18 +150,115 @@ function mergeChangelogJsonConfig(partial) {
|
|
|
100
150
|
devOnlySections: partial.devOnlySections ?? [...DEFAULT_CHANGELOG_JSON_CONFIG.devOnlySections]
|
|
101
151
|
};
|
|
102
152
|
}
|
|
153
|
+
function assertLegacyIdentityDoesNotMatchCurrent(dir, currentName, currentTagPrefix, legacyIdentities) {
|
|
154
|
+
for (const identity of legacyIdentities) {
|
|
155
|
+
if (identity.name === currentName && identity.tagPrefix === currentTagPrefix) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Workspace '${dir}': legacyIdentities must not match the current identity (name='${currentName}', tagPrefix='${currentTagPrefix}'). The current identity is always searched; listing it again is a no-op.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages) {
|
|
163
|
+
const workspaceByDerivedPrefix = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const workspace of workspaces) {
|
|
165
|
+
workspaceByDerivedPrefix.set(workspace.tagPrefix, workspace);
|
|
166
|
+
}
|
|
167
|
+
for (const retired of retiredPackages) {
|
|
168
|
+
const active = workspaceByDerivedPrefix.get(retired.tagPrefix);
|
|
169
|
+
if (active !== void 0) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`retiredPackages: tagPrefix '${retired.tagPrefix}' collides with active workspace '${active.dir}' (derived prefix '${active.tagPrefix}'). A retired package's tagPrefix cannot belong to an active workspace.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function resolveProjectConfig(userProject, rootPackage) {
|
|
177
|
+
if (userProject === void 0) {
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
if (rootPackage === void 0 || !rootPackage.exists) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`project block requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (rootPackage.version === void 0) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`project block requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to your root package.json`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return { tagPrefix: userProject.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX };
|
|
191
|
+
}
|
|
192
|
+
function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
|
|
193
|
+
const sources = [];
|
|
194
|
+
for (const workspace of workspaces) {
|
|
195
|
+
const owner = `ws:${workspace.dir}`;
|
|
196
|
+
sources.push({ prefix: workspace.tagPrefix, label: `workspace '${workspace.dir}'`, owner });
|
|
197
|
+
for (const identity of workspace.legacyIdentities ?? []) {
|
|
198
|
+
sources.push({
|
|
199
|
+
prefix: identity.tagPrefix,
|
|
200
|
+
label: `workspace '${workspace.dir}' legacyIdentities entry (name='${identity.name}')`,
|
|
201
|
+
owner
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const [index, retired] of (retiredPackages ?? []).entries()) {
|
|
206
|
+
sources.push({
|
|
207
|
+
prefix: retired.tagPrefix,
|
|
208
|
+
label: `retiredPackages entry (name='${retired.name}')`,
|
|
209
|
+
owner: `retired:${index}`
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (project !== void 0) {
|
|
213
|
+
sources.push({ prefix: project.tagPrefix, label: "project", owner: "project" });
|
|
214
|
+
}
|
|
215
|
+
for (let i = 0; i < sources.length; i++) {
|
|
216
|
+
for (let j = i + 1; j < sources.length; j++) {
|
|
217
|
+
const a = sources[i];
|
|
218
|
+
const b = sources[j];
|
|
219
|
+
if (a === void 0 || b === void 0) continue;
|
|
220
|
+
if (a.owner === b.owner) continue;
|
|
221
|
+
if (isPrefixCollision(a.prefix, b.prefix)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). One prefix is identical to or a strict prefix of the other; this would cause \`git describe --match=<prefix>*\` to return cross-matches.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function isPrefixCollision(a, b) {
|
|
230
|
+
return a === b || a.startsWith(b) || b.startsWith(a);
|
|
231
|
+
}
|
|
232
|
+
function assertUniqueTagPrefixes(workspaces) {
|
|
233
|
+
const pathsByPrefix = /* @__PURE__ */ new Map();
|
|
234
|
+
for (const workspace of workspaces) {
|
|
235
|
+
const existing = pathsByPrefix.get(workspace.tagPrefix);
|
|
236
|
+
if (existing === void 0) {
|
|
237
|
+
pathsByPrefix.set(workspace.tagPrefix, [workspace.workspacePath]);
|
|
238
|
+
} else {
|
|
239
|
+
existing.push(workspace.workspacePath);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const [prefix, paths] of pathsByPrefix) {
|
|
243
|
+
if (paths.length > 1) {
|
|
244
|
+
throw new Error(`Duplicate tag prefix '${prefix}' for workspaces: ${paths.join(", ")}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
103
248
|
function mergeReleaseNotesConfig(partial) {
|
|
104
249
|
if (partial === void 0) {
|
|
105
250
|
return { ...DEFAULT_RELEASE_NOTES_CONFIG };
|
|
106
251
|
}
|
|
107
252
|
return {
|
|
108
|
-
shouldInjectIntoReadme: partial.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme
|
|
109
|
-
shouldCreateGithubRelease: partial.shouldCreateGithubRelease ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldCreateGithubRelease
|
|
253
|
+
shouldInjectIntoReadme: partial.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme
|
|
110
254
|
};
|
|
111
255
|
}
|
|
112
256
|
export {
|
|
113
257
|
CONFIG_FILE_PATH,
|
|
258
|
+
ROOT_PACKAGE_JSON_PATH,
|
|
114
259
|
loadConfig,
|
|
115
260
|
mergeMonorepoConfig,
|
|
116
|
-
mergeSinglePackageConfig
|
|
261
|
+
mergeSinglePackageConfig,
|
|
262
|
+
readRootPackageVersion,
|
|
263
|
+
resolveWorkTypes
|
|
117
264
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseRequestedTags(flagValue: string | undefined): string[] | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WriteResult } from '@williamthorsen/
|
|
1
|
+
import type { WriteResult } from '@williamthorsen/nmr-core';
|
|
2
2
|
import type { ReleaseType } from './types.ts';
|
|
3
3
|
export declare const RELEASE_TAGS_FILE = "tmp/.release-tags";
|
|
4
4
|
export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
|
|
@@ -8,6 +8,8 @@ export declare function parseArgs(argv: string[]): {
|
|
|
8
8
|
noGitChecks: boolean;
|
|
9
9
|
bumpOverride: ReleaseType | undefined;
|
|
10
10
|
only: string[] | undefined;
|
|
11
|
+
setVersion: string | undefined;
|
|
12
|
+
withReleaseNotes: boolean;
|
|
11
13
|
};
|
|
12
14
|
export declare function writeReleaseTags(tags: string[], dryRun: boolean): WriteResult | undefined;
|
|
13
15
|
export declare function prepareCommand(argv: string[]): Promise<void>;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseArgs as coreParseArgs,
|
|
3
|
-
translateParseError,
|
|
4
|
-
writeFileWithCheck
|
|
5
|
-
} from "@williamthorsen/node-monorepo-core";
|
|
1
|
+
import { parseArgs as coreParseArgs, translateParseError, writeFileWithCheck } from "@williamthorsen/nmr-core";
|
|
6
2
|
import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
|
|
3
|
+
import { buildDependencyGraph } from "./buildDependencyGraph.js";
|
|
7
4
|
import { buildReleaseSummary } from "./buildReleaseSummary.js";
|
|
8
5
|
import { discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
9
6
|
import { dim } from "./format.js";
|
|
10
|
-
import {
|
|
7
|
+
import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
|
|
8
|
+
import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
|
|
11
9
|
import { releasePrepare } from "./releasePrepare.js";
|
|
12
10
|
import { releasePrepareMono } from "./releasePrepareMono.js";
|
|
13
11
|
import { reportPrepare } from "./reportPrepare.js";
|
|
14
12
|
import { validateConfig } from "./validateConfig.js";
|
|
13
|
+
import { validateOnlyExcludesStrandedDependents } from "./validateOnlyExcludesStrandedDependents.js";
|
|
15
14
|
const RELEASE_TAGS_FILE = "tmp/.release-tags";
|
|
16
15
|
const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
|
|
17
16
|
const VALID_BUMP_TYPES = ["major", "minor", "patch"];
|
|
17
|
+
const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
18
18
|
function isReleaseType(value) {
|
|
19
19
|
return VALID_BUMP_TYPES.includes(value);
|
|
20
20
|
}
|
|
@@ -24,10 +24,18 @@ Usage: npx @williamthorsen/release-kit prepare [options]
|
|
|
24
24
|
|
|
25
25
|
Options:
|
|
26
26
|
--dry-run Run without modifying any files
|
|
27
|
-
--bump=major|minor|patch Override the bump type for all
|
|
28
|
-
--
|
|
27
|
+
--bump=major|minor|patch Override the bump type for all workspaces
|
|
28
|
+
--set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.
|
|
29
|
+
Requires --only in monorepo mode (rejected when a 'project' block is configured).
|
|
30
|
+
--force Release even when no commits or no bump-worthy commits exist
|
|
31
|
+
since the last tag. Defaults to patch when --bump is not given;
|
|
32
|
+
use --bump=X to release at a different level.
|
|
29
33
|
--no-git-checks, -n Skip the clean-working-tree check
|
|
30
|
-
--only=name1,name2 Only process the named
|
|
34
|
+
--only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;
|
|
35
|
+
rejected when a 'project' block is configured)
|
|
36
|
+
--with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
|
|
37
|
+
(docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
|
|
38
|
+
Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
|
|
31
39
|
--help Show this help message
|
|
32
40
|
`);
|
|
33
41
|
}
|
|
@@ -40,7 +48,9 @@ const prepareFlagSchema = {
|
|
|
40
48
|
short: "-n"
|
|
41
49
|
},
|
|
42
50
|
bump: { long: "--bump", type: "string" },
|
|
51
|
+
setVersion: { long: "--set-version", type: "string" },
|
|
43
52
|
only: { long: "--only", type: "string" },
|
|
53
|
+
withReleaseNotes: { long: "--with-release-notes", type: "boolean" },
|
|
44
54
|
help: { long: "--help", type: "boolean", short: "-h" }
|
|
45
55
|
};
|
|
46
56
|
function parseArgs(argv) {
|
|
@@ -62,19 +72,33 @@ function parseArgs(argv) {
|
|
|
62
72
|
}
|
|
63
73
|
bumpOverride = flags.bump;
|
|
64
74
|
}
|
|
75
|
+
let setVersion;
|
|
76
|
+
if (flags.setVersion !== void 0) {
|
|
77
|
+
if (!CANONICAL_SEMVER_PATTERN.test(flags.setVersion)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Invalid --set-version value "${flags.setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
setVersion = flags.setVersion;
|
|
83
|
+
}
|
|
65
84
|
let only;
|
|
66
85
|
if (flags.only !== void 0) {
|
|
67
86
|
only = flags.only.split(",");
|
|
68
87
|
}
|
|
69
|
-
if (
|
|
70
|
-
throw new Error("--
|
|
88
|
+
if (setVersion !== void 0 && bumpOverride !== void 0) {
|
|
89
|
+
throw new Error("--set-version cannot be combined with --bump");
|
|
90
|
+
}
|
|
91
|
+
if (setVersion !== void 0 && flags.force) {
|
|
92
|
+
throw new Error("--set-version cannot be combined with --force");
|
|
71
93
|
}
|
|
72
94
|
return {
|
|
73
95
|
dryRun: flags.dryRun,
|
|
74
96
|
force: flags.force,
|
|
75
97
|
noGitChecks: flags.noGitChecks,
|
|
76
98
|
bumpOverride,
|
|
77
|
-
only
|
|
99
|
+
only,
|
|
100
|
+
setVersion,
|
|
101
|
+
withReleaseNotes: flags.withReleaseNotes
|
|
78
102
|
};
|
|
79
103
|
}
|
|
80
104
|
function writeReleaseTags(tags, dryRun) {
|
|
@@ -92,8 +116,10 @@ async function prepareCommand(argv) {
|
|
|
92
116
|
let noGitChecks;
|
|
93
117
|
let bumpOverride;
|
|
94
118
|
let only;
|
|
119
|
+
let setVersion;
|
|
120
|
+
let withReleaseNotes;
|
|
95
121
|
try {
|
|
96
|
-
({ dryRun, force, noGitChecks, bumpOverride, only } = parseArgs(argv));
|
|
122
|
+
({ dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv));
|
|
97
123
|
} catch (error) {
|
|
98
124
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
99
125
|
process.exit(1);
|
|
@@ -101,7 +127,9 @@ async function prepareCommand(argv) {
|
|
|
101
127
|
const options = {
|
|
102
128
|
dryRun,
|
|
103
129
|
force,
|
|
104
|
-
...bumpOverride === void 0 ? {} : { bumpOverride }
|
|
130
|
+
...bumpOverride === void 0 ? {} : { bumpOverride },
|
|
131
|
+
...setVersion === void 0 ? {} : { setVersion },
|
|
132
|
+
...withReleaseNotes ? { withReleaseNotes: true } : {}
|
|
105
133
|
};
|
|
106
134
|
if (dryRun) {
|
|
107
135
|
console.info("\n\u{1F50D} DRY RUN \u2014 no files will be modified\n");
|
|
@@ -123,26 +151,88 @@ async function prepareCommand(argv) {
|
|
|
123
151
|
process.exit(1);
|
|
124
152
|
}
|
|
125
153
|
if (discoveredPaths === void 0) {
|
|
126
|
-
|
|
127
|
-
console.error("Error: --only is only supported for monorepo configurations");
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
const config = mergeSinglePackageConfig(userConfig);
|
|
131
|
-
runAndReport(() => releasePrepare(config, options), dryRun);
|
|
154
|
+
runSinglePackageMode(userConfig, options, only, dryRun);
|
|
132
155
|
} else {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function runSinglePackageMode(userConfig, options, only, dryRun) {
|
|
160
|
+
if (only !== void 0) {
|
|
161
|
+
console.error("Error: --only is only supported for monorepo configurations");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (options.force && options.bumpOverride === void 0) {
|
|
165
|
+
console.error(
|
|
166
|
+
"Error: --force without --bump is only supported for monorepo configurations. Use --bump=major|minor|patch to set the level for a single-package release."
|
|
167
|
+
);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const config = mergeSinglePackageConfig(userConfig);
|
|
171
|
+
runAndReport(() => releasePrepare(config, options), dryRun);
|
|
172
|
+
}
|
|
173
|
+
function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun) {
|
|
174
|
+
let config;
|
|
175
|
+
try {
|
|
176
|
+
const rootPackage = readRootPackageVersion();
|
|
177
|
+
config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
if (setVersion !== void 0 && config.project !== void 0) {
|
|
183
|
+
console.error(
|
|
184
|
+
"Error: --set-version cannot be combined with a project release. --set-version operates on a single workspace; a project release rolls up every contributing workspace. To use --set-version, run on a config without a `project` block."
|
|
185
|
+
);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
if (only !== void 0 && config.project !== void 0) {
|
|
189
|
+
console.error(
|
|
190
|
+
"Error: --only cannot be combined with a project release. To release a single workspace, use a config without a `project` block, or run a full `prepare` (no --only) to include the project release."
|
|
191
|
+
);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
if (only !== void 0) {
|
|
195
|
+
const knownNames = config.workspaces.map((w) => w.dir);
|
|
196
|
+
for (const name of only) {
|
|
197
|
+
if (!knownNames.includes(name)) {
|
|
198
|
+
console.error(`Error: Unknown workspace "${name}". Known workspaces: ${knownNames.join(", ")}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const graph = buildDependencyGraph(config.workspaces);
|
|
203
|
+
const violations = validateOnlyExcludesStrandedDependents(config.workspaces, only, graph, (workspace) => {
|
|
204
|
+
const tagPrefixes = [
|
|
205
|
+
workspace.tagPrefix,
|
|
206
|
+
...workspace.legacyIdentities?.map((identity) => identity.tagPrefix) ?? []
|
|
207
|
+
];
|
|
208
|
+
const result = getCommitsSinceTarget(tagPrefixes, workspace.paths);
|
|
209
|
+
return { has: result.commits.length > 0, tag: result.tag };
|
|
210
|
+
});
|
|
211
|
+
if (violations !== void 0) {
|
|
212
|
+
console.error("Error: --only excludes packages with changes that would be stranded by the release.");
|
|
213
|
+
console.error("The following packages must be added to --only or have their dependencies removed:");
|
|
214
|
+
for (const violation of violations) {
|
|
215
|
+
const since = violation.tag ?? "the beginning";
|
|
216
|
+
console.error(` - ${violation.dir} (downstream of ${violation.downstreamOf}; has commits since ${since})`);
|
|
141
217
|
}
|
|
142
|
-
|
|
218
|
+
console.error("Alternatively, run `release-kit prepare` without --only to release everything.");
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
config.workspaces = config.workspaces.filter((w) => only.includes(w.dir));
|
|
222
|
+
}
|
|
223
|
+
if (setVersion !== void 0) {
|
|
224
|
+
if (only === void 0) {
|
|
225
|
+
console.error("Error: --set-version requires --only in monorepo mode");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
if (config.workspaces.length !== 1) {
|
|
229
|
+
console.error(
|
|
230
|
+
`Error: --set-version requires --only to match exactly one workspace; matched ${config.workspaces.length}`
|
|
231
|
+
);
|
|
232
|
+
process.exit(1);
|
|
143
233
|
}
|
|
144
|
-
runAndReport(() => releasePrepareMono(config, options), dryRun);
|
|
145
234
|
}
|
|
235
|
+
runAndReport(() => releasePrepareMono(config, options), dryRun);
|
|
146
236
|
}
|
|
147
237
|
async function loadAndValidateConfig() {
|
|
148
238
|
let rawConfig;
|
|
@@ -173,7 +263,7 @@ function runAndReport(execute, dryRun) {
|
|
|
173
263
|
try {
|
|
174
264
|
result = execute();
|
|
175
265
|
} catch (error) {
|
|
176
|
-
console.error(
|
|
266
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
177
267
|
process.exit(1);
|
|
178
268
|
}
|
|
179
269
|
process.stdout.write(reportPrepare(result) + "\n");
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { UndeclaredTagPrefix } from './detectUndeclaredTagPrefixes.ts';
|
|
2
|
+
export interface TagPrefixPreviewRow {
|
|
3
|
+
workspacePath: string;
|
|
4
|
+
dir: string;
|
|
5
|
+
derivedPrefix: string | null;
|
|
6
|
+
derivationError: string | null;
|
|
7
|
+
derivedTagCount: number;
|
|
8
|
+
legacyEntries: LegacyTagPrefixEntry[];
|
|
9
|
+
}
|
|
10
|
+
export interface LegacyTagPrefixEntry {
|
|
11
|
+
prefix: string;
|
|
12
|
+
tagCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RetiredPackagePreviewEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
tagPrefix: string;
|
|
17
|
+
successor?: string;
|
|
18
|
+
tagCount: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TagPrefixCollision {
|
|
21
|
+
tagPrefix: string;
|
|
22
|
+
workspacePaths: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface TagPrefixPreview {
|
|
25
|
+
workspaces: TagPrefixPreviewRow[];
|
|
26
|
+
collisions: TagPrefixCollision[];
|
|
27
|
+
undeclaredCandidates: UndeclaredTagPrefix[];
|
|
28
|
+
retiredPackages: RetiredPackagePreviewEntry[];
|
|
29
|
+
}
|
|
30
|
+
export declare function previewTagPrefixes(): Promise<TagPrefixPreview>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
4
|
+
import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
|
|
5
|
+
import { discoverWorkspaces } from "./discoverWorkspaces.js";
|
|
6
|
+
import { loadConfig } from "./loadConfig.js";
|
|
7
|
+
import { validateConfig } from "./validateConfig.js";
|
|
8
|
+
async function previewTagPrefixes() {
|
|
9
|
+
const workspacePaths = await discoverWorkspaces() ?? [];
|
|
10
|
+
const userConfig = await loadUserConfig();
|
|
11
|
+
const overridesByDir = buildOverrideMap(userConfig);
|
|
12
|
+
const workspaces = [];
|
|
13
|
+
for (const workspacePath of workspacePaths) {
|
|
14
|
+
workspaces.push(buildPreviewRow(workspacePath, overridesByDir));
|
|
15
|
+
}
|
|
16
|
+
const retiredPackages = buildRetiredPreviewEntries(userConfig?.retiredPackages ?? []);
|
|
17
|
+
const collisions = detectCollisions(workspaces);
|
|
18
|
+
const knownPrefixes = collectKnownPrefixes(workspaces, retiredPackages);
|
|
19
|
+
const undeclaredCandidates = detectUndeclaredTagPrefixes(knownPrefixes);
|
|
20
|
+
return { workspaces, collisions, undeclaredCandidates, retiredPackages };
|
|
21
|
+
}
|
|
22
|
+
async function loadUserConfig() {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = await loadConfig();
|
|
26
|
+
} catch {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
if (raw === void 0) return void 0;
|
|
30
|
+
const { config, errors } = validateConfig(raw);
|
|
31
|
+
return errors.length === 0 ? config : void 0;
|
|
32
|
+
}
|
|
33
|
+
function buildOverrideMap(userConfig) {
|
|
34
|
+
const map = /* @__PURE__ */ new Map();
|
|
35
|
+
if (userConfig?.workspaces === void 0) return map;
|
|
36
|
+
for (const entry of userConfig.workspaces) {
|
|
37
|
+
if (entry.legacyIdentities !== void 0) {
|
|
38
|
+
map.set(entry.dir, entry.legacyIdentities);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
function buildPreviewRow(workspacePath, overridesByDir) {
|
|
44
|
+
const dir = basename(workspacePath);
|
|
45
|
+
let derivedPrefix = null;
|
|
46
|
+
let derivationError = null;
|
|
47
|
+
try {
|
|
48
|
+
derivedPrefix = deriveWorkspaceConfig(workspacePath).tagPrefix;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
derivationError = error instanceof Error ? error.message : String(error);
|
|
51
|
+
}
|
|
52
|
+
const derivedTagCount = derivedPrefix === null ? 0 : countTagsMatching(derivedPrefix);
|
|
53
|
+
const declaredIdentities = overridesByDir.get(dir) ?? [];
|
|
54
|
+
const legacyEntries = declaredIdentities.map((identity) => ({
|
|
55
|
+
prefix: identity.tagPrefix,
|
|
56
|
+
tagCount: countTagsMatching(identity.tagPrefix)
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
59
|
+
workspacePath,
|
|
60
|
+
dir,
|
|
61
|
+
derivedPrefix,
|
|
62
|
+
derivationError,
|
|
63
|
+
derivedTagCount,
|
|
64
|
+
legacyEntries
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function countTagsMatching(prefix) {
|
|
68
|
+
try {
|
|
69
|
+
const output = execFileSync("git", ["tag", "--list", `${prefix}*`], {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
72
|
+
});
|
|
73
|
+
return output.split("\n").filter((line) => line.trim() !== "").length;
|
|
74
|
+
} catch {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function detectCollisions(rows) {
|
|
79
|
+
const pathsByPrefix = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
if (row.derivedPrefix === null) continue;
|
|
82
|
+
const existing = pathsByPrefix.get(row.derivedPrefix);
|
|
83
|
+
if (existing === void 0) {
|
|
84
|
+
pathsByPrefix.set(row.derivedPrefix, [row.workspacePath]);
|
|
85
|
+
} else {
|
|
86
|
+
existing.push(row.workspacePath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const collisions = [];
|
|
90
|
+
for (const [tagPrefix, workspacePaths] of pathsByPrefix) {
|
|
91
|
+
if (workspacePaths.length > 1) {
|
|
92
|
+
collisions.push({ tagPrefix, workspacePaths });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return collisions;
|
|
96
|
+
}
|
|
97
|
+
function collectKnownPrefixes(rows, retiredPackages) {
|
|
98
|
+
const known = /* @__PURE__ */ new Set();
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
if (row.derivedPrefix !== null) known.add(row.derivedPrefix);
|
|
101
|
+
for (const entry of row.legacyEntries) {
|
|
102
|
+
known.add(entry.prefix);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const retired of retiredPackages) {
|
|
106
|
+
known.add(retired.tagPrefix);
|
|
107
|
+
}
|
|
108
|
+
return [...known];
|
|
109
|
+
}
|
|
110
|
+
function buildRetiredPreviewEntries(retiredPackages) {
|
|
111
|
+
return retiredPackages.map((retired) => ({
|
|
112
|
+
name: retired.name,
|
|
113
|
+
tagPrefix: retired.tagPrefix,
|
|
114
|
+
tagCount: countTagsMatching(retired.tagPrefix),
|
|
115
|
+
...retired.successor !== void 0 ? { successor: retired.successor } : {}
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
previewTagPrefixes
|
|
120
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { PropagationSource, ReleaseType } from './types.ts';
|
|
|
3
3
|
export interface ReleaseEntry {
|
|
4
4
|
releaseType: ReleaseType;
|
|
5
5
|
propagatedFrom?: PropagationSource[];
|
|
6
|
+
newVersionOverride?: string;
|
|
6
7
|
}
|
|
7
8
|
export type CurrentVersions = Map<string, string>;
|
|
8
9
|
export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph, currentVersions: CurrentVersions): Map<string, ReleaseEntry>;
|
|
@@ -24,7 +24,7 @@ function propagateBumps(directBumps, graph, currentVersions) {
|
|
|
24
24
|
if (currentVersion === void 0 || entry === void 0) {
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
|
-
const newVersion = bumpVersion(currentVersion, entry.releaseType);
|
|
27
|
+
const newVersion = entry.newVersionOverride ?? bumpVersion(currentVersion, entry.releaseType);
|
|
28
28
|
const dependents = graph.dependentsOf.get(packageName);
|
|
29
29
|
if (dependents === void 0) {
|
|
30
30
|
continue;
|
package/dist/esm/publish.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { PackageManager } from './detectPackageManager.ts';
|
|
|
2
2
|
import type { ResolvedTag } from './resolveReleaseTags.ts';
|
|
3
3
|
export interface PublishOptions {
|
|
4
4
|
dryRun: boolean;
|
|
5
|
-
noGitChecks: boolean;
|
|
6
5
|
provenance: boolean;
|
|
7
6
|
}
|
|
8
7
|
export declare function publishPackage(resolvedTag: ResolvedTag, packageManager: PackageManager, options: PublishOptions): void;
|