@ucdjs/release-scripts 0.1.0-beta.1 → 0.1.0-beta.11
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/dist/eta-Boh7yPZi.mjs +477 -0
- package/dist/index.d.mts +81 -43
- package/dist/index.mjs +546 -439
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { getCommits } from "commit-parser";
|
|
3
|
-
import createDebug from "debug";
|
|
1
|
+
import { t as Eta } from "./eta-Boh7yPZi.mjs";
|
|
4
2
|
import farver from "farver";
|
|
3
|
+
import { getCommits } from "commit-parser";
|
|
4
|
+
import process from "node:process";
|
|
5
5
|
import { exec } from "tinyexec";
|
|
6
|
-
import {
|
|
6
|
+
import { dedent } from "@luxass/utils";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
8
9
|
import prompts from "prompts";
|
|
9
10
|
|
|
10
|
-
//#region src/
|
|
11
|
-
function
|
|
12
|
-
const debug$2 = createDebug(namespace);
|
|
13
|
-
if (debug$2.enabled) return debug$2;
|
|
14
|
-
}
|
|
11
|
+
//#region src/publish.ts
|
|
12
|
+
function publish(_options) {}
|
|
15
13
|
|
|
16
14
|
//#endregion
|
|
17
15
|
//#region src/utils.ts
|
|
18
|
-
const globalOptions = {
|
|
16
|
+
const globalOptions = {
|
|
17
|
+
dryRun: false,
|
|
18
|
+
verbose: false
|
|
19
|
+
};
|
|
20
|
+
const isCI = typeof process.env.CI === "string" && process.env.CI !== "" && process.env.CI.toLowerCase() !== "false";
|
|
21
|
+
const logger = {
|
|
22
|
+
info: (...args) => {
|
|
23
|
+
console.info(farver.cyan("[info]:"), ...args);
|
|
24
|
+
},
|
|
25
|
+
debug: (...args) => {
|
|
26
|
+
console.debug(farver.gray("[debug]:"), ...args);
|
|
27
|
+
},
|
|
28
|
+
warn: (...args) => {
|
|
29
|
+
console.warn(farver.yellow("[warn]:"), ...args);
|
|
30
|
+
},
|
|
31
|
+
error: (...args) => {
|
|
32
|
+
console.error(farver.red("[error]:"), ...args);
|
|
33
|
+
},
|
|
34
|
+
log: (...args) => {
|
|
35
|
+
if (!globalOptions.verbose) return;
|
|
36
|
+
console.log(...args);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
19
39
|
async function run(bin, args, opts = {}) {
|
|
20
40
|
return exec(bin, args, {
|
|
21
41
|
throwOnError: true,
|
|
@@ -27,19 +47,49 @@ async function run(bin, args, opts = {}) {
|
|
|
27
47
|
});
|
|
28
48
|
}
|
|
29
49
|
async function dryRun(bin, args, opts) {
|
|
30
|
-
return
|
|
50
|
+
return logger.log(farver.blue(`[dryrun] ${bin} ${args.join(" ")}`), opts || "");
|
|
31
51
|
}
|
|
32
52
|
const runIfNotDry = globalOptions.dryRun ? dryRun : run;
|
|
53
|
+
function exitWithError(message, hint) {
|
|
54
|
+
logger.error(farver.bold(message));
|
|
55
|
+
if (hint) console.error(farver.gray(` ${hint}`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
function normalizeSharedOptions(options) {
|
|
59
|
+
const { workspaceRoot = process.cwd(), githubToken = "", verbose = false, repo: fullRepo, packages = true, prompts: prompts$1 = {
|
|
60
|
+
packages: true,
|
|
61
|
+
versions: true
|
|
62
|
+
},...rest } = options;
|
|
63
|
+
globalOptions.verbose = verbose;
|
|
64
|
+
if (!githubToken.trim()) exitWithError("GitHub token is required", "Set GITHUB_TOKEN environment variable or pass it in options");
|
|
65
|
+
if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) exitWithError("Repository (repo) is required", "Specify the repository in 'owner/repo' format (e.g., 'octocat/hello-world')");
|
|
66
|
+
const [owner, repo] = fullRepo.split("/");
|
|
67
|
+
if (!owner || !repo) exitWithError(`Invalid repo format: "${fullRepo}"`, "Expected format: \"owner/repo\" (e.g., \"octocat/hello-world\")");
|
|
68
|
+
return {
|
|
69
|
+
...rest,
|
|
70
|
+
packages,
|
|
71
|
+
prompts: prompts$1,
|
|
72
|
+
workspaceRoot,
|
|
73
|
+
githubToken,
|
|
74
|
+
owner,
|
|
75
|
+
repo,
|
|
76
|
+
verbose
|
|
77
|
+
};
|
|
78
|
+
}
|
|
33
79
|
|
|
34
80
|
//#endregion
|
|
35
81
|
//#region src/commits.ts
|
|
36
|
-
const debug$1 = createDebugger("ucdjs:release-scripts:commits");
|
|
37
82
|
async function getLastPackageTag(packageName, workspaceRoot) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
83
|
+
try {
|
|
84
|
+
const { stdout } = await run("git", ["tag", "--list"], { nodeOptions: {
|
|
85
|
+
cwd: workspaceRoot,
|
|
86
|
+
stdio: "pipe"
|
|
87
|
+
} });
|
|
88
|
+
return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse().find((tag) => tag.startsWith(`${packageName}@`));
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logger.warn(`Failed to get tags for package ${packageName}: ${err.message}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
43
93
|
}
|
|
44
94
|
function determineHighestBump(commits) {
|
|
45
95
|
if (commits.length === 0) return "none";
|
|
@@ -52,21 +102,43 @@ function determineHighestBump(commits) {
|
|
|
52
102
|
}
|
|
53
103
|
return highestBump;
|
|
54
104
|
}
|
|
55
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Retrieves commits that affect a specific workspace package since its last tag.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} workspaceRoot - The root directory of the workspace.
|
|
109
|
+
* @param {WorkspacePackage} pkg - The workspace package to analyze.
|
|
110
|
+
* @returns {Promise<GitCommit[]>} A promise that resolves to an array of GitCommit objects affecting the package.
|
|
111
|
+
*/
|
|
112
|
+
async function getCommitsForWorkspacePackage(workspaceRoot, pkg) {
|
|
56
113
|
const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
|
|
57
114
|
const allCommits = getCommits({
|
|
58
115
|
from: lastTag,
|
|
59
|
-
to: "HEAD"
|
|
116
|
+
to: "HEAD",
|
|
117
|
+
cwd: workspaceRoot
|
|
118
|
+
});
|
|
119
|
+
logger.log(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
|
|
120
|
+
const touchedCommitHashes = getCommits({
|
|
121
|
+
from: lastTag,
|
|
122
|
+
to: "HEAD",
|
|
123
|
+
cwd: workspaceRoot,
|
|
124
|
+
folder: pkg.path
|
|
60
125
|
});
|
|
61
|
-
debug$1?.(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
|
|
62
|
-
const touchedCommitHashes = await getCommitsTouchingPackage(lastTag || "HEAD", "HEAD", pkg.path, workspaceRoot);
|
|
63
126
|
const touchedSet = new Set(touchedCommitHashes);
|
|
64
|
-
const packageCommits = allCommits.filter((commit) => touchedSet.has(commit
|
|
65
|
-
|
|
127
|
+
const packageCommits = allCommits.filter((commit) => touchedSet.has(commit));
|
|
128
|
+
logger.log(`${packageCommits.length} commits affect ${pkg.name}`);
|
|
66
129
|
return packageCommits;
|
|
67
130
|
}
|
|
68
|
-
async function
|
|
69
|
-
|
|
131
|
+
async function getWorkspacePackageCommits(workspaceRoot, packages) {
|
|
132
|
+
const changedPackages = /* @__PURE__ */ new Map();
|
|
133
|
+
const promises = packages.map(async (pkg) => {
|
|
134
|
+
return {
|
|
135
|
+
pkgName: pkg.name,
|
|
136
|
+
commits: await getCommitsForWorkspacePackage(workspaceRoot, pkg)
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
const results = await Promise.all(promises);
|
|
140
|
+
for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
|
|
141
|
+
return changedPackages;
|
|
70
142
|
}
|
|
71
143
|
function determineBumpType(commit) {
|
|
72
144
|
if (commit.isBreaking) return "major";
|
|
@@ -86,148 +158,6 @@ function determineBumpType(commit) {
|
|
|
86
158
|
default: return "none";
|
|
87
159
|
}
|
|
88
160
|
}
|
|
89
|
-
async function getCommitsTouchingPackage(from, to, packagePath, workspaceRoot) {
|
|
90
|
-
try {
|
|
91
|
-
const { stdout } = await run("git", [
|
|
92
|
-
"log",
|
|
93
|
-
"--pretty=format:%h",
|
|
94
|
-
from === "HEAD" ? "HEAD" : `${from}...${to}`,
|
|
95
|
-
"--",
|
|
96
|
-
packagePath
|
|
97
|
-
], { nodeOptions: {
|
|
98
|
-
cwd: workspaceRoot,
|
|
99
|
-
stdio: "pipe"
|
|
100
|
-
} });
|
|
101
|
-
return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
debug$1?.(`Error getting commits touching package: ${error}`);
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/validation.ts
|
|
110
|
-
/**
|
|
111
|
-
* Validation utilities for release scripts
|
|
112
|
-
*/
|
|
113
|
-
function isValidSemver(version) {
|
|
114
|
-
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
|
|
115
|
-
}
|
|
116
|
-
function validateSemver(version) {
|
|
117
|
-
if (!isValidSemver(version)) throw new Error(`Invalid semver version: ${version}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region src/version.ts
|
|
122
|
-
/**
|
|
123
|
-
* Calculate the new version based on current version and bump type
|
|
124
|
-
* Pure function - no side effects, easily testable
|
|
125
|
-
*/
|
|
126
|
-
function calculateNewVersion(currentVersion, bump) {
|
|
127
|
-
if (bump === "none") return currentVersion;
|
|
128
|
-
validateSemver(currentVersion);
|
|
129
|
-
const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
130
|
-
if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
|
|
131
|
-
const [, major, minor, patch, suffix] = match;
|
|
132
|
-
let newMajor = Number.parseInt(major, 10);
|
|
133
|
-
let newMinor = Number.parseInt(minor, 10);
|
|
134
|
-
let newPatch = Number.parseInt(patch, 10);
|
|
135
|
-
switch (bump) {
|
|
136
|
-
case "major":
|
|
137
|
-
newMajor += 1;
|
|
138
|
-
newMinor = 0;
|
|
139
|
-
newPatch = 0;
|
|
140
|
-
break;
|
|
141
|
-
case "minor":
|
|
142
|
-
newMinor += 1;
|
|
143
|
-
newPatch = 0;
|
|
144
|
-
break;
|
|
145
|
-
case "patch":
|
|
146
|
-
newPatch += 1;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
return `${newMajor}.${newMinor}.${newPatch}`;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Create a version update object
|
|
153
|
-
*/
|
|
154
|
-
function createVersionUpdate(pkg, bump, hasDirectChanges) {
|
|
155
|
-
const newVersion = calculateNewVersion(pkg.version, bump);
|
|
156
|
-
return {
|
|
157
|
-
package: pkg,
|
|
158
|
-
currentVersion: pkg.version,
|
|
159
|
-
newVersion,
|
|
160
|
-
bumpType: bump,
|
|
161
|
-
hasDirectChanges
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Update a package.json file with new version and dependency versions
|
|
166
|
-
*/
|
|
167
|
-
async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
|
|
168
|
-
const packageJsonPath = join(pkg.path, "package.json");
|
|
169
|
-
const content = await readFile(packageJsonPath, "utf-8");
|
|
170
|
-
const packageJson = JSON.parse(content);
|
|
171
|
-
packageJson.version = newVersion;
|
|
172
|
-
for (const [depName, depVersion] of dependencyUpdates) {
|
|
173
|
-
if (packageJson.dependencies?.[depName]) {
|
|
174
|
-
if (packageJson.dependencies[depName] === "workspace:*") continue;
|
|
175
|
-
packageJson.dependencies[depName] = `^${depVersion}`;
|
|
176
|
-
}
|
|
177
|
-
if (packageJson.devDependencies?.[depName]) {
|
|
178
|
-
if (packageJson.devDependencies[depName] === "workspace:*") continue;
|
|
179
|
-
packageJson.devDependencies[depName] = `^${depVersion}`;
|
|
180
|
-
}
|
|
181
|
-
if (packageJson.peerDependencies?.[depName]) {
|
|
182
|
-
if (packageJson.peerDependencies[depName] === "workspace:*") continue;
|
|
183
|
-
packageJson.peerDependencies[depName] = `^${depVersion}`;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Get all dependency updates needed for a package
|
|
190
|
-
*/
|
|
191
|
-
function getDependencyUpdates(pkg, allUpdates) {
|
|
192
|
-
const updates = /* @__PURE__ */ new Map();
|
|
193
|
-
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
194
|
-
for (const dep of allDeps) {
|
|
195
|
-
const update = allUpdates.find((u) => u.package.name === dep);
|
|
196
|
-
if (update) updates.set(dep, update.newVersion);
|
|
197
|
-
}
|
|
198
|
-
return updates;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
//#endregion
|
|
202
|
-
//#region src/dependencies.ts
|
|
203
|
-
/**
|
|
204
|
-
* Pure function: Determine which packages need updates due to dependency changes
|
|
205
|
-
*
|
|
206
|
-
* When a package is updated, all packages that depend on it should also be updated.
|
|
207
|
-
* This function calculates which additional packages need patch bumps.
|
|
208
|
-
*
|
|
209
|
-
* @param updateOrder - Packages in topological order with their dependency levels
|
|
210
|
-
* @param directUpdates - Packages with direct code changes
|
|
211
|
-
* @returns All updates including dependent packages
|
|
212
|
-
*/
|
|
213
|
-
function createDependentUpdates(updateOrder, directUpdates) {
|
|
214
|
-
const allUpdates = [...directUpdates];
|
|
215
|
-
const updatedPackages = new Set(directUpdates.map((u) => u.package.name));
|
|
216
|
-
for (const { package: pkg } of updateOrder) {
|
|
217
|
-
if (updatedPackages.has(pkg.name)) continue;
|
|
218
|
-
if (hasUpdatedDependencies(pkg, updatedPackages)) {
|
|
219
|
-
allUpdates.push(createVersionUpdate(pkg, "patch", false));
|
|
220
|
-
updatedPackages.add(pkg.name);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return allUpdates;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Pure function: Check if a package has any updated dependencies
|
|
227
|
-
*/
|
|
228
|
-
function hasUpdatedDependencies(pkg, updatedPackages) {
|
|
229
|
-
return [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies].some((dep) => updatedPackages.has(dep));
|
|
230
|
-
}
|
|
231
161
|
|
|
232
162
|
//#endregion
|
|
233
163
|
//#region src/git.ts
|
|
@@ -244,7 +174,7 @@ async function isWorkingDirectoryClean(workspaceRoot) {
|
|
|
244
174
|
} })).stdout.trim() !== "") return false;
|
|
245
175
|
return true;
|
|
246
176
|
} catch (err) {
|
|
247
|
-
|
|
177
|
+
logger.error("Error checking git status:", err);
|
|
248
178
|
return false;
|
|
249
179
|
}
|
|
250
180
|
}
|
|
@@ -297,12 +227,20 @@ async function pullLatestChanges(branch, workspaceRoot) {
|
|
|
297
227
|
* @param workspaceRoot - The root directory of the workspace
|
|
298
228
|
*/
|
|
299
229
|
async function createBranch(branch, base, workspaceRoot) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
"
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
230
|
+
try {
|
|
231
|
+
logger.info(`Creating branch: ${farver.green(branch)} from ${farver.cyan(base)}`);
|
|
232
|
+
await runIfNotDry("git", [
|
|
233
|
+
"checkout",
|
|
234
|
+
"-b",
|
|
235
|
+
branch,
|
|
236
|
+
base
|
|
237
|
+
], { nodeOptions: {
|
|
238
|
+
cwd: workspaceRoot,
|
|
239
|
+
stdio: "pipe"
|
|
240
|
+
} });
|
|
241
|
+
} catch {
|
|
242
|
+
exitWithError(`Failed to create branch: ${branch}`, `Make sure the branch doesn't already exist and you have a clean working directory`);
|
|
243
|
+
}
|
|
306
244
|
}
|
|
307
245
|
/**
|
|
308
246
|
* Checkout a git branch
|
|
@@ -312,7 +250,11 @@ async function createBranch(branch, base, workspaceRoot) {
|
|
|
312
250
|
*/
|
|
313
251
|
async function checkoutBranch(branch, workspaceRoot) {
|
|
314
252
|
try {
|
|
315
|
-
|
|
253
|
+
logger.info(`Switching to branch: ${farver.green(branch)}`);
|
|
254
|
+
await run("git", ["checkout", branch], { nodeOptions: {
|
|
255
|
+
cwd: workspaceRoot,
|
|
256
|
+
stdio: "pipe"
|
|
257
|
+
} });
|
|
316
258
|
return true;
|
|
317
259
|
} catch {
|
|
318
260
|
return false;
|
|
@@ -339,7 +281,36 @@ async function getCurrentBranch(workspaceRoot) {
|
|
|
339
281
|
* @param workspaceRoot - The root directory of the workspace
|
|
340
282
|
*/
|
|
341
283
|
async function rebaseBranch(ontoBranch, workspaceRoot) {
|
|
342
|
-
|
|
284
|
+
try {
|
|
285
|
+
logger.info(`Rebasing onto: ${farver.cyan(ontoBranch)}`);
|
|
286
|
+
await run("git", ["rebase", ontoBranch], { nodeOptions: {
|
|
287
|
+
cwd: workspaceRoot,
|
|
288
|
+
stdio: "pipe"
|
|
289
|
+
} });
|
|
290
|
+
} catch {
|
|
291
|
+
exitWithError(`Failed to rebase onto: ${ontoBranch}`, `You may have merge conflicts. Run 'git rebase --abort' to undo the rebase`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Check if local branch is ahead of remote (has commits to push)
|
|
296
|
+
* @param branch - The branch name to check
|
|
297
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
298
|
+
* @returns Promise resolving to true if local is ahead, false otherwise
|
|
299
|
+
*/
|
|
300
|
+
async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
301
|
+
try {
|
|
302
|
+
const result = await run("git", [
|
|
303
|
+
"rev-list",
|
|
304
|
+
`origin/${branch}..${branch}`,
|
|
305
|
+
"--count"
|
|
306
|
+
], { nodeOptions: {
|
|
307
|
+
cwd: workspaceRoot,
|
|
308
|
+
stdio: "pipe"
|
|
309
|
+
} });
|
|
310
|
+
return Number.parseInt(result.stdout.trim(), 10) > 0;
|
|
311
|
+
} catch {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
343
314
|
}
|
|
344
315
|
/**
|
|
345
316
|
* Check if there are any changes to commit (staged or unstaged)
|
|
@@ -359,14 +330,25 @@ async function hasChangesToCommit(workspaceRoot) {
|
|
|
359
330
|
* @returns Promise resolving to true if commit was made, false if there were no changes
|
|
360
331
|
*/
|
|
361
332
|
async function commitChanges(message, workspaceRoot) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
333
|
+
try {
|
|
334
|
+
await run("git", ["add", "."], { nodeOptions: {
|
|
335
|
+
cwd: workspaceRoot,
|
|
336
|
+
stdio: "pipe"
|
|
337
|
+
} });
|
|
338
|
+
if (!await hasChangesToCommit(workspaceRoot)) return false;
|
|
339
|
+
logger.info(`Committing changes: ${farver.dim(message)}`);
|
|
340
|
+
await run("git", [
|
|
341
|
+
"commit",
|
|
342
|
+
"-m",
|
|
343
|
+
message
|
|
344
|
+
], { nodeOptions: {
|
|
345
|
+
cwd: workspaceRoot,
|
|
346
|
+
stdio: "pipe"
|
|
347
|
+
} });
|
|
348
|
+
return true;
|
|
349
|
+
} catch {
|
|
350
|
+
exitWithError(`Failed to commit changes`, `Make sure you have git configured properly with user.name and user.email`);
|
|
351
|
+
}
|
|
370
352
|
}
|
|
371
353
|
/**
|
|
372
354
|
* Push branch to remote
|
|
@@ -377,48 +359,42 @@ async function commitChanges(message, workspaceRoot) {
|
|
|
377
359
|
* @param options.forceWithLease - Force push with safety check (won't overwrite unexpected changes)
|
|
378
360
|
*/
|
|
379
361
|
async function pushBranch(branch, workspaceRoot, options) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
362
|
+
try {
|
|
363
|
+
const args = [
|
|
364
|
+
"push",
|
|
365
|
+
"origin",
|
|
366
|
+
branch
|
|
367
|
+
];
|
|
368
|
+
if (options?.forceWithLease) {
|
|
369
|
+
args.push("--force-with-lease");
|
|
370
|
+
logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`);
|
|
371
|
+
} else if (options?.force) {
|
|
372
|
+
args.push("--force");
|
|
373
|
+
logger.info(`Force pushing branch: ${farver.green(branch)}`);
|
|
374
|
+
} else logger.info(`Pushing branch: ${farver.green(branch)}`);
|
|
375
|
+
await run("git", args, { nodeOptions: {
|
|
376
|
+
cwd: workspaceRoot,
|
|
377
|
+
stdio: "pipe"
|
|
378
|
+
} });
|
|
379
|
+
} catch {
|
|
380
|
+
exitWithError(`Failed to push branch: ${branch}`, `Make sure you have permission to push to the remote repository`);
|
|
381
|
+
}
|
|
388
382
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
lines.push("");
|
|
398
|
-
const directChanges = updates.filter((u) => u.hasDirectChanges);
|
|
399
|
-
const dependencyUpdates = updates.filter((u) => !u.hasDirectChanges);
|
|
400
|
-
if (directChanges.length > 0) {
|
|
401
|
-
lines.push("### Direct Changes");
|
|
402
|
-
lines.push("");
|
|
403
|
-
for (const update of directChanges) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`);
|
|
404
|
-
lines.push("");
|
|
405
|
-
}
|
|
406
|
-
if (dependencyUpdates.length > 0) {
|
|
407
|
-
lines.push("### Dependency Updates");
|
|
408
|
-
lines.push("");
|
|
409
|
-
for (const update of dependencyUpdates) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (dependencies changed)`);
|
|
410
|
-
lines.push("");
|
|
411
|
-
}
|
|
412
|
-
lines.push("---");
|
|
413
|
-
lines.push("");
|
|
414
|
-
lines.push("This release PR was automatically generated.");
|
|
415
|
-
return lines.join("\n");
|
|
383
|
+
async function getDefaultBranch() {
|
|
384
|
+
try {
|
|
385
|
+
const match = (await run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { nodeOptions: { stdio: "pipe" } })).stdout.trim().match(/^refs\/remotes\/origin\/(.+)$/);
|
|
386
|
+
if (match && match[1]) return match[1];
|
|
387
|
+
return "main";
|
|
388
|
+
} catch {
|
|
389
|
+
return "main";
|
|
390
|
+
}
|
|
416
391
|
}
|
|
417
392
|
|
|
418
393
|
//#endregion
|
|
419
394
|
//#region src/github.ts
|
|
420
395
|
async function getExistingPullRequest({ owner, repo, branch, githubToken }) {
|
|
421
396
|
try {
|
|
397
|
+
logger.debug(`Requesting pull request for branch: ${branch} (url: https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch})`);
|
|
422
398
|
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch}`, { headers: {
|
|
423
399
|
Accept: "application/vnd.github.v3+json",
|
|
424
400
|
Authorization: `token ${githubToken}`
|
|
@@ -435,10 +411,10 @@ async function getExistingPullRequest({ owner, repo, branch, githubToken }) {
|
|
|
435
411
|
draft: firstPullRequest.draft,
|
|
436
412
|
html_url: firstPullRequest.html_url
|
|
437
413
|
};
|
|
438
|
-
|
|
414
|
+
logger.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
|
|
439
415
|
return pullRequest;
|
|
440
416
|
} catch (err) {
|
|
441
|
-
|
|
417
|
+
logger.error("Error fetching pull request:", err);
|
|
442
418
|
return null;
|
|
443
419
|
}
|
|
444
420
|
}
|
|
@@ -456,6 +432,7 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
|
|
|
456
432
|
head,
|
|
457
433
|
base
|
|
458
434
|
};
|
|
435
|
+
logger.debug(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${url})`);
|
|
459
436
|
const res = await fetch(url, {
|
|
460
437
|
method,
|
|
461
438
|
headers: {
|
|
@@ -468,7 +445,7 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
|
|
|
468
445
|
const pr = await res.json();
|
|
469
446
|
if (typeof pr !== "object" || pr === null || !("number" in pr) || typeof pr.number !== "number" || !("title" in pr) || typeof pr.title !== "string" || !("body" in pr) || typeof pr.body !== "string" || !("draft" in pr) || typeof pr.draft !== "boolean" || !("html_url" in pr) || typeof pr.html_url !== "string") throw new TypeError("Pull request data validation failed");
|
|
470
447
|
const action = isUpdate ? "Updated" : "Created";
|
|
471
|
-
|
|
448
|
+
logger.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
|
|
472
449
|
return {
|
|
473
450
|
number: pr.number,
|
|
474
451
|
title: pr.title,
|
|
@@ -477,30 +454,63 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
|
|
|
477
454
|
html_url: pr.html_url
|
|
478
455
|
};
|
|
479
456
|
} catch (err) {
|
|
480
|
-
|
|
457
|
+
logger.error(`Error upserting pull request:`, err);
|
|
481
458
|
throw err;
|
|
482
459
|
}
|
|
483
460
|
}
|
|
461
|
+
const defaultTemplate = dedent`
|
|
462
|
+
This PR was automatically generated by the release script.
|
|
463
|
+
|
|
464
|
+
The following packages have been prepared for release:
|
|
465
|
+
|
|
466
|
+
<% it.packages.forEach((pkg) => { %>
|
|
467
|
+
- **<%= pkg.name %>**: <%= pkg.currentVersion %> → <%= pkg.newVersion %> (<%= pkg.bumpType %>)
|
|
468
|
+
<% }) %>
|
|
469
|
+
|
|
470
|
+
Please review the changes and merge when ready.
|
|
471
|
+
|
|
472
|
+
For a more in-depth look at the changes, please refer to the individual package changelogs.
|
|
473
|
+
|
|
474
|
+
> [!NOTE]
|
|
475
|
+
> When this PR is merged, the release process will be triggered automatically, publishing the new package versions to the registry.
|
|
476
|
+
`;
|
|
477
|
+
function dedentString(str) {
|
|
478
|
+
const lines = str.split("\n");
|
|
479
|
+
const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity);
|
|
480
|
+
return lines.map((line) => minIndent === Infinity ? line : line.slice(minIndent)).join("\n").trim();
|
|
481
|
+
}
|
|
482
|
+
function generatePullRequestBody(updates, body) {
|
|
483
|
+
const eta = new Eta();
|
|
484
|
+
const bodyTemplate = body ? dedentString(body) : defaultTemplate;
|
|
485
|
+
return eta.renderString(bodyTemplate, { packages: updates.map((u) => ({
|
|
486
|
+
name: u.package.name,
|
|
487
|
+
currentVersion: u.currentVersion,
|
|
488
|
+
newVersion: u.newVersion,
|
|
489
|
+
bumpType: u.bumpType,
|
|
490
|
+
hasDirectChanges: u.hasDirectChanges
|
|
491
|
+
})) });
|
|
492
|
+
}
|
|
484
493
|
|
|
485
494
|
//#endregion
|
|
486
495
|
//#region src/prompts.ts
|
|
487
|
-
async function
|
|
496
|
+
async function selectPackagePrompt(packages) {
|
|
488
497
|
const response = await prompts({
|
|
489
498
|
type: "multiselect",
|
|
490
499
|
name: "selectedPackages",
|
|
491
500
|
message: "Select packages to release",
|
|
492
501
|
choices: packages.map((pkg) => ({
|
|
493
|
-
title: `${pkg.name} (${pkg.version})`,
|
|
502
|
+
title: `${pkg.name} (${farver.bold(pkg.version)})`,
|
|
494
503
|
value: pkg.name,
|
|
495
504
|
selected: true
|
|
496
505
|
})),
|
|
497
506
|
min: 1,
|
|
498
|
-
hint: "Space to select/deselect. Return to submit."
|
|
507
|
+
hint: "Space to select/deselect. Return to submit.",
|
|
508
|
+
instructions: false
|
|
499
509
|
});
|
|
500
|
-
if (!response.selectedPackages || response.selectedPackages.length === 0)
|
|
510
|
+
if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
|
|
501
511
|
return response.selectedPackages;
|
|
502
512
|
}
|
|
503
|
-
async function promptVersionOverride(
|
|
513
|
+
async function promptVersionOverride(pkg, workspaceRoot, currentVersion, suggestedVersion, suggestedBumpType) {
|
|
504
514
|
const choices = [{
|
|
505
515
|
title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
|
|
506
516
|
value: "suggested"
|
|
@@ -510,7 +520,7 @@ async function promptVersionOverride(packageName, currentVersion, suggestedVersi
|
|
|
510
520
|
"minor",
|
|
511
521
|
"major"
|
|
512
522
|
]) if (bumpType !== suggestedBumpType) {
|
|
513
|
-
const version =
|
|
523
|
+
const version = getNextVersion(currentVersion, bumpType);
|
|
514
524
|
choices.push({
|
|
515
525
|
title: `${bumpType}: ${version}`,
|
|
516
526
|
value: bumpType
|
|
@@ -523,7 +533,7 @@ async function promptVersionOverride(packageName, currentVersion, suggestedVersi
|
|
|
523
533
|
const response = await prompts([{
|
|
524
534
|
type: "select",
|
|
525
535
|
name: "choice",
|
|
526
|
-
message: `${
|
|
536
|
+
message: `${pkg.name} (${currentVersion}):`,
|
|
527
537
|
choices,
|
|
528
538
|
initial: 0
|
|
529
539
|
}, {
|
|
@@ -537,68 +547,134 @@ async function promptVersionOverride(packageName, currentVersion, suggestedVersi
|
|
|
537
547
|
}]);
|
|
538
548
|
if (response.choice === "suggested") return suggestedVersion;
|
|
539
549
|
else if (response.choice === "custom") return response.customVersion;
|
|
540
|
-
else return
|
|
541
|
-
}
|
|
542
|
-
async function promptVersionOverrides(packages) {
|
|
543
|
-
const overrides = /* @__PURE__ */ new Map();
|
|
544
|
-
for (const pkg of packages) {
|
|
545
|
-
const newVersion = await promptVersionOverride(pkg.name, pkg.currentVersion, pkg.suggestedVersion, pkg.bumpType);
|
|
546
|
-
overrides.set(pkg.name, newVersion);
|
|
547
|
-
}
|
|
548
|
-
return overrides;
|
|
550
|
+
else return getNextVersion(currentVersion, response.choice);
|
|
549
551
|
}
|
|
550
552
|
|
|
551
553
|
//#endregion
|
|
552
|
-
//#region src/
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
|
|
554
|
+
//#region src/version.ts
|
|
555
|
+
function isValidSemver(version) {
|
|
556
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
|
|
557
|
+
}
|
|
558
|
+
function validateSemver(version) {
|
|
559
|
+
if (!isValidSemver(version)) throw new Error(`Invalid semver version: ${version}`);
|
|
560
|
+
}
|
|
561
|
+
function getNextVersion(currentVersion, bump) {
|
|
562
|
+
if (bump === "none") return currentVersion;
|
|
563
|
+
validateSemver(currentVersion);
|
|
564
|
+
const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
565
|
+
if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
|
|
566
|
+
const [, major, minor, patch] = match;
|
|
567
|
+
let newMajor = Number.parseInt(major, 10);
|
|
568
|
+
let newMinor = Number.parseInt(minor, 10);
|
|
569
|
+
let newPatch = Number.parseInt(patch, 10);
|
|
570
|
+
switch (bump) {
|
|
571
|
+
case "major":
|
|
572
|
+
newMajor += 1;
|
|
573
|
+
newMinor = 0;
|
|
574
|
+
newPatch = 0;
|
|
575
|
+
break;
|
|
576
|
+
case "minor":
|
|
577
|
+
newMinor += 1;
|
|
578
|
+
newPatch = 0;
|
|
579
|
+
break;
|
|
580
|
+
case "patch":
|
|
581
|
+
newPatch += 1;
|
|
582
|
+
break;
|
|
559
583
|
}
|
|
560
|
-
|
|
561
|
-
return true;
|
|
584
|
+
return `${newMajor}.${newMinor}.${newPatch}`;
|
|
562
585
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Create a version update object
|
|
588
|
+
*/
|
|
589
|
+
function createVersionUpdate(pkg, bump, hasDirectChanges) {
|
|
590
|
+
const newVersion = getNextVersion(pkg.version, bump);
|
|
591
|
+
return {
|
|
592
|
+
package: pkg,
|
|
593
|
+
currentVersion: pkg.version,
|
|
594
|
+
newVersion,
|
|
595
|
+
bumpType: bump,
|
|
596
|
+
hasDirectChanges
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Infer version updates from package commits with optional interactive overrides
|
|
601
|
+
*
|
|
602
|
+
* @param workspacePackages - All workspace packages
|
|
603
|
+
* @param packageCommits - Map of package names to their commits
|
|
604
|
+
* @param workspaceRoot - Root directory for prompts
|
|
605
|
+
* @param showPrompt - Whether to show prompts for version overrides
|
|
606
|
+
* @returns Version updates for packages with changes
|
|
607
|
+
*/
|
|
608
|
+
async function inferVersionUpdates(workspacePackages, packageCommits, workspaceRoot, showPrompt) {
|
|
609
|
+
const versionUpdates = [];
|
|
610
|
+
for (const [pkgName, commits] of packageCommits) {
|
|
611
|
+
if (commits.length === 0) continue;
|
|
612
|
+
const pkg = workspacePackages.find((p) => p.name === pkgName);
|
|
613
|
+
if (!pkg) continue;
|
|
614
|
+
const bump = determineHighestBump(commits);
|
|
615
|
+
if (bump === "none") {
|
|
616
|
+
logger.info(`No version bump needed for package ${pkg.name}`);
|
|
580
617
|
continue;
|
|
581
618
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
workspaceDevDependencies: workspaceDevDeps
|
|
619
|
+
let newVersion = getNextVersion(pkg.version, bump);
|
|
620
|
+
if (!isCI && showPrompt) newVersion = await promptVersionOverride(pkg, workspaceRoot, pkg.version, newVersion, bump);
|
|
621
|
+
versionUpdates.push({
|
|
622
|
+
package: pkg,
|
|
623
|
+
currentVersion: pkg.version,
|
|
624
|
+
newVersion,
|
|
625
|
+
bumpType: bump,
|
|
626
|
+
hasDirectChanges: true
|
|
591
627
|
});
|
|
592
628
|
}
|
|
593
|
-
return
|
|
629
|
+
return versionUpdates;
|
|
594
630
|
}
|
|
595
|
-
function
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
631
|
+
async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
|
|
632
|
+
const packageJsonPath = join(pkg.path, "package.json");
|
|
633
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
634
|
+
const packageJson = JSON.parse(content);
|
|
635
|
+
packageJson.version = newVersion;
|
|
636
|
+
for (const [depName, depVersion] of dependencyUpdates) {
|
|
637
|
+
if (packageJson.dependencies?.[depName]) {
|
|
638
|
+
if (packageJson.dependencies[depName] === "workspace:*") continue;
|
|
639
|
+
packageJson.dependencies[depName] = `^${depVersion}`;
|
|
640
|
+
}
|
|
641
|
+
if (packageJson.devDependencies?.[depName]) {
|
|
642
|
+
if (packageJson.devDependencies[depName] === "workspace:*") continue;
|
|
643
|
+
packageJson.devDependencies[depName] = `^${depVersion}`;
|
|
644
|
+
}
|
|
645
|
+
if (packageJson.peerDependencies?.[depName]) {
|
|
646
|
+
if (packageJson.peerDependencies[depName] === "workspace:*") continue;
|
|
647
|
+
packageJson.peerDependencies[depName] = `^${depVersion}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Get all dependency updates needed for a package
|
|
654
|
+
*/
|
|
655
|
+
function getDependencyUpdates(pkg, allUpdates) {
|
|
656
|
+
const updates = /* @__PURE__ */ new Map();
|
|
657
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
658
|
+
for (const dep of allDeps) {
|
|
659
|
+
const update = allUpdates.find((u) => u.package.name === dep);
|
|
660
|
+
if (update) updates.set(dep, update.newVersion);
|
|
661
|
+
}
|
|
662
|
+
return updates;
|
|
600
663
|
}
|
|
601
|
-
|
|
664
|
+
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/package.ts
|
|
667
|
+
/**
|
|
668
|
+
* Build a dependency graph from workspace packages
|
|
669
|
+
*
|
|
670
|
+
* Creates a bidirectional graph that maps:
|
|
671
|
+
* - packages: Map of package name → WorkspacePackage
|
|
672
|
+
* - dependents: Map of package name → Set of packages that depend on it
|
|
673
|
+
*
|
|
674
|
+
* @param packages - All workspace packages
|
|
675
|
+
* @returns Dependency graph with packages and dependents maps
|
|
676
|
+
*/
|
|
677
|
+
function buildPackageDependencyGraph(packages) {
|
|
602
678
|
const packagesMap = /* @__PURE__ */ new Map();
|
|
603
679
|
const dependents = /* @__PURE__ */ new Map();
|
|
604
680
|
for (const pkg of packages) {
|
|
@@ -617,201 +693,232 @@ function buildDependencyGraph(packages) {
|
|
|
617
693
|
dependents
|
|
618
694
|
};
|
|
619
695
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Get all packages affected by changes (including transitive dependents)
|
|
698
|
+
*
|
|
699
|
+
* Uses graph traversal to find all packages that need updates:
|
|
700
|
+
* - Packages with direct changes
|
|
701
|
+
* - All packages that depend on changed packages (transitively)
|
|
702
|
+
*
|
|
703
|
+
* @param graph - Dependency graph
|
|
704
|
+
* @param changedPackages - Set of package names with direct changes
|
|
705
|
+
* @returns Set of all package names that need updates
|
|
706
|
+
*/
|
|
707
|
+
function getAllAffectedPackages(graph, changedPackages) {
|
|
708
|
+
const affected = /* @__PURE__ */ new Set();
|
|
709
|
+
function visitDependents(pkgName) {
|
|
710
|
+
if (affected.has(pkgName)) return;
|
|
711
|
+
affected.add(pkgName);
|
|
712
|
+
const dependents = graph.dependents.get(pkgName);
|
|
713
|
+
if (dependents) for (const dependent of dependents) visitDependents(dependent);
|
|
631
714
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
715
|
+
for (const pkg of changedPackages) visitDependents(pkg);
|
|
716
|
+
return affected;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Create version updates for all packages affected by dependency changes
|
|
720
|
+
*
|
|
721
|
+
* When a package is updated, all packages that depend on it should also be updated.
|
|
722
|
+
* This function calculates which additional packages need patch bumps due to dependency changes.
|
|
723
|
+
*
|
|
724
|
+
* @param graph - Dependency graph
|
|
725
|
+
* @param workspacePackages - All workspace packages
|
|
726
|
+
* @param directUpdates - Packages with direct code changes
|
|
727
|
+
* @returns All updates including dependent packages that need patch bumps
|
|
728
|
+
*/
|
|
729
|
+
function createDependentUpdates(graph, workspacePackages, directUpdates) {
|
|
730
|
+
const allUpdates = [...directUpdates];
|
|
731
|
+
const directUpdateMap = new Map(directUpdates.map((u) => [u.package.name, u]));
|
|
732
|
+
const affectedPackages = getAllAffectedPackages(graph, new Set(directUpdates.map((u) => u.package.name)));
|
|
733
|
+
for (const pkgName of affectedPackages) {
|
|
734
|
+
if (directUpdateMap.has(pkgName)) continue;
|
|
735
|
+
const pkg = workspacePackages.find((p) => p.name === pkgName);
|
|
736
|
+
if (!pkg) continue;
|
|
737
|
+
allUpdates.push(createVersionUpdate(pkg, "patch", false));
|
|
648
738
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
739
|
+
return allUpdates;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Update all package.json files with new versions and dependency updates
|
|
743
|
+
*
|
|
744
|
+
* Updates are performed in parallel for better performance.
|
|
745
|
+
*
|
|
746
|
+
* @param updates - Version updates to apply
|
|
747
|
+
*/
|
|
748
|
+
async function updateAllPackageJsonFiles(updates) {
|
|
749
|
+
await Promise.all(updates.map(async (update) => {
|
|
750
|
+
const depUpdates = getDependencyUpdates(update.package, updates);
|
|
751
|
+
await updatePackageJson(update.package, update.newVersion, depUpdates);
|
|
752
|
+
}));
|
|
652
753
|
}
|
|
653
754
|
|
|
654
755
|
//#endregion
|
|
655
|
-
//#region src/
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
756
|
+
//#region src/workspace.ts
|
|
757
|
+
async function discoverWorkspacePackages(workspaceRoot, options) {
|
|
758
|
+
let workspaceOptions;
|
|
759
|
+
let explicitPackages;
|
|
760
|
+
if (options.packages == null || options.packages === true) workspaceOptions = { excludePrivate: false };
|
|
761
|
+
else if (Array.isArray(options.packages)) {
|
|
762
|
+
workspaceOptions = {
|
|
763
|
+
excludePrivate: false,
|
|
764
|
+
include: options.packages
|
|
765
|
+
};
|
|
766
|
+
explicitPackages = options.packages;
|
|
767
|
+
} else {
|
|
768
|
+
workspaceOptions = options.packages;
|
|
769
|
+
if (options.packages.include) explicitPackages = options.packages.include;
|
|
770
|
+
}
|
|
771
|
+
let workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
|
|
772
|
+
if (explicitPackages) {
|
|
773
|
+
const foundNames = new Set(workspacePackages.map((p) => p.name));
|
|
774
|
+
const missing = explicitPackages.filter((p) => !foundNames.has(p));
|
|
775
|
+
if (missing.length > 0) exitWithError(`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`, "Check your package names or run 'pnpm ls' to see available packages");
|
|
666
776
|
}
|
|
667
|
-
const { workspacePackages, packagesToAnalyze: initialPackages } = await discoverPackages(workspaceRoot, options);
|
|
668
|
-
if (initialPackages.length === 0) return null;
|
|
669
777
|
const isPackagePromptEnabled = options.prompts?.packages !== false;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
778
|
+
if (!isCI && isPackagePromptEnabled && !explicitPackages) {
|
|
779
|
+
const selectedNames = await selectPackagePrompt(workspacePackages);
|
|
780
|
+
workspacePackages = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
|
|
781
|
+
}
|
|
782
|
+
return workspacePackages;
|
|
783
|
+
}
|
|
784
|
+
async function findWorkspacePackages(workspaceRoot, options) {
|
|
785
|
+
try {
|
|
786
|
+
const result = await run("pnpm", [
|
|
787
|
+
"-r",
|
|
788
|
+
"ls",
|
|
789
|
+
"--json"
|
|
790
|
+
], { nodeOptions: {
|
|
791
|
+
cwd: workspaceRoot,
|
|
792
|
+
stdio: "pipe"
|
|
793
|
+
} });
|
|
794
|
+
const rawProjects = JSON.parse(result.stdout);
|
|
795
|
+
const allPackageNames = new Set(rawProjects.map((p) => p.name));
|
|
796
|
+
const excludedPackages = /* @__PURE__ */ new Set();
|
|
797
|
+
const promises = rawProjects.map(async (rawProject) => {
|
|
798
|
+
const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
|
|
799
|
+
const packageJson = JSON.parse(content);
|
|
800
|
+
if (!shouldIncludePackage(packageJson, options)) {
|
|
801
|
+
excludedPackages.add(rawProject.name);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
name: rawProject.name,
|
|
806
|
+
version: rawProject.version,
|
|
807
|
+
path: rawProject.path,
|
|
808
|
+
packageJson,
|
|
809
|
+
workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => {
|
|
810
|
+
return allPackageNames.has(dep);
|
|
811
|
+
}),
|
|
812
|
+
workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => {
|
|
813
|
+
return allPackageNames.has(dep);
|
|
814
|
+
})
|
|
692
815
|
};
|
|
693
|
-
return update;
|
|
694
816
|
});
|
|
817
|
+
const packages = await Promise.all(promises);
|
|
818
|
+
if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
|
|
819
|
+
return packages.filter((pkg) => pkg !== null);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
logger.error("Error discovering workspace packages:", err);
|
|
822
|
+
throw err;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function shouldIncludePackage(pkg, options) {
|
|
826
|
+
if (!options) return true;
|
|
827
|
+
if (options.excludePrivate && pkg.private) return false;
|
|
828
|
+
if (options.include && options.include.length > 0) {
|
|
829
|
+
if (!options.include.includes(pkg.name)) return false;
|
|
830
|
+
}
|
|
831
|
+
if (options.exclude?.includes(pkg.name)) return false;
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/release.ts
|
|
837
|
+
async function release(options) {
|
|
838
|
+
const normalizedOptions = normalizeSharedOptions(options);
|
|
839
|
+
normalizedOptions.dryRun ??= false;
|
|
840
|
+
normalizedOptions.branch ??= {};
|
|
841
|
+
normalizedOptions.branch.release ??= "release/next";
|
|
842
|
+
normalizedOptions.branch.default = await getDefaultBranch();
|
|
843
|
+
normalizedOptions.safeguards ??= true;
|
|
844
|
+
globalOptions.dryRun = normalizedOptions.dryRun;
|
|
845
|
+
const workspaceRoot = normalizedOptions.workspaceRoot;
|
|
846
|
+
if (normalizedOptions.safeguards && !await isWorkingDirectoryClean(workspaceRoot)) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
847
|
+
const workspacePackages = await discoverWorkspacePackages(workspaceRoot, options);
|
|
848
|
+
if (workspacePackages.length === 0) {
|
|
849
|
+
logger.log("No packages found to release.");
|
|
850
|
+
return null;
|
|
695
851
|
}
|
|
696
|
-
const
|
|
852
|
+
const versionUpdates = await inferVersionUpdates(workspacePackages, await getWorkspacePackageCommits(workspaceRoot, workspacePackages), workspaceRoot, options.prompts?.versions !== false);
|
|
853
|
+
if (versionUpdates.length === 0) logger.warn("No packages have changes requiring a release");
|
|
854
|
+
const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, versionUpdates);
|
|
697
855
|
const currentBranch = await getCurrentBranch(workspaceRoot);
|
|
856
|
+
if (currentBranch !== normalizedOptions.branch.default) exitWithError(`Current branch is '${currentBranch}'. Please switch to the default branch '${normalizedOptions.branch.default}' before proceeding.`, `git checkout ${normalizedOptions.branch.default}`);
|
|
698
857
|
const existingPullRequest = await getExistingPullRequest({
|
|
699
|
-
owner,
|
|
700
|
-
repo,
|
|
701
|
-
branch:
|
|
702
|
-
githubToken
|
|
858
|
+
owner: normalizedOptions.owner,
|
|
859
|
+
repo: normalizedOptions.repo,
|
|
860
|
+
branch: normalizedOptions.branch.release,
|
|
861
|
+
githubToken: normalizedOptions.githubToken
|
|
703
862
|
});
|
|
704
863
|
const prExists = !!existingPullRequest;
|
|
705
|
-
if (prExists)
|
|
706
|
-
else
|
|
707
|
-
const branchExists = await doesBranchExist(
|
|
864
|
+
if (prExists) logger.log("Existing pull request found:", existingPullRequest.html_url);
|
|
865
|
+
else logger.log("No existing pull request found, will create new one");
|
|
866
|
+
const branchExists = await doesBranchExist(normalizedOptions.branch.release, workspaceRoot);
|
|
708
867
|
if (!branchExists) {
|
|
709
|
-
|
|
710
|
-
await createBranch(
|
|
868
|
+
logger.log("Creating release branch:", normalizedOptions.branch.release);
|
|
869
|
+
await createBranch(normalizedOptions.branch.release, normalizedOptions.branch.default, workspaceRoot);
|
|
711
870
|
}
|
|
712
|
-
if (!await checkoutBranch(
|
|
871
|
+
if (!await checkoutBranch(normalizedOptions.branch.release, workspaceRoot)) throw new Error(`Failed to checkout branch: ${normalizedOptions.branch.release}`);
|
|
713
872
|
if (branchExists) {
|
|
714
|
-
|
|
715
|
-
if (!await pullLatestChanges(
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
await rebaseBranch(
|
|
719
|
-
await
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
873
|
+
logger.log("Pulling latest changes from remote");
|
|
874
|
+
if (!await pullLatestChanges(normalizedOptions.branch.release, workspaceRoot)) logger.log("Warning: Failed to pull latest changes, continuing anyway");
|
|
875
|
+
}
|
|
876
|
+
logger.log("Rebasing release branch onto", normalizedOptions.branch.default);
|
|
877
|
+
await rebaseBranch(normalizedOptions.branch.default, workspaceRoot);
|
|
878
|
+
await updateAllPackageJsonFiles(allUpdates);
|
|
879
|
+
const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
|
|
880
|
+
const isBranchAhead = await isBranchAheadOfRemote(normalizedOptions.branch.release, workspaceRoot);
|
|
881
|
+
if (!hasCommitted && !isBranchAhead) {
|
|
882
|
+
logger.log("No changes to commit and branch is in sync with remote");
|
|
883
|
+
await checkoutBranch(normalizedOptions.branch.default, workspaceRoot);
|
|
723
884
|
if (prExists) {
|
|
724
|
-
|
|
885
|
+
logger.log("No updates needed, PR is already up to date");
|
|
725
886
|
return {
|
|
726
887
|
updates: allUpdates,
|
|
727
888
|
prUrl: existingPullRequest.html_url,
|
|
728
889
|
created: false
|
|
729
890
|
};
|
|
730
891
|
} else {
|
|
731
|
-
|
|
892
|
+
logger.error("No changes to commit, and no existing PR. Nothing to do.");
|
|
732
893
|
return null;
|
|
733
894
|
}
|
|
734
895
|
}
|
|
735
|
-
|
|
736
|
-
await pushBranch(
|
|
737
|
-
const prTitle = existingPullRequest?.title || "
|
|
738
|
-
const prBody =
|
|
896
|
+
logger.log("Pushing changes to remote");
|
|
897
|
+
await pushBranch(normalizedOptions.branch.release, workspaceRoot, { forceWithLease: true });
|
|
898
|
+
const prTitle = existingPullRequest?.title || options.pullRequest?.title || "chore: update package versions";
|
|
899
|
+
const prBody = generatePullRequestBody(allUpdates, options.pullRequest?.body);
|
|
739
900
|
const pullRequest = await upsertPullRequest({
|
|
740
|
-
owner,
|
|
741
|
-
repo,
|
|
901
|
+
owner: normalizedOptions.owner,
|
|
902
|
+
repo: normalizedOptions.repo,
|
|
742
903
|
pullNumber: existingPullRequest?.number,
|
|
743
904
|
title: prTitle,
|
|
744
905
|
body: prBody,
|
|
745
|
-
head:
|
|
746
|
-
base:
|
|
747
|
-
githubToken
|
|
906
|
+
head: normalizedOptions.branch.release,
|
|
907
|
+
base: normalizedOptions.branch.default,
|
|
908
|
+
githubToken: normalizedOptions.githubToken
|
|
748
909
|
});
|
|
749
|
-
|
|
750
|
-
await checkoutBranch(
|
|
910
|
+
logger.log(prExists ? "Updated pull request:" : "Created pull request:", pullRequest?.html_url);
|
|
911
|
+
await checkoutBranch(normalizedOptions.branch.default, workspaceRoot);
|
|
912
|
+
if (pullRequest?.html_url) {
|
|
913
|
+
logger.info();
|
|
914
|
+
logger.info(`${farver.green("✓")} Pull request ${prExists ? "updated" : "created"}: ${farver.cyan(pullRequest.html_url)}`);
|
|
915
|
+
}
|
|
751
916
|
return {
|
|
752
917
|
updates: allUpdates,
|
|
753
918
|
prUrl: pullRequest?.html_url,
|
|
754
919
|
created: !prExists
|
|
755
920
|
};
|
|
756
921
|
}
|
|
757
|
-
async function discoverPackages(workspaceRoot, options) {
|
|
758
|
-
let workspacePackages;
|
|
759
|
-
let packagesToAnalyze;
|
|
760
|
-
if (typeof options.packages === "boolean" && options.packages === true) {
|
|
761
|
-
workspacePackages = await findWorkspacePackages(workspaceRoot, { excludePrivate: false });
|
|
762
|
-
packagesToAnalyze = workspacePackages;
|
|
763
|
-
return {
|
|
764
|
-
workspacePackages,
|
|
765
|
-
packagesToAnalyze
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
if (Array.isArray(options.packages)) {
|
|
769
|
-
const packageNames = options.packages;
|
|
770
|
-
workspacePackages = await findWorkspacePackages(workspaceRoot, {
|
|
771
|
-
excludePrivate: false,
|
|
772
|
-
included: packageNames
|
|
773
|
-
});
|
|
774
|
-
packagesToAnalyze = workspacePackages.filter((pkg) => packageNames.includes(pkg.name));
|
|
775
|
-
if (packagesToAnalyze.length !== packageNames.length) {
|
|
776
|
-
const found = new Set(packagesToAnalyze.map((p) => p.name));
|
|
777
|
-
const missing = packageNames.filter((p) => !found.has(p));
|
|
778
|
-
throw new Error(`Packages not found in workspace: ${missing.join(", ")}`);
|
|
779
|
-
}
|
|
780
|
-
return {
|
|
781
|
-
workspacePackages,
|
|
782
|
-
packagesToAnalyze
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
workspacePackages = await findWorkspacePackages(workspaceRoot, options.packages);
|
|
786
|
-
packagesToAnalyze = workspacePackages;
|
|
787
|
-
return {
|
|
788
|
-
workspacePackages,
|
|
789
|
-
packagesToAnalyze
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
async function analyzeCommits(packages, workspaceRoot) {
|
|
793
|
-
const changedPackages = /* @__PURE__ */ new Map();
|
|
794
|
-
for (const pkg of packages) {
|
|
795
|
-
const bump = await analyzePackageCommits(pkg, workspaceRoot);
|
|
796
|
-
if (bump !== "none") changedPackages.set(pkg.name, bump);
|
|
797
|
-
}
|
|
798
|
-
return changedPackages;
|
|
799
|
-
}
|
|
800
|
-
function calculateVersions(allPackages, changedPackages) {
|
|
801
|
-
const updates = [];
|
|
802
|
-
for (const [pkgName, bump] of changedPackages) {
|
|
803
|
-
const pkg = allPackages.find((p) => p.name === pkgName);
|
|
804
|
-
if (!pkg) continue;
|
|
805
|
-
updates.push(createVersionUpdate(pkg, bump, true));
|
|
806
|
-
}
|
|
807
|
-
return updates;
|
|
808
|
-
}
|
|
809
|
-
async function updatePackageJsonFiles(updates) {
|
|
810
|
-
await Promise.all(updates.map(async (update) => {
|
|
811
|
-
const depUpdates = getDependencyUpdates(update.package, updates);
|
|
812
|
-
await updatePackageJson(update.package, update.newVersion, depUpdates);
|
|
813
|
-
}));
|
|
814
|
-
}
|
|
815
922
|
|
|
816
923
|
//#endregion
|
|
817
|
-
export { release };
|
|
924
|
+
export { publish, release };
|