@ucdjs/release-scripts 0.1.0-beta.11 → 0.1.0-beta.13
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/index.d.mts +13 -14
- package/dist/index.mjs +786 -430
- package/package.json +9 -1
package/dist/index.mjs
CHANGED
|
@@ -1,43 +1,66 @@
|
|
|
1
1
|
import { t as Eta } from "./eta-Boh7yPZi.mjs";
|
|
2
|
-
import farver from "farver";
|
|
3
|
-
import { getCommits } from "commit-parser";
|
|
4
2
|
import process from "node:process";
|
|
3
|
+
import farver from "farver";
|
|
4
|
+
import mri from "mri";
|
|
5
5
|
import { exec } from "tinyexec";
|
|
6
6
|
import { dedent } from "@luxass/utils";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { getCommits } from "commit-parser";
|
|
9
10
|
import prompts from "prompts";
|
|
10
11
|
|
|
11
12
|
//#region src/publish.ts
|
|
12
13
|
function publish(_options) {}
|
|
13
14
|
|
|
14
15
|
//#endregion
|
|
15
|
-
//#region src/utils.ts
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
//#region src/shared/utils.ts
|
|
17
|
+
const args = mri(process.argv.slice(2));
|
|
18
|
+
const isDryRun = !!args.dry;
|
|
19
|
+
const isVerbose = !!args.verbose;
|
|
20
|
+
const isForce = !!args.force;
|
|
20
21
|
const isCI = typeof process.env.CI === "string" && process.env.CI !== "" && process.env.CI.toLowerCase() !== "false";
|
|
21
22
|
const logger = {
|
|
22
|
-
info: (...args) => {
|
|
23
|
-
console.info(
|
|
23
|
+
info: (...args$1) => {
|
|
24
|
+
console.info(...args$1);
|
|
25
|
+
},
|
|
26
|
+
warn: (...args$1) => {
|
|
27
|
+
console.warn(` ${farver.yellow("⚠")}`, ...args$1);
|
|
28
|
+
},
|
|
29
|
+
error: (...args$1) => {
|
|
30
|
+
console.error(` ${farver.red("✖")}`, ...args$1);
|
|
31
|
+
},
|
|
32
|
+
verbose: (...args$1) => {
|
|
33
|
+
if (!isVerbose) return;
|
|
34
|
+
if (args$1.length === 0) {
|
|
35
|
+
console.log();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (args$1.length > 1 && typeof args$1[0] === "string") {
|
|
39
|
+
console.log(farver.dim(args$1[0]), ...args$1.slice(1));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(...args$1);
|
|
24
43
|
},
|
|
25
|
-
|
|
26
|
-
console.
|
|
44
|
+
section: (title) => {
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(` ${farver.bold(title)}`);
|
|
47
|
+
console.log(` ${farver.gray("─".repeat(title.length + 2))}`);
|
|
27
48
|
},
|
|
28
|
-
|
|
29
|
-
console.
|
|
49
|
+
emptyLine: () => {
|
|
50
|
+
console.log();
|
|
30
51
|
},
|
|
31
|
-
|
|
32
|
-
console.
|
|
52
|
+
item: (message) => {
|
|
53
|
+
console.log(` ${message}`);
|
|
33
54
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
step: (message) => {
|
|
56
|
+
console.log(` ${farver.blue("→")} ${message}`);
|
|
57
|
+
},
|
|
58
|
+
success: (message) => {
|
|
59
|
+
console.log(` ${farver.green("✓")} ${message}`);
|
|
37
60
|
}
|
|
38
61
|
};
|
|
39
|
-
async function run(bin, args, opts = {}) {
|
|
40
|
-
return exec(bin, args, {
|
|
62
|
+
async function run(bin, args$1, opts = {}) {
|
|
63
|
+
return exec(bin, args$1, {
|
|
41
64
|
throwOnError: true,
|
|
42
65
|
...opts,
|
|
43
66
|
nodeOptions: {
|
|
@@ -46,21 +69,20 @@ async function run(bin, args, opts = {}) {
|
|
|
46
69
|
}
|
|
47
70
|
});
|
|
48
71
|
}
|
|
49
|
-
async function dryRun(bin, args, opts) {
|
|
50
|
-
return logger.
|
|
72
|
+
async function dryRun(bin, args$1, opts) {
|
|
73
|
+
return logger.verbose(farver.blue(`[dryrun] ${bin} ${args$1.join(" ")}`), opts || "");
|
|
51
74
|
}
|
|
52
|
-
const runIfNotDry =
|
|
75
|
+
const runIfNotDry = isDryRun ? dryRun : run;
|
|
53
76
|
function exitWithError(message, hint) {
|
|
54
77
|
logger.error(farver.bold(message));
|
|
55
78
|
if (hint) console.error(farver.gray(` ${hint}`));
|
|
56
79
|
process.exit(1);
|
|
57
80
|
}
|
|
58
81
|
function normalizeSharedOptions(options) {
|
|
59
|
-
const { workspaceRoot = process.cwd(), githubToken = "",
|
|
82
|
+
const { workspaceRoot = process.cwd(), githubToken = "", repo: fullRepo, packages = true, prompts: prompts$1 = {
|
|
60
83
|
packages: true,
|
|
61
84
|
versions: true
|
|
62
85
|
},...rest } = options;
|
|
63
|
-
globalOptions.verbose = verbose;
|
|
64
86
|
if (!githubToken.trim()) exitWithError("GitHub token is required", "Set GITHUB_TOKEN environment variable or pass it in options");
|
|
65
87
|
if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) exitWithError("Repository (repo) is required", "Specify the repository in 'owner/repo' format (e.g., 'octocat/hello-world')");
|
|
66
88
|
const [owner, repo] = fullRepo.split("/");
|
|
@@ -68,99 +90,28 @@ function normalizeSharedOptions(options) {
|
|
|
68
90
|
return {
|
|
69
91
|
...rest,
|
|
70
92
|
packages,
|
|
71
|
-
prompts:
|
|
93
|
+
prompts: {
|
|
94
|
+
packages: prompts$1?.packages ?? true,
|
|
95
|
+
versions: prompts$1?.versions ?? true
|
|
96
|
+
},
|
|
72
97
|
workspaceRoot,
|
|
73
98
|
githubToken,
|
|
74
99
|
owner,
|
|
75
|
-
repo
|
|
76
|
-
verbose
|
|
100
|
+
repo
|
|
77
101
|
};
|
|
78
102
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function determineHighestBump(commits) {
|
|
95
|
-
if (commits.length === 0) return "none";
|
|
96
|
-
let highestBump = "none";
|
|
97
|
-
for (const commit of commits) {
|
|
98
|
-
const bump = determineBumpType(commit);
|
|
99
|
-
if (bump === "major") return "major";
|
|
100
|
-
if (bump === "minor") highestBump = "minor";
|
|
101
|
-
else if (bump === "patch" && highestBump === "none") highestBump = "patch";
|
|
102
|
-
}
|
|
103
|
-
return highestBump;
|
|
104
|
-
}
|
|
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) {
|
|
113
|
-
const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
|
|
114
|
-
const allCommits = getCommits({
|
|
115
|
-
from: lastTag,
|
|
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
|
|
125
|
-
});
|
|
126
|
-
const touchedSet = new Set(touchedCommitHashes);
|
|
127
|
-
const packageCommits = allCommits.filter((commit) => touchedSet.has(commit));
|
|
128
|
-
logger.log(`${packageCommits.length} commits affect ${pkg.name}`);
|
|
129
|
-
return packageCommits;
|
|
130
|
-
}
|
|
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
|
-
};
|
|
103
|
+
if (isDryRun || isVerbose || isForce) {
|
|
104
|
+
logger.verbose(farver.inverse(farver.yellow(" Running with special flags ")));
|
|
105
|
+
logger.verbose({
|
|
106
|
+
isDryRun,
|
|
107
|
+
isVerbose,
|
|
108
|
+
isForce
|
|
138
109
|
});
|
|
139
|
-
|
|
140
|
-
for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
|
|
141
|
-
return changedPackages;
|
|
142
|
-
}
|
|
143
|
-
function determineBumpType(commit) {
|
|
144
|
-
if (commit.isBreaking) return "major";
|
|
145
|
-
if (!commit.isConventional || !commit.type) return "none";
|
|
146
|
-
switch (commit.type) {
|
|
147
|
-
case "feat": return "minor";
|
|
148
|
-
case "fix":
|
|
149
|
-
case "perf": return "patch";
|
|
150
|
-
case "docs":
|
|
151
|
-
case "style":
|
|
152
|
-
case "refactor":
|
|
153
|
-
case "test":
|
|
154
|
-
case "build":
|
|
155
|
-
case "ci":
|
|
156
|
-
case "chore":
|
|
157
|
-
case "revert": return "none";
|
|
158
|
-
default: return "none";
|
|
159
|
-
}
|
|
110
|
+
logger.verbose();
|
|
160
111
|
}
|
|
161
112
|
|
|
162
113
|
//#endregion
|
|
163
|
-
//#region src/git.ts
|
|
114
|
+
//#region src/core/git.ts
|
|
164
115
|
/**
|
|
165
116
|
* Check if the working directory is clean (no uncommitted changes)
|
|
166
117
|
* @param {string} workspaceRoot - The root directory of the workspace
|
|
@@ -200,31 +151,64 @@ async function doesBranchExist(branch, workspaceRoot) {
|
|
|
200
151
|
}
|
|
201
152
|
}
|
|
202
153
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* @
|
|
206
|
-
* @returns Promise resolving to true if pull succeeded, false otherwise
|
|
154
|
+
* Retrieves the default branch name from the remote repository.
|
|
155
|
+
* Falls back to "main" if the default branch cannot be determined.
|
|
156
|
+
* @returns {Promise<string>} A Promise resolving to the default branch name as a string.
|
|
207
157
|
*/
|
|
208
|
-
async function
|
|
158
|
+
async function getDefaultBranch(workspaceRoot) {
|
|
209
159
|
try {
|
|
210
|
-
await run("git", [
|
|
211
|
-
"pull",
|
|
212
|
-
"origin",
|
|
213
|
-
branch
|
|
214
|
-
], { nodeOptions: {
|
|
160
|
+
const match = (await run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { nodeOptions: {
|
|
215
161
|
cwd: workspaceRoot,
|
|
216
162
|
stdio: "pipe"
|
|
217
|
-
} });
|
|
218
|
-
return
|
|
163
|
+
} })).stdout.trim().match(/^refs\/remotes\/origin\/(.+)$/);
|
|
164
|
+
if (match && match[1]) return match[1];
|
|
165
|
+
return "main";
|
|
219
166
|
} catch {
|
|
220
|
-
return
|
|
167
|
+
return "main";
|
|
221
168
|
}
|
|
222
169
|
}
|
|
223
170
|
/**
|
|
224
|
-
*
|
|
225
|
-
* @param
|
|
226
|
-
* @
|
|
227
|
-
|
|
171
|
+
* Retrieves the name of the current branch in the repository.
|
|
172
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
173
|
+
* @returns {Promise<string>} A Promise resolving to the current branch name as a string
|
|
174
|
+
*/
|
|
175
|
+
async function getCurrentBranch(workspaceRoot) {
|
|
176
|
+
try {
|
|
177
|
+
return (await run("git", [
|
|
178
|
+
"rev-parse",
|
|
179
|
+
"--abbrev-ref",
|
|
180
|
+
"HEAD"
|
|
181
|
+
], { nodeOptions: {
|
|
182
|
+
cwd: workspaceRoot,
|
|
183
|
+
stdio: "pipe"
|
|
184
|
+
} })).stdout.trim();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
logger.error("Error getting current branch:", err);
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Retrieves the list of available branches in the repository.
|
|
192
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
193
|
+
* @returns {Promise<string[]>} A Promise resolving to an array of branch names
|
|
194
|
+
*/
|
|
195
|
+
async function getAvailableBranches(workspaceRoot) {
|
|
196
|
+
try {
|
|
197
|
+
return (await run("git", ["branch", "--list"], { nodeOptions: {
|
|
198
|
+
cwd: workspaceRoot,
|
|
199
|
+
stdio: "pipe"
|
|
200
|
+
} })).stdout.split("\n").map((line) => line.replace("*", "").trim()).filter((line) => line.length > 0);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
logger.error("Error getting available branches:", err);
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Creates a new branch from the specified base branch.
|
|
208
|
+
* @param {string} branch - The name of the new branch to create
|
|
209
|
+
* @param {string} base - The base branch to create the new branch from
|
|
210
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
211
|
+
* @returns {Promise<void>} A Promise that resolves when the branch is created
|
|
228
212
|
*/
|
|
229
213
|
async function createBranch(branch, base, workspaceRoot) {
|
|
230
214
|
try {
|
|
@@ -242,16 +226,29 @@ async function createBranch(branch, base, workspaceRoot) {
|
|
|
242
226
|
exitWithError(`Failed to create branch: ${branch}`, `Make sure the branch doesn't already exist and you have a clean working directory`);
|
|
243
227
|
}
|
|
244
228
|
}
|
|
245
|
-
/**
|
|
246
|
-
* Checkout a git branch
|
|
247
|
-
* @param branch - The branch name to checkout
|
|
248
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
249
|
-
* @returns Promise resolving to true if checkout succeeded, false otherwise
|
|
250
|
-
*/
|
|
251
229
|
async function checkoutBranch(branch, workspaceRoot) {
|
|
252
230
|
try {
|
|
253
231
|
logger.info(`Switching to branch: ${farver.green(branch)}`);
|
|
254
|
-
await run("git", ["checkout", branch], { nodeOptions: {
|
|
232
|
+
const match = (await run("git", ["checkout", branch], { nodeOptions: {
|
|
233
|
+
cwd: workspaceRoot,
|
|
234
|
+
stdio: "pipe"
|
|
235
|
+
} })).stderr.trim().match(/Switched to branch '(.+)'/);
|
|
236
|
+
if (match && match[1] === branch) {
|
|
237
|
+
logger.info(`Successfully switched to branch: ${farver.green(branch)}`);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function pullLatestChanges(branch, workspaceRoot) {
|
|
246
|
+
try {
|
|
247
|
+
await run("git", [
|
|
248
|
+
"pull",
|
|
249
|
+
"origin",
|
|
250
|
+
branch
|
|
251
|
+
], { nodeOptions: {
|
|
255
252
|
cwd: workspaceRoot,
|
|
256
253
|
stdio: "pipe"
|
|
257
254
|
} });
|
|
@@ -260,43 +257,18 @@ async function checkoutBranch(branch, workspaceRoot) {
|
|
|
260
257
|
return false;
|
|
261
258
|
}
|
|
262
259
|
}
|
|
263
|
-
/**
|
|
264
|
-
* Get the current branch name
|
|
265
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
266
|
-
* @returns Promise resolving to the current branch name
|
|
267
|
-
*/
|
|
268
|
-
async function getCurrentBranch(workspaceRoot) {
|
|
269
|
-
return (await run("git", [
|
|
270
|
-
"rev-parse",
|
|
271
|
-
"--abbrev-ref",
|
|
272
|
-
"HEAD"
|
|
273
|
-
], { nodeOptions: {
|
|
274
|
-
cwd: workspaceRoot,
|
|
275
|
-
stdio: "pipe"
|
|
276
|
-
} })).stdout.trim();
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Rebase current branch onto another branch
|
|
280
|
-
* @param ontoBranch - The target branch to rebase onto
|
|
281
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
282
|
-
*/
|
|
283
260
|
async function rebaseBranch(ontoBranch, workspaceRoot) {
|
|
284
261
|
try {
|
|
285
262
|
logger.info(`Rebasing onto: ${farver.cyan(ontoBranch)}`);
|
|
286
|
-
await
|
|
263
|
+
await runIfNotDry("git", ["rebase", ontoBranch], { nodeOptions: {
|
|
287
264
|
cwd: workspaceRoot,
|
|
288
265
|
stdio: "pipe"
|
|
289
266
|
} });
|
|
267
|
+
return true;
|
|
290
268
|
} catch {
|
|
291
269
|
exitWithError(`Failed to rebase onto: ${ontoBranch}`, `You may have merge conflicts. Run 'git rebase --abort' to undo the rebase`);
|
|
292
270
|
}
|
|
293
271
|
}
|
|
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
272
|
async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
301
273
|
try {
|
|
302
274
|
const result = await run("git", [
|
|
@@ -312,23 +284,12 @@ async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
|
312
284
|
return true;
|
|
313
285
|
}
|
|
314
286
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Check if there are any changes to commit (staged or unstaged)
|
|
317
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
318
|
-
* @returns Promise resolving to true if there are changes, false otherwise
|
|
319
|
-
*/
|
|
320
287
|
async function hasChangesToCommit(workspaceRoot) {
|
|
321
288
|
return (await run("git", ["status", "--porcelain"], { nodeOptions: {
|
|
322
289
|
cwd: workspaceRoot,
|
|
323
290
|
stdio: "pipe"
|
|
324
291
|
} })).stdout.trim() !== "";
|
|
325
292
|
}
|
|
326
|
-
/**
|
|
327
|
-
* Commit changes with a message
|
|
328
|
-
* @param message - The commit message
|
|
329
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
330
|
-
* @returns Promise resolving to true if commit was made, false if there were no changes
|
|
331
|
-
*/
|
|
332
293
|
async function commitChanges(message, workspaceRoot) {
|
|
333
294
|
try {
|
|
334
295
|
await run("git", ["add", "."], { nodeOptions: {
|
|
@@ -350,51 +311,35 @@ async function commitChanges(message, workspaceRoot) {
|
|
|
350
311
|
exitWithError(`Failed to commit changes`, `Make sure you have git configured properly with user.name and user.email`);
|
|
351
312
|
}
|
|
352
313
|
}
|
|
353
|
-
/**
|
|
354
|
-
* Push branch to remote
|
|
355
|
-
* @param branch - The branch name to push
|
|
356
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
357
|
-
* @param options - Push options
|
|
358
|
-
* @param options.force - Force push (overwrite remote)
|
|
359
|
-
* @param options.forceWithLease - Force push with safety check (won't overwrite unexpected changes)
|
|
360
|
-
*/
|
|
361
314
|
async function pushBranch(branch, workspaceRoot, options) {
|
|
362
315
|
try {
|
|
363
|
-
const args = [
|
|
316
|
+
const args$1 = [
|
|
364
317
|
"push",
|
|
365
318
|
"origin",
|
|
366
319
|
branch
|
|
367
320
|
];
|
|
368
321
|
if (options?.forceWithLease) {
|
|
369
|
-
args.push("--force-with-lease");
|
|
322
|
+
args$1.push("--force-with-lease");
|
|
370
323
|
logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`);
|
|
371
324
|
} else if (options?.force) {
|
|
372
|
-
args.push("--force");
|
|
325
|
+
args$1.push("--force");
|
|
373
326
|
logger.info(`Force pushing branch: ${farver.green(branch)}`);
|
|
374
327
|
} else logger.info(`Pushing branch: ${farver.green(branch)}`);
|
|
375
|
-
await
|
|
328
|
+
await runIfNotDry("git", args$1, { nodeOptions: {
|
|
376
329
|
cwd: workspaceRoot,
|
|
377
330
|
stdio: "pipe"
|
|
378
331
|
} });
|
|
332
|
+
return true;
|
|
379
333
|
} catch {
|
|
380
334
|
exitWithError(`Failed to push branch: ${branch}`, `Make sure you have permission to push to the remote repository`);
|
|
381
335
|
}
|
|
382
336
|
}
|
|
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
|
-
}
|
|
391
|
-
}
|
|
392
337
|
|
|
393
338
|
//#endregion
|
|
394
|
-
//#region src/github.ts
|
|
339
|
+
//#region src/core/github.ts
|
|
395
340
|
async function getExistingPullRequest({ owner, repo, branch, githubToken }) {
|
|
396
341
|
try {
|
|
397
|
-
logger.
|
|
342
|
+
logger.verbose(`Requesting pull request for branch: ${branch} (url: https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch})`);
|
|
398
343
|
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch}`, { headers: {
|
|
399
344
|
Accept: "application/vnd.github.v3+json",
|
|
400
345
|
Authorization: `token ${githubToken}`
|
|
@@ -430,9 +375,10 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
|
|
|
430
375
|
title,
|
|
431
376
|
body,
|
|
432
377
|
head,
|
|
433
|
-
base
|
|
378
|
+
base,
|
|
379
|
+
draft: true
|
|
434
380
|
};
|
|
435
|
-
logger.
|
|
381
|
+
logger.verbose(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${url})`);
|
|
436
382
|
const res = await fetch(url, {
|
|
437
383
|
method,
|
|
438
384
|
headers: {
|
|
@@ -492,178 +438,226 @@ function generatePullRequestBody(updates, body) {
|
|
|
492
438
|
}
|
|
493
439
|
|
|
494
440
|
//#endregion
|
|
495
|
-
//#region src/
|
|
496
|
-
async function
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
|
|
511
|
-
return response.selectedPackages;
|
|
512
|
-
}
|
|
513
|
-
async function promptVersionOverride(pkg, workspaceRoot, currentVersion, suggestedVersion, suggestedBumpType) {
|
|
514
|
-
const choices = [{
|
|
515
|
-
title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
|
|
516
|
-
value: "suggested"
|
|
517
|
-
}];
|
|
518
|
-
for (const bumpType of [
|
|
519
|
-
"patch",
|
|
520
|
-
"minor",
|
|
521
|
-
"major"
|
|
522
|
-
]) if (bumpType !== suggestedBumpType) {
|
|
523
|
-
const version = getNextVersion(currentVersion, bumpType);
|
|
524
|
-
choices.push({
|
|
525
|
-
title: `${bumpType}: ${version}`,
|
|
526
|
-
value: bumpType
|
|
527
|
-
});
|
|
441
|
+
//#region src/versioning/commits.ts
|
|
442
|
+
async function getLastPackageTag(packageName, workspaceRoot) {
|
|
443
|
+
try {
|
|
444
|
+
const { stdout } = await run("git", [
|
|
445
|
+
"tag",
|
|
446
|
+
"--list",
|
|
447
|
+
`${packageName}@*`
|
|
448
|
+
], { nodeOptions: {
|
|
449
|
+
cwd: workspaceRoot,
|
|
450
|
+
stdio: "pipe"
|
|
451
|
+
} });
|
|
452
|
+
return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse()[0];
|
|
453
|
+
} catch (err) {
|
|
454
|
+
logger.warn(`Failed to get tags for package ${packageName}: ${err.message}`);
|
|
455
|
+
return;
|
|
528
456
|
}
|
|
529
|
-
choices.push({
|
|
530
|
-
title: "Custom version",
|
|
531
|
-
value: "custom"
|
|
532
|
-
});
|
|
533
|
-
const response = await prompts([{
|
|
534
|
-
type: "select",
|
|
535
|
-
name: "choice",
|
|
536
|
-
message: `${pkg.name} (${currentVersion}):`,
|
|
537
|
-
choices,
|
|
538
|
-
initial: 0
|
|
539
|
-
}, {
|
|
540
|
-
type: (prev) => prev === "custom" ? "text" : null,
|
|
541
|
-
name: "customVersion",
|
|
542
|
-
message: "Enter custom version:",
|
|
543
|
-
initial: suggestedVersion,
|
|
544
|
-
validate: (value) => {
|
|
545
|
-
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(value) || "Invalid semver version (e.g., 1.0.0)";
|
|
546
|
-
}
|
|
547
|
-
}]);
|
|
548
|
-
if (response.choice === "suggested") return suggestedVersion;
|
|
549
|
-
else if (response.choice === "custom") return response.customVersion;
|
|
550
|
-
else return getNextVersion(currentVersion, response.choice);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
//#endregion
|
|
554
|
-
//#region src/version.ts
|
|
555
|
-
function isValidSemver(version) {
|
|
556
|
-
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
|
|
557
457
|
}
|
|
558
|
-
function
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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;
|
|
458
|
+
function determineHighestBump(commits) {
|
|
459
|
+
if (commits.length === 0) return "none";
|
|
460
|
+
let highestBump = "none";
|
|
461
|
+
for (const commit of commits) {
|
|
462
|
+
const bump = determineBumpType(commit);
|
|
463
|
+
if (bump === "major") return "major";
|
|
464
|
+
if (bump === "minor") highestBump = "minor";
|
|
465
|
+
else if (bump === "patch" && highestBump === "none") highestBump = "patch";
|
|
583
466
|
}
|
|
584
|
-
return
|
|
585
|
-
}
|
|
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
|
-
};
|
|
467
|
+
return highestBump;
|
|
598
468
|
}
|
|
599
469
|
/**
|
|
600
|
-
*
|
|
470
|
+
* Retrieves commits that affect a specific workspace package since its last tag.
|
|
601
471
|
*
|
|
602
|
-
* @param
|
|
603
|
-
* @param
|
|
604
|
-
* @
|
|
605
|
-
* @param showPrompt - Whether to show prompts for version overrides
|
|
606
|
-
* @returns Version updates for packages with changes
|
|
472
|
+
* @param {string} workspaceRoot - The root directory of the workspace.
|
|
473
|
+
* @param {WorkspacePackage} pkg - The workspace package to analyze.
|
|
474
|
+
* @returns {Promise<GitCommit[]>} A promise that resolves to an array of GitCommit objects affecting the package.
|
|
607
475
|
*/
|
|
608
|
-
async function
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
476
|
+
async function getCommitsForWorkspacePackage(workspaceRoot, pkg) {
|
|
477
|
+
const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
|
|
478
|
+
const allCommits = getCommits({
|
|
479
|
+
from: lastTag,
|
|
480
|
+
to: "HEAD",
|
|
481
|
+
cwd: workspaceRoot
|
|
482
|
+
});
|
|
483
|
+
logger.verbose("Found commits for package", `${farver.cyan(allCommits.length)} for ${farver.bold(pkg.name)} since ${lastTag || "beginning"}`);
|
|
484
|
+
const commitsAffectingPackage = getCommits({
|
|
485
|
+
from: lastTag,
|
|
486
|
+
to: "HEAD",
|
|
487
|
+
cwd: workspaceRoot,
|
|
488
|
+
folder: pkg.path
|
|
489
|
+
});
|
|
490
|
+
const affectingCommitShas = /* @__PURE__ */ new Set();
|
|
491
|
+
for (const commit of commitsAffectingPackage) affectingCommitShas.add(commit.shortHash);
|
|
492
|
+
const packageCommits = allCommits.filter((commit) => {
|
|
493
|
+
return affectingCommitShas.has(commit.shortHash);
|
|
494
|
+
});
|
|
495
|
+
logger.verbose("Commits affect package", `${farver.cyan(packageCommits.length)} affect ${farver.bold(pkg.name)}`);
|
|
496
|
+
return packageCommits;
|
|
497
|
+
}
|
|
498
|
+
async function getWorkspacePackageCommits(workspaceRoot, packages) {
|
|
499
|
+
const changedPackages = /* @__PURE__ */ new Map();
|
|
500
|
+
const promises = packages.map(async (pkg) => {
|
|
501
|
+
return {
|
|
502
|
+
pkgName: pkg.name,
|
|
503
|
+
commits: await getCommitsForWorkspacePackage(workspaceRoot, pkg)
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
const results = await Promise.all(promises);
|
|
507
|
+
for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
|
|
508
|
+
return changedPackages;
|
|
509
|
+
}
|
|
510
|
+
async function getCommitFileList(workspaceRoot, from, to) {
|
|
511
|
+
const map = /* @__PURE__ */ new Map();
|
|
512
|
+
try {
|
|
513
|
+
const { stdout } = await run("git", [
|
|
514
|
+
"log",
|
|
515
|
+
"--name-only",
|
|
516
|
+
"--format=%H",
|
|
517
|
+
`${from}^..${to}`
|
|
518
|
+
], { nodeOptions: {
|
|
519
|
+
cwd: workspaceRoot,
|
|
520
|
+
stdio: "pipe"
|
|
521
|
+
} });
|
|
522
|
+
const lines = stdout.trim().split("\n");
|
|
523
|
+
let currentSha = null;
|
|
524
|
+
for (const line of lines) {
|
|
525
|
+
const trimmedLine = line.trim();
|
|
526
|
+
if (trimmedLine === "") {
|
|
527
|
+
currentSha = null;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (currentSha === null) {
|
|
531
|
+
currentSha = trimmedLine;
|
|
532
|
+
map.set(currentSha, []);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
map.get(currentSha).push(trimmedLine);
|
|
618
536
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
package: pkg,
|
|
623
|
-
currentVersion: pkg.version,
|
|
624
|
-
newVersion,
|
|
625
|
-
bumpType: bump,
|
|
626
|
-
hasDirectChanges: true
|
|
627
|
-
});
|
|
537
|
+
return map;
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
628
540
|
}
|
|
629
|
-
return versionUpdates;
|
|
630
541
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
packageJson.devDependencies[depName] = `^${depVersion}`;
|
|
644
|
-
}
|
|
645
|
-
if (packageJson.peerDependencies?.[depName]) {
|
|
646
|
-
if (packageJson.peerDependencies[depName] === "workspace:*") continue;
|
|
647
|
-
packageJson.peerDependencies[depName] = `^${depVersion}`;
|
|
648
|
-
}
|
|
542
|
+
/**
|
|
543
|
+
* Check if a file path touches any package folder.
|
|
544
|
+
* @param file - The file path to check
|
|
545
|
+
* @param packagePaths - Set of normalized package paths
|
|
546
|
+
* @param workspaceRoot - The workspace root for path normalization
|
|
547
|
+
* @returns true if the file is inside a package folder
|
|
548
|
+
*/
|
|
549
|
+
function fileMatchesPackageFolder(file, packagePaths, workspaceRoot) {
|
|
550
|
+
const normalizedFile = file.startsWith("./") ? file.slice(2) : file;
|
|
551
|
+
for (const pkgPath of packagePaths) {
|
|
552
|
+
const normalizedPkgPath = pkgPath.startsWith(workspaceRoot) ? pkgPath.slice(workspaceRoot.length + 1) : pkgPath;
|
|
553
|
+
if (normalizedFile.startsWith(`${normalizedPkgPath}/`) || normalizedFile === normalizedPkgPath) return true;
|
|
649
554
|
}
|
|
650
|
-
|
|
555
|
+
return false;
|
|
651
556
|
}
|
|
652
557
|
/**
|
|
653
|
-
*
|
|
558
|
+
* Check if a commit is a "global" commit (doesn't touch any package folder).
|
|
559
|
+
* @param files - Array of files changed in the commit
|
|
560
|
+
* @param packagePaths - Set of normalized package paths
|
|
561
|
+
* @param workspaceRoot - The workspace root
|
|
562
|
+
* @returns true if this is a global commit
|
|
654
563
|
*/
|
|
655
|
-
function
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
564
|
+
function isGlobalCommit(files, packagePaths, workspaceRoot) {
|
|
565
|
+
if (!files || files.length === 0) return false;
|
|
566
|
+
return !files.some((file) => fileMatchesPackageFolder(file, packagePaths, workspaceRoot));
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Get global commits for each package based on their individual commit timelines.
|
|
570
|
+
* This solves the problem where packages with different release histories need different global commits.
|
|
571
|
+
*
|
|
572
|
+
* A "global commit" is a commit that doesn't touch any package folder but may affect all packages
|
|
573
|
+
* (e.g., root package.json, CI config, README).
|
|
574
|
+
*
|
|
575
|
+
* Performance: Makes ONE batched git call to get files for all commits across all packages.
|
|
576
|
+
*
|
|
577
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
578
|
+
* @param packageCommits - Map of package name to their commits (from getWorkspacePackageCommits)
|
|
579
|
+
* @param allPackages - All workspace packages (used to identify package folders)
|
|
580
|
+
* @param mode - Filter mode: false (disabled), "all" (all global commits), or "dependencies" (only dependency-related)
|
|
581
|
+
* @returns Map of package name to their global commits
|
|
582
|
+
*/
|
|
583
|
+
async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPackages, mode) {
|
|
584
|
+
const result = /* @__PURE__ */ new Map();
|
|
585
|
+
if (!mode) {
|
|
586
|
+
logger.verbose("Global commits mode disabled");
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
logger.verbose(`Computing global commits per-package (mode: ${farver.cyan(mode)})`);
|
|
590
|
+
let oldestCommit = null;
|
|
591
|
+
let newestCommit = null;
|
|
592
|
+
for (const commits of packageCommits.values()) if (commits.length > 0) {
|
|
593
|
+
if (!newestCommit) newestCommit = commits[0].shortHash;
|
|
594
|
+
oldestCommit = commits[commits.length - 1].shortHash;
|
|
595
|
+
}
|
|
596
|
+
if (!oldestCommit || !newestCommit) {
|
|
597
|
+
logger.verbose("No commits found across packages");
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
logger.verbose("Fetching files for commits range", `${farver.cyan(oldestCommit)}..${farver.cyan(newestCommit)}`);
|
|
601
|
+
const commitFilesMap = await getCommitFileList(workspaceRoot, oldestCommit, newestCommit);
|
|
602
|
+
if (!commitFilesMap) {
|
|
603
|
+
logger.warn("Failed to get commit file list, returning empty global commits");
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.size)} commits in ONE git call`);
|
|
607
|
+
const packagePaths = new Set(allPackages.map((p) => p.path));
|
|
608
|
+
for (const [pkgName, commits] of packageCommits) {
|
|
609
|
+
const globalCommitsForPackage = [];
|
|
610
|
+
logger.verbose("Filtering global commits for package", `${farver.bold(pkgName)} from ${farver.cyan(commits.length)} commits`);
|
|
611
|
+
for (const commit of commits) if (isGlobalCommit(commitFilesMap.get(commit.shortHash), packagePaths, workspaceRoot)) globalCommitsForPackage.push(commit);
|
|
612
|
+
logger.verbose("Package global commits found", `${farver.bold(pkgName)}: ${farver.cyan(globalCommitsForPackage.length)} global commits`);
|
|
613
|
+
if (mode === "all") result.set(pkgName, globalCommitsForPackage);
|
|
614
|
+
else if (mode === "dependencies") {
|
|
615
|
+
const dependencyCommits = [];
|
|
616
|
+
const dependencyFiles = [
|
|
617
|
+
"package.json",
|
|
618
|
+
"pnpm-lock.yaml",
|
|
619
|
+
"pnpm-workspace.yaml",
|
|
620
|
+
"yarn.lock",
|
|
621
|
+
"package-lock.json"
|
|
622
|
+
];
|
|
623
|
+
for (const commit of globalCommitsForPackage) {
|
|
624
|
+
const files = commitFilesMap.get(commit.shortHash);
|
|
625
|
+
if (!files) continue;
|
|
626
|
+
if (files.some((file) => {
|
|
627
|
+
const normalizedFile = file.startsWith("./") ? file.slice(2) : file;
|
|
628
|
+
return dependencyFiles.includes(normalizedFile);
|
|
629
|
+
})) {
|
|
630
|
+
logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`);
|
|
631
|
+
dependencyCommits.push(commit);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
logger.verbose("Global commits affect dependencies", `${farver.bold(pkgName)}: ${farver.cyan(dependencyCommits.length)} global commits affect dependencies`);
|
|
635
|
+
result.set(pkgName, dependencyCommits);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
function determineBumpType(commit) {
|
|
641
|
+
if (commit.isBreaking) return "major";
|
|
642
|
+
if (!commit.isConventional || !commit.type) return "none";
|
|
643
|
+
switch (commit.type) {
|
|
644
|
+
case "feat": return "minor";
|
|
645
|
+
case "fix":
|
|
646
|
+
case "perf": return "patch";
|
|
647
|
+
case "docs":
|
|
648
|
+
case "style":
|
|
649
|
+
case "refactor":
|
|
650
|
+
case "test":
|
|
651
|
+
case "build":
|
|
652
|
+
case "ci":
|
|
653
|
+
case "chore":
|
|
654
|
+
case "revert": return "none";
|
|
655
|
+
default: return "none";
|
|
661
656
|
}
|
|
662
|
-
return updates;
|
|
663
657
|
}
|
|
664
658
|
|
|
665
659
|
//#endregion
|
|
666
|
-
//#region src/package.ts
|
|
660
|
+
//#region src/versioning/package.ts
|
|
667
661
|
/**
|
|
668
662
|
* Build a dependency graph from workspace packages
|
|
669
663
|
*
|
|
@@ -731,29 +725,326 @@ function createDependentUpdates(graph, workspacePackages, directUpdates) {
|
|
|
731
725
|
const directUpdateMap = new Map(directUpdates.map((u) => [u.package.name, u]));
|
|
732
726
|
const affectedPackages = getAllAffectedPackages(graph, new Set(directUpdates.map((u) => u.package.name)));
|
|
733
727
|
for (const pkgName of affectedPackages) {
|
|
734
|
-
|
|
728
|
+
logger.verbose(`Processing affected package: ${pkgName}`);
|
|
729
|
+
if (directUpdateMap.has(pkgName)) {
|
|
730
|
+
logger.verbose(`Skipping ${pkgName}, already has a direct update`);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
735
733
|
const pkg = workspacePackages.find((p) => p.name === pkgName);
|
|
736
734
|
if (!pkg) continue;
|
|
737
735
|
allUpdates.push(createVersionUpdate(pkg, "patch", false));
|
|
738
736
|
}
|
|
739
737
|
return allUpdates;
|
|
740
738
|
}
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/versioning/version.ts
|
|
742
|
+
function isValidSemver(version) {
|
|
743
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
|
|
744
|
+
}
|
|
745
|
+
function getNextVersion(currentVersion, bump) {
|
|
746
|
+
if (bump === "none") {
|
|
747
|
+
logger.verbose(`No version bump needed, keeping version ${currentVersion}`);
|
|
748
|
+
return currentVersion;
|
|
749
|
+
}
|
|
750
|
+
if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
|
|
751
|
+
const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
752
|
+
if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
|
|
753
|
+
const [, major, minor, patch] = match;
|
|
754
|
+
let newMajor = Number.parseInt(major, 10);
|
|
755
|
+
let newMinor = Number.parseInt(minor, 10);
|
|
756
|
+
let newPatch = Number.parseInt(patch, 10);
|
|
757
|
+
switch (bump) {
|
|
758
|
+
case "major":
|
|
759
|
+
newMajor += 1;
|
|
760
|
+
newMinor = 0;
|
|
761
|
+
newPatch = 0;
|
|
762
|
+
break;
|
|
763
|
+
case "minor":
|
|
764
|
+
newMinor += 1;
|
|
765
|
+
newPatch = 0;
|
|
766
|
+
break;
|
|
767
|
+
case "patch":
|
|
768
|
+
newPatch += 1;
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
return `${newMajor}.${newMinor}.${newPatch}`;
|
|
772
|
+
}
|
|
741
773
|
/**
|
|
742
|
-
*
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
774
|
+
* Create a version update object
|
|
775
|
+
*/
|
|
776
|
+
function createVersionUpdate(pkg, bump, hasDirectChanges) {
|
|
777
|
+
const newVersion = getNextVersion(pkg.version, bump);
|
|
778
|
+
return {
|
|
779
|
+
package: pkg,
|
|
780
|
+
currentVersion: pkg.version,
|
|
781
|
+
newVersion,
|
|
782
|
+
bumpType: bump,
|
|
783
|
+
hasDirectChanges
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function _calculateBumpType(oldVersion, newVersion) {
|
|
787
|
+
if (!isValidSemver(oldVersion) || !isValidSemver(newVersion)) throw new Error(`Cannot calculate bump type for invalid semver: ${oldVersion} or ${newVersion}`);
|
|
788
|
+
const oldParts = oldVersion.split(".").map(Number);
|
|
789
|
+
const newParts = newVersion.split(".").map(Number);
|
|
790
|
+
if (newParts[0] > oldParts[0]) return "major";
|
|
791
|
+
if (newParts[1] > oldParts[1]) return "minor";
|
|
792
|
+
if (newParts[2] > oldParts[2]) return "patch";
|
|
793
|
+
return "none";
|
|
794
|
+
}
|
|
795
|
+
const messageColorMap = {
|
|
796
|
+
feat: farver.green,
|
|
797
|
+
feature: farver.green,
|
|
798
|
+
refactor: farver.cyan,
|
|
799
|
+
style: farver.cyan,
|
|
800
|
+
docs: farver.blue,
|
|
801
|
+
doc: farver.blue,
|
|
802
|
+
types: farver.blue,
|
|
803
|
+
type: farver.blue,
|
|
804
|
+
chore: farver.gray,
|
|
805
|
+
ci: farver.gray,
|
|
806
|
+
build: farver.gray,
|
|
807
|
+
deps: farver.gray,
|
|
808
|
+
dev: farver.gray,
|
|
809
|
+
fix: farver.yellow,
|
|
810
|
+
test: farver.yellow,
|
|
811
|
+
perf: farver.magenta,
|
|
812
|
+
revert: farver.red,
|
|
813
|
+
breaking: farver.red
|
|
814
|
+
};
|
|
815
|
+
function formatCommitsForDisplay(commits) {
|
|
816
|
+
if (commits.length === 0) return farver.dim("No commits found");
|
|
817
|
+
const maxCommitsToShow = 10;
|
|
818
|
+
const commitsToShow = commits.slice(0, maxCommitsToShow);
|
|
819
|
+
const hasMore = commits.length > maxCommitsToShow;
|
|
820
|
+
const typeLength = commits.map(({ type }) => type.length).reduce((a, b) => Math.max(a, b), 0);
|
|
821
|
+
const scopeLength = commits.map(({ scope }) => scope.length).reduce((a, b) => Math.max(a, b), 0);
|
|
822
|
+
const formattedCommits = commitsToShow.map((commit) => {
|
|
823
|
+
let color = messageColorMap[commit.type] || ((c) => c);
|
|
824
|
+
if (commit.isBreaking) color = (s) => farver.inverse.red(s);
|
|
825
|
+
const paddedType = commit.type.padStart(typeLength + 1, " ");
|
|
826
|
+
const paddedScope = !commit.scope ? " ".repeat(scopeLength ? scopeLength + 2 : 0) : farver.dim("(") + commit.scope + farver.dim(")") + " ".repeat(scopeLength - commit.scope.length);
|
|
827
|
+
return [
|
|
828
|
+
farver.dim(commit.shortHash),
|
|
829
|
+
" ",
|
|
830
|
+
color === farver.gray ? color(paddedType) : farver.bold(color(paddedType)),
|
|
831
|
+
" ",
|
|
832
|
+
paddedScope,
|
|
833
|
+
farver.dim(":"),
|
|
834
|
+
" ",
|
|
835
|
+
color === farver.gray ? color(commit.description) : commit.description
|
|
836
|
+
].join("");
|
|
837
|
+
}).join("\n");
|
|
838
|
+
if (hasMore) return `${formattedCommits}\n ${farver.dim(`... and ${commits.length - maxCommitsToShow} more commits`)}`;
|
|
839
|
+
return formattedCommits;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Calculate version updates for packages based on their commits
|
|
843
|
+
*/
|
|
844
|
+
async function calculateVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage }) {
|
|
845
|
+
const versionUpdates = [];
|
|
846
|
+
const processedPackages = /* @__PURE__ */ new Set();
|
|
847
|
+
logger.verbose(`Starting version inference for ${packageCommits.size} packages with commits`);
|
|
848
|
+
for (const [pkgName, pkgCommits] of packageCommits) {
|
|
849
|
+
const pkg = workspacePackages.find((p) => p.name === pkgName);
|
|
850
|
+
if (!pkg) {
|
|
851
|
+
logger.error(`Package ${pkgName} not found in workspace packages, skipping`);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
processedPackages.add(pkgName);
|
|
855
|
+
const globalCommits = globalCommitsPerPackage.get(pkgName) || [];
|
|
856
|
+
if (globalCommits.length > 0) logger.verbose(` - Global commits for this package: ${globalCommits.length}`);
|
|
857
|
+
const allCommitsForPackage = [...pkgCommits, ...globalCommits];
|
|
858
|
+
const bump = determineHighestBump(allCommitsForPackage);
|
|
859
|
+
if (bump === "none") continue;
|
|
860
|
+
let newVersion = getNextVersion(pkg.version, bump);
|
|
861
|
+
if (!isCI && showPrompt) {
|
|
862
|
+
logger.section("📝 Commits affecting this package");
|
|
863
|
+
formatCommitsForDisplay(allCommitsForPackage).split("\n").forEach((line) => logger.item(line));
|
|
864
|
+
logger.emptyLine();
|
|
865
|
+
const selectedVersion = await selectVersionPrompt(workspaceRoot, pkg, pkg.version, newVersion);
|
|
866
|
+
if (selectedVersion === null) continue;
|
|
867
|
+
newVersion = selectedVersion;
|
|
868
|
+
}
|
|
869
|
+
logger.item(`Version update: ${pkg.version} → ${newVersion}`);
|
|
870
|
+
versionUpdates.push({
|
|
871
|
+
package: pkg,
|
|
872
|
+
currentVersion: pkg.version,
|
|
873
|
+
newVersion,
|
|
874
|
+
bumpType: bump,
|
|
875
|
+
hasDirectChanges: true
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
if (!isCI && showPrompt) for (const pkg of workspacePackages) {
|
|
879
|
+
if (processedPackages.has(pkg.name)) continue;
|
|
880
|
+
logger.section(`📦 Package: ${pkg.name}`);
|
|
881
|
+
logger.item("No direct commits found");
|
|
882
|
+
const newVersion = await selectVersionPrompt(workspaceRoot, pkg, pkg.version, pkg.version);
|
|
883
|
+
if (newVersion === null) break;
|
|
884
|
+
if (newVersion !== pkg.version) {
|
|
885
|
+
const bumpType = _calculateBumpType(pkg.version, newVersion);
|
|
886
|
+
versionUpdates.push({
|
|
887
|
+
package: pkg,
|
|
888
|
+
currentVersion: pkg.version,
|
|
889
|
+
newVersion,
|
|
890
|
+
bumpType,
|
|
891
|
+
hasDirectChanges: false
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return versionUpdates;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Calculate version updates and prepare dependent updates
|
|
899
|
+
* Returns both the updates and a function to apply them
|
|
747
900
|
*/
|
|
748
|
-
async function
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
901
|
+
async function calculateAndPrepareVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage }) {
|
|
902
|
+
const directUpdates = await calculateVersionUpdates({
|
|
903
|
+
workspacePackages,
|
|
904
|
+
packageCommits,
|
|
905
|
+
workspaceRoot,
|
|
906
|
+
showPrompt,
|
|
907
|
+
globalCommitsPerPackage
|
|
908
|
+
});
|
|
909
|
+
const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, directUpdates);
|
|
910
|
+
const applyUpdates = async () => {
|
|
911
|
+
await Promise.all(allUpdates.map(async (update) => {
|
|
912
|
+
const depUpdates = getDependencyUpdates(update.package, allUpdates);
|
|
913
|
+
await updatePackageJson(update.package, update.newVersion, depUpdates);
|
|
914
|
+
}));
|
|
915
|
+
};
|
|
916
|
+
return {
|
|
917
|
+
allUpdates,
|
|
918
|
+
applyUpdates
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
|
|
922
|
+
const packageJsonPath = join(pkg.path, "package.json");
|
|
923
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
924
|
+
const packageJson = JSON.parse(content);
|
|
925
|
+
packageJson.version = newVersion;
|
|
926
|
+
for (const [depName, depVersion] of dependencyUpdates) {
|
|
927
|
+
if (packageJson.dependencies?.[depName]) {
|
|
928
|
+
const oldVersion = packageJson.dependencies[depName];
|
|
929
|
+
if (oldVersion === "workspace:*") {
|
|
930
|
+
logger.verbose(` - Skipping workspace:* dependency: ${depName}`);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
packageJson.dependencies[depName] = `^${depVersion}`;
|
|
934
|
+
logger.verbose(` - Updated dependency ${depName}: ${oldVersion} → ^${depVersion}`);
|
|
935
|
+
}
|
|
936
|
+
if (packageJson.devDependencies?.[depName]) {
|
|
937
|
+
const oldVersion = packageJson.devDependencies[depName];
|
|
938
|
+
if (oldVersion === "workspace:*") {
|
|
939
|
+
logger.verbose(` - Skipping workspace:* devDependency: ${depName}`);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
packageJson.devDependencies[depName] = `^${depVersion}`;
|
|
943
|
+
logger.verbose(` - Updated devDependency ${depName}: ${oldVersion} → ^${depVersion}`);
|
|
944
|
+
}
|
|
945
|
+
if (packageJson.peerDependencies?.[depName]) {
|
|
946
|
+
const oldVersion = packageJson.peerDependencies[depName];
|
|
947
|
+
if (oldVersion === "workspace:*") {
|
|
948
|
+
logger.verbose(` - Skipping workspace:* peerDependency: ${depName}`);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
const majorVersion = depVersion.split(".")[0];
|
|
952
|
+
packageJson.peerDependencies[depName] = `>=${depVersion} <${Number(majorVersion) + 1}.0.0`;
|
|
953
|
+
logger.verbose(` - Updated peerDependency ${depName}: ${oldVersion} → ^${depVersion}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
|
|
957
|
+
logger.verbose(` - Successfully wrote updated package.json`);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Get all dependency updates needed for a package
|
|
961
|
+
*/
|
|
962
|
+
function getDependencyUpdates(pkg, allUpdates) {
|
|
963
|
+
const updates = /* @__PURE__ */ new Map();
|
|
964
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
965
|
+
for (const dep of allDeps) {
|
|
966
|
+
const update = allUpdates.find((u) => u.package.name === dep);
|
|
967
|
+
if (update) {
|
|
968
|
+
logger.verbose(` - Dependency ${dep} will be updated: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`);
|
|
969
|
+
updates.set(dep, update.newVersion);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (updates.size === 0) logger.verbose(` - No dependency updates needed`);
|
|
973
|
+
return updates;
|
|
753
974
|
}
|
|
754
975
|
|
|
755
976
|
//#endregion
|
|
756
|
-
//#region src/
|
|
977
|
+
//#region src/core/prompts.ts
|
|
978
|
+
async function selectPackagePrompt(packages) {
|
|
979
|
+
const response = await prompts({
|
|
980
|
+
type: "multiselect",
|
|
981
|
+
name: "selectedPackages",
|
|
982
|
+
message: "Select packages to release",
|
|
983
|
+
choices: packages.map((pkg) => ({
|
|
984
|
+
title: `${pkg.name} (${farver.bold(pkg.version)})`,
|
|
985
|
+
value: pkg.name,
|
|
986
|
+
selected: true
|
|
987
|
+
})),
|
|
988
|
+
min: 1,
|
|
989
|
+
hint: "Space to select/deselect. Return to submit.",
|
|
990
|
+
instructions: false
|
|
991
|
+
});
|
|
992
|
+
if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
|
|
993
|
+
return response.selectedPackages;
|
|
994
|
+
}
|
|
995
|
+
async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggestedVersion) {
|
|
996
|
+
const answers = await prompts([{
|
|
997
|
+
type: "autocomplete",
|
|
998
|
+
name: "version",
|
|
999
|
+
message: `${pkg.name}: ${farver.green(pkg.version)}`,
|
|
1000
|
+
choices: [
|
|
1001
|
+
{
|
|
1002
|
+
value: "skip",
|
|
1003
|
+
title: `skip ${farver.dim("(no change)")}`
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
value: "major",
|
|
1007
|
+
title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}`
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
value: "minor",
|
|
1011
|
+
title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}`
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
value: "patch",
|
|
1015
|
+
title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
value: "suggested",
|
|
1019
|
+
title: `suggested ${farver.bold(suggestedVersion)}`
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
value: "custom",
|
|
1023
|
+
title: "custom"
|
|
1024
|
+
}
|
|
1025
|
+
],
|
|
1026
|
+
initial: suggestedVersion === currentVersion ? 0 : 4
|
|
1027
|
+
}, {
|
|
1028
|
+
type: (prev) => prev === "custom" ? "text" : null,
|
|
1029
|
+
name: "custom",
|
|
1030
|
+
message: "Enter the new version number:",
|
|
1031
|
+
initial: suggestedVersion,
|
|
1032
|
+
validate: (custom) => {
|
|
1033
|
+
if (isValidSemver(custom)) return true;
|
|
1034
|
+
return "That's not a valid version number";
|
|
1035
|
+
}
|
|
1036
|
+
}]);
|
|
1037
|
+
if (!answers.version) return null;
|
|
1038
|
+
if (answers.version === "skip") return null;
|
|
1039
|
+
else if (answers.version === "suggested") return suggestedVersion;
|
|
1040
|
+
else if (answers.version === "custom") {
|
|
1041
|
+
if (!answers.custom) return null;
|
|
1042
|
+
return answers.custom;
|
|
1043
|
+
} else return getNextVersion(pkg.version, answers.version);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region src/core/workspace.ts
|
|
757
1048
|
async function discoverWorkspacePackages(workspaceRoot, options) {
|
|
758
1049
|
let workspaceOptions;
|
|
759
1050
|
let explicitPackages;
|
|
@@ -835,88 +1126,153 @@ function shouldIncludePackage(pkg, options) {
|
|
|
835
1126
|
//#endregion
|
|
836
1127
|
//#region src/release.ts
|
|
837
1128
|
async function release(options) {
|
|
838
|
-
const normalizedOptions =
|
|
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;
|
|
1129
|
+
const { workspaceRoot,...normalizedOptions } = await normalizeReleaseOptions(options);
|
|
846
1130
|
if (normalizedOptions.safeguards && !await isWorkingDirectoryClean(workspaceRoot)) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
847
1131
|
const workspacePackages = await discoverWorkspacePackages(workspaceRoot, options);
|
|
848
1132
|
if (workspacePackages.length === 0) {
|
|
849
|
-
logger.
|
|
1133
|
+
logger.warn("No packages found to release");
|
|
850
1134
|
return null;
|
|
851
1135
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const existingPullRequest = await getExistingPullRequest({
|
|
858
|
-
owner: normalizedOptions.owner,
|
|
859
|
-
repo: normalizedOptions.repo,
|
|
860
|
-
branch: normalizedOptions.branch.release,
|
|
861
|
-
githubToken: normalizedOptions.githubToken
|
|
862
|
-
});
|
|
863
|
-
const prExists = !!existingPullRequest;
|
|
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);
|
|
867
|
-
if (!branchExists) {
|
|
868
|
-
logger.log("Creating release branch:", normalizedOptions.branch.release);
|
|
869
|
-
await createBranch(normalizedOptions.branch.release, normalizedOptions.branch.default, workspaceRoot);
|
|
870
|
-
}
|
|
871
|
-
if (!await checkoutBranch(normalizedOptions.branch.release, workspaceRoot)) throw new Error(`Failed to checkout branch: ${normalizedOptions.branch.release}`);
|
|
872
|
-
if (branchExists) {
|
|
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);
|
|
884
|
-
if (prExists) {
|
|
885
|
-
logger.log("No updates needed, PR is already up to date");
|
|
886
|
-
return {
|
|
887
|
-
updates: allUpdates,
|
|
888
|
-
prUrl: existingPullRequest.html_url,
|
|
889
|
-
created: false
|
|
890
|
-
};
|
|
891
|
-
} else {
|
|
892
|
-
logger.error("No changes to commit, and no existing PR. Nothing to do.");
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
1136
|
+
logger.section("📦 Workspace Packages");
|
|
1137
|
+
logger.item(`Found ${workspacePackages.length} packages`);
|
|
1138
|
+
for (const pkg of workspacePackages) {
|
|
1139
|
+
logger.item(`${farver.cyan(pkg.name)} (${farver.bold(pkg.version)})`);
|
|
1140
|
+
logger.item(` ${farver.gray("→")} ${farver.gray(pkg.path)}`);
|
|
895
1141
|
}
|
|
896
|
-
logger.
|
|
897
|
-
await
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
|
|
1142
|
+
logger.emptyLine();
|
|
1143
|
+
const packageCommits = await getWorkspacePackageCommits(workspaceRoot, workspacePackages);
|
|
1144
|
+
const globalCommitsPerPackage = await getGlobalCommitsPerPackage(workspaceRoot, packageCommits, workspacePackages, normalizedOptions.globalCommitMode);
|
|
1145
|
+
const { allUpdates, applyUpdates } = await calculateAndPrepareVersionUpdates({
|
|
1146
|
+
workspacePackages,
|
|
1147
|
+
packageCommits,
|
|
1148
|
+
workspaceRoot,
|
|
1149
|
+
showPrompt: options.prompts?.versions !== false,
|
|
1150
|
+
globalCommitsPerPackage
|
|
1151
|
+
});
|
|
1152
|
+
if (allUpdates.filter((u) => u.hasDirectChanges).length === 0) logger.warn("No packages have changes requiring a release");
|
|
1153
|
+
logger.section("🔄 Version Updates");
|
|
1154
|
+
logger.item(`Updating ${allUpdates.length} packages (including dependents)`);
|
|
1155
|
+
for (const update of allUpdates) logger.item(`${update.package.name}: ${update.currentVersion} → ${update.newVersion}`);
|
|
1156
|
+
const prOps = await orchestrateReleasePullRequest({
|
|
1157
|
+
workspaceRoot,
|
|
901
1158
|
owner: normalizedOptions.owner,
|
|
902
1159
|
repo: normalizedOptions.repo,
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
githubToken: normalizedOptions.githubToken
|
|
1160
|
+
githubToken: normalizedOptions.githubToken,
|
|
1161
|
+
releaseBranch: normalizedOptions.branch.release,
|
|
1162
|
+
defaultBranch: normalizedOptions.branch.default,
|
|
1163
|
+
pullRequestTitle: options.pullRequest?.title,
|
|
1164
|
+
pullRequestBody: options.pullRequest?.body
|
|
909
1165
|
});
|
|
910
|
-
|
|
911
|
-
await
|
|
1166
|
+
await prOps.prepareBranch();
|
|
1167
|
+
await applyUpdates();
|
|
1168
|
+
if (!await prOps.commitAndPush(true)) if (prOps.doesReleasePRExist && prOps.existingPullRequest) {
|
|
1169
|
+
logger.item("No updates needed, PR is already up to date");
|
|
1170
|
+
return {
|
|
1171
|
+
updates: allUpdates,
|
|
1172
|
+
prUrl: prOps.existingPullRequest.html_url,
|
|
1173
|
+
created: false
|
|
1174
|
+
};
|
|
1175
|
+
} else {
|
|
1176
|
+
logger.error("No changes to commit, and no existing PR. Nothing to do.");
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
const { pullRequest, created } = await prOps.createOrUpdatePullRequest(allUpdates);
|
|
1180
|
+
await prOps.checkoutDefaultBranch();
|
|
912
1181
|
if (pullRequest?.html_url) {
|
|
913
|
-
logger.
|
|
914
|
-
logger.
|
|
1182
|
+
logger.section("🚀 Pull Request");
|
|
1183
|
+
logger.success(`Pull request ${created ? "created" : "updated"}: ${pullRequest.html_url}`);
|
|
915
1184
|
}
|
|
916
1185
|
return {
|
|
917
1186
|
updates: allUpdates,
|
|
918
1187
|
prUrl: pullRequest?.html_url,
|
|
919
|
-
created
|
|
1188
|
+
created
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
async function normalizeReleaseOptions(options) {
|
|
1192
|
+
const normalized = normalizeSharedOptions(options);
|
|
1193
|
+
let defaultBranch = options.branch?.default?.trim();
|
|
1194
|
+
const releaseBranch = options.branch?.release?.trim() ?? "release/next";
|
|
1195
|
+
if (defaultBranch == null || defaultBranch === "") {
|
|
1196
|
+
defaultBranch = await getDefaultBranch(normalized.workspaceRoot);
|
|
1197
|
+
if (!defaultBranch) exitWithError("Could not determine default branch", "Please specify the default branch in options");
|
|
1198
|
+
}
|
|
1199
|
+
if (defaultBranch === releaseBranch) exitWithError(`Default branch and release branch cannot be the same: "${defaultBranch}"`, "Specify different branches for default and release");
|
|
1200
|
+
const availableBranches = await getAvailableBranches(normalized.workspaceRoot);
|
|
1201
|
+
if (!availableBranches.includes(defaultBranch)) exitWithError(`Default branch "${defaultBranch}" does not exist in the repository`, `Available branches: ${availableBranches.join(", ")}`);
|
|
1202
|
+
logger.verbose(`Using default branch: ${farver.green(defaultBranch)}`);
|
|
1203
|
+
return {
|
|
1204
|
+
...normalized,
|
|
1205
|
+
branch: {
|
|
1206
|
+
release: releaseBranch,
|
|
1207
|
+
default: defaultBranch
|
|
1208
|
+
},
|
|
1209
|
+
safeguards: options.safeguards ?? true,
|
|
1210
|
+
globalCommitMode: options.globalCommitMode ?? "dependencies",
|
|
1211
|
+
pullRequest: options.pullRequest,
|
|
1212
|
+
changelog: { enabled: options.changelog?.enabled ?? true }
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async function orchestrateReleasePullRequest({ workspaceRoot, owner, repo, githubToken, releaseBranch, defaultBranch, pullRequestTitle, pullRequestBody }) {
|
|
1216
|
+
const currentBranch = await getCurrentBranch(workspaceRoot);
|
|
1217
|
+
if (currentBranch !== defaultBranch) exitWithError(`Current branch is '${currentBranch}'. Please switch to the default branch '${defaultBranch}' before proceeding.`, `git checkout ${defaultBranch}`);
|
|
1218
|
+
const existingPullRequest = await getExistingPullRequest({
|
|
1219
|
+
owner,
|
|
1220
|
+
repo,
|
|
1221
|
+
branch: releaseBranch,
|
|
1222
|
+
githubToken
|
|
1223
|
+
});
|
|
1224
|
+
const doesReleasePRExist = !!existingPullRequest;
|
|
1225
|
+
if (doesReleasePRExist) logger.item("Found existing release pull request");
|
|
1226
|
+
else logger.item("Will create new pull request");
|
|
1227
|
+
const branchExists = await doesBranchExist(releaseBranch, workspaceRoot);
|
|
1228
|
+
return {
|
|
1229
|
+
existingPullRequest,
|
|
1230
|
+
doesReleasePRExist,
|
|
1231
|
+
prepareBranch: async () => {
|
|
1232
|
+
if (!branchExists) await createBranch(releaseBranch, defaultBranch, workspaceRoot);
|
|
1233
|
+
logger.step(`Checking out release branch: ${releaseBranch}`);
|
|
1234
|
+
if (!await checkoutBranch(releaseBranch, workspaceRoot)) throw new Error(`Failed to checkout branch: ${releaseBranch}`);
|
|
1235
|
+
if (branchExists) {
|
|
1236
|
+
logger.step("Pulling latest changes from remote");
|
|
1237
|
+
if (!await pullLatestChanges(releaseBranch, workspaceRoot)) logger.warn("Failed to pull latest changes, continuing anyway");
|
|
1238
|
+
}
|
|
1239
|
+
logger.step(`Rebasing onto ${defaultBranch}`);
|
|
1240
|
+
if (!await rebaseBranch(defaultBranch, workspaceRoot)) throw new Error(`Failed to rebase onto ${defaultBranch}. Please resolve conflicts manually.`);
|
|
1241
|
+
},
|
|
1242
|
+
commitAndPush: async (hasChanges) => {
|
|
1243
|
+
const hasCommitted = hasChanges ? await commitChanges("chore: update release versions", workspaceRoot) : false;
|
|
1244
|
+
const isBranchAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
|
|
1245
|
+
if (!hasCommitted && !isBranchAhead) {
|
|
1246
|
+
logger.item("No changes to commit and branch is in sync with remote");
|
|
1247
|
+
await checkoutBranch(defaultBranch, workspaceRoot);
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
logger.step("Pushing changes to remote");
|
|
1251
|
+
if (!await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true })) throw new Error(`Failed to push changes to ${releaseBranch}. Remote may have been updated.`);
|
|
1252
|
+
return true;
|
|
1253
|
+
},
|
|
1254
|
+
createOrUpdatePullRequest: async (updates) => {
|
|
1255
|
+
const prTitle = existingPullRequest?.title || pullRequestTitle || "chore: update package versions";
|
|
1256
|
+
const prBody = pullRequestBody || generatePullRequestBody(updates);
|
|
1257
|
+
const pullRequest = await upsertPullRequest({
|
|
1258
|
+
owner,
|
|
1259
|
+
repo,
|
|
1260
|
+
pullNumber: existingPullRequest?.number,
|
|
1261
|
+
title: prTitle,
|
|
1262
|
+
body: prBody,
|
|
1263
|
+
head: releaseBranch,
|
|
1264
|
+
base: defaultBranch,
|
|
1265
|
+
githubToken
|
|
1266
|
+
});
|
|
1267
|
+
logger.success(`${doesReleasePRExist ? "Updated" : "Created"} pull request: ${pullRequest?.html_url}`);
|
|
1268
|
+
return {
|
|
1269
|
+
pullRequest,
|
|
1270
|
+
created: !doesReleasePRExist
|
|
1271
|
+
};
|
|
1272
|
+
},
|
|
1273
|
+
checkoutDefaultBranch: async () => {
|
|
1274
|
+
await checkoutBranch(defaultBranch, workspaceRoot);
|
|
1275
|
+
}
|
|
920
1276
|
};
|
|
921
1277
|
}
|
|
922
1278
|
|