@ucdjs/release-scripts 0.1.0-beta.35 → 0.1.0-beta.37
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 +3 -3
- package/dist/index.mjs +843 -736
- package/package.json +2 -5
package/dist/index.mjs
CHANGED
|
@@ -1,128 +1,57 @@
|
|
|
1
1
|
import { t as Eta } from "./eta-DAZlmVBQ.mjs";
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, relative } from "node:path";
|
|
4
|
-
import
|
|
5
|
-
import prompts from "prompts";
|
|
4
|
+
import { getCommits, groupByType } from "commit-parser";
|
|
6
5
|
import process from "node:process";
|
|
7
6
|
import readline from "node:readline";
|
|
7
|
+
import farver from "farver";
|
|
8
8
|
import mri from "mri";
|
|
9
9
|
import { exec } from "tinyexec";
|
|
10
10
|
import { dedent } from "@luxass/utils";
|
|
11
|
-
import
|
|
11
|
+
import prompts from "prompts";
|
|
12
12
|
import { compare, gt } from "semver";
|
|
13
13
|
|
|
14
|
-
//#region src/operations/
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case "major":
|
|
29
|
-
newMajor += 1;
|
|
30
|
-
newMinor = 0;
|
|
31
|
-
newPatch = 0;
|
|
32
|
-
break;
|
|
33
|
-
case "minor":
|
|
34
|
-
newMinor += 1;
|
|
35
|
-
newPatch = 0;
|
|
36
|
-
break;
|
|
37
|
-
case "patch":
|
|
38
|
-
newPatch += 1;
|
|
39
|
-
break;
|
|
14
|
+
//#region src/operations/changelog-format.ts
|
|
15
|
+
function formatCommitLine({ commit, owner, repo, authors }) {
|
|
16
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.hash}`;
|
|
17
|
+
let line = `${commit.description}`;
|
|
18
|
+
const references = commit.references ?? [];
|
|
19
|
+
for (const ref of references) {
|
|
20
|
+
if (!ref.value) continue;
|
|
21
|
+
const number = Number.parseInt(ref.value.replace(/^#/, ""), 10);
|
|
22
|
+
if (Number.isNaN(number)) continue;
|
|
23
|
+
if (ref.type === "issue") {
|
|
24
|
+
line += ` ([Issue ${ref.value}](https://github.com/${owner}/${repo}/issues/${number}))`;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
line += ` ([PR ${ref.value}](https://github.com/${owner}/${repo}/pull/${number}))`;
|
|
40
28
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (newParts[0] > oldParts[0]) return "major";
|
|
48
|
-
if (newParts[1] > oldParts[1]) return "minor";
|
|
49
|
-
if (newParts[2] > oldParts[2]) return "patch";
|
|
50
|
-
return "none";
|
|
29
|
+
line += ` ([${commit.shortHash}](${commitUrl}))`;
|
|
30
|
+
if (authors.length > 0) {
|
|
31
|
+
const authorList = authors.map((author) => author.login ? `[@${author.login}](https://github.com/${author.login})` : author.name).join(", ");
|
|
32
|
+
line += ` (by ${authorList})`;
|
|
33
|
+
}
|
|
34
|
+
return line;
|
|
51
35
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
36
|
+
function buildTemplateGroups(options) {
|
|
37
|
+
const { commits, owner, repo, types, commitAuthors } = options;
|
|
38
|
+
const grouped = groupByType(commits, {
|
|
39
|
+
includeNonConventional: false,
|
|
40
|
+
mergeKeys: Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.types ?? [key]]))
|
|
41
|
+
});
|
|
42
|
+
return Object.entries(types).map(([key, value]) => {
|
|
43
|
+
const formattedCommits = (grouped.get(key) ?? []).map((commit) => ({ line: formatCommitLine({
|
|
44
|
+
commit,
|
|
45
|
+
owner,
|
|
46
|
+
repo,
|
|
47
|
+
authors: commitAuthors.get(commit.hash) ?? []
|
|
48
|
+
}) }));
|
|
49
|
+
return {
|
|
50
|
+
name: key,
|
|
51
|
+
title: value.title,
|
|
52
|
+
commits: formattedCommits
|
|
53
|
+
};
|
|
68
54
|
});
|
|
69
|
-
if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
|
|
70
|
-
return response.selectedPackages;
|
|
71
|
-
}
|
|
72
|
-
async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggestedVersion) {
|
|
73
|
-
const answers = await prompts([{
|
|
74
|
-
type: "autocomplete",
|
|
75
|
-
name: "version",
|
|
76
|
-
message: `${pkg.name}: ${farver.green(pkg.version)}`,
|
|
77
|
-
choices: [
|
|
78
|
-
{
|
|
79
|
-
value: "skip",
|
|
80
|
-
title: `skip ${farver.dim("(no change)")}`
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
value: "major",
|
|
84
|
-
title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}`
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
value: "minor",
|
|
88
|
-
title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}`
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
value: "patch",
|
|
92
|
-
title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
value: "suggested",
|
|
96
|
-
title: `suggested ${farver.bold(suggestedVersion)}`
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
value: "as-is",
|
|
100
|
-
title: `as-is ${farver.dim("(keep current version)")}`
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
value: "custom",
|
|
104
|
-
title: "custom"
|
|
105
|
-
}
|
|
106
|
-
],
|
|
107
|
-
initial: suggestedVersion === currentVersion ? 0 : 4
|
|
108
|
-
}, {
|
|
109
|
-
type: (prev) => prev === "custom" ? "text" : null,
|
|
110
|
-
name: "custom",
|
|
111
|
-
message: "Enter the new version number:",
|
|
112
|
-
initial: suggestedVersion,
|
|
113
|
-
validate: (custom) => {
|
|
114
|
-
if (isValidSemver(custom)) return true;
|
|
115
|
-
return "That's not a valid version number";
|
|
116
|
-
}
|
|
117
|
-
}]);
|
|
118
|
-
if (!answers.version) return null;
|
|
119
|
-
if (answers.version === "skip") return null;
|
|
120
|
-
else if (answers.version === "suggested") return suggestedVersion;
|
|
121
|
-
else if (answers.version === "custom") {
|
|
122
|
-
if (!answers.custom) return null;
|
|
123
|
-
return answers.custom;
|
|
124
|
-
} else if (answers.version === "as-is") return currentVersion;
|
|
125
|
-
else return getNextVersion(pkg.version, answers.version);
|
|
126
55
|
}
|
|
127
56
|
|
|
128
57
|
//#endregion
|
|
@@ -210,118 +139,135 @@ if (isDryRun || isVerbose || isForce) {
|
|
|
210
139
|
}
|
|
211
140
|
|
|
212
141
|
//#endregion
|
|
213
|
-
//#region src/
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
error
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
//#endregion
|
|
228
|
-
//#region src/core/workspace.ts
|
|
229
|
-
async function discoverWorkspacePackages(workspaceRoot, options) {
|
|
230
|
-
let workspaceOptions;
|
|
231
|
-
let explicitPackages;
|
|
232
|
-
if (options.packages == null || options.packages === true) workspaceOptions = { excludePrivate: false };
|
|
233
|
-
else if (Array.isArray(options.packages)) {
|
|
234
|
-
workspaceOptions = {
|
|
235
|
-
excludePrivate: false,
|
|
236
|
-
include: options.packages
|
|
237
|
-
};
|
|
238
|
-
explicitPackages = options.packages;
|
|
239
|
-
} else {
|
|
240
|
-
workspaceOptions = options.packages;
|
|
241
|
-
if (options.packages.include) explicitPackages = options.packages.include;
|
|
142
|
+
//#region src/core/github.ts
|
|
143
|
+
var GitHubClient = class {
|
|
144
|
+
owner;
|
|
145
|
+
repo;
|
|
146
|
+
githubToken;
|
|
147
|
+
apiBase = "https://api.github.com";
|
|
148
|
+
constructor({ owner, repo, githubToken }) {
|
|
149
|
+
this.owner = owner;
|
|
150
|
+
this.repo = repo;
|
|
151
|
+
this.githubToken = githubToken;
|
|
242
152
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
153
|
+
async request(path, init = {}) {
|
|
154
|
+
const url = path.startsWith("http") ? path : `${this.apiBase}${path}`;
|
|
155
|
+
const res = await fetch(url, {
|
|
156
|
+
...init,
|
|
157
|
+
headers: {
|
|
158
|
+
...init.headers,
|
|
159
|
+
"Accept": "application/vnd.github.v3+json",
|
|
160
|
+
"Authorization": `token ${this.githubToken}`,
|
|
161
|
+
"User-Agent": "ucdjs-release-scripts (+https://github.com/ucdjs/ucdjs-release-scripts)"
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const errorText = await res.text();
|
|
166
|
+
throw new Error(`GitHub API request failed with status ${res.status}: ${errorText || "No response body"}`);
|
|
167
|
+
}
|
|
168
|
+
if (res.status === 204) return;
|
|
169
|
+
return res.json();
|
|
248
170
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
171
|
+
async getExistingPullRequest(branch) {
|
|
172
|
+
const head = branch.includes(":") ? branch : `${this.owner}:${branch}`;
|
|
173
|
+
const endpoint = `/repos/${this.owner}/${this.repo}/pulls?state=open&head=${encodeURIComponent(head)}`;
|
|
174
|
+
logger.verbose(`Requesting pull request for branch: ${branch} (url: ${this.apiBase}${endpoint})`);
|
|
175
|
+
const pulls = await this.request(endpoint);
|
|
176
|
+
if (!Array.isArray(pulls) || pulls.length === 0) return null;
|
|
177
|
+
const firstPullRequest = pulls[0];
|
|
178
|
+
if (typeof firstPullRequest !== "object" || firstPullRequest === null || !("number" in firstPullRequest) || typeof firstPullRequest.number !== "number" || !("title" in firstPullRequest) || typeof firstPullRequest.title !== "string" || !("body" in firstPullRequest) || typeof firstPullRequest.body !== "string" || !("draft" in firstPullRequest) || typeof firstPullRequest.draft !== "boolean" || !("html_url" in firstPullRequest) || typeof firstPullRequest.html_url !== "string") throw new TypeError("Pull request data validation failed");
|
|
179
|
+
const pullRequest = {
|
|
180
|
+
number: firstPullRequest.number,
|
|
181
|
+
title: firstPullRequest.title,
|
|
182
|
+
body: firstPullRequest.body,
|
|
183
|
+
draft: firstPullRequest.draft,
|
|
184
|
+
html_url: firstPullRequest.html_url,
|
|
185
|
+
head: "head" in firstPullRequest && typeof firstPullRequest.head === "object" && firstPullRequest.head !== null && "sha" in firstPullRequest.head && typeof firstPullRequest.head.sha === "string" ? { sha: firstPullRequest.head.sha } : void 0
|
|
186
|
+
};
|
|
187
|
+
logger.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
|
|
188
|
+
return pullRequest;
|
|
253
189
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
190
|
+
async upsertPullRequest({ title, body, head, base, pullNumber }) {
|
|
191
|
+
const isUpdate = typeof pullNumber === "number";
|
|
192
|
+
const endpoint = isUpdate ? `/repos/${this.owner}/${this.repo}/pulls/${pullNumber}` : `/repos/${this.owner}/${this.repo}/pulls`;
|
|
193
|
+
const requestBody = isUpdate ? {
|
|
194
|
+
title,
|
|
195
|
+
body
|
|
196
|
+
} : {
|
|
197
|
+
title,
|
|
198
|
+
body,
|
|
199
|
+
head,
|
|
200
|
+
base,
|
|
201
|
+
draft: true
|
|
202
|
+
};
|
|
203
|
+
logger.verbose(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${this.apiBase}${endpoint})`);
|
|
204
|
+
const pr = await this.request(endpoint, {
|
|
205
|
+
method: isUpdate ? "PATCH" : "POST",
|
|
206
|
+
body: JSON.stringify(requestBody)
|
|
207
|
+
});
|
|
208
|
+
if (typeof pr !== "object" || pr === null || !("number" in pr) || typeof pr.number !== "number" || !("title" in pr) || typeof pr.title !== "string" || !("body" in pr) || typeof pr.body !== "string" || !("draft" in pr) || typeof pr.draft !== "boolean" || !("html_url" in pr) || typeof pr.html_url !== "string") throw new TypeError("Pull request data validation failed");
|
|
209
|
+
const action = isUpdate ? "Updated" : "Created";
|
|
210
|
+
logger.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
|
|
211
|
+
return {
|
|
212
|
+
number: pr.number,
|
|
213
|
+
title: pr.title,
|
|
214
|
+
body: pr.body,
|
|
215
|
+
draft: pr.draft,
|
|
216
|
+
html_url: pr.html_url
|
|
217
|
+
};
|
|
268
218
|
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"ls",
|
|
281
|
-
"--json"
|
|
282
|
-
], { nodeOptions: {
|
|
283
|
-
cwd: workspaceRoot,
|
|
284
|
-
stdio: "pipe"
|
|
285
|
-
} });
|
|
286
|
-
const rawProjects = JSON.parse(result.stdout);
|
|
287
|
-
const allPackageNames = new Set(rawProjects.map((p) => p.name));
|
|
288
|
-
const excludedPackages = /* @__PURE__ */ new Set();
|
|
289
|
-
const promises = rawProjects.map(async (rawProject) => {
|
|
290
|
-
const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
|
|
291
|
-
const packageJson = JSON.parse(content);
|
|
292
|
-
if (!shouldIncludePackage(packageJson, options)) {
|
|
293
|
-
excludedPackages.add(rawProject.name);
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
name: rawProject.name,
|
|
298
|
-
version: rawProject.version,
|
|
299
|
-
path: rawProject.path,
|
|
300
|
-
packageJson,
|
|
301
|
-
workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => {
|
|
302
|
-
return allPackageNames.has(dep);
|
|
303
|
-
}),
|
|
304
|
-
workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => {
|
|
305
|
-
return allPackageNames.has(dep);
|
|
306
|
-
})
|
|
307
|
-
};
|
|
219
|
+
async setCommitStatus({ sha, state, targetUrl, description, context }) {
|
|
220
|
+
const endpoint = `/repos/${this.owner}/${this.repo}/statuses/${sha}`;
|
|
221
|
+
logger.verbose(`Setting commit status on ${sha} to ${state} (url: ${this.apiBase}${endpoint})`);
|
|
222
|
+
await this.request(endpoint, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: JSON.stringify({
|
|
225
|
+
state,
|
|
226
|
+
target_url: targetUrl,
|
|
227
|
+
description: description || "",
|
|
228
|
+
context
|
|
229
|
+
})
|
|
308
230
|
});
|
|
309
|
-
|
|
310
|
-
if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
|
|
311
|
-
return packages.filter((pkg) => pkg !== null);
|
|
312
|
-
} catch (err) {
|
|
313
|
-
logger.error("Error discovering workspace packages:", err);
|
|
314
|
-
throw err;
|
|
231
|
+
logger.info(`Commit status set to ${farver.cyan(state)} for ${farver.gray(sha.substring(0, 7))}`);
|
|
315
232
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
233
|
+
async resolveAuthorInfo(info) {
|
|
234
|
+
if (info.login) return info;
|
|
235
|
+
try {
|
|
236
|
+
const q = encodeURIComponent(`${info.email} type:user in:email`);
|
|
237
|
+
const data = await this.request(`/search/users?q=${q}`);
|
|
238
|
+
if (!data.items || data.items.length === 0) return info;
|
|
239
|
+
info.login = data.items[0].login;
|
|
240
|
+
} catch (err) {
|
|
241
|
+
logger.warn(`Failed to resolve author info for email ${info.email}: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
if (info.login) return info;
|
|
244
|
+
if (info.commits.length > 0) try {
|
|
245
|
+
const data = await this.request(`/repos/${this.owner}/${this.repo}/commits/${info.commits[0]}`);
|
|
246
|
+
if (data.author && data.author.login) info.login = data.author.login;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
logger.warn(`Failed to resolve author info from commits for email ${info.email}: ${err.message}`);
|
|
249
|
+
}
|
|
250
|
+
return info;
|
|
322
251
|
}
|
|
323
|
-
|
|
324
|
-
|
|
252
|
+
};
|
|
253
|
+
function createGitHubClient(options) {
|
|
254
|
+
return new GitHubClient(options);
|
|
255
|
+
}
|
|
256
|
+
function dedentString(str) {
|
|
257
|
+
const lines = str.split("\n");
|
|
258
|
+
const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity);
|
|
259
|
+
return lines.map((line) => minIndent === Infinity ? line : line.slice(minIndent)).join("\n").trim();
|
|
260
|
+
}
|
|
261
|
+
function generatePullRequestBody(updates, body) {
|
|
262
|
+
const eta = new Eta();
|
|
263
|
+
const bodyTemplate = body ? dedentString(body) : DEFAULT_PR_BODY_TEMPLATE;
|
|
264
|
+
return eta.renderString(bodyTemplate, { packages: updates.map((u) => ({
|
|
265
|
+
name: u.package.name,
|
|
266
|
+
currentVersion: u.currentVersion,
|
|
267
|
+
newVersion: u.newVersion,
|
|
268
|
+
bumpType: u.bumpType,
|
|
269
|
+
hasDirectChanges: u.hasDirectChanges
|
|
270
|
+
})) });
|
|
325
271
|
}
|
|
326
272
|
|
|
327
273
|
//#endregion
|
|
@@ -389,6 +335,11 @@ function normalizeReleaseScriptsOptions(options) {
|
|
|
389
335
|
githubToken: token,
|
|
390
336
|
owner,
|
|
391
337
|
repo,
|
|
338
|
+
githubClient: createGitHubClient({
|
|
339
|
+
owner,
|
|
340
|
+
repo,
|
|
341
|
+
githubToken: token
|
|
342
|
+
}),
|
|
392
343
|
packages: normalizedPackages,
|
|
393
344
|
branch: {
|
|
394
345
|
release: branch.release ?? "release/next",
|
|
@@ -411,7 +362,9 @@ function normalizeReleaseScriptsOptions(options) {
|
|
|
411
362
|
} : DEFAULT_TYPES,
|
|
412
363
|
npm: {
|
|
413
364
|
otp: npm.otp,
|
|
414
|
-
provenance: npm.provenance ?? true
|
|
365
|
+
provenance: npm.provenance ?? true,
|
|
366
|
+
access: npm.access ?? "public",
|
|
367
|
+
runBuild: npm.runBuild ?? true
|
|
415
368
|
},
|
|
416
369
|
prompts: {
|
|
417
370
|
versions: prompts.versions ?? !isCI,
|
|
@@ -421,68 +374,22 @@ function normalizeReleaseScriptsOptions(options) {
|
|
|
421
374
|
}
|
|
422
375
|
|
|
423
376
|
//#endregion
|
|
424
|
-
//#region src/
|
|
425
|
-
function
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (!ref.value) continue;
|
|
431
|
-
const number = Number.parseInt(ref.value.replace(/^#/, ""), 10);
|
|
432
|
-
if (Number.isNaN(number)) continue;
|
|
433
|
-
if (ref.type === "issue") {
|
|
434
|
-
line += ` ([Issue ${ref.value}](https://github.com/${owner}/${repo}/issues/${number}))`;
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
line += ` ([PR ${ref.value}](https://github.com/${owner}/${repo}/pull/${number}))`;
|
|
438
|
-
}
|
|
439
|
-
line += ` ([${commit.shortHash}](${commitUrl}))`;
|
|
440
|
-
if (authors.length > 0) {
|
|
441
|
-
const authorList = authors.map((author) => author.login ? `[@${author.login}](https://github.com/${author.login})` : author.name).join(", ");
|
|
442
|
-
line += ` (by ${authorList})`;
|
|
443
|
-
}
|
|
444
|
-
return line;
|
|
377
|
+
//#region src/types.ts
|
|
378
|
+
function ok(value) {
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
value
|
|
382
|
+
};
|
|
445
383
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
});
|
|
452
|
-
return Object.entries(types).map(([key, value]) => {
|
|
453
|
-
const formattedCommits = (grouped.get(key) ?? []).map((commit) => ({ line: formatCommitLine({
|
|
454
|
-
commit,
|
|
455
|
-
owner,
|
|
456
|
-
repo,
|
|
457
|
-
authors: commitAuthors.get(commit.hash) ?? []
|
|
458
|
-
}) }));
|
|
459
|
-
return {
|
|
460
|
-
name: key,
|
|
461
|
-
title: value.title,
|
|
462
|
-
commits: formattedCommits
|
|
463
|
-
};
|
|
464
|
-
});
|
|
384
|
+
function err(error) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
error
|
|
388
|
+
};
|
|
465
389
|
}
|
|
466
390
|
|
|
467
391
|
//#endregion
|
|
468
392
|
//#region src/core/git.ts
|
|
469
|
-
/**
|
|
470
|
-
* Check if the working directory is clean (no uncommitted changes)
|
|
471
|
-
* @param {string} workspaceRoot - The root directory of the workspace
|
|
472
|
-
* @returns {Promise<boolean>} A Promise resolving to true if clean, false otherwise
|
|
473
|
-
*/
|
|
474
|
-
async function isWorkingDirectoryClean(workspaceRoot) {
|
|
475
|
-
try {
|
|
476
|
-
if ((await run("git", ["status", "--porcelain"], { nodeOptions: {
|
|
477
|
-
cwd: workspaceRoot,
|
|
478
|
-
stdio: "pipe"
|
|
479
|
-
} })).stdout.trim() !== "") return false;
|
|
480
|
-
return true;
|
|
481
|
-
} catch (err) {
|
|
482
|
-
logger.error("Error checking git status:", err);
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
393
|
function toGitError(operation, error) {
|
|
487
394
|
return {
|
|
488
395
|
type: "git",
|
|
@@ -491,71 +398,16 @@ function toGitError(operation, error) {
|
|
|
491
398
|
stderr: (typeof error === "object" && error && "stderr" in error ? String(error.stderr ?? "") : void 0)?.trim() || void 0
|
|
492
399
|
};
|
|
493
400
|
}
|
|
494
|
-
async function
|
|
401
|
+
async function isWorkingDirectoryClean(workspaceRoot) {
|
|
495
402
|
try {
|
|
496
|
-
return ok(await
|
|
403
|
+
return ok((await run("git", ["status", "--porcelain"], { nodeOptions: {
|
|
404
|
+
cwd: workspaceRoot,
|
|
405
|
+
stdio: "pipe"
|
|
406
|
+
} })).stdout.trim() === "");
|
|
497
407
|
} catch (error) {
|
|
498
|
-
return err(toGitError(
|
|
408
|
+
return err(toGitError("isWorkingDirectoryClean", error));
|
|
499
409
|
}
|
|
500
410
|
}
|
|
501
|
-
function createGitOperations(overrides = {}) {
|
|
502
|
-
return {
|
|
503
|
-
isWorkingDirectoryClean: (workspaceRoot) => wrapGit("isWorkingDirectoryClean", async () => {
|
|
504
|
-
if (overrides.isWorkingDirectoryClean) return overrides.isWorkingDirectoryClean(workspaceRoot);
|
|
505
|
-
return isWorkingDirectoryClean(workspaceRoot);
|
|
506
|
-
}),
|
|
507
|
-
doesBranchExist: (branch, workspaceRoot) => wrapGit("doesBranchExist", async () => {
|
|
508
|
-
if (overrides.doesBranchExist) return overrides.doesBranchExist(branch, workspaceRoot);
|
|
509
|
-
return doesBranchExist(branch, workspaceRoot);
|
|
510
|
-
}),
|
|
511
|
-
getCurrentBranch: (workspaceRoot) => wrapGit("getCurrentBranch", async () => {
|
|
512
|
-
if (overrides.getCurrentBranch) return overrides.getCurrentBranch(workspaceRoot);
|
|
513
|
-
return getCurrentBranch(workspaceRoot);
|
|
514
|
-
}),
|
|
515
|
-
checkoutBranch: (branch, workspaceRoot) => wrapGit("checkoutBranch", async () => {
|
|
516
|
-
if (overrides.checkoutBranch) return overrides.checkoutBranch(branch, workspaceRoot);
|
|
517
|
-
return checkoutBranch(branch, workspaceRoot);
|
|
518
|
-
}),
|
|
519
|
-
createBranch: (branch, base, workspaceRoot) => wrapGit("createBranch", async () => {
|
|
520
|
-
if (overrides.createBranch) {
|
|
521
|
-
await overrides.createBranch(branch, base, workspaceRoot);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
await createBranch(branch, base, workspaceRoot);
|
|
525
|
-
}),
|
|
526
|
-
pullLatestChanges: (branch, workspaceRoot) => wrapGit("pullLatestChanges", async () => {
|
|
527
|
-
if (overrides.pullLatestChanges) return overrides.pullLatestChanges(branch, workspaceRoot);
|
|
528
|
-
return pullLatestChanges(branch, workspaceRoot);
|
|
529
|
-
}),
|
|
530
|
-
rebaseBranch: (ontoBranch, workspaceRoot) => wrapGit("rebaseBranch", async () => {
|
|
531
|
-
if (overrides.rebaseBranch) {
|
|
532
|
-
await overrides.rebaseBranch(ontoBranch, workspaceRoot);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
await rebaseBranch(ontoBranch, workspaceRoot);
|
|
536
|
-
}),
|
|
537
|
-
isBranchAheadOfRemote: (branch, workspaceRoot) => wrapGit("isBranchAheadOfRemote", async () => {
|
|
538
|
-
if (overrides.isBranchAheadOfRemote) return overrides.isBranchAheadOfRemote(branch, workspaceRoot);
|
|
539
|
-
return isBranchAheadOfRemote(branch, workspaceRoot);
|
|
540
|
-
}),
|
|
541
|
-
commitChanges: (message, workspaceRoot) => wrapGit("commitChanges", async () => {
|
|
542
|
-
if (overrides.commitChanges) return overrides.commitChanges(message, workspaceRoot);
|
|
543
|
-
return commitChanges(message, workspaceRoot);
|
|
544
|
-
}),
|
|
545
|
-
pushBranch: (branch, workspaceRoot, options) => wrapGit("pushBranch", async () => {
|
|
546
|
-
if (overrides.pushBranch) return overrides.pushBranch(branch, workspaceRoot, options);
|
|
547
|
-
return pushBranch(branch, workspaceRoot, options);
|
|
548
|
-
}),
|
|
549
|
-
readFileFromGit: (workspaceRoot, ref, filePath) => wrapGit("readFileFromGit", async () => {
|
|
550
|
-
if (overrides.readFileFromGit) return overrides.readFileFromGit(workspaceRoot, ref, filePath);
|
|
551
|
-
return readFileFromGit(workspaceRoot, ref, filePath);
|
|
552
|
-
}),
|
|
553
|
-
getMostRecentPackageTag: (workspaceRoot, packageName) => wrapGit("getMostRecentPackageTag", async () => {
|
|
554
|
-
if (overrides.getMostRecentPackageTag) return overrides.getMostRecentPackageTag(workspaceRoot, packageName);
|
|
555
|
-
return getMostRecentPackageTag(workspaceRoot, packageName);
|
|
556
|
-
})
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
411
|
/**
|
|
560
412
|
* Check if a git branch exists locally
|
|
561
413
|
* @param {string} branch - The branch name to check
|
|
@@ -572,9 +424,9 @@ async function doesBranchExist(branch, workspaceRoot) {
|
|
|
572
424
|
cwd: workspaceRoot,
|
|
573
425
|
stdio: "pipe"
|
|
574
426
|
} });
|
|
575
|
-
return true;
|
|
427
|
+
return ok(true);
|
|
576
428
|
} catch {
|
|
577
|
-
return false;
|
|
429
|
+
return ok(false);
|
|
578
430
|
}
|
|
579
431
|
}
|
|
580
432
|
/**
|
|
@@ -584,17 +436,16 @@ async function doesBranchExist(branch, workspaceRoot) {
|
|
|
584
436
|
*/
|
|
585
437
|
async function getCurrentBranch(workspaceRoot) {
|
|
586
438
|
try {
|
|
587
|
-
return (await run("git", [
|
|
439
|
+
return ok((await run("git", [
|
|
588
440
|
"rev-parse",
|
|
589
441
|
"--abbrev-ref",
|
|
590
442
|
"HEAD"
|
|
591
443
|
], { nodeOptions: {
|
|
592
444
|
cwd: workspaceRoot,
|
|
593
445
|
stdio: "pipe"
|
|
594
|
-
} })).stdout.trim();
|
|
595
|
-
} catch (
|
|
596
|
-
|
|
597
|
-
throw err;
|
|
446
|
+
} })).stdout.trim());
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return err(toGitError("getCurrentBranch", error));
|
|
598
449
|
}
|
|
599
450
|
}
|
|
600
451
|
/**
|
|
@@ -615,8 +466,9 @@ async function createBranch(branch, base, workspaceRoot) {
|
|
|
615
466
|
cwd: workspaceRoot,
|
|
616
467
|
stdio: "pipe"
|
|
617
468
|
} });
|
|
618
|
-
|
|
619
|
-
|
|
469
|
+
return ok(void 0);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return err(toGitError("createBranch", error));
|
|
620
472
|
}
|
|
621
473
|
}
|
|
622
474
|
async function checkoutBranch(branch, workspaceRoot) {
|
|
@@ -628,11 +480,23 @@ async function checkoutBranch(branch, workspaceRoot) {
|
|
|
628
480
|
} })).stderr.trim().match(/Switched to branch '(.+)'/);
|
|
629
481
|
if (match && match[1] === branch) {
|
|
630
482
|
logger.info(`Successfully switched to branch: ${farver.green(branch)}`);
|
|
631
|
-
return true;
|
|
483
|
+
return ok(true);
|
|
632
484
|
}
|
|
633
|
-
return false;
|
|
634
|
-
} catch {
|
|
635
|
-
|
|
485
|
+
return ok(false);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const gitError = toGitError("checkoutBranch", error);
|
|
488
|
+
logger.error(`Git checkout failed: ${gitError.message}`);
|
|
489
|
+
if (gitError.stderr) logger.error(`Git stderr: ${gitError.stderr}`);
|
|
490
|
+
try {
|
|
491
|
+
const branchResult = await run("git", ["branch", "-a"], { nodeOptions: {
|
|
492
|
+
cwd: workspaceRoot,
|
|
493
|
+
stdio: "pipe"
|
|
494
|
+
} });
|
|
495
|
+
logger.verbose(`Available branches:\n${branchResult.stdout}`);
|
|
496
|
+
} catch {
|
|
497
|
+
logger.verbose("Could not list available branches");
|
|
498
|
+
}
|
|
499
|
+
return err(gitError);
|
|
636
500
|
}
|
|
637
501
|
}
|
|
638
502
|
async function pullLatestChanges(branch, workspaceRoot) {
|
|
@@ -645,9 +509,9 @@ async function pullLatestChanges(branch, workspaceRoot) {
|
|
|
645
509
|
cwd: workspaceRoot,
|
|
646
510
|
stdio: "pipe"
|
|
647
511
|
} });
|
|
648
|
-
return true;
|
|
649
|
-
} catch {
|
|
650
|
-
return
|
|
512
|
+
return ok(true);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return err(toGitError("pullLatestChanges", error));
|
|
651
515
|
}
|
|
652
516
|
}
|
|
653
517
|
async function rebaseBranch(ontoBranch, workspaceRoot) {
|
|
@@ -657,9 +521,9 @@ async function rebaseBranch(ontoBranch, workspaceRoot) {
|
|
|
657
521
|
cwd: workspaceRoot,
|
|
658
522
|
stdio: "pipe"
|
|
659
523
|
} });
|
|
660
|
-
return
|
|
661
|
-
} catch {
|
|
662
|
-
|
|
524
|
+
return ok(void 0);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
return err(toGitError("rebaseBranch", error));
|
|
663
527
|
}
|
|
664
528
|
}
|
|
665
529
|
async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
@@ -672,9 +536,9 @@ async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
|
672
536
|
cwd: workspaceRoot,
|
|
673
537
|
stdio: "pipe"
|
|
674
538
|
} });
|
|
675
|
-
return Number.parseInt(result.stdout.trim(), 10) > 0;
|
|
539
|
+
return ok(Number.parseInt(result.stdout.trim(), 10) > 0);
|
|
676
540
|
} catch {
|
|
677
|
-
return true;
|
|
541
|
+
return ok(true);
|
|
678
542
|
}
|
|
679
543
|
}
|
|
680
544
|
async function commitChanges(message, workspaceRoot) {
|
|
@@ -683,7 +547,8 @@ async function commitChanges(message, workspaceRoot) {
|
|
|
683
547
|
cwd: workspaceRoot,
|
|
684
548
|
stdio: "pipe"
|
|
685
549
|
} });
|
|
686
|
-
|
|
550
|
+
const isClean = await isWorkingDirectoryClean(workspaceRoot);
|
|
551
|
+
if (!isClean.ok || isClean.value) return ok(false);
|
|
687
552
|
logger.info(`Committing changes: ${farver.dim(message)}`);
|
|
688
553
|
await runIfNotDry("git", [
|
|
689
554
|
"commit",
|
|
@@ -693,9 +558,9 @@ async function commitChanges(message, workspaceRoot) {
|
|
|
693
558
|
cwd: workspaceRoot,
|
|
694
559
|
stdio: "pipe"
|
|
695
560
|
} });
|
|
696
|
-
return true;
|
|
697
|
-
} catch {
|
|
698
|
-
|
|
561
|
+
return ok(true);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return err(toGitError("commitChanges", error));
|
|
699
564
|
}
|
|
700
565
|
}
|
|
701
566
|
async function pushBranch(branch, workspaceRoot, options) {
|
|
@@ -706,6 +571,14 @@ async function pushBranch(branch, workspaceRoot, options) {
|
|
|
706
571
|
branch
|
|
707
572
|
];
|
|
708
573
|
if (options?.forceWithLease) {
|
|
574
|
+
await run("git", [
|
|
575
|
+
"fetch",
|
|
576
|
+
"origin",
|
|
577
|
+
branch
|
|
578
|
+
], { nodeOptions: {
|
|
579
|
+
cwd: workspaceRoot,
|
|
580
|
+
stdio: "pipe"
|
|
581
|
+
} });
|
|
709
582
|
args.push("--force-with-lease");
|
|
710
583
|
logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`);
|
|
711
584
|
} else if (options?.force) {
|
|
@@ -716,19 +589,19 @@ async function pushBranch(branch, workspaceRoot, options) {
|
|
|
716
589
|
cwd: workspaceRoot,
|
|
717
590
|
stdio: "pipe"
|
|
718
591
|
} });
|
|
719
|
-
return true;
|
|
720
|
-
} catch {
|
|
721
|
-
|
|
592
|
+
return ok(true);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
return err(toGitError("pushBranch", error));
|
|
722
595
|
}
|
|
723
596
|
}
|
|
724
597
|
async function readFileFromGit(workspaceRoot, ref, filePath) {
|
|
725
598
|
try {
|
|
726
|
-
return (await run("git", ["show", `${ref}:${filePath}`], { nodeOptions: {
|
|
599
|
+
return ok((await run("git", ["show", `${ref}:${filePath}`], { nodeOptions: {
|
|
727
600
|
cwd: workspaceRoot,
|
|
728
601
|
stdio: "pipe"
|
|
729
|
-
} })).stdout;
|
|
602
|
+
} })).stdout);
|
|
730
603
|
} catch {
|
|
731
|
-
return null;
|
|
604
|
+
return ok(null);
|
|
732
605
|
}
|
|
733
606
|
}
|
|
734
607
|
async function getMostRecentPackageTag(workspaceRoot, packageName) {
|
|
@@ -742,11 +615,10 @@ async function getMostRecentPackageTag(workspaceRoot, packageName) {
|
|
|
742
615
|
stdio: "pipe"
|
|
743
616
|
} });
|
|
744
617
|
const tags = stdout.split("\n").map((tag) => tag.trim()).filter(Boolean);
|
|
745
|
-
if (tags.length === 0) return;
|
|
746
|
-
return tags.reverse()[0];
|
|
747
|
-
} catch (
|
|
748
|
-
|
|
749
|
-
return;
|
|
618
|
+
if (tags.length === 0) return ok(void 0);
|
|
619
|
+
return ok(tags.reverse()[0]);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
return err(toGitError("getMostRecentPackageTag", error));
|
|
750
622
|
}
|
|
751
623
|
}
|
|
752
624
|
/**
|
|
@@ -793,11 +665,65 @@ async function getGroupedFilesByCommitSha(workspaceRoot, from, to) {
|
|
|
793
665
|
if (currentSha === null) continue;
|
|
794
666
|
commitsMap.get(currentSha).push(trimmedLine);
|
|
795
667
|
}
|
|
796
|
-
return commitsMap;
|
|
797
|
-
} catch {
|
|
798
|
-
return
|
|
668
|
+
return ok(commitsMap);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
return err(toGitError("getGroupedFilesByCommitSha", error));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Create a git tag for a package release
|
|
675
|
+
* @param packageName - The package name (e.g., "@scope/name")
|
|
676
|
+
* @param version - The version to tag (e.g., "1.2.3")
|
|
677
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
678
|
+
* @returns Result indicating success or failure
|
|
679
|
+
*/
|
|
680
|
+
async function createPackageTag(packageName, version, workspaceRoot) {
|
|
681
|
+
const tagName = `${packageName}@${version}`;
|
|
682
|
+
try {
|
|
683
|
+
logger.info(`Creating tag: ${farver.green(tagName)}`);
|
|
684
|
+
await runIfNotDry("git", ["tag", tagName], { nodeOptions: {
|
|
685
|
+
cwd: workspaceRoot,
|
|
686
|
+
stdio: "pipe"
|
|
687
|
+
} });
|
|
688
|
+
return ok(void 0);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
return err(toGitError("createPackageTag", error));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Push a specific tag to the remote repository
|
|
695
|
+
* @param tagName - The tag name to push
|
|
696
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
697
|
+
* @returns Result indicating success or failure
|
|
698
|
+
*/
|
|
699
|
+
async function pushTag(tagName, workspaceRoot) {
|
|
700
|
+
try {
|
|
701
|
+
logger.info(`Pushing tag: ${farver.green(tagName)}`);
|
|
702
|
+
await runIfNotDry("git", [
|
|
703
|
+
"push",
|
|
704
|
+
"origin",
|
|
705
|
+
tagName
|
|
706
|
+
], { nodeOptions: {
|
|
707
|
+
cwd: workspaceRoot,
|
|
708
|
+
stdio: "pipe"
|
|
709
|
+
} });
|
|
710
|
+
return ok(void 0);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
return err(toGitError("pushTag", error));
|
|
799
713
|
}
|
|
800
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* Create and push a package tag in one operation
|
|
717
|
+
* @param packageName - The package name
|
|
718
|
+
* @param version - The version to tag
|
|
719
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
720
|
+
* @returns Result indicating success or failure
|
|
721
|
+
*/
|
|
722
|
+
async function createAndPushPackageTag(packageName, version, workspaceRoot) {
|
|
723
|
+
const createResult = await createPackageTag(packageName, version, workspaceRoot);
|
|
724
|
+
if (!createResult.ok) return createResult;
|
|
725
|
+
return pushTag(`${packageName}@${version}`, workspaceRoot);
|
|
726
|
+
}
|
|
801
727
|
|
|
802
728
|
//#endregion
|
|
803
729
|
//#region src/core/changelog.ts
|
|
@@ -833,7 +759,7 @@ async function updateChangelog(options) {
|
|
|
833
759
|
const changelogPath = join(workspacePackage.path, "CHANGELOG.md");
|
|
834
760
|
const changelogRelativePath = relative(normalizedOptions.workspaceRoot, join(workspacePackage.path, "CHANGELOG.md"));
|
|
835
761
|
const existingContent = await readFileFromGit(normalizedOptions.workspaceRoot, normalizedOptions.branch.default, changelogRelativePath);
|
|
836
|
-
logger.verbose("Existing content found: ", Boolean(existingContent));
|
|
762
|
+
logger.verbose("Existing content found: ", existingContent.ok && Boolean(existingContent.value));
|
|
837
763
|
const newEntry = await generateChangelogEntry({
|
|
838
764
|
packageName: workspacePackage.name,
|
|
839
765
|
version,
|
|
@@ -847,13 +773,13 @@ async function updateChangelog(options) {
|
|
|
847
773
|
githubClient
|
|
848
774
|
});
|
|
849
775
|
let updatedContent;
|
|
850
|
-
if (!existingContent) {
|
|
776
|
+
if (!existingContent.ok || !existingContent.value) {
|
|
851
777
|
updatedContent = `# ${workspacePackage.name}\n\n${newEntry}\n`;
|
|
852
778
|
await writeFile(changelogPath, updatedContent, "utf-8");
|
|
853
779
|
return;
|
|
854
780
|
}
|
|
855
|
-
const parsed = parseChangelog(existingContent);
|
|
856
|
-
const lines = existingContent.split("\n");
|
|
781
|
+
const parsed = parseChangelog(existingContent.value);
|
|
782
|
+
const lines = existingContent.value.split("\n");
|
|
857
783
|
const existingVersionIndex = parsed.versions.findIndex((v) => v.version === version);
|
|
858
784
|
if (existingVersionIndex !== -1) {
|
|
859
785
|
const existingVersion = parsed.versions[existingVersionIndex];
|
|
@@ -944,269 +870,252 @@ function parseChangelog(content) {
|
|
|
944
870
|
}
|
|
945
871
|
|
|
946
872
|
//#endregion
|
|
947
|
-
//#region src/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
873
|
+
//#region src/operations/semver.ts
|
|
874
|
+
function isValidSemver(version) {
|
|
875
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
|
|
876
|
+
}
|
|
877
|
+
function getNextVersion(currentVersion, bump) {
|
|
878
|
+
if (bump === "none") return currentVersion;
|
|
879
|
+
if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
|
|
880
|
+
const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
881
|
+
if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
|
|
882
|
+
const [, major, minor, patch] = match;
|
|
883
|
+
let newMajor = Number.parseInt(major, 10);
|
|
884
|
+
let newMinor = Number.parseInt(minor, 10);
|
|
885
|
+
let newPatch = Number.parseInt(patch, 10);
|
|
886
|
+
switch (bump) {
|
|
887
|
+
case "major":
|
|
888
|
+
newMajor += 1;
|
|
889
|
+
newMinor = 0;
|
|
890
|
+
newPatch = 0;
|
|
891
|
+
break;
|
|
892
|
+
case "minor":
|
|
893
|
+
newMinor += 1;
|
|
894
|
+
newPatch = 0;
|
|
895
|
+
break;
|
|
896
|
+
case "patch":
|
|
897
|
+
newPatch += 1;
|
|
898
|
+
break;
|
|
957
899
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
900
|
+
return `${newMajor}.${newMinor}.${newPatch}`;
|
|
901
|
+
}
|
|
902
|
+
function calculateBumpType(oldVersion, newVersion) {
|
|
903
|
+
if (!isValidSemver(oldVersion) || !isValidSemver(newVersion)) throw new Error(`Cannot calculate bump type for invalid semver: ${oldVersion} or ${newVersion}`);
|
|
904
|
+
const oldParts = oldVersion.split(".").map(Number);
|
|
905
|
+
const newParts = newVersion.split(".").map(Number);
|
|
906
|
+
if (newParts[0] > oldParts[0]) return "major";
|
|
907
|
+
if (newParts[1] > oldParts[1]) return "minor";
|
|
908
|
+
if (newParts[2] > oldParts[2]) return "patch";
|
|
909
|
+
return "none";
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/core/prompts.ts
|
|
914
|
+
async function selectPackagePrompt(packages) {
|
|
915
|
+
const response = await prompts({
|
|
916
|
+
type: "multiselect",
|
|
917
|
+
name: "selectedPackages",
|
|
918
|
+
message: "Select packages to release",
|
|
919
|
+
choices: packages.map((pkg) => ({
|
|
920
|
+
title: `${pkg.name} (${farver.bold(pkg.version)})`,
|
|
921
|
+
value: pkg.name,
|
|
922
|
+
selected: true
|
|
923
|
+
})),
|
|
924
|
+
min: 1,
|
|
925
|
+
hint: "Space to select/deselect. Return to submit.",
|
|
926
|
+
instructions: false
|
|
927
|
+
});
|
|
928
|
+
if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
|
|
929
|
+
return response.selectedPackages;
|
|
930
|
+
}
|
|
931
|
+
async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggestedVersion) {
|
|
932
|
+
const answers = await prompts([{
|
|
933
|
+
type: "autocomplete",
|
|
934
|
+
name: "version",
|
|
935
|
+
message: `${pkg.name}: ${farver.green(pkg.version)}`,
|
|
936
|
+
choices: [
|
|
937
|
+
{
|
|
938
|
+
value: "skip",
|
|
939
|
+
title: `skip ${farver.dim("(no change)")}`
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
value: "major",
|
|
943
|
+
title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}`
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
value: "minor",
|
|
947
|
+
title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}`
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
value: "patch",
|
|
951
|
+
title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
value: "suggested",
|
|
955
|
+
title: `suggested ${farver.bold(suggestedVersion)}`
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
value: "as-is",
|
|
959
|
+
title: `as-is ${farver.dim("(keep current version)")}`
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
value: "custom",
|
|
963
|
+
title: "custom"
|
|
967
964
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const endpoint = `/repos/${this.owner}/${this.repo}/pulls?state=open&head=${encodeURIComponent(head)}`;
|
|
979
|
-
logger.verbose(`Requesting pull request for branch: ${branch} (url: ${this.apiBase}${endpoint})`);
|
|
980
|
-
const pulls = await this.request(endpoint);
|
|
981
|
-
if (!Array.isArray(pulls) || pulls.length === 0) return null;
|
|
982
|
-
const firstPullRequest = pulls[0];
|
|
983
|
-
if (typeof firstPullRequest !== "object" || firstPullRequest === null || !("number" in firstPullRequest) || typeof firstPullRequest.number !== "number" || !("title" in firstPullRequest) || typeof firstPullRequest.title !== "string" || !("body" in firstPullRequest) || typeof firstPullRequest.body !== "string" || !("draft" in firstPullRequest) || typeof firstPullRequest.draft !== "boolean" || !("html_url" in firstPullRequest) || typeof firstPullRequest.html_url !== "string") throw new TypeError("Pull request data validation failed");
|
|
984
|
-
const pullRequest = {
|
|
985
|
-
number: firstPullRequest.number,
|
|
986
|
-
title: firstPullRequest.title,
|
|
987
|
-
body: firstPullRequest.body,
|
|
988
|
-
draft: firstPullRequest.draft,
|
|
989
|
-
html_url: firstPullRequest.html_url,
|
|
990
|
-
head: "head" in firstPullRequest && typeof firstPullRequest.head === "object" && firstPullRequest.head !== null && "sha" in firstPullRequest.head && typeof firstPullRequest.head.sha === "string" ? { sha: firstPullRequest.head.sha } : void 0
|
|
991
|
-
};
|
|
992
|
-
logger.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
|
|
993
|
-
return pullRequest;
|
|
994
|
-
}
|
|
995
|
-
async upsertPullRequest({ title, body, head, base, pullNumber }) {
|
|
996
|
-
const isUpdate = typeof pullNumber === "number";
|
|
997
|
-
const endpoint = isUpdate ? `/repos/${this.owner}/${this.repo}/pulls/${pullNumber}` : `/repos/${this.owner}/${this.repo}/pulls`;
|
|
998
|
-
const requestBody = isUpdate ? {
|
|
999
|
-
title,
|
|
1000
|
-
body
|
|
1001
|
-
} : {
|
|
1002
|
-
title,
|
|
1003
|
-
body,
|
|
1004
|
-
head,
|
|
1005
|
-
base,
|
|
1006
|
-
draft: true
|
|
1007
|
-
};
|
|
1008
|
-
logger.verbose(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${this.apiBase}${endpoint})`);
|
|
1009
|
-
const pr = await this.request(endpoint, {
|
|
1010
|
-
method: isUpdate ? "PATCH" : "POST",
|
|
1011
|
-
body: JSON.stringify(requestBody)
|
|
1012
|
-
});
|
|
1013
|
-
if (typeof pr !== "object" || pr === null || !("number" in pr) || typeof pr.number !== "number" || !("title" in pr) || typeof pr.title !== "string" || !("body" in pr) || typeof pr.body !== "string" || !("draft" in pr) || typeof pr.draft !== "boolean" || !("html_url" in pr) || typeof pr.html_url !== "string") throw new TypeError("Pull request data validation failed");
|
|
1014
|
-
const action = isUpdate ? "Updated" : "Created";
|
|
1015
|
-
logger.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
|
|
1016
|
-
return {
|
|
1017
|
-
number: pr.number,
|
|
1018
|
-
title: pr.title,
|
|
1019
|
-
body: pr.body,
|
|
1020
|
-
draft: pr.draft,
|
|
1021
|
-
html_url: pr.html_url
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
async setCommitStatus({ sha, state, targetUrl, description, context }) {
|
|
1025
|
-
const endpoint = `/repos/${this.owner}/${this.repo}/statuses/${sha}`;
|
|
1026
|
-
logger.verbose(`Setting commit status on ${sha} to ${state} (url: ${this.apiBase}${endpoint})`);
|
|
1027
|
-
await this.request(endpoint, {
|
|
1028
|
-
method: "POST",
|
|
1029
|
-
body: JSON.stringify({
|
|
1030
|
-
state,
|
|
1031
|
-
target_url: targetUrl,
|
|
1032
|
-
description: description || "",
|
|
1033
|
-
context
|
|
1034
|
-
})
|
|
1035
|
-
});
|
|
1036
|
-
logger.info(`Commit status set to ${farver.cyan(state)} for ${farver.gray(sha.substring(0, 7))}`);
|
|
1037
|
-
}
|
|
1038
|
-
async resolveAuthorInfo(info) {
|
|
1039
|
-
if (info.login) return info;
|
|
1040
|
-
try {
|
|
1041
|
-
const q = encodeURIComponent(`${info.email} type:user in:email`);
|
|
1042
|
-
const data = await this.request(`/search/users?q=${q}`);
|
|
1043
|
-
if (!data.items || data.items.length === 0) return info;
|
|
1044
|
-
info.login = data.items[0].login;
|
|
1045
|
-
} catch (err) {
|
|
1046
|
-
logger.warn(`Failed to resolve author info for email ${info.email}: ${err.message}`);
|
|
1047
|
-
}
|
|
1048
|
-
if (info.login) return info;
|
|
1049
|
-
if (info.commits.length > 0) try {
|
|
1050
|
-
const data = await this.request(`/repos/${this.owner}/${this.repo}/commits/${info.commits[0]}`);
|
|
1051
|
-
if (data.author && data.author.login) info.login = data.author.login;
|
|
1052
|
-
} catch (err) {
|
|
1053
|
-
logger.warn(`Failed to resolve author info from commits for email ${info.email}: ${err.message}`);
|
|
965
|
+
],
|
|
966
|
+
initial: suggestedVersion === currentVersion ? 0 : 4
|
|
967
|
+
}, {
|
|
968
|
+
type: (prev) => prev === "custom" ? "text" : null,
|
|
969
|
+
name: "custom",
|
|
970
|
+
message: "Enter the new version number:",
|
|
971
|
+
initial: suggestedVersion,
|
|
972
|
+
validate: (custom) => {
|
|
973
|
+
if (isValidSemver(custom)) return true;
|
|
974
|
+
return "That's not a valid version number";
|
|
1054
975
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
976
|
+
}]);
|
|
977
|
+
if (!answers.version) return null;
|
|
978
|
+
if (answers.version === "skip") return null;
|
|
979
|
+
else if (answers.version === "suggested") return suggestedVersion;
|
|
980
|
+
else if (answers.version === "custom") {
|
|
981
|
+
if (!answers.custom) return null;
|
|
982
|
+
return answers.custom;
|
|
983
|
+
} else if (answers.version === "as-is") return currentVersion;
|
|
984
|
+
else return getNextVersion(pkg.version, answers.version);
|
|
1060
985
|
}
|
|
1061
|
-
|
|
986
|
+
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/core/workspace.ts
|
|
989
|
+
function toWorkspaceError(operation, error) {
|
|
1062
990
|
return {
|
|
1063
|
-
type: "
|
|
991
|
+
type: "workspace",
|
|
1064
992
|
operation,
|
|
1065
993
|
message: error instanceof Error ? error.message : String(error)
|
|
1066
994
|
};
|
|
1067
995
|
}
|
|
1068
|
-
async function
|
|
996
|
+
async function discoverWorkspacePackages(workspaceRoot, options) {
|
|
997
|
+
let workspaceOptions;
|
|
998
|
+
let explicitPackages;
|
|
999
|
+
if (options.packages == null || options.packages === true) workspaceOptions = { excludePrivate: false };
|
|
1000
|
+
else if (Array.isArray(options.packages)) {
|
|
1001
|
+
workspaceOptions = {
|
|
1002
|
+
excludePrivate: false,
|
|
1003
|
+
include: options.packages
|
|
1004
|
+
};
|
|
1005
|
+
explicitPackages = options.packages;
|
|
1006
|
+
} else {
|
|
1007
|
+
workspaceOptions = options.packages;
|
|
1008
|
+
if (options.packages.include) explicitPackages = options.packages.include;
|
|
1009
|
+
}
|
|
1010
|
+
let workspacePackages;
|
|
1069
1011
|
try {
|
|
1070
|
-
|
|
1012
|
+
workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
|
|
1071
1013
|
} catch (error) {
|
|
1072
|
-
return err(
|
|
1014
|
+
return err(toWorkspaceError("discoverWorkspacePackages", error));
|
|
1015
|
+
}
|
|
1016
|
+
if (explicitPackages) {
|
|
1017
|
+
const foundNames = new Set(workspacePackages.map((p) => p.name));
|
|
1018
|
+
const missing = explicitPackages.filter((p) => !foundNames.has(p));
|
|
1019
|
+
if (missing.length > 0) exitWithError(`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`, "Check your package names or run 'pnpm ls' to see available packages");
|
|
1020
|
+
}
|
|
1021
|
+
const isPackagePromptEnabled = options.prompts?.packages !== false;
|
|
1022
|
+
if (!isCI && isPackagePromptEnabled && !explicitPackages) {
|
|
1023
|
+
const selectedNames = await selectPackagePrompt(workspacePackages);
|
|
1024
|
+
workspacePackages = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
|
|
1073
1025
|
}
|
|
1026
|
+
return ok(workspacePackages);
|
|
1074
1027
|
}
|
|
1075
|
-
function
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1028
|
+
async function findWorkspacePackages(workspaceRoot, options) {
|
|
1029
|
+
try {
|
|
1030
|
+
const result = await run("pnpm", [
|
|
1031
|
+
"-r",
|
|
1032
|
+
"ls",
|
|
1033
|
+
"--json"
|
|
1034
|
+
], { nodeOptions: {
|
|
1035
|
+
cwd: workspaceRoot,
|
|
1036
|
+
stdio: "pipe"
|
|
1037
|
+
} });
|
|
1038
|
+
const rawProjects = JSON.parse(result.stdout);
|
|
1039
|
+
const allPackageNames = new Set(rawProjects.map((p) => p.name));
|
|
1040
|
+
const excludedPackages = /* @__PURE__ */ new Set();
|
|
1041
|
+
const promises = rawProjects.map(async (rawProject) => {
|
|
1042
|
+
const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
|
|
1043
|
+
const packageJson = JSON.parse(content);
|
|
1044
|
+
if (!shouldIncludePackage(packageJson, options)) {
|
|
1045
|
+
excludedPackages.add(rawProject.name);
|
|
1046
|
+
return null;
|
|
1090
1047
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1048
|
+
return {
|
|
1049
|
+
name: rawProject.name,
|
|
1050
|
+
version: rawProject.version,
|
|
1051
|
+
path: rawProject.path,
|
|
1052
|
+
packageJson,
|
|
1053
|
+
workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => {
|
|
1054
|
+
return allPackageNames.has(dep);
|
|
1055
|
+
}),
|
|
1056
|
+
workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => {
|
|
1057
|
+
return allPackageNames.has(dep);
|
|
1058
|
+
})
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
1061
|
+
const packages = await Promise.all(promises);
|
|
1062
|
+
if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
|
|
1063
|
+
return packages.filter((pkg) => pkg !== null);
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
logger.error("Error discovering workspace packages:", err);
|
|
1066
|
+
throw err;
|
|
1067
|
+
}
|
|
1103
1068
|
}
|
|
1104
|
-
function
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
hasDirectChanges: u.hasDirectChanges
|
|
1113
|
-
})) });
|
|
1069
|
+
function shouldIncludePackage(pkg, options) {
|
|
1070
|
+
if (!options) return true;
|
|
1071
|
+
if (options.excludePrivate && pkg.private) return false;
|
|
1072
|
+
if (options.include && options.include.length > 0) {
|
|
1073
|
+
if (!options.include.includes(pkg.name)) return false;
|
|
1074
|
+
}
|
|
1075
|
+
if (options.exclude?.includes(pkg.name)) return false;
|
|
1076
|
+
return true;
|
|
1114
1077
|
}
|
|
1115
1078
|
|
|
1116
1079
|
//#endregion
|
|
1117
1080
|
//#region src/operations/branch.ts
|
|
1118
1081
|
async function prepareReleaseBranch(options) {
|
|
1119
|
-
const {
|
|
1120
|
-
const currentBranch = await
|
|
1082
|
+
const { workspaceRoot, releaseBranch, defaultBranch } = options;
|
|
1083
|
+
const currentBranch = await getCurrentBranch(workspaceRoot);
|
|
1121
1084
|
if (!currentBranch.ok) return currentBranch;
|
|
1122
1085
|
if (currentBranch.value !== defaultBranch) return err({
|
|
1123
1086
|
type: "git",
|
|
1124
1087
|
operation: "validateBranch",
|
|
1125
1088
|
message: `Current branch is '${currentBranch.value}'. Please switch to '${defaultBranch}'.`
|
|
1126
1089
|
});
|
|
1127
|
-
const branchExists = await
|
|
1090
|
+
const branchExists = await doesBranchExist(releaseBranch, workspaceRoot);
|
|
1128
1091
|
if (!branchExists.ok) return branchExists;
|
|
1129
1092
|
if (!branchExists.value) {
|
|
1130
|
-
const created = await
|
|
1093
|
+
const created = await createBranch(releaseBranch, defaultBranch, workspaceRoot);
|
|
1131
1094
|
if (!created.ok) return created;
|
|
1132
1095
|
}
|
|
1133
|
-
const checkedOut = await
|
|
1096
|
+
const checkedOut = await checkoutBranch(releaseBranch, workspaceRoot);
|
|
1134
1097
|
if (!checkedOut.ok) return checkedOut;
|
|
1135
1098
|
if (branchExists.value) {
|
|
1136
|
-
const pulled = await
|
|
1099
|
+
const pulled = await pullLatestChanges(releaseBranch, workspaceRoot);
|
|
1137
1100
|
if (!pulled.ok) return pulled;
|
|
1138
1101
|
if (!pulled.value) logger.warn("Failed to pull latest changes, continuing anyway.");
|
|
1139
1102
|
}
|
|
1140
|
-
const rebased = await
|
|
1103
|
+
const rebased = await rebaseBranch(defaultBranch, workspaceRoot);
|
|
1141
1104
|
if (!rebased.ok) return rebased;
|
|
1142
1105
|
return ok(void 0);
|
|
1143
1106
|
}
|
|
1144
1107
|
async function syncReleaseChanges(options) {
|
|
1145
|
-
const {
|
|
1146
|
-
const committed = hasChanges ? await
|
|
1108
|
+
const { workspaceRoot, releaseBranch, commitMessage, hasChanges } = options;
|
|
1109
|
+
const committed = hasChanges ? await commitChanges(commitMessage, workspaceRoot) : ok(false);
|
|
1147
1110
|
if (!committed.ok) return committed;
|
|
1148
|
-
const isAhead = await
|
|
1111
|
+
const isAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
|
|
1149
1112
|
if (!isAhead.ok) return isAhead;
|
|
1150
1113
|
if (!committed.value && !isAhead.value) return ok(false);
|
|
1151
|
-
const pushed = await
|
|
1114
|
+
const pushed = await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
|
|
1152
1115
|
if (!pushed.ok) return pushed;
|
|
1153
1116
|
return ok(true);
|
|
1154
1117
|
}
|
|
1155
1118
|
|
|
1156
|
-
//#endregion
|
|
1157
|
-
//#region src/operations/calculate.ts
|
|
1158
|
-
async function calculateUpdates(options) {
|
|
1159
|
-
const { versioning, workspacePackages, workspaceRoot, showPrompt, overrides, globalCommitMode } = options;
|
|
1160
|
-
const grouped = await versioning.getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages);
|
|
1161
|
-
if (!grouped.ok) return grouped;
|
|
1162
|
-
const global = await versioning.getGlobalCommitsPerPackage(workspaceRoot, grouped.value, workspacePackages, globalCommitMode);
|
|
1163
|
-
if (!global.ok) return global;
|
|
1164
|
-
const updates = await versioning.calculateAndPrepareVersionUpdates({
|
|
1165
|
-
workspacePackages,
|
|
1166
|
-
packageCommits: grouped.value,
|
|
1167
|
-
workspaceRoot,
|
|
1168
|
-
showPrompt,
|
|
1169
|
-
globalCommitsPerPackage: global.value,
|
|
1170
|
-
overrides
|
|
1171
|
-
});
|
|
1172
|
-
if (!updates.ok) return updates;
|
|
1173
|
-
return updates;
|
|
1174
|
-
}
|
|
1175
|
-
function ensureHasPackages(packages) {
|
|
1176
|
-
if (packages.length === 0) return err({
|
|
1177
|
-
type: "git",
|
|
1178
|
-
operation: "discoverPackages",
|
|
1179
|
-
message: "No packages found to release"
|
|
1180
|
-
});
|
|
1181
|
-
return {
|
|
1182
|
-
ok: true,
|
|
1183
|
-
value: packages
|
|
1184
|
-
};
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
//#endregion
|
|
1188
|
-
//#region src/operations/pr.ts
|
|
1189
|
-
async function syncPullRequest(options) {
|
|
1190
|
-
const { github, releaseBranch, defaultBranch, pullRequestTitle, pullRequestBody, updates } = options;
|
|
1191
|
-
const existing = await github.getExistingPullRequest(releaseBranch);
|
|
1192
|
-
if (!existing.ok) return existing;
|
|
1193
|
-
const doesExist = !!existing.value;
|
|
1194
|
-
const title = existing.value?.title || pullRequestTitle || "chore: update package versions";
|
|
1195
|
-
const body = generatePullRequestBody(updates, pullRequestBody);
|
|
1196
|
-
const pr = await github.upsertPullRequest({
|
|
1197
|
-
pullNumber: existing.value?.number,
|
|
1198
|
-
title,
|
|
1199
|
-
body,
|
|
1200
|
-
head: releaseBranch,
|
|
1201
|
-
base: defaultBranch
|
|
1202
|
-
});
|
|
1203
|
-
if (!pr.ok) return pr;
|
|
1204
|
-
return ok({
|
|
1205
|
-
pullRequest: pr.value,
|
|
1206
|
-
created: !doesExist
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
1119
|
//#endregion
|
|
1211
1120
|
//#region src/versioning/commits.ts
|
|
1212
1121
|
/**
|
|
@@ -1220,7 +1129,8 @@ async function syncPullRequest(options) {
|
|
|
1220
1129
|
async function getWorkspacePackageGroupedCommits(workspaceRoot, packages) {
|
|
1221
1130
|
const changedPackages = /* @__PURE__ */ new Map();
|
|
1222
1131
|
const promises = packages.map(async (pkg) => {
|
|
1223
|
-
const
|
|
1132
|
+
const lastTagResult = await getMostRecentPackageTag(workspaceRoot, pkg.name);
|
|
1133
|
+
const lastTag = lastTagResult.ok ? lastTagResult.value : void 0;
|
|
1224
1134
|
const allCommits = await getCommits({
|
|
1225
1135
|
from: lastTag,
|
|
1226
1136
|
to: "HEAD",
|
|
@@ -1320,17 +1230,17 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
|
|
|
1320
1230
|
}
|
|
1321
1231
|
logger.verbose("Fetching files for commits range", `${farver.cyan(commitRange.oldest)}..${farver.cyan(commitRange.newest)}`);
|
|
1322
1232
|
const commitFilesMap = await getGroupedFilesByCommitSha(workspaceRoot, commitRange.oldest, commitRange.newest);
|
|
1323
|
-
if (!commitFilesMap) {
|
|
1233
|
+
if (!commitFilesMap.ok) {
|
|
1324
1234
|
logger.warn("Failed to get commit file list, returning empty global commits");
|
|
1325
1235
|
return result;
|
|
1326
1236
|
}
|
|
1327
|
-
logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.size)} commits in ONE git call`);
|
|
1237
|
+
logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.value.size)} commits in ONE git call`);
|
|
1328
1238
|
const packagePaths = new Set(allPackages.map((p) => p.path));
|
|
1329
1239
|
for (const [pkgName, commits] of packageCommits) {
|
|
1330
1240
|
const globalCommitsAffectingPackage = [];
|
|
1331
1241
|
logger.verbose("Filtering global commits for package", `${farver.bold(pkgName)} from ${farver.cyan(commits.length)} commits`);
|
|
1332
1242
|
for (const commit of commits) {
|
|
1333
|
-
const files = commitFilesMap.get(commit.shortHash);
|
|
1243
|
+
const files = commitFilesMap.value.get(commit.shortHash);
|
|
1334
1244
|
if (!files) continue;
|
|
1335
1245
|
if (isGlobalCommit(workspaceRoot, files, packagePaths)) globalCommitsAffectingPackage.push(commit);
|
|
1336
1246
|
}
|
|
@@ -1341,7 +1251,7 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
|
|
|
1341
1251
|
}
|
|
1342
1252
|
const dependencyCommits = [];
|
|
1343
1253
|
for (const commit of globalCommitsAffectingPackage) {
|
|
1344
|
-
const files = commitFilesMap.get(commit.shortHash);
|
|
1254
|
+
const files = commitFilesMap.value.get(commit.shortHash);
|
|
1345
1255
|
if (!files) continue;
|
|
1346
1256
|
if (files.some((file) => DEPENDENCY_FILES.includes(file.startsWith("./") ? file.slice(2) : file))) {
|
|
1347
1257
|
logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`);
|
|
@@ -1439,6 +1349,51 @@ function getAllAffectedPackages(graph, changedPackages) {
|
|
|
1439
1349
|
return affected;
|
|
1440
1350
|
}
|
|
1441
1351
|
/**
|
|
1352
|
+
* Calculate the order in which packages should be published
|
|
1353
|
+
*
|
|
1354
|
+
* Performs topological sorting to ensure dependencies are published before dependents.
|
|
1355
|
+
* Assigns a "level" to each package based on its depth in the dependency tree.
|
|
1356
|
+
*
|
|
1357
|
+
* This is used by the publish command to publish packages in the correct order.
|
|
1358
|
+
*
|
|
1359
|
+
* @param graph - Dependency graph
|
|
1360
|
+
* @param packagesToPublish - Set of package names to publish
|
|
1361
|
+
* @returns Array of packages in publish order with their dependency level
|
|
1362
|
+
*/
|
|
1363
|
+
function getPackagePublishOrder(graph, packagesToPublish) {
|
|
1364
|
+
const result = [];
|
|
1365
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1366
|
+
const toUpdate = new Set(packagesToPublish);
|
|
1367
|
+
const packagesToProcess = new Set(packagesToPublish);
|
|
1368
|
+
for (const pkg of packagesToPublish) {
|
|
1369
|
+
const deps = graph.dependents.get(pkg);
|
|
1370
|
+
if (deps) for (const dep of deps) {
|
|
1371
|
+
packagesToProcess.add(dep);
|
|
1372
|
+
toUpdate.add(dep);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function visit(pkgName, level) {
|
|
1376
|
+
if (visited.has(pkgName)) return;
|
|
1377
|
+
visited.add(pkgName);
|
|
1378
|
+
const pkg = graph.packages.get(pkgName);
|
|
1379
|
+
if (!pkg) return;
|
|
1380
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
1381
|
+
let maxDepLevel = level;
|
|
1382
|
+
for (const dep of allDeps) if (toUpdate.has(dep)) {
|
|
1383
|
+
visit(dep, level);
|
|
1384
|
+
const depResult = result.find((r) => r.package.name === dep);
|
|
1385
|
+
if (depResult && depResult.level >= maxDepLevel) maxDepLevel = depResult.level + 1;
|
|
1386
|
+
}
|
|
1387
|
+
result.push({
|
|
1388
|
+
package: pkg,
|
|
1389
|
+
level: maxDepLevel
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
for (const pkg of toUpdate) visit(pkg, 0);
|
|
1393
|
+
result.sort((a, b) => a.level - b.level);
|
|
1394
|
+
return result;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1442
1397
|
* Create version updates for all packages affected by dependency changes
|
|
1443
1398
|
*
|
|
1444
1399
|
* When a package is updated, all packages that depend on it should also be updated.
|
|
@@ -1663,79 +1618,89 @@ function getDependencyUpdates(pkg, allUpdates) {
|
|
|
1663
1618
|
}
|
|
1664
1619
|
|
|
1665
1620
|
//#endregion
|
|
1666
|
-
//#region src/
|
|
1667
|
-
function
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1621
|
+
//#region src/operations/calculate.ts
|
|
1622
|
+
async function calculateUpdates(options) {
|
|
1623
|
+
const { workspacePackages, workspaceRoot, showPrompt, overrides, globalCommitMode } = options;
|
|
1624
|
+
try {
|
|
1625
|
+
const grouped = await getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages);
|
|
1626
|
+
return ok(await calculateAndPrepareVersionUpdates({
|
|
1627
|
+
workspacePackages,
|
|
1628
|
+
packageCommits: grouped,
|
|
1629
|
+
workspaceRoot,
|
|
1630
|
+
showPrompt,
|
|
1631
|
+
globalCommitsPerPackage: await getGlobalCommitsPerPackage(workspaceRoot, grouped, workspacePackages, globalCommitMode),
|
|
1632
|
+
overrides
|
|
1633
|
+
}));
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
return err({
|
|
1636
|
+
type: "git",
|
|
1637
|
+
operation: "calculateUpdates",
|
|
1638
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function ensureHasPackages(packages) {
|
|
1643
|
+
if (packages.length === 0) return err({
|
|
1644
|
+
type: "git",
|
|
1645
|
+
operation: "discoverWorkspacePackages",
|
|
1646
|
+
message: "No packages found to release"
|
|
1647
|
+
});
|
|
1648
|
+
return ok(packages);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
//#endregion
|
|
1652
|
+
//#region src/operations/pr.ts
|
|
1653
|
+
async function syncPullRequest(options) {
|
|
1654
|
+
const { github, releaseBranch, defaultBranch, pullRequestTitle, pullRequestBody, updates } = options;
|
|
1655
|
+
let existing = null;
|
|
1656
|
+
try {
|
|
1657
|
+
existing = await github.getExistingPullRequest(releaseBranch);
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
return {
|
|
1660
|
+
ok: false,
|
|
1661
|
+
error: {
|
|
1662
|
+
type: "github",
|
|
1663
|
+
operation: "getExistingPullRequest",
|
|
1664
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1701
1665
|
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
const doesExist = !!existing;
|
|
1669
|
+
const title = existing?.title || pullRequestTitle || "chore: update package versions";
|
|
1670
|
+
const body = generatePullRequestBody(updates, pullRequestBody);
|
|
1671
|
+
let pr = null;
|
|
1672
|
+
try {
|
|
1673
|
+
pr = await github.upsertPullRequest({
|
|
1674
|
+
pullNumber: existing?.number,
|
|
1675
|
+
title,
|
|
1676
|
+
body,
|
|
1677
|
+
head: releaseBranch,
|
|
1678
|
+
base: defaultBranch
|
|
1679
|
+
});
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
return {
|
|
1682
|
+
ok: false,
|
|
1683
|
+
error: {
|
|
1684
|
+
type: "github",
|
|
1685
|
+
operation: "upsertPullRequest",
|
|
1686
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1718
1687
|
}
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
return ok({
|
|
1691
|
+
pullRequest: pr,
|
|
1692
|
+
created: !doesExist
|
|
1693
|
+
});
|
|
1721
1694
|
}
|
|
1722
1695
|
|
|
1723
1696
|
//#endregion
|
|
1724
1697
|
//#region src/workflows/prepare.ts
|
|
1725
1698
|
async function prepareWorkflow(options) {
|
|
1726
|
-
const gitOps = createGitOperations();
|
|
1727
|
-
const githubOps = createGitHubOperations({
|
|
1728
|
-
owner: options.owner,
|
|
1729
|
-
repo: options.repo,
|
|
1730
|
-
githubToken: options.githubToken
|
|
1731
|
-
});
|
|
1732
|
-
const workspaceOps = createWorkspaceOperations();
|
|
1733
|
-
const versioningOps = createVersioningOperations();
|
|
1734
1699
|
if (options.safeguards) {
|
|
1735
|
-
const clean = await
|
|
1700
|
+
const clean = await isWorkingDirectoryClean(options.workspaceRoot);
|
|
1736
1701
|
if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
1737
1702
|
}
|
|
1738
|
-
const discovered = await
|
|
1703
|
+
const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
|
|
1739
1704
|
if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
|
|
1740
1705
|
const ensured = ensureHasPackages(discovered.value);
|
|
1741
1706
|
if (!ensured.ok) {
|
|
@@ -1751,7 +1716,6 @@ async function prepareWorkflow(options) {
|
|
|
1751
1716
|
}
|
|
1752
1717
|
logger.emptyLine();
|
|
1753
1718
|
const prepareBranchResult = await prepareReleaseBranch({
|
|
1754
|
-
git: gitOps,
|
|
1755
1719
|
workspaceRoot: options.workspaceRoot,
|
|
1756
1720
|
releaseBranch: options.branch.release,
|
|
1757
1721
|
defaultBranch: options.branch.default
|
|
@@ -1767,7 +1731,6 @@ async function prepareWorkflow(options) {
|
|
|
1767
1731
|
logger.info("No existing version overrides file found. Continuing...");
|
|
1768
1732
|
}
|
|
1769
1733
|
const updatesResult = await calculateUpdates({
|
|
1770
|
-
versioning: versioningOps,
|
|
1771
1734
|
workspacePackages,
|
|
1772
1735
|
workspaceRoot: options.workspaceRoot,
|
|
1773
1736
|
showPrompt: options.prompts?.versions !== false,
|
|
@@ -1814,13 +1777,11 @@ async function prepareWorkflow(options) {
|
|
|
1814
1777
|
await applyUpdates();
|
|
1815
1778
|
if (options.changelog?.enabled) {
|
|
1816
1779
|
logger.step("Updating changelogs");
|
|
1817
|
-
const groupedPackageCommits = await
|
|
1818
|
-
|
|
1819
|
-
const globalCommitsPerPackage = await versioningOps.getGlobalCommitsPerPackage(options.workspaceRoot, groupedPackageCommits.value, workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode);
|
|
1820
|
-
if (!globalCommitsPerPackage.ok) exitWithError(globalCommitsPerPackage.error.message);
|
|
1780
|
+
const groupedPackageCommits = await getWorkspacePackageGroupedCommits(options.workspaceRoot, workspacePackages);
|
|
1781
|
+
const globalCommitsPerPackage = await getGlobalCommitsPerPackage(options.workspaceRoot, groupedPackageCommits, workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode);
|
|
1821
1782
|
const changelogPromises = allUpdates.map((update) => {
|
|
1822
|
-
const pkgCommits = groupedPackageCommits.
|
|
1823
|
-
const globalCommits = globalCommitsPerPackage.
|
|
1783
|
+
const pkgCommits = groupedPackageCommits.get(update.package.name) || [];
|
|
1784
|
+
const globalCommits = globalCommitsPerPackage.get(update.package.name) || [];
|
|
1824
1785
|
const allCommits = [...pkgCommits, ...globalCommits];
|
|
1825
1786
|
if (allCommits.length === 0) {
|
|
1826
1787
|
logger.verbose(`No commits for ${update.package.name}, skipping changelog`);
|
|
@@ -1832,11 +1793,7 @@ async function prepareWorkflow(options) {
|
|
|
1832
1793
|
...options,
|
|
1833
1794
|
workspaceRoot: options.workspaceRoot
|
|
1834
1795
|
},
|
|
1835
|
-
githubClient:
|
|
1836
|
-
owner: options.owner,
|
|
1837
|
-
repo: options.repo,
|
|
1838
|
-
githubToken: options.githubToken
|
|
1839
|
-
}),
|
|
1796
|
+
githubClient: options.githubClient,
|
|
1840
1797
|
workspacePackage: update.package,
|
|
1841
1798
|
version: update.newVersion,
|
|
1842
1799
|
previousVersion: update.currentVersion !== "0.0.0" ? update.currentVersion : void 0,
|
|
@@ -1848,7 +1805,6 @@ async function prepareWorkflow(options) {
|
|
|
1848
1805
|
logger.success(`Updated ${updates.length} changelog(s)`);
|
|
1849
1806
|
}
|
|
1850
1807
|
const hasChangesToPush = await syncReleaseChanges({
|
|
1851
|
-
git: gitOps,
|
|
1852
1808
|
workspaceRoot: options.workspaceRoot,
|
|
1853
1809
|
releaseBranch: options.branch.release,
|
|
1854
1810
|
commitMessage: "chore: update release versions",
|
|
@@ -1857,7 +1813,7 @@ async function prepareWorkflow(options) {
|
|
|
1857
1813
|
if (!hasChangesToPush.ok) exitWithError(hasChangesToPush.error.message);
|
|
1858
1814
|
if (!hasChangesToPush.value) {
|
|
1859
1815
|
const prResult = await syncPullRequest({
|
|
1860
|
-
github:
|
|
1816
|
+
github: options.githubClient,
|
|
1861
1817
|
releaseBranch: options.branch.release,
|
|
1862
1818
|
defaultBranch: options.branch.default,
|
|
1863
1819
|
pullRequestTitle: options.pullRequest?.title,
|
|
@@ -1877,7 +1833,7 @@ async function prepareWorkflow(options) {
|
|
|
1877
1833
|
return null;
|
|
1878
1834
|
}
|
|
1879
1835
|
const prResult = await syncPullRequest({
|
|
1880
|
-
github:
|
|
1836
|
+
github: options.githubClient,
|
|
1881
1837
|
releaseBranch: options.branch.release,
|
|
1882
1838
|
defaultBranch: options.branch.default,
|
|
1883
1839
|
pullRequestTitle: options.pullRequest?.title,
|
|
@@ -1889,6 +1845,8 @@ async function prepareWorkflow(options) {
|
|
|
1889
1845
|
logger.section("🚀 Pull Request");
|
|
1890
1846
|
logger.success(`Pull request ${prResult.value.created ? "created" : "updated"}: ${prResult.value.pullRequest.html_url}`);
|
|
1891
1847
|
}
|
|
1848
|
+
const returnToDefault = await checkoutBranch(options.branch.default, options.workspaceRoot);
|
|
1849
|
+
if (!returnToDefault.ok || !returnToDefault.value) exitWithError(`Failed to checkout branch: ${options.branch.default}`);
|
|
1892
1850
|
return {
|
|
1893
1851
|
updates: allUpdates,
|
|
1894
1852
|
prUrl: prResult.value.pullRequest?.html_url,
|
|
@@ -1897,62 +1855,218 @@ async function prepareWorkflow(options) {
|
|
|
1897
1855
|
}
|
|
1898
1856
|
|
|
1899
1857
|
//#endregion
|
|
1900
|
-
//#region src/
|
|
1901
|
-
|
|
1902
|
-
return
|
|
1858
|
+
//#region src/core/npm.ts
|
|
1859
|
+
function toNPMError(operation, error, code) {
|
|
1860
|
+
return {
|
|
1861
|
+
type: "npm",
|
|
1862
|
+
operation,
|
|
1863
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1864
|
+
code
|
|
1865
|
+
};
|
|
1903
1866
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1867
|
+
/**
|
|
1868
|
+
* Get the NPM registry URL
|
|
1869
|
+
* Respects NPM_CONFIG_REGISTRY environment variable, defaults to npmjs.org
|
|
1870
|
+
*/
|
|
1871
|
+
function getRegistryURL() {
|
|
1872
|
+
return process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
|
|
1910
1873
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1874
|
+
/**
|
|
1875
|
+
* Fetch package metadata from NPM registry
|
|
1876
|
+
* @param packageName - The package name (e.g., "lodash" or "@scope/name")
|
|
1877
|
+
* @returns Result with package metadata or error
|
|
1878
|
+
*/
|
|
1879
|
+
async function getPackageMetadata(packageName) {
|
|
1880
|
+
try {
|
|
1881
|
+
const registry = getRegistryURL();
|
|
1882
|
+
const encodedName = packageName.startsWith("@") ? `@${encodeURIComponent(packageName.slice(1))}` : encodeURIComponent(packageName);
|
|
1883
|
+
const response = await fetch(`${registry}/${encodedName}`, { headers: { Accept: "application/json" } });
|
|
1884
|
+
if (!response.ok) {
|
|
1885
|
+
if (response.status === 404) return err(toNPMError("getPackageMetadata", `Package not found: ${packageName}`, "E404"));
|
|
1886
|
+
return err(toNPMError("getPackageMetadata", `HTTP ${response.status}: ${response.statusText}`));
|
|
1887
|
+
}
|
|
1888
|
+
return ok(await response.json());
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
return err(toNPMError("getPackageMetadata", error, "ENETWORK"));
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Check if a specific package version exists on NPM
|
|
1895
|
+
* @param packageName - The package name
|
|
1896
|
+
* @param version - The version to check (e.g., "1.2.3")
|
|
1897
|
+
* @returns Result with boolean (true if version exists) or error
|
|
1898
|
+
*/
|
|
1899
|
+
async function checkVersionExists(packageName, version) {
|
|
1900
|
+
const metadataResult = await getPackageMetadata(packageName);
|
|
1901
|
+
if (!metadataResult.ok) {
|
|
1902
|
+
if (metadataResult.error.code === "E404") return ok(false);
|
|
1903
|
+
return err(metadataResult.error);
|
|
1904
|
+
}
|
|
1905
|
+
return ok(version in metadataResult.value.versions);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Build a package before publishing
|
|
1909
|
+
* @param packageName - The package name to build
|
|
1910
|
+
* @param workspaceRoot - Path to the workspace root
|
|
1911
|
+
* @param options - Normalized release scripts options
|
|
1912
|
+
* @returns Result indicating success or failure
|
|
1913
|
+
*/
|
|
1914
|
+
async function buildPackage(packageName, workspaceRoot, options) {
|
|
1915
|
+
if (!options.npm.runBuild) return ok(void 0);
|
|
1916
|
+
try {
|
|
1917
|
+
await runIfNotDry("pnpm", [
|
|
1918
|
+
"--filter",
|
|
1919
|
+
packageName,
|
|
1920
|
+
"build"
|
|
1921
|
+
], { nodeOptions: {
|
|
1922
|
+
cwd: workspaceRoot,
|
|
1923
|
+
stdio: "inherit"
|
|
1924
|
+
} });
|
|
1925
|
+
return ok(void 0);
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
return err(toNPMError("buildPackage", error));
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Publish a package to NPM
|
|
1932
|
+
* Uses pnpm to handle workspace protocol and catalog: resolution automatically
|
|
1933
|
+
* @param packageName - The package name to publish
|
|
1934
|
+
* @param workspaceRoot - Path to the workspace root
|
|
1935
|
+
* @param options - Normalized release scripts options
|
|
1936
|
+
* @returns Result indicating success or failure
|
|
1937
|
+
*/
|
|
1938
|
+
async function publishPackage(packageName, workspaceRoot, options) {
|
|
1939
|
+
const args = [
|
|
1940
|
+
"--filter",
|
|
1941
|
+
packageName,
|
|
1942
|
+
"publish",
|
|
1943
|
+
"--access",
|
|
1944
|
+
options.npm.access,
|
|
1945
|
+
"--no-git-checks"
|
|
1946
|
+
];
|
|
1947
|
+
if (options.npm.otp) args.push("--otp", options.npm.otp);
|
|
1948
|
+
if (process.env.NPM_CONFIG_TAG) args.push("--tag", process.env.NPM_CONFIG_TAG);
|
|
1949
|
+
const env = { ...process.env };
|
|
1950
|
+
if (options.npm.provenance) env.NPM_CONFIG_PROVENANCE = "true";
|
|
1951
|
+
try {
|
|
1952
|
+
await runIfNotDry("pnpm", args, { nodeOptions: {
|
|
1953
|
+
cwd: workspaceRoot,
|
|
1954
|
+
stdio: "inherit",
|
|
1955
|
+
env
|
|
1956
|
+
} });
|
|
1957
|
+
return ok(void 0);
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1960
|
+
return err(toNPMError("publishPackage", error, errorMessage.includes("E403") ? "E403" : errorMessage.includes("EPUBLISHCONFLICT") ? "EPUBLISHCONFLICT" : errorMessage.includes("EOTP") ? "EOTP" : void 0));
|
|
1961
|
+
}
|
|
1916
1962
|
}
|
|
1917
1963
|
|
|
1918
1964
|
//#endregion
|
|
1919
|
-
//#region src/
|
|
1920
|
-
async function
|
|
1921
|
-
|
|
1965
|
+
//#region src/workflows/publish.ts
|
|
1966
|
+
async function publishWorkflow(options) {
|
|
1967
|
+
logger.section("📦 Publishing Packages");
|
|
1968
|
+
const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
|
|
1969
|
+
if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
|
|
1970
|
+
const workspacePackages = discovered.value;
|
|
1971
|
+
logger.item(`Found ${workspacePackages.length} packages in workspace`);
|
|
1972
|
+
const graph = buildPackageDependencyGraph(workspacePackages);
|
|
1973
|
+
const publicPackages = workspacePackages.filter((pkg) => !pkg.packageJson.private);
|
|
1974
|
+
logger.item(`Publishing ${publicPackages.length} public packages (private packages excluded)`);
|
|
1975
|
+
if (publicPackages.length === 0) {
|
|
1976
|
+
logger.warn("No public packages to publish");
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
const publishOrder = getPackagePublishOrder(graph, new Set(publicPackages.map((p) => p.name)));
|
|
1980
|
+
const status = {
|
|
1981
|
+
published: [],
|
|
1982
|
+
skipped: [],
|
|
1983
|
+
failed: []
|
|
1984
|
+
};
|
|
1985
|
+
for (const order of publishOrder) {
|
|
1986
|
+
const pkg = order.package;
|
|
1987
|
+
const version = pkg.version;
|
|
1988
|
+
const packageName = pkg.name;
|
|
1989
|
+
logger.section(`📦 ${farver.cyan(packageName)} ${farver.gray(`(level ${order.level})`)}`);
|
|
1990
|
+
logger.step(`Checking if ${farver.cyan(`${packageName}@${version}`)} exists on NPM...`);
|
|
1991
|
+
const existsResult = await checkVersionExists(packageName, version);
|
|
1992
|
+
if (!existsResult.ok) {
|
|
1993
|
+
logger.error(`Failed to check version: ${existsResult.error.message}`);
|
|
1994
|
+
status.failed.push(packageName);
|
|
1995
|
+
exitWithError(`Publishing failed for ${packageName}: ${existsResult.error.message}`, "Check your network connection and NPM registry access");
|
|
1996
|
+
}
|
|
1997
|
+
if (existsResult.value) {
|
|
1998
|
+
logger.info(`Version ${farver.cyan(version)} already exists on NPM, skipping`);
|
|
1999
|
+
status.skipped.push(packageName);
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
if (options.npm.runBuild) {
|
|
2003
|
+
logger.step(`Building ${farver.cyan(packageName)}...`);
|
|
2004
|
+
const buildResult = await buildPackage(packageName, options.workspaceRoot, options);
|
|
2005
|
+
if (!buildResult.ok) {
|
|
2006
|
+
logger.error(`Failed to build package: ${buildResult.error.message}`);
|
|
2007
|
+
status.failed.push(packageName);
|
|
2008
|
+
exitWithError(`Publishing failed for ${packageName}: build failed`, "Check your build scripts and dependencies");
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
logger.step(`Publishing ${farver.cyan(`${packageName}@${version}`)} to NPM...`);
|
|
2012
|
+
const publishResult = await publishPackage(packageName, options.workspaceRoot, options);
|
|
2013
|
+
if (!publishResult.ok) {
|
|
2014
|
+
logger.error(`Failed to publish: ${publishResult.error.message}`);
|
|
2015
|
+
status.failed.push(packageName);
|
|
2016
|
+
let hint;
|
|
2017
|
+
if (publishResult.error.code === "E403") hint = "Authentication failed. Ensure your NPM token or OIDC configuration is correct";
|
|
2018
|
+
else if (publishResult.error.code === "EPUBLISHCONFLICT") hint = "Version conflict. The version may have been published recently";
|
|
2019
|
+
else if (publishResult.error.code === "EOTP") hint = "2FA/OTP required. Provide the otp option or use OIDC authentication";
|
|
2020
|
+
exitWithError(`Publishing failed for ${packageName}`, hint);
|
|
2021
|
+
}
|
|
2022
|
+
logger.success(`Published ${farver.cyan(`${packageName}@${version}`)}`);
|
|
2023
|
+
status.published.push(packageName);
|
|
2024
|
+
logger.step(`Creating git tag ${farver.cyan(`${packageName}@${version}`)}...`);
|
|
2025
|
+
const tagResult = await createAndPushPackageTag(packageName, version, options.workspaceRoot);
|
|
2026
|
+
if (!tagResult.ok) {
|
|
2027
|
+
logger.error(`Failed to create/push tag: ${tagResult.error.message}`);
|
|
2028
|
+
logger.warn(`Package was published but tag was not created. You may need to create it manually.`);
|
|
2029
|
+
} else logger.success(`Created and pushed tag ${farver.cyan(`${packageName}@${version}`)}`);
|
|
2030
|
+
}
|
|
2031
|
+
logger.section("📊 Publishing Summary");
|
|
2032
|
+
logger.item(`${farver.green("✓")} Published: ${status.published.length} package(s)`);
|
|
2033
|
+
if (status.published.length > 0) for (const pkg of status.published) logger.item(` ${farver.green("•")} ${pkg}`);
|
|
2034
|
+
if (status.skipped.length > 0) {
|
|
2035
|
+
logger.item(`${farver.yellow("⚠")} Skipped (already exists): ${status.skipped.length} package(s)`);
|
|
2036
|
+
for (const pkg of status.skipped) logger.item(` ${farver.yellow("•")} ${pkg}`);
|
|
2037
|
+
}
|
|
2038
|
+
if (status.failed.length > 0) {
|
|
2039
|
+
logger.item(`${farver.red("✖")} Failed: ${status.failed.length} package(s)`);
|
|
2040
|
+
for (const pkg of status.failed) logger.item(` ${farver.red("•")} ${pkg}`);
|
|
2041
|
+
}
|
|
2042
|
+
if (status.failed.length > 0) exitWithError(`Publishing completed with ${status.failed.length} failure(s)`);
|
|
2043
|
+
logger.success("All packages published successfully!");
|
|
1922
2044
|
}
|
|
1923
2045
|
|
|
1924
2046
|
//#endregion
|
|
1925
2047
|
//#region src/workflows/verify.ts
|
|
1926
2048
|
async function verifyWorkflow(options) {
|
|
1927
|
-
const gitOps = createGitOperations();
|
|
1928
|
-
const githubOps = createGitHubOperations({
|
|
1929
|
-
owner: options.owner,
|
|
1930
|
-
repo: options.repo,
|
|
1931
|
-
githubToken: options.githubToken
|
|
1932
|
-
});
|
|
1933
|
-
const workspaceOps = createWorkspaceOperations();
|
|
1934
2049
|
if (options.safeguards) {
|
|
1935
|
-
const clean = await
|
|
2050
|
+
const clean = await isWorkingDirectoryClean(options.workspaceRoot);
|
|
1936
2051
|
if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
1937
2052
|
}
|
|
1938
2053
|
const releaseBranch = options.branch.release;
|
|
1939
2054
|
const defaultBranch = options.branch.default;
|
|
1940
|
-
const releasePr = await
|
|
1941
|
-
if (!releasePr
|
|
1942
|
-
if (!releasePr.value || !releasePr.value.head) {
|
|
2055
|
+
const releasePr = await options.githubClient.getExistingPullRequest(releaseBranch);
|
|
2056
|
+
if (!releasePr || !releasePr.head) {
|
|
1943
2057
|
logger.warn(`No open release pull request found for branch "${releaseBranch}". Nothing to verify.`);
|
|
1944
2058
|
return;
|
|
1945
2059
|
}
|
|
1946
|
-
logger.info(`Found release PR #${releasePr.
|
|
1947
|
-
const originalBranch = await
|
|
2060
|
+
logger.info(`Found release PR #${releasePr.number}. Verifying against default branch "${defaultBranch}"...`);
|
|
2061
|
+
const originalBranch = await getCurrentBranch(options.workspaceRoot);
|
|
1948
2062
|
if (!originalBranch.ok) exitWithError(originalBranch.error.message);
|
|
1949
2063
|
if (originalBranch.value !== defaultBranch) {
|
|
1950
|
-
const checkout = await
|
|
2064
|
+
const checkout = await checkoutBranch(defaultBranch, options.workspaceRoot);
|
|
1951
2065
|
if (!checkout.ok || !checkout.value) exitWithError(`Failed to checkout branch: ${defaultBranch}`);
|
|
1952
2066
|
}
|
|
1953
2067
|
let existingOverrides = {};
|
|
1954
2068
|
try {
|
|
1955
|
-
const overridesContent = await
|
|
2069
|
+
const overridesContent = await readFileFromGit(options.workspaceRoot, releasePr.head.sha, ucdjsReleaseOverridesPath);
|
|
1956
2070
|
if (overridesContent.ok && overridesContent.value) {
|
|
1957
2071
|
existingOverrides = JSON.parse(overridesContent.value);
|
|
1958
2072
|
logger.info("Found existing version overrides file on release branch.");
|
|
@@ -1960,11 +2074,7 @@ async function verifyWorkflow(options) {
|
|
|
1960
2074
|
} catch {
|
|
1961
2075
|
logger.info("No version overrides file found on release branch. Continuing...");
|
|
1962
2076
|
}
|
|
1963
|
-
const discovered = await
|
|
1964
|
-
workspace: workspaceOps,
|
|
1965
|
-
workspaceRoot: options.workspaceRoot,
|
|
1966
|
-
options
|
|
1967
|
-
});
|
|
2077
|
+
const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
|
|
1968
2078
|
if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
|
|
1969
2079
|
const ensured = ensureHasPackages(discovered.value);
|
|
1970
2080
|
if (!ensured.ok) {
|
|
@@ -1973,7 +2083,6 @@ async function verifyWorkflow(options) {
|
|
|
1973
2083
|
}
|
|
1974
2084
|
const mainPackages = ensured.value;
|
|
1975
2085
|
const updatesResult = await calculateUpdates({
|
|
1976
|
-
versioning: createVersioningOperations(),
|
|
1977
2086
|
workspacePackages: mainPackages,
|
|
1978
2087
|
workspaceRoot: options.workspaceRoot,
|
|
1979
2088
|
showPrompt: false,
|
|
@@ -1986,13 +2095,13 @@ async function verifyWorkflow(options) {
|
|
|
1986
2095
|
const prVersionMap = /* @__PURE__ */ new Map();
|
|
1987
2096
|
for (const pkg of mainPackages) {
|
|
1988
2097
|
const pkgJsonPath = relative(options.workspaceRoot, join(pkg.path, "package.json"));
|
|
1989
|
-
const pkgJsonContent = await
|
|
2098
|
+
const pkgJsonContent = await readFileFromGit(options.workspaceRoot, releasePr.head.sha, pkgJsonPath);
|
|
1990
2099
|
if (pkgJsonContent.ok && pkgJsonContent.value) {
|
|
1991
2100
|
const pkgJson = JSON.parse(pkgJsonContent.value);
|
|
1992
2101
|
prVersionMap.set(pkg.name, pkgJson.version);
|
|
1993
2102
|
}
|
|
1994
2103
|
}
|
|
1995
|
-
if (originalBranch.value !== defaultBranch) await
|
|
2104
|
+
if (originalBranch.value !== defaultBranch) await checkoutBranch(originalBranch.value, options.workspaceRoot);
|
|
1996
2105
|
let isOutOfSync = false;
|
|
1997
2106
|
for (const [pkgName, expectedVersion] of expectedVersionMap.entries()) {
|
|
1998
2107
|
const prVersion = prVersionMap.get(pkgName);
|
|
@@ -2007,51 +2116,49 @@ async function verifyWorkflow(options) {
|
|
|
2007
2116
|
}
|
|
2008
2117
|
const statusContext = "ucdjs/release-verify";
|
|
2009
2118
|
if (isOutOfSync) {
|
|
2010
|
-
await
|
|
2011
|
-
sha: releasePr.
|
|
2119
|
+
await options.githubClient.setCommitStatus({
|
|
2120
|
+
sha: releasePr.head.sha,
|
|
2012
2121
|
state: "failure",
|
|
2013
2122
|
context: statusContext,
|
|
2014
2123
|
description: "Release PR is out of sync with the default branch. Please re-run the release process."
|
|
2015
2124
|
});
|
|
2016
2125
|
logger.error("Verification failed. Commit status set to 'failure'.");
|
|
2017
2126
|
} else {
|
|
2018
|
-
await
|
|
2019
|
-
sha: releasePr.
|
|
2127
|
+
await options.githubClient.setCommitStatus({
|
|
2128
|
+
sha: releasePr.head.sha,
|
|
2020
2129
|
state: "success",
|
|
2021
2130
|
context: statusContext,
|
|
2022
2131
|
description: "Release PR is up to date.",
|
|
2023
|
-
targetUrl: `https://github.com/${options.owner}/${options.repo}/pull/${releasePr.
|
|
2132
|
+
targetUrl: `https://github.com/${options.owner}/${options.repo}/pull/${releasePr.number}`
|
|
2024
2133
|
});
|
|
2025
2134
|
logger.success("Verification successful. Commit status set to 'success'.");
|
|
2026
2135
|
}
|
|
2027
2136
|
}
|
|
2028
2137
|
|
|
2029
|
-
//#endregion
|
|
2030
|
-
//#region src/verify.ts
|
|
2031
|
-
async function verify(options) {
|
|
2032
|
-
return verifyWorkflow(options);
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
2138
|
//#endregion
|
|
2036
2139
|
//#region src/index.ts
|
|
2037
2140
|
async function createReleaseScripts(options) {
|
|
2038
2141
|
const normalizedOptions = normalizeReleaseScriptsOptions(options);
|
|
2039
2142
|
return {
|
|
2040
2143
|
async verify() {
|
|
2041
|
-
return
|
|
2144
|
+
return verifyWorkflow(normalizedOptions);
|
|
2042
2145
|
},
|
|
2043
2146
|
async prepare() {
|
|
2044
|
-
return
|
|
2147
|
+
return prepareWorkflow(normalizedOptions);
|
|
2045
2148
|
},
|
|
2046
2149
|
async publish() {
|
|
2047
|
-
return
|
|
2150
|
+
return publishWorkflow(normalizedOptions);
|
|
2048
2151
|
},
|
|
2049
2152
|
packages: {
|
|
2050
2153
|
async list() {
|
|
2051
|
-
|
|
2154
|
+
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
|
|
2155
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
2156
|
+
return result.value;
|
|
2052
2157
|
},
|
|
2053
2158
|
async get(packageName) {
|
|
2054
|
-
|
|
2159
|
+
const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
|
|
2160
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
2161
|
+
return result.value.find((p) => p.name === packageName);
|
|
2055
2162
|
}
|
|
2056
2163
|
}
|
|
2057
2164
|
};
|