@ucdjs/release-scripts 0.1.0-beta.12 → 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 +11 -40
- package/dist/index.mjs +780 -598
- package/package.json +9 -1
package/dist/index.mjs
CHANGED
|
@@ -1,44 +1,66 @@
|
|
|
1
1
|
import { t as Eta } from "./eta-Boh7yPZi.mjs";
|
|
2
|
-
import farver from "farver";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
2
|
import process from "node:process";
|
|
3
|
+
import farver from "farver";
|
|
4
|
+
import mri from "mri";
|
|
7
5
|
import { exec } from "tinyexec";
|
|
8
|
-
import { getCommits } from "commit-parser";
|
|
9
6
|
import { dedent } from "@luxass/utils";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { getCommits } from "commit-parser";
|
|
10
10
|
import prompts from "prompts";
|
|
11
11
|
|
|
12
12
|
//#region src/publish.ts
|
|
13
13
|
function publish(_options) {}
|
|
14
14
|
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region src/utils.ts
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
21
|
const isCI = typeof process.env.CI === "string" && process.env.CI !== "" && process.env.CI.toLowerCase() !== "false";
|
|
22
22
|
const logger = {
|
|
23
|
-
info: (...args) => {
|
|
24
|
-
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);
|
|
25
43
|
},
|
|
26
|
-
|
|
27
|
-
console.
|
|
44
|
+
section: (title) => {
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(` ${farver.bold(title)}`);
|
|
47
|
+
console.log(` ${farver.gray("─".repeat(title.length + 2))}`);
|
|
28
48
|
},
|
|
29
|
-
|
|
30
|
-
console.
|
|
49
|
+
emptyLine: () => {
|
|
50
|
+
console.log();
|
|
31
51
|
},
|
|
32
|
-
|
|
33
|
-
console.
|
|
52
|
+
item: (message) => {
|
|
53
|
+
console.log(` ${message}`);
|
|
34
54
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
step: (message) => {
|
|
56
|
+
console.log(` ${farver.blue("→")} ${message}`);
|
|
57
|
+
},
|
|
58
|
+
success: (message) => {
|
|
59
|
+
console.log(` ${farver.green("✓")} ${message}`);
|
|
38
60
|
}
|
|
39
61
|
};
|
|
40
|
-
async function run(bin, args, opts = {}) {
|
|
41
|
-
return exec(bin, args, {
|
|
62
|
+
async function run(bin, args$1, opts = {}) {
|
|
63
|
+
return exec(bin, args$1, {
|
|
42
64
|
throwOnError: true,
|
|
43
65
|
...opts,
|
|
44
66
|
nodeOptions: {
|
|
@@ -47,21 +69,20 @@ async function run(bin, args, opts = {}) {
|
|
|
47
69
|
}
|
|
48
70
|
});
|
|
49
71
|
}
|
|
50
|
-
async function dryRun(bin, args, opts) {
|
|
51
|
-
return logger.
|
|
72
|
+
async function dryRun(bin, args$1, opts) {
|
|
73
|
+
return logger.verbose(farver.blue(`[dryrun] ${bin} ${args$1.join(" ")}`), opts || "");
|
|
52
74
|
}
|
|
53
|
-
const runIfNotDry =
|
|
75
|
+
const runIfNotDry = isDryRun ? dryRun : run;
|
|
54
76
|
function exitWithError(message, hint) {
|
|
55
77
|
logger.error(farver.bold(message));
|
|
56
78
|
if (hint) console.error(farver.gray(` ${hint}`));
|
|
57
79
|
process.exit(1);
|
|
58
80
|
}
|
|
59
81
|
function normalizeSharedOptions(options) {
|
|
60
|
-
const { workspaceRoot = process.cwd(), githubToken = "",
|
|
82
|
+
const { workspaceRoot = process.cwd(), githubToken = "", repo: fullRepo, packages = true, prompts: prompts$1 = {
|
|
61
83
|
packages: true,
|
|
62
84
|
versions: true
|
|
63
85
|
},...rest } = options;
|
|
64
|
-
globalOptions.verbose = verbose;
|
|
65
86
|
if (!githubToken.trim()) exitWithError("GitHub token is required", "Set GITHUB_TOKEN environment variable or pass it in options");
|
|
66
87
|
if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) exitWithError("Repository (repo) is required", "Specify the repository in 'owner/repo' format (e.g., 'octocat/hello-world')");
|
|
67
88
|
const [owner, repo] = fullRepo.split("/");
|
|
@@ -69,259 +90,28 @@ function normalizeSharedOptions(options) {
|
|
|
69
90
|
return {
|
|
70
91
|
...rest,
|
|
71
92
|
packages,
|
|
72
|
-
prompts:
|
|
93
|
+
prompts: {
|
|
94
|
+
packages: prompts$1?.packages ?? true,
|
|
95
|
+
versions: prompts$1?.versions ?? true
|
|
96
|
+
},
|
|
73
97
|
workspaceRoot,
|
|
74
98
|
githubToken,
|
|
75
99
|
owner,
|
|
76
|
-
repo
|
|
77
|
-
verbose
|
|
100
|
+
repo
|
|
78
101
|
};
|
|
79
102
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function getSectionLabel(type) {
|
|
87
|
-
return {
|
|
88
|
-
feat: "Features",
|
|
89
|
-
fix: "Bug Fixes",
|
|
90
|
-
docs: "Documentation",
|
|
91
|
-
style: "Styles",
|
|
92
|
-
refactor: "Code Refactoring",
|
|
93
|
-
perf: "Performance Improvements",
|
|
94
|
-
test: "Tests",
|
|
95
|
-
build: "Build System",
|
|
96
|
-
ci: "Continuous Integration",
|
|
97
|
-
chore: "Miscellaneous Chores",
|
|
98
|
-
revert: "Reverts"
|
|
99
|
-
}[type] || "Other Changes";
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Generate changelog content from commits
|
|
103
|
-
*/
|
|
104
|
-
function generateChangelog(pkg, newVersion, commits, previousVersion, repository) {
|
|
105
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
106
|
-
let versionHeader = `## `;
|
|
107
|
-
if (repository && previousVersion) {
|
|
108
|
-
const compareUrl = `https://github.com/${repository.owner}/${repository.repo}/compare/${pkg.name}@${previousVersion}...${pkg.name}@${newVersion}`;
|
|
109
|
-
versionHeader += `[${newVersion}](${compareUrl})`;
|
|
110
|
-
} else versionHeader += newVersion;
|
|
111
|
-
versionHeader += ` (${date})\n\n`;
|
|
112
|
-
let changelog = versionHeader;
|
|
113
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
114
|
-
for (const commit of commits) {
|
|
115
|
-
if (!commit.isConventional || !commit.type) continue;
|
|
116
|
-
const type = commit.type;
|
|
117
|
-
if (!grouped.has(type)) grouped.set(type, []);
|
|
118
|
-
grouped.get(type).push(commit);
|
|
119
|
-
}
|
|
120
|
-
for (const type of [
|
|
121
|
-
"feat",
|
|
122
|
-
"fix",
|
|
123
|
-
"perf",
|
|
124
|
-
"refactor",
|
|
125
|
-
"docs",
|
|
126
|
-
"test",
|
|
127
|
-
"build",
|
|
128
|
-
"ci",
|
|
129
|
-
"chore",
|
|
130
|
-
"revert",
|
|
131
|
-
"style"
|
|
132
|
-
]) {
|
|
133
|
-
const commits$1 = grouped.get(type);
|
|
134
|
-
if (!commits$1 || commits$1.length === 0) continue;
|
|
135
|
-
const label = getSectionLabel(type);
|
|
136
|
-
changelog += `### ${label}\n\n`;
|
|
137
|
-
for (const commit of commits$1) {
|
|
138
|
-
const scope = commit.scope ? `**${commit.scope}:** ` : "";
|
|
139
|
-
const breaking = commit.isBreaking ? " **BREAKING CHANGE**" : "";
|
|
140
|
-
let entry = `* ${scope}${commit.description}${breaking}`;
|
|
141
|
-
if (repository) {
|
|
142
|
-
const commitUrl = `https://github.com/${repository.owner}/${repository.repo}/commit/${commit.shortHash}`;
|
|
143
|
-
entry += ` ([${commit.shortHash}](${commitUrl}))`;
|
|
144
|
-
} else entry += ` (${commit.shortHash})`;
|
|
145
|
-
changelog += `${entry}\n`;
|
|
146
|
-
}
|
|
147
|
-
changelog += "\n";
|
|
148
|
-
}
|
|
149
|
-
return changelog.trim();
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Write changelog to package's CHANGELOG.md file
|
|
153
|
-
*/
|
|
154
|
-
async function writeChangelog(pkg, newContent, version) {
|
|
155
|
-
const changelogPath = join(pkg.path, "CHANGELOG.md");
|
|
156
|
-
let existingContent = "";
|
|
157
|
-
if (existsSync(changelogPath)) existingContent = await readFile(changelogPath, "utf-8");
|
|
158
|
-
let updatedContent;
|
|
159
|
-
if (existingContent) {
|
|
160
|
-
const withoutTitle = existingContent.replace(/^# Changelog\n\n/, "");
|
|
161
|
-
if (new RegExp(`^## ${version.replace(/\./g, "\\.")}(\\s|$)`, "m").test(withoutTitle)) {
|
|
162
|
-
const versionSectionRegex = new RegExp(`^## ${version.replace(/\./g, "\\.")}[\\s\\S]*?(?=^## |$)`, "m");
|
|
163
|
-
updatedContent = `# Changelog\n\n${withoutTitle.replace(versionSectionRegex, `${newContent}\n\n`)}`;
|
|
164
|
-
} else updatedContent = `# Changelog\n\n${newContent}\n\n${withoutTitle}`;
|
|
165
|
-
} else updatedContent = `# Changelog\n\n${newContent}\n`;
|
|
166
|
-
await writeFile(changelogPath, updatedContent, "utf-8");
|
|
167
|
-
logger.log(`Updated changelog: ${changelogPath}`);
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Generate and write changelogs for all updated packages
|
|
171
|
-
*/
|
|
172
|
-
async function updateChangelogs(updates, packageCommits, options) {
|
|
173
|
-
if (!options?.enabled) {
|
|
174
|
-
logger.log("Changelog generation is disabled");
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
logger.log("Generating changelogs...");
|
|
178
|
-
for (const update of updates) {
|
|
179
|
-
if (!update.hasDirectChanges) continue;
|
|
180
|
-
const commits = packageCommits.get(update.package.name) || [];
|
|
181
|
-
if (commits.length === 0) continue;
|
|
182
|
-
let changelog = generateChangelog(update.package, update.newVersion, commits, update.currentVersion, options.repository);
|
|
183
|
-
if (options.transform) changelog = await options.transform(changelog, update.package);
|
|
184
|
-
logger.info(`Generating changelog for package ${update.package.name}`);
|
|
185
|
-
await writeChangelog(update.package, changelog, update.newVersion);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
//#endregion
|
|
190
|
-
//#region src/commits.ts
|
|
191
|
-
async function getLastPackageTag(packageName, workspaceRoot) {
|
|
192
|
-
try {
|
|
193
|
-
const { stdout } = await run("git", ["tag", "--list"], { nodeOptions: {
|
|
194
|
-
cwd: workspaceRoot,
|
|
195
|
-
stdio: "pipe"
|
|
196
|
-
} });
|
|
197
|
-
return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse().find((tag) => tag.startsWith(`${packageName}@`));
|
|
198
|
-
} catch (err) {
|
|
199
|
-
logger.warn(`Failed to get tags for package ${packageName}: ${err.message}`);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
function determineHighestBump(commits) {
|
|
204
|
-
if (commits.length === 0) return "none";
|
|
205
|
-
let highestBump = "none";
|
|
206
|
-
for (const commit of commits) {
|
|
207
|
-
const bump = determineBumpType(commit);
|
|
208
|
-
if (bump === "major") return "major";
|
|
209
|
-
if (bump === "minor") highestBump = "minor";
|
|
210
|
-
else if (bump === "patch" && highestBump === "none") highestBump = "patch";
|
|
211
|
-
}
|
|
212
|
-
return highestBump;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Retrieves commits that affect a specific workspace package since its last tag.
|
|
216
|
-
*
|
|
217
|
-
* @param {string} workspaceRoot - The root directory of the workspace.
|
|
218
|
-
* @param {WorkspacePackage} pkg - The workspace package to analyze.
|
|
219
|
-
* @returns {Promise<GitCommit[]>} A promise that resolves to an array of GitCommit objects affecting the package.
|
|
220
|
-
*/
|
|
221
|
-
async function getCommitsForWorkspacePackage(workspaceRoot, pkg) {
|
|
222
|
-
const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
|
|
223
|
-
const allCommits = getCommits({
|
|
224
|
-
from: lastTag,
|
|
225
|
-
to: "HEAD",
|
|
226
|
-
cwd: workspaceRoot
|
|
227
|
-
});
|
|
228
|
-
logger.log(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
|
|
229
|
-
const touchedCommitHashes = getCommits({
|
|
230
|
-
from: lastTag,
|
|
231
|
-
to: "HEAD",
|
|
232
|
-
cwd: workspaceRoot,
|
|
233
|
-
folder: pkg.path
|
|
234
|
-
});
|
|
235
|
-
const touchedSet = new Set(touchedCommitHashes);
|
|
236
|
-
const packageCommits = allCommits.filter((commit) => touchedSet.has(commit));
|
|
237
|
-
logger.log(`${packageCommits.length} commits affect ${pkg.name}`);
|
|
238
|
-
return packageCommits;
|
|
239
|
-
}
|
|
240
|
-
async function getWorkspacePackageCommits(workspaceRoot, packages) {
|
|
241
|
-
const changedPackages = /* @__PURE__ */ new Map();
|
|
242
|
-
const promises = packages.map(async (pkg) => {
|
|
243
|
-
return {
|
|
244
|
-
pkgName: pkg.name,
|
|
245
|
-
commits: await getCommitsForWorkspacePackage(workspaceRoot, pkg)
|
|
246
|
-
};
|
|
247
|
-
});
|
|
248
|
-
const results = await Promise.all(promises);
|
|
249
|
-
for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
|
|
250
|
-
return changedPackages;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Get all commits for the workspace (not filtered by package)
|
|
254
|
-
*/
|
|
255
|
-
async function getAllWorkspaceCommits(workspaceRoot, lastTag) {
|
|
256
|
-
return getCommits({
|
|
257
|
-
from: lastTag,
|
|
258
|
-
to: "HEAD",
|
|
259
|
-
cwd: workspaceRoot
|
|
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
|
|
260
109
|
});
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Get files changed in a specific commit
|
|
264
|
-
*/
|
|
265
|
-
async function getFilesChangedInCommit(commitHash, workspaceRoot) {
|
|
266
|
-
try {
|
|
267
|
-
const { stdout } = await run("git", [
|
|
268
|
-
"diff-tree",
|
|
269
|
-
"--no-commit-id",
|
|
270
|
-
"--name-only",
|
|
271
|
-
"-r",
|
|
272
|
-
commitHash
|
|
273
|
-
], { nodeOptions: {
|
|
274
|
-
cwd: workspaceRoot,
|
|
275
|
-
stdio: "pipe"
|
|
276
|
-
} });
|
|
277
|
-
return stdout.split("\n").map((file) => file.trim()).filter(Boolean);
|
|
278
|
-
} catch {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Filter and combine package commits with global commits
|
|
284
|
-
*/
|
|
285
|
-
function combineWithGlobalCommits(workspaceRoot, packageCommits, allCommits, mode) {
|
|
286
|
-
if (!mode) return packageCommits;
|
|
287
|
-
const packageCommitShas = new Set(packageCommits.map((c) => c.shortHash));
|
|
288
|
-
const globalCommits = allCommits.filter((c) => !packageCommitShas.has(c.shortHash));
|
|
289
|
-
if (mode === "all") return [...packageCommits, ...globalCommits];
|
|
290
|
-
if (mode === "dependencies") {
|
|
291
|
-
const dependencyCommits = globalCommits.filter(async (c) => {
|
|
292
|
-
const affectedFiles = await getFilesChangedInCommit(c.shortHash, workspaceRoot);
|
|
293
|
-
if (affectedFiles == null) return false;
|
|
294
|
-
return affectedFiles.some((file) => [
|
|
295
|
-
"package.json",
|
|
296
|
-
"pnpm-lock.yaml",
|
|
297
|
-
"pnpm-workspace.yaml"
|
|
298
|
-
].includes(file));
|
|
299
|
-
});
|
|
300
|
-
return [...packageCommits, ...dependencyCommits];
|
|
301
|
-
}
|
|
302
|
-
return packageCommits;
|
|
303
|
-
}
|
|
304
|
-
function determineBumpType(commit) {
|
|
305
|
-
if (commit.isBreaking) return "major";
|
|
306
|
-
if (!commit.isConventional || !commit.type) return "none";
|
|
307
|
-
switch (commit.type) {
|
|
308
|
-
case "feat": return "minor";
|
|
309
|
-
case "fix":
|
|
310
|
-
case "perf": return "patch";
|
|
311
|
-
case "docs":
|
|
312
|
-
case "style":
|
|
313
|
-
case "refactor":
|
|
314
|
-
case "test":
|
|
315
|
-
case "build":
|
|
316
|
-
case "ci":
|
|
317
|
-
case "chore":
|
|
318
|
-
case "revert": return "none";
|
|
319
|
-
default: return "none";
|
|
320
|
-
}
|
|
110
|
+
logger.verbose();
|
|
321
111
|
}
|
|
322
112
|
|
|
323
113
|
//#endregion
|
|
324
|
-
//#region src/git.ts
|
|
114
|
+
//#region src/core/git.ts
|
|
325
115
|
/**
|
|
326
116
|
* Check if the working directory is clean (no uncommitted changes)
|
|
327
117
|
* @param {string} workspaceRoot - The root directory of the workspace
|
|
@@ -361,31 +151,64 @@ async function doesBranchExist(branch, workspaceRoot) {
|
|
|
361
151
|
}
|
|
362
152
|
}
|
|
363
153
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* @
|
|
367
|
-
* @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.
|
|
368
157
|
*/
|
|
369
|
-
async function
|
|
158
|
+
async function getDefaultBranch(workspaceRoot) {
|
|
370
159
|
try {
|
|
371
|
-
await run("git", [
|
|
372
|
-
"pull",
|
|
373
|
-
"origin",
|
|
374
|
-
branch
|
|
375
|
-
], { nodeOptions: {
|
|
160
|
+
const match = (await run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { nodeOptions: {
|
|
376
161
|
cwd: workspaceRoot,
|
|
377
162
|
stdio: "pipe"
|
|
378
|
-
} });
|
|
379
|
-
return
|
|
163
|
+
} })).stdout.trim().match(/^refs\/remotes\/origin\/(.+)$/);
|
|
164
|
+
if (match && match[1]) return match[1];
|
|
165
|
+
return "main";
|
|
380
166
|
} catch {
|
|
381
|
-
return
|
|
167
|
+
return "main";
|
|
382
168
|
}
|
|
383
169
|
}
|
|
384
170
|
/**
|
|
385
|
-
*
|
|
386
|
-
* @param
|
|
387
|
-
* @
|
|
388
|
-
|
|
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
|
|
389
212
|
*/
|
|
390
213
|
async function createBranch(branch, base, workspaceRoot) {
|
|
391
214
|
try {
|
|
@@ -403,16 +226,29 @@ async function createBranch(branch, base, workspaceRoot) {
|
|
|
403
226
|
exitWithError(`Failed to create branch: ${branch}`, `Make sure the branch doesn't already exist and you have a clean working directory`);
|
|
404
227
|
}
|
|
405
228
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Checkout a git branch
|
|
408
|
-
* @param branch - The branch name to checkout
|
|
409
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
410
|
-
* @returns Promise resolving to true if checkout succeeded, false otherwise
|
|
411
|
-
*/
|
|
412
229
|
async function checkoutBranch(branch, workspaceRoot) {
|
|
413
230
|
try {
|
|
414
231
|
logger.info(`Switching to branch: ${farver.green(branch)}`);
|
|
415
|
-
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: {
|
|
416
252
|
cwd: workspaceRoot,
|
|
417
253
|
stdio: "pipe"
|
|
418
254
|
} });
|
|
@@ -421,43 +257,18 @@ async function checkoutBranch(branch, workspaceRoot) {
|
|
|
421
257
|
return false;
|
|
422
258
|
}
|
|
423
259
|
}
|
|
424
|
-
/**
|
|
425
|
-
* Get the current branch name
|
|
426
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
427
|
-
* @returns Promise resolving to the current branch name
|
|
428
|
-
*/
|
|
429
|
-
async function getCurrentBranch(workspaceRoot) {
|
|
430
|
-
return (await run("git", [
|
|
431
|
-
"rev-parse",
|
|
432
|
-
"--abbrev-ref",
|
|
433
|
-
"HEAD"
|
|
434
|
-
], { nodeOptions: {
|
|
435
|
-
cwd: workspaceRoot,
|
|
436
|
-
stdio: "pipe"
|
|
437
|
-
} })).stdout.trim();
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Rebase current branch onto another branch
|
|
441
|
-
* @param ontoBranch - The target branch to rebase onto
|
|
442
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
443
|
-
*/
|
|
444
260
|
async function rebaseBranch(ontoBranch, workspaceRoot) {
|
|
445
261
|
try {
|
|
446
262
|
logger.info(`Rebasing onto: ${farver.cyan(ontoBranch)}`);
|
|
447
|
-
await
|
|
263
|
+
await runIfNotDry("git", ["rebase", ontoBranch], { nodeOptions: {
|
|
448
264
|
cwd: workspaceRoot,
|
|
449
265
|
stdio: "pipe"
|
|
450
266
|
} });
|
|
267
|
+
return true;
|
|
451
268
|
} catch {
|
|
452
269
|
exitWithError(`Failed to rebase onto: ${ontoBranch}`, `You may have merge conflicts. Run 'git rebase --abort' to undo the rebase`);
|
|
453
270
|
}
|
|
454
271
|
}
|
|
455
|
-
/**
|
|
456
|
-
* Check if local branch is ahead of remote (has commits to push)
|
|
457
|
-
* @param branch - The branch name to check
|
|
458
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
459
|
-
* @returns Promise resolving to true if local is ahead, false otherwise
|
|
460
|
-
*/
|
|
461
272
|
async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
462
273
|
try {
|
|
463
274
|
const result = await run("git", [
|
|
@@ -473,23 +284,12 @@ async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
|
473
284
|
return true;
|
|
474
285
|
}
|
|
475
286
|
}
|
|
476
|
-
/**
|
|
477
|
-
* Check if there are any changes to commit (staged or unstaged)
|
|
478
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
479
|
-
* @returns Promise resolving to true if there are changes, false otherwise
|
|
480
|
-
*/
|
|
481
287
|
async function hasChangesToCommit(workspaceRoot) {
|
|
482
288
|
return (await run("git", ["status", "--porcelain"], { nodeOptions: {
|
|
483
289
|
cwd: workspaceRoot,
|
|
484
290
|
stdio: "pipe"
|
|
485
291
|
} })).stdout.trim() !== "";
|
|
486
292
|
}
|
|
487
|
-
/**
|
|
488
|
-
* Commit changes with a message
|
|
489
|
-
* @param message - The commit message
|
|
490
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
491
|
-
* @returns Promise resolving to true if commit was made, false if there were no changes
|
|
492
|
-
*/
|
|
493
293
|
async function commitChanges(message, workspaceRoot) {
|
|
494
294
|
try {
|
|
495
295
|
await run("git", ["add", "."], { nodeOptions: {
|
|
@@ -511,51 +311,35 @@ async function commitChanges(message, workspaceRoot) {
|
|
|
511
311
|
exitWithError(`Failed to commit changes`, `Make sure you have git configured properly with user.name and user.email`);
|
|
512
312
|
}
|
|
513
313
|
}
|
|
514
|
-
/**
|
|
515
|
-
* Push branch to remote
|
|
516
|
-
* @param branch - The branch name to push
|
|
517
|
-
* @param workspaceRoot - The root directory of the workspace
|
|
518
|
-
* @param options - Push options
|
|
519
|
-
* @param options.force - Force push (overwrite remote)
|
|
520
|
-
* @param options.forceWithLease - Force push with safety check (won't overwrite unexpected changes)
|
|
521
|
-
*/
|
|
522
314
|
async function pushBranch(branch, workspaceRoot, options) {
|
|
523
315
|
try {
|
|
524
|
-
const args = [
|
|
316
|
+
const args$1 = [
|
|
525
317
|
"push",
|
|
526
318
|
"origin",
|
|
527
319
|
branch
|
|
528
320
|
];
|
|
529
321
|
if (options?.forceWithLease) {
|
|
530
|
-
args.push("--force-with-lease");
|
|
322
|
+
args$1.push("--force-with-lease");
|
|
531
323
|
logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`);
|
|
532
324
|
} else if (options?.force) {
|
|
533
|
-
args.push("--force");
|
|
325
|
+
args$1.push("--force");
|
|
534
326
|
logger.info(`Force pushing branch: ${farver.green(branch)}`);
|
|
535
327
|
} else logger.info(`Pushing branch: ${farver.green(branch)}`);
|
|
536
|
-
await
|
|
328
|
+
await runIfNotDry("git", args$1, { nodeOptions: {
|
|
537
329
|
cwd: workspaceRoot,
|
|
538
330
|
stdio: "pipe"
|
|
539
331
|
} });
|
|
332
|
+
return true;
|
|
540
333
|
} catch {
|
|
541
334
|
exitWithError(`Failed to push branch: ${branch}`, `Make sure you have permission to push to the remote repository`);
|
|
542
335
|
}
|
|
543
336
|
}
|
|
544
|
-
async function getDefaultBranch() {
|
|
545
|
-
try {
|
|
546
|
-
const match = (await run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { nodeOptions: { stdio: "pipe" } })).stdout.trim().match(/^refs\/remotes\/origin\/(.+)$/);
|
|
547
|
-
if (match && match[1]) return match[1];
|
|
548
|
-
return "main";
|
|
549
|
-
} catch {
|
|
550
|
-
return "main";
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
337
|
|
|
554
338
|
//#endregion
|
|
555
|
-
//#region src/github.ts
|
|
339
|
+
//#region src/core/github.ts
|
|
556
340
|
async function getExistingPullRequest({ owner, repo, branch, githubToken }) {
|
|
557
341
|
try {
|
|
558
|
-
logger.
|
|
342
|
+
logger.verbose(`Requesting pull request for branch: ${branch} (url: https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch})`);
|
|
559
343
|
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch}`, { headers: {
|
|
560
344
|
Accept: "application/vnd.github.v3+json",
|
|
561
345
|
Authorization: `token ${githubToken}`
|
|
@@ -591,9 +375,10 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
|
|
|
591
375
|
title,
|
|
592
376
|
body,
|
|
593
377
|
head,
|
|
594
|
-
base
|
|
378
|
+
base,
|
|
379
|
+
draft: true
|
|
595
380
|
};
|
|
596
|
-
logger.
|
|
381
|
+
logger.verbose(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${url})`);
|
|
597
382
|
const res = await fetch(url, {
|
|
598
383
|
method,
|
|
599
384
|
headers: {
|
|
@@ -653,174 +438,226 @@ function generatePullRequestBody(updates, body) {
|
|
|
653
438
|
}
|
|
654
439
|
|
|
655
440
|
//#endregion
|
|
656
|
-
//#region src/
|
|
657
|
-
async function
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
})
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
|
|
672
|
-
return response.selectedPackages;
|
|
673
|
-
}
|
|
674
|
-
async function promptVersionOverride(pkg, workspaceRoot, currentVersion, suggestedVersion, suggestedBumpType) {
|
|
675
|
-
const choices = [{
|
|
676
|
-
title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
|
|
677
|
-
value: "suggested"
|
|
678
|
-
}];
|
|
679
|
-
for (const bumpType of [
|
|
680
|
-
"patch",
|
|
681
|
-
"minor",
|
|
682
|
-
"major"
|
|
683
|
-
]) if (bumpType !== suggestedBumpType) {
|
|
684
|
-
const version = getNextVersion(currentVersion, bumpType);
|
|
685
|
-
choices.push({
|
|
686
|
-
title: `${bumpType}: ${version}`,
|
|
687
|
-
value: bumpType
|
|
688
|
-
});
|
|
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;
|
|
689
456
|
}
|
|
690
|
-
choices.push({
|
|
691
|
-
title: "Custom version",
|
|
692
|
-
value: "custom"
|
|
693
|
-
});
|
|
694
|
-
const response = await prompts([{
|
|
695
|
-
type: "select",
|
|
696
|
-
name: "choice",
|
|
697
|
-
message: `${pkg.name} (${currentVersion}):`,
|
|
698
|
-
choices,
|
|
699
|
-
initial: 0
|
|
700
|
-
}, {
|
|
701
|
-
type: (prev) => prev === "custom" ? "text" : null,
|
|
702
|
-
name: "customVersion",
|
|
703
|
-
message: "Enter custom version:",
|
|
704
|
-
initial: suggestedVersion,
|
|
705
|
-
validate: (value) => {
|
|
706
|
-
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(value) || "Invalid semver version (e.g., 1.0.0)";
|
|
707
|
-
}
|
|
708
|
-
}]);
|
|
709
|
-
if (response.choice === "suggested") return suggestedVersion;
|
|
710
|
-
else if (response.choice === "custom") return response.customVersion;
|
|
711
|
-
else return getNextVersion(currentVersion, response.choice);
|
|
712
457
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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";
|
|
466
|
+
}
|
|
467
|
+
return highestBump;
|
|
718
468
|
}
|
|
719
|
-
|
|
720
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Retrieves commits that affect a specific workspace package since its last tag.
|
|
471
|
+
*
|
|
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.
|
|
475
|
+
*/
|
|
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;
|
|
721
497
|
}
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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);
|
|
536
|
+
}
|
|
537
|
+
return map;
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
744
540
|
}
|
|
745
|
-
return `${newMajor}.${newMinor}.${newPatch}`;
|
|
746
541
|
}
|
|
747
542
|
/**
|
|
748
|
-
*
|
|
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
|
|
749
548
|
*/
|
|
750
|
-
function
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
hasDirectChanges
|
|
758
|
-
};
|
|
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;
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
759
556
|
}
|
|
760
557
|
/**
|
|
761
|
-
*
|
|
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
|
|
563
|
+
*/
|
|
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.
|
|
762
576
|
*
|
|
763
|
-
* @
|
|
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
|
|
764
582
|
*/
|
|
765
|
-
async function
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (!pkg) continue;
|
|
771
|
-
const bump = determineHighestBump(combineWithGlobalCommits(workspaceRoot, pkgCommits, allCommits, globalCommitMode));
|
|
772
|
-
if (bump === "none") {
|
|
773
|
-
logger.info(`No version bump needed for package ${pkg.name}`);
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
let newVersion = getNextVersion(pkg.version, bump);
|
|
777
|
-
if (!isCI && showPrompt) newVersion = await promptVersionOverride(pkg, workspaceRoot, pkg.version, newVersion, bump);
|
|
778
|
-
versionUpdates.push({
|
|
779
|
-
package: pkg,
|
|
780
|
-
currentVersion: pkg.version,
|
|
781
|
-
newVersion,
|
|
782
|
-
bumpType: bump,
|
|
783
|
-
hasDirectChanges: true
|
|
784
|
-
});
|
|
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;
|
|
785
588
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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);
|
|
805
636
|
}
|
|
806
637
|
}
|
|
807
|
-
|
|
638
|
+
return result;
|
|
808
639
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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";
|
|
818
656
|
}
|
|
819
|
-
return updates;
|
|
820
657
|
}
|
|
821
658
|
|
|
822
659
|
//#endregion
|
|
823
|
-
//#region src/package.ts
|
|
660
|
+
//#region src/versioning/package.ts
|
|
824
661
|
/**
|
|
825
662
|
* Build a dependency graph from workspace packages
|
|
826
663
|
*
|
|
@@ -888,29 +725,326 @@ function createDependentUpdates(graph, workspacePackages, directUpdates) {
|
|
|
888
725
|
const directUpdateMap = new Map(directUpdates.map((u) => [u.package.name, u]));
|
|
889
726
|
const affectedPackages = getAllAffectedPackages(graph, new Set(directUpdates.map((u) => u.package.name)));
|
|
890
727
|
for (const pkgName of affectedPackages) {
|
|
891
|
-
|
|
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
|
+
}
|
|
892
733
|
const pkg = workspacePackages.find((p) => p.name === pkgName);
|
|
893
734
|
if (!pkg) continue;
|
|
894
735
|
allUpdates.push(createVersionUpdate(pkg, "patch", false));
|
|
895
736
|
}
|
|
896
737
|
return allUpdates;
|
|
897
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
|
+
}
|
|
898
773
|
/**
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
* Updates are performed in parallel for better performance.
|
|
902
|
-
*
|
|
903
|
-
* @param updates - Version updates to apply
|
|
774
|
+
* Create a version update object
|
|
904
775
|
*/
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
|
900
|
+
*/
|
|
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;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
//#endregion
|
|
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);
|
|
910
1044
|
}
|
|
911
1045
|
|
|
912
1046
|
//#endregion
|
|
913
|
-
//#region src/workspace.ts
|
|
1047
|
+
//#region src/core/workspace.ts
|
|
914
1048
|
async function discoverWorkspacePackages(workspaceRoot, options) {
|
|
915
1049
|
let workspaceOptions;
|
|
916
1050
|
let explicitPackages;
|
|
@@ -992,105 +1126,153 @@ function shouldIncludePackage(pkg, options) {
|
|
|
992
1126
|
//#endregion
|
|
993
1127
|
//#region src/release.ts
|
|
994
1128
|
async function release(options) {
|
|
995
|
-
const normalizedOptions =
|
|
996
|
-
normalizedOptions.dryRun ??= false;
|
|
997
|
-
normalizedOptions.branch ??= {};
|
|
998
|
-
normalizedOptions.branch.release ??= "release/next";
|
|
999
|
-
normalizedOptions.branch.default = await getDefaultBranch();
|
|
1000
|
-
normalizedOptions.safeguards ??= true;
|
|
1001
|
-
normalizedOptions.changelog ??= { enabled: true };
|
|
1002
|
-
normalizedOptions.globalCommitMode ??= "dependencies";
|
|
1003
|
-
globalOptions.dryRun = normalizedOptions.dryRun;
|
|
1004
|
-
const workspaceRoot = normalizedOptions.workspaceRoot;
|
|
1129
|
+
const { workspaceRoot,...normalizedOptions } = await normalizeReleaseOptions(options);
|
|
1005
1130
|
if (normalizedOptions.safeguards && !await isWorkingDirectoryClean(workspaceRoot)) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
1006
1131
|
const workspacePackages = await discoverWorkspacePackages(workspaceRoot, options);
|
|
1007
1132
|
if (workspacePackages.length === 0) {
|
|
1008
|
-
logger.
|
|
1133
|
+
logger.warn("No packages found to release");
|
|
1009
1134
|
return null;
|
|
1010
1135
|
}
|
|
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)}`);
|
|
1141
|
+
}
|
|
1142
|
+
logger.emptyLine();
|
|
1011
1143
|
const packageCommits = await getWorkspacePackageCommits(workspaceRoot, workspacePackages);
|
|
1012
|
-
const
|
|
1144
|
+
const globalCommitsPerPackage = await getGlobalCommitsPerPackage(workspaceRoot, packageCommits, workspacePackages, normalizedOptions.globalCommitMode);
|
|
1145
|
+
const { allUpdates, applyUpdates } = await calculateAndPrepareVersionUpdates({
|
|
1013
1146
|
workspacePackages,
|
|
1014
1147
|
packageCommits,
|
|
1015
1148
|
workspaceRoot,
|
|
1016
1149
|
showPrompt: options.prompts?.versions !== false,
|
|
1017
|
-
|
|
1018
|
-
globalCommitMode: options.globalCommitMode
|
|
1150
|
+
globalCommitsPerPackage
|
|
1019
1151
|
});
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const
|
|
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,
|
|
1025
1158
|
owner: normalizedOptions.owner,
|
|
1026
1159
|
repo: normalizedOptions.repo,
|
|
1027
|
-
|
|
1028
|
-
|
|
1160
|
+
githubToken: normalizedOptions.githubToken,
|
|
1161
|
+
releaseBranch: normalizedOptions.branch.release,
|
|
1162
|
+
defaultBranch: normalizedOptions.branch.default,
|
|
1163
|
+
pullRequestTitle: options.pullRequest?.title,
|
|
1164
|
+
pullRequestBody: options.pullRequest?.body
|
|
1029
1165
|
});
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
logger.
|
|
1041
|
-
|
|
1042
|
-
}
|
|
1043
|
-
logger.log("Rebasing release branch onto", normalizedOptions.branch.default);
|
|
1044
|
-
await rebaseBranch(normalizedOptions.branch.default, workspaceRoot);
|
|
1045
|
-
await updateAllPackageJsonFiles(allUpdates);
|
|
1046
|
-
await updateChangelogs(versionUpdates, packageCommits, {
|
|
1047
|
-
...options.changelog,
|
|
1048
|
-
repository: options.changelog?.repository || {
|
|
1049
|
-
owner: normalizedOptions.owner,
|
|
1050
|
-
repo: normalizedOptions.repo
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
|
-
const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
|
|
1054
|
-
const isBranchAhead = await isBranchAheadOfRemote(normalizedOptions.branch.release, workspaceRoot);
|
|
1055
|
-
if (!hasCommitted && !isBranchAhead) {
|
|
1056
|
-
logger.log("No changes to commit and branch is in sync with remote");
|
|
1057
|
-
await checkoutBranch(normalizedOptions.branch.default, workspaceRoot);
|
|
1058
|
-
if (prExists) {
|
|
1059
|
-
logger.log("No updates needed, PR is already up to date");
|
|
1060
|
-
return {
|
|
1061
|
-
updates: allUpdates,
|
|
1062
|
-
prUrl: existingPullRequest.html_url,
|
|
1063
|
-
created: false
|
|
1064
|
-
};
|
|
1065
|
-
} else {
|
|
1066
|
-
logger.error("No changes to commit, and no existing PR. Nothing to do.");
|
|
1067
|
-
return null;
|
|
1068
|
-
}
|
|
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;
|
|
1069
1178
|
}
|
|
1070
|
-
|
|
1071
|
-
await
|
|
1072
|
-
const prTitle = existingPullRequest?.title || options.pullRequest?.title || "chore: update package versions";
|
|
1073
|
-
const prBody = generatePullRequestBody(allUpdates, options.pullRequest?.body);
|
|
1074
|
-
const pullRequest = await upsertPullRequest({
|
|
1075
|
-
owner: normalizedOptions.owner,
|
|
1076
|
-
repo: normalizedOptions.repo,
|
|
1077
|
-
pullNumber: existingPullRequest?.number,
|
|
1078
|
-
title: prTitle,
|
|
1079
|
-
body: prBody,
|
|
1080
|
-
head: normalizedOptions.branch.release,
|
|
1081
|
-
base: normalizedOptions.branch.default,
|
|
1082
|
-
githubToken: normalizedOptions.githubToken
|
|
1083
|
-
});
|
|
1084
|
-
logger.log(prExists ? "Updated pull request:" : "Created pull request:", pullRequest?.html_url);
|
|
1085
|
-
await checkoutBranch(normalizedOptions.branch.default, workspaceRoot);
|
|
1179
|
+
const { pullRequest, created } = await prOps.createOrUpdatePullRequest(allUpdates);
|
|
1180
|
+
await prOps.checkoutDefaultBranch();
|
|
1086
1181
|
if (pullRequest?.html_url) {
|
|
1087
|
-
logger.
|
|
1088
|
-
logger.
|
|
1182
|
+
logger.section("🚀 Pull Request");
|
|
1183
|
+
logger.success(`Pull request ${created ? "created" : "updated"}: ${pullRequest.html_url}`);
|
|
1089
1184
|
}
|
|
1090
1185
|
return {
|
|
1091
1186
|
updates: allUpdates,
|
|
1092
1187
|
prUrl: pullRequest?.html_url,
|
|
1093
|
-
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
|
+
}
|
|
1094
1276
|
};
|
|
1095
1277
|
}
|
|
1096
1278
|
|