@ucdjs/release-scripts 0.1.0-beta.34 → 0.1.0-beta.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{eta-BV8TCRDW.mjs → eta-DAZlmVBQ.mjs} +7 -7
- package/dist/index.d.mts +76 -26
- package/dist/index.mjs +1877 -1815
- package/package.json +10 -7
package/dist/index.mjs
CHANGED
|
@@ -1,45 +1,377 @@
|
|
|
1
|
-
import { t as Eta } from "./eta-
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { Command, CommandExecutor } from "@effect/platform";
|
|
6
|
-
import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node";
|
|
7
|
-
import * as CommitParser from "commit-parser";
|
|
8
|
-
import semver from "semver";
|
|
9
|
-
import fs from "node:fs/promises";
|
|
1
|
+
import { t as Eta } from "./eta-DAZlmVBQ.mjs";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import farver from "farver";
|
|
10
5
|
import prompts from "prompts";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
import mri from "mri";
|
|
9
|
+
import { exec } from "tinyexec";
|
|
10
|
+
import { dedent } from "@luxass/utils";
|
|
11
|
+
import { getCommits, groupByType } from "commit-parser";
|
|
12
|
+
import { compare, gt } from "semver";
|
|
11
13
|
|
|
12
|
-
//#region src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
//#region src/operations/semver.ts
|
|
15
|
+
function isValidSemver(version) {
|
|
16
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
|
|
17
|
+
}
|
|
18
|
+
function getNextVersion(currentVersion, bump) {
|
|
19
|
+
if (bump === "none") return currentVersion;
|
|
20
|
+
if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
|
|
21
|
+
const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
22
|
+
if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
|
|
23
|
+
const [, major, minor, patch] = match;
|
|
24
|
+
let newMajor = Number.parseInt(major, 10);
|
|
25
|
+
let newMinor = Number.parseInt(minor, 10);
|
|
26
|
+
let newPatch = Number.parseInt(patch, 10);
|
|
27
|
+
switch (bump) {
|
|
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;
|
|
40
|
+
}
|
|
41
|
+
return `${newMajor}.${newMinor}.${newPatch}`;
|
|
42
|
+
}
|
|
43
|
+
function calculateBumpType(oldVersion, newVersion) {
|
|
44
|
+
if (!isValidSemver(oldVersion) || !isValidSemver(newVersion)) throw new Error(`Cannot calculate bump type for invalid semver: ${oldVersion} or ${newVersion}`);
|
|
45
|
+
const oldParts = oldVersion.split(".").map(Number);
|
|
46
|
+
const newParts = newVersion.split(".").map(Number);
|
|
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";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/core/prompts.ts
|
|
55
|
+
async function selectPackagePrompt(packages) {
|
|
56
|
+
const response = await prompts({
|
|
57
|
+
type: "multiselect",
|
|
58
|
+
name: "selectedPackages",
|
|
59
|
+
message: "Select packages to release",
|
|
60
|
+
choices: packages.map((pkg) => ({
|
|
61
|
+
title: `${pkg.name} (${farver.bold(pkg.version)})`,
|
|
62
|
+
value: pkg.name,
|
|
63
|
+
selected: true
|
|
64
|
+
})),
|
|
65
|
+
min: 1,
|
|
66
|
+
hint: "Space to select/deselect. Return to submit.",
|
|
67
|
+
instructions: false
|
|
68
|
+
});
|
|
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
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/shared/utils.ts
|
|
130
|
+
const args = mri(process.argv.slice(2));
|
|
131
|
+
const isDryRun = !!args.dry;
|
|
132
|
+
const isVerbose = !!args.verbose;
|
|
133
|
+
const isForce = !!args.force;
|
|
134
|
+
const ucdjsReleaseOverridesPath = ".github/ucdjs-release.overrides.json";
|
|
135
|
+
const isCI = typeof process.env.CI === "string" && process.env.CI !== "" && process.env.CI.toLowerCase() !== "false";
|
|
136
|
+
const logger = {
|
|
137
|
+
info: (...args) => {
|
|
138
|
+
console.info(...args);
|
|
139
|
+
},
|
|
140
|
+
warn: (...args) => {
|
|
141
|
+
console.warn(` ${farver.yellow("⚠")}`, ...args);
|
|
142
|
+
},
|
|
143
|
+
error: (...args) => {
|
|
144
|
+
console.error(` ${farver.red("✖")}`, ...args);
|
|
145
|
+
},
|
|
146
|
+
verbose: (...args) => {
|
|
147
|
+
if (!isVerbose) return;
|
|
148
|
+
if (args.length === 0) {
|
|
149
|
+
console.log();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (args.length > 1 && typeof args[0] === "string") {
|
|
153
|
+
console.log(farver.dim(args[0]), ...args.slice(1));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
console.log(...args);
|
|
157
|
+
},
|
|
158
|
+
section: (title) => {
|
|
159
|
+
console.log();
|
|
160
|
+
console.log(` ${farver.bold(title)}`);
|
|
161
|
+
console.log(` ${farver.gray("─".repeat(title.length + 2))}`);
|
|
19
162
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
color: "red"
|
|
163
|
+
emptyLine: () => {
|
|
164
|
+
console.log();
|
|
23
165
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
color: "blue"
|
|
166
|
+
item: (message, ...args) => {
|
|
167
|
+
console.log(` ${message}`, ...args);
|
|
27
168
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
color: "orange"
|
|
169
|
+
step: (message) => {
|
|
170
|
+
console.log(` ${farver.blue("→")} ${message}`);
|
|
31
171
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
color: "purple"
|
|
172
|
+
success: (message) => {
|
|
173
|
+
console.log(` ${farver.green("✓")} ${message}`);
|
|
35
174
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
175
|
+
clearScreen: () => {
|
|
176
|
+
const repeatCount = process.stdout.rows - 2;
|
|
177
|
+
const blank = repeatCount > 0 ? "\n".repeat(repeatCount) : "";
|
|
178
|
+
console.log(blank);
|
|
179
|
+
readline.cursorTo(process.stdout, 0, 0);
|
|
180
|
+
readline.clearScreenDown(process.stdout);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
async function run(bin, args, opts = {}) {
|
|
184
|
+
return exec(bin, args, {
|
|
185
|
+
throwOnError: true,
|
|
186
|
+
...opts,
|
|
187
|
+
nodeOptions: {
|
|
188
|
+
stdio: "inherit",
|
|
189
|
+
...opts.nodeOptions
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function dryRun(bin, args, opts) {
|
|
194
|
+
return logger.verbose(farver.blue(`[dryrun] ${bin} ${args.join(" ")}`), opts || "");
|
|
195
|
+
}
|
|
196
|
+
const runIfNotDry = isDryRun ? dryRun : run;
|
|
197
|
+
function exitWithError(message, hint) {
|
|
198
|
+
logger.error(farver.bold(message));
|
|
199
|
+
if (hint) console.error(farver.gray(` ${hint}`));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
if (isDryRun || isVerbose || isForce) {
|
|
203
|
+
logger.verbose(farver.inverse(farver.yellow(" Running with special flags ")));
|
|
204
|
+
logger.verbose({
|
|
205
|
+
isDryRun,
|
|
206
|
+
isVerbose,
|
|
207
|
+
isForce
|
|
208
|
+
});
|
|
209
|
+
logger.verbose();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/types/result.ts
|
|
214
|
+
function ok(value) {
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
value
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function err(error) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
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;
|
|
242
|
+
}
|
|
243
|
+
let workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
|
|
244
|
+
if (explicitPackages) {
|
|
245
|
+
const foundNames = new Set(workspacePackages.map((p) => p.name));
|
|
246
|
+
const missing = explicitPackages.filter((p) => !foundNames.has(p));
|
|
247
|
+
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");
|
|
248
|
+
}
|
|
249
|
+
const isPackagePromptEnabled = options.prompts?.packages !== false;
|
|
250
|
+
if (!isCI && isPackagePromptEnabled && !explicitPackages) {
|
|
251
|
+
const selectedNames = await selectPackagePrompt(workspacePackages);
|
|
252
|
+
workspacePackages = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
|
|
253
|
+
}
|
|
254
|
+
return workspacePackages;
|
|
255
|
+
}
|
|
256
|
+
function toWorkspaceError(operation, error) {
|
|
257
|
+
return {
|
|
258
|
+
type: "workspace",
|
|
259
|
+
operation,
|
|
260
|
+
message: error instanceof Error ? error.message : String(error)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function wrapWorkspace(operation, fn) {
|
|
264
|
+
try {
|
|
265
|
+
return ok(await fn());
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return err(toWorkspaceError(operation, error));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function createWorkspaceOperations(overrides = {}) {
|
|
271
|
+
return { discoverWorkspacePackages: (workspaceRoot, options) => wrapWorkspace("discoverWorkspacePackages", async () => {
|
|
272
|
+
if (overrides.discoverWorkspacePackages) return overrides.discoverWorkspacePackages(workspaceRoot, options);
|
|
273
|
+
return discoverWorkspacePackages(workspaceRoot, options);
|
|
274
|
+
}) };
|
|
275
|
+
}
|
|
276
|
+
async function findWorkspacePackages(workspaceRoot, options) {
|
|
277
|
+
try {
|
|
278
|
+
const result = await run("pnpm", [
|
|
279
|
+
"-r",
|
|
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
|
+
};
|
|
308
|
+
});
|
|
309
|
+
const packages = await Promise.all(promises);
|
|
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;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function shouldIncludePackage(pkg, options) {
|
|
318
|
+
if (!options) return true;
|
|
319
|
+
if (options.excludePrivate && pkg.private) return false;
|
|
320
|
+
if (options.include && options.include.length > 0) {
|
|
321
|
+
if (!options.include.includes(pkg.name)) return false;
|
|
39
322
|
}
|
|
323
|
+
if (options.exclude?.includes(pkg.name)) return false;
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/options.ts
|
|
329
|
+
const DEFAULT_PR_BODY_TEMPLATE = dedent`
|
|
330
|
+
This PR was automatically generated by the UCD release scripts.
|
|
331
|
+
|
|
332
|
+
The following packages have been prepared for release:
|
|
333
|
+
|
|
334
|
+
<% if (it.packages.length > 0) { %>
|
|
335
|
+
<% it.packages.forEach((pkg) => { %>
|
|
336
|
+
- **<%= pkg.name %>**: <%= pkg.currentVersion %> → <%= pkg.newVersion %> (<%= pkg.bumpType %>)
|
|
337
|
+
<% }) %>
|
|
338
|
+
<% } else { %>
|
|
339
|
+
There are no packages to release.
|
|
340
|
+
<% } %>
|
|
341
|
+
|
|
342
|
+
Please review the changes and merge when ready.
|
|
343
|
+
|
|
344
|
+
> [!NOTE]
|
|
345
|
+
> When this PR is merged, the release process will be triggered automatically, publishing the new package versions to the registry.
|
|
346
|
+
`;
|
|
347
|
+
const DEFAULT_CHANGELOG_TEMPLATE = dedent`
|
|
348
|
+
<% if (it.previousVersion) { -%>
|
|
349
|
+
## [<%= it.version %>](<%= it.compareUrl %>) (<%= it.date %>)
|
|
350
|
+
<% } else { -%>
|
|
351
|
+
## <%= it.version %> (<%= it.date %>)
|
|
352
|
+
<% } %>
|
|
353
|
+
|
|
354
|
+
<% it.groups.forEach((group) => { %>
|
|
355
|
+
<% if (group.commits.length > 0) { %>
|
|
356
|
+
|
|
357
|
+
### <%= group.title %>
|
|
358
|
+
<% group.commits.forEach((commit) => { %>
|
|
359
|
+
|
|
360
|
+
* <%= commit.line %>
|
|
361
|
+
<% }); %>
|
|
362
|
+
|
|
363
|
+
<% } %>
|
|
364
|
+
<% }); %>
|
|
365
|
+
`;
|
|
366
|
+
const DEFAULT_TYPES = {
|
|
367
|
+
feat: { title: "🚀 Features" },
|
|
368
|
+
fix: { title: "🐞 Bug Fixes" },
|
|
369
|
+
perf: { title: "🏎 Performance" },
|
|
370
|
+
docs: { title: "📚 Documentation" },
|
|
371
|
+
style: { title: "🎨 Styles" }
|
|
40
372
|
};
|
|
41
373
|
function normalizeReleaseScriptsOptions(options) {
|
|
42
|
-
const { workspaceRoot = process.cwd(), githubToken = "", repo: fullRepo, packages = true, branch = {}, globalCommitMode = "dependencies", pullRequest = {}, changelog = {}, types =
|
|
374
|
+
const { workspaceRoot = process.cwd(), githubToken = "", repo: fullRepo, packages = true, branch = {}, globalCommitMode = "dependencies", pullRequest = {}, changelog = {}, types, safeguards = true, dryRun = false, npm = {}, prompts = {} } = options;
|
|
43
375
|
const token = githubToken.trim();
|
|
44
376
|
if (!token) throw new Error("GitHub token is required. Pass it in via options.");
|
|
45
377
|
if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) throw new Error("Repository (repo) is required. Specify in 'owner/repo' format (e.g., 'octocat/hello-world').");
|
|
@@ -63,6 +395,7 @@ function normalizeReleaseScriptsOptions(options) {
|
|
|
63
395
|
default: branch.default ?? "main"
|
|
64
396
|
},
|
|
65
397
|
globalCommitMode,
|
|
398
|
+
safeguards,
|
|
66
399
|
pullRequest: {
|
|
67
400
|
title: pullRequest.title ?? "chore: release new version",
|
|
68
401
|
body: pullRequest.body ?? DEFAULT_PR_BODY_TEMPLATE
|
|
@@ -72,7 +405,7 @@ function normalizeReleaseScriptsOptions(options) {
|
|
|
72
405
|
template: changelog.template ?? DEFAULT_CHANGELOG_TEMPLATE,
|
|
73
406
|
emojis: changelog.emojis ?? true
|
|
74
407
|
},
|
|
75
|
-
types:
|
|
408
|
+
types: types ? {
|
|
76
409
|
...DEFAULT_TYPES,
|
|
77
410
|
...types
|
|
78
411
|
} : DEFAULT_TYPES,
|
|
@@ -80,1916 +413,1645 @@ function normalizeReleaseScriptsOptions(options) {
|
|
|
80
413
|
otp: npm.otp,
|
|
81
414
|
provenance: npm.provenance ?? true
|
|
82
415
|
},
|
|
83
|
-
prompts: {
|
|
416
|
+
prompts: {
|
|
417
|
+
versions: prompts.versions ?? !isCI,
|
|
418
|
+
packages: prompts.packages ?? !isCI
|
|
419
|
+
}
|
|
84
420
|
};
|
|
85
421
|
}
|
|
86
|
-
var ReleaseScriptsOptions = class extends Context.Tag("@ucdjs/release-scripts/ReleaseScriptsOptions")() {};
|
|
87
422
|
|
|
88
423
|
//#endregion
|
|
89
|
-
//#region src/
|
|
90
|
-
|
|
424
|
+
//#region src/operations/changelog-format.ts
|
|
425
|
+
function formatCommitLine({ commit, owner, repo, authors }) {
|
|
426
|
+
const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.hash}`;
|
|
427
|
+
let line = `${commit.description}`;
|
|
428
|
+
const references = commit.references ?? [];
|
|
429
|
+
for (const ref of references) {
|
|
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;
|
|
445
|
+
}
|
|
446
|
+
function buildTemplateGroups(options) {
|
|
447
|
+
const { commits, owner, repo, types, commitAuthors } = options;
|
|
448
|
+
const grouped = groupByType(commits, {
|
|
449
|
+
includeNonConventional: false,
|
|
450
|
+
mergeKeys: Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.types ?? [key]]))
|
|
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
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/core/git.ts
|
|
91
469
|
/**
|
|
92
|
-
*
|
|
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
|
|
93
473
|
*/
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
function toGitError(operation, error) {
|
|
487
|
+
return {
|
|
488
|
+
type: "git",
|
|
489
|
+
operation,
|
|
490
|
+
message: error instanceof Error ? error.message : String(error),
|
|
491
|
+
stderr: (typeof error === "object" && error && "stderr" in error ? String(error.stderr ?? "") : void 0)?.trim() || void 0
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
async function wrapGit(operation, fn) {
|
|
495
|
+
try {
|
|
496
|
+
return ok(await fn());
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return err(toGitError(operation, error));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
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
|
+
};
|
|
112
558
|
}
|
|
113
559
|
/**
|
|
114
|
-
*
|
|
560
|
+
* Check if a git branch exists locally
|
|
561
|
+
* @param {string} branch - The branch name to check
|
|
562
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
563
|
+
* @returns {Promise<boolean>} Promise resolving to true if branch exists, false otherwise
|
|
115
564
|
*/
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
565
|
+
async function doesBranchExist(branch, workspaceRoot) {
|
|
566
|
+
try {
|
|
567
|
+
await run("git", [
|
|
568
|
+
"rev-parse",
|
|
569
|
+
"--verify",
|
|
570
|
+
branch
|
|
571
|
+
], { nodeOptions: {
|
|
572
|
+
cwd: workspaceRoot,
|
|
573
|
+
stdio: "pipe"
|
|
574
|
+
} });
|
|
575
|
+
return true;
|
|
576
|
+
} catch {
|
|
577
|
+
return false;
|
|
122
578
|
}
|
|
123
|
-
return groups;
|
|
124
579
|
}
|
|
125
580
|
/**
|
|
126
|
-
*
|
|
581
|
+
* Retrieves the name of the current branch in the repository.
|
|
582
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
583
|
+
* @returns {Promise<string>} A Promise resolving to the current branch name as a string
|
|
127
584
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
test: "Tests",
|
|
144
|
-
build: "Build",
|
|
145
|
-
ci: "CI",
|
|
146
|
-
chore: "Chores"
|
|
147
|
-
}; %>
|
|
148
|
-
|
|
149
|
-
<% const formatAuthor = (entry) => {
|
|
150
|
-
const author = entry.authors && entry.authors.length > 0 ? entry.authors[0] : null;
|
|
151
|
-
if (!author) return "unknown";
|
|
152
|
-
if (author.profile && author.profile.includes("github.com/")) {
|
|
153
|
-
const username = author.profile.split("github.com/")[1];
|
|
154
|
-
return "@" + username;
|
|
155
|
-
}
|
|
156
|
-
return author.name || "unknown";
|
|
157
|
-
}; %>
|
|
158
|
-
|
|
159
|
-
<% const commitUrl = (hash) => it.repo ? "https://github.com/" + it.repo + "/commit/" + hash : ""; %>
|
|
160
|
-
|
|
161
|
-
<% const formatLine = (entry) => {
|
|
162
|
-
const authorText = formatAuthor(entry);
|
|
163
|
-
const commitLink = commitUrl(entry.hash);
|
|
164
|
-
const hashPart = commitLink
|
|
165
|
-
? " [<samp>(" + entry.shortHash + ")</samp>](" + commitLink + ")"
|
|
166
|
-
: " <samp>(" + entry.shortHash + ")</samp>";
|
|
167
|
-
return entry.description + " - by " + authorText + hashPart;
|
|
168
|
-
}; %>
|
|
169
|
-
|
|
170
|
-
<% for (const type of typeOrder) { %>
|
|
171
|
-
<% const entries = groups.get(type); %>
|
|
172
|
-
<% if (entries && entries.length > 0) { %>
|
|
173
|
-
### <%= typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1) %>
|
|
174
|
-
|
|
175
|
-
<% const unscoped = entries.filter(e => !e.scope); %>
|
|
176
|
-
<% const scoped = entries.filter(e => e.scope); %>
|
|
177
|
-
|
|
178
|
-
<% for (const entry of unscoped) { %>
|
|
179
|
-
- <%= formatLine(entry) %>
|
|
180
|
-
<% } %>
|
|
181
|
-
|
|
182
|
-
<% const scopes = [...new Set(scoped.map(e => e.scope))]; %>
|
|
183
|
-
<% for (const scope of scopes) { %>
|
|
184
|
-
- **<%= scope %>**:
|
|
185
|
-
<% const scopeEntries = scoped.filter(e => e.scope === scope); %>
|
|
186
|
-
<% for (const entry of scopeEntries) { %>
|
|
187
|
-
- <%= formatLine(entry) %>
|
|
188
|
-
<% } %>
|
|
189
|
-
<% } %>
|
|
190
|
-
|
|
191
|
-
<% } %>
|
|
192
|
-
<% } %>
|
|
193
|
-
|
|
194
|
-
<% for (const [type, entries] of groups) { %>
|
|
195
|
-
<% if (!typeOrder.includes(type)) { %>
|
|
196
|
-
### <%= type.charAt(0).toUpperCase() + type.slice(1) %>
|
|
197
|
-
|
|
198
|
-
<% for (const entry of entries) { %>
|
|
199
|
-
- <%= formatLine(entry) %>
|
|
200
|
-
<% } %>
|
|
201
|
-
|
|
202
|
-
<% } %>
|
|
203
|
-
<% } %>
|
|
204
|
-
|
|
205
|
-
<% if (it.repo) { %>
|
|
206
|
-
##### [View changes on GitHub](https://github.com/<%= it.repo %>/compare/v<%= it.previousVersion %>...v<%= it.version %>)
|
|
207
|
-
<% } %>
|
|
208
|
-
<% } %>`;
|
|
585
|
+
async function getCurrentBranch(workspaceRoot) {
|
|
586
|
+
try {
|
|
587
|
+
return (await run("git", [
|
|
588
|
+
"rev-parse",
|
|
589
|
+
"--abbrev-ref",
|
|
590
|
+
"HEAD"
|
|
591
|
+
], { nodeOptions: {
|
|
592
|
+
cwd: workspaceRoot,
|
|
593
|
+
stdio: "pipe"
|
|
594
|
+
} })).stdout.trim();
|
|
595
|
+
} catch (err) {
|
|
596
|
+
logger.error("Error getting current branch:", err);
|
|
597
|
+
throw err;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
209
600
|
/**
|
|
210
|
-
*
|
|
601
|
+
* Creates a new branch from the specified base branch.
|
|
602
|
+
* @param {string} branch - The name of the new branch to create
|
|
603
|
+
* @param {string} base - The base branch to create the new branch from
|
|
604
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
605
|
+
* @returns {Promise<void>} A Promise that resolves when the branch is created
|
|
211
606
|
*/
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
607
|
+
async function createBranch(branch, base, workspaceRoot) {
|
|
608
|
+
try {
|
|
609
|
+
logger.info(`Creating branch: ${farver.green(branch)} from ${farver.cyan(base)}`);
|
|
610
|
+
await runIfNotDry("git", [
|
|
611
|
+
"branch",
|
|
612
|
+
branch,
|
|
613
|
+
base
|
|
614
|
+
], { nodeOptions: {
|
|
615
|
+
cwd: workspaceRoot,
|
|
616
|
+
stdio: "pipe"
|
|
617
|
+
} });
|
|
618
|
+
} catch {
|
|
619
|
+
exitWithError(`Failed to create branch: ${branch}`, `Make sure the branch doesn't already exist and you have a clean working directory`);
|
|
620
|
+
}
|
|
222
621
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
622
|
+
async function checkoutBranch(branch, workspaceRoot) {
|
|
623
|
+
try {
|
|
624
|
+
logger.info(`Switching to branch: ${farver.green(branch)}`);
|
|
625
|
+
const match = (await run("git", ["checkout", branch], { nodeOptions: {
|
|
626
|
+
cwd: workspaceRoot,
|
|
627
|
+
stdio: "pipe"
|
|
628
|
+
} })).stderr.trim().match(/Switched to branch '(.+)'/);
|
|
629
|
+
if (match && match[1] === branch) {
|
|
630
|
+
logger.info(`Successfully switched to branch: ${farver.green(branch)}`);
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
} catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function pullLatestChanges(branch, workspaceRoot) {
|
|
639
|
+
try {
|
|
640
|
+
await run("git", [
|
|
641
|
+
"pull",
|
|
642
|
+
"origin",
|
|
643
|
+
branch
|
|
644
|
+
], { nodeOptions: {
|
|
645
|
+
cwd: workspaceRoot,
|
|
646
|
+
stdio: "pipe"
|
|
647
|
+
} });
|
|
648
|
+
return true;
|
|
649
|
+
} catch {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
async function rebaseBranch(ontoBranch, workspaceRoot) {
|
|
654
|
+
try {
|
|
655
|
+
logger.info(`Rebasing onto: ${farver.cyan(ontoBranch)}`);
|
|
656
|
+
await runIfNotDry("git", ["rebase", ontoBranch], { nodeOptions: {
|
|
657
|
+
cwd: workspaceRoot,
|
|
658
|
+
stdio: "pipe"
|
|
659
|
+
} });
|
|
660
|
+
return true;
|
|
661
|
+
} catch {
|
|
662
|
+
exitWithError(`Failed to rebase onto: ${ontoBranch}`, `You may have merge conflicts. Run 'git rebase --abort' to undo the rebase`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async function isBranchAheadOfRemote(branch, workspaceRoot) {
|
|
666
|
+
try {
|
|
667
|
+
const result = await run("git", [
|
|
668
|
+
"rev-list",
|
|
669
|
+
`origin/${branch}..${branch}`,
|
|
670
|
+
"--count"
|
|
671
|
+
], { nodeOptions: {
|
|
672
|
+
cwd: workspaceRoot,
|
|
673
|
+
stdio: "pipe"
|
|
674
|
+
} });
|
|
675
|
+
return Number.parseInt(result.stdout.trim(), 10) > 0;
|
|
676
|
+
} catch {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function commitChanges(message, workspaceRoot) {
|
|
681
|
+
try {
|
|
682
|
+
await run("git", ["add", "."], { nodeOptions: {
|
|
683
|
+
cwd: workspaceRoot,
|
|
684
|
+
stdio: "pipe"
|
|
685
|
+
} });
|
|
686
|
+
if (await isWorkingDirectoryClean(workspaceRoot)) return false;
|
|
687
|
+
logger.info(`Committing changes: ${farver.dim(message)}`);
|
|
688
|
+
await runIfNotDry("git", [
|
|
689
|
+
"commit",
|
|
690
|
+
"-m",
|
|
691
|
+
message
|
|
692
|
+
], { nodeOptions: {
|
|
693
|
+
cwd: workspaceRoot,
|
|
694
|
+
stdio: "pipe"
|
|
695
|
+
} });
|
|
696
|
+
return true;
|
|
697
|
+
} catch {
|
|
698
|
+
exitWithError(`Failed to commit changes`, `Make sure you have git configured properly with user.name and user.email`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function pushBranch(branch, workspaceRoot, options) {
|
|
702
|
+
try {
|
|
703
|
+
const args = [
|
|
704
|
+
"push",
|
|
705
|
+
"origin",
|
|
706
|
+
branch
|
|
707
|
+
];
|
|
708
|
+
if (options?.forceWithLease) {
|
|
709
|
+
args.push("--force-with-lease");
|
|
710
|
+
logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`);
|
|
711
|
+
} else if (options?.force) {
|
|
712
|
+
args.push("--force");
|
|
713
|
+
logger.info(`Force pushing branch: ${farver.green(branch)}`);
|
|
714
|
+
} else logger.info(`Pushing branch: ${farver.green(branch)}`);
|
|
715
|
+
await runIfNotDry("git", args, { nodeOptions: {
|
|
716
|
+
cwd: workspaceRoot,
|
|
717
|
+
stdio: "pipe"
|
|
718
|
+
} });
|
|
719
|
+
return true;
|
|
720
|
+
} catch {
|
|
721
|
+
exitWithError(`Failed to push branch: ${branch}`, `Make sure you have permission to push to the remote repository`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function readFileFromGit(workspaceRoot, ref, filePath) {
|
|
725
|
+
try {
|
|
726
|
+
return (await run("git", ["show", `${ref}:${filePath}`], { nodeOptions: {
|
|
727
|
+
cwd: workspaceRoot,
|
|
728
|
+
stdio: "pipe"
|
|
729
|
+
} })).stdout;
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function getMostRecentPackageTag(workspaceRoot, packageName) {
|
|
735
|
+
try {
|
|
736
|
+
const { stdout } = await run("git", [
|
|
737
|
+
"tag",
|
|
738
|
+
"--list",
|
|
739
|
+
`${packageName}@*`
|
|
740
|
+
], { nodeOptions: {
|
|
741
|
+
cwd: workspaceRoot,
|
|
742
|
+
stdio: "pipe"
|
|
743
|
+
} });
|
|
744
|
+
const tags = stdout.split("\n").map((tag) => tag.trim()).filter(Boolean);
|
|
745
|
+
if (tags.length === 0) return;
|
|
746
|
+
return tags.reverse()[0];
|
|
747
|
+
} catch (err) {
|
|
748
|
+
logger.warn(`Failed to get tags for package ${packageName}: ${err.message}`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
235
751
|
}
|
|
236
752
|
/**
|
|
237
|
-
*
|
|
753
|
+
* Builds a mapping of commit SHAs to the list of files changed in each commit
|
|
754
|
+
* within a given inclusive range.
|
|
755
|
+
*
|
|
756
|
+
* Internally runs:
|
|
757
|
+
* git log --name-only --format=%H <from>^..<to>
|
|
758
|
+
*
|
|
759
|
+
* Notes
|
|
760
|
+
* - This includes the commit identified by `from` (via `from^..to`).
|
|
761
|
+
* - Order of commits in the resulting Map follows `git log` output
|
|
762
|
+
* (reverse chronological, newest first).
|
|
763
|
+
* - On failure (e.g., invalid refs), the function returns null.
|
|
764
|
+
*
|
|
765
|
+
* @param {string} workspaceRoot Absolute path to the git repository root used as cwd.
|
|
766
|
+
* @param {string} from Starting commit/ref (inclusive).
|
|
767
|
+
* @param {string} to Ending commit/ref (inclusive).
|
|
768
|
+
* @returns {Promise<Map<string, string[]> | null>} Promise resolving to a Map where keys are commit SHAs and values are
|
|
769
|
+
* arrays of file paths changed by that commit, or null on error.
|
|
238
770
|
*/
|
|
239
|
-
function
|
|
240
|
-
|
|
771
|
+
async function getGroupedFilesByCommitSha(workspaceRoot, from, to) {
|
|
772
|
+
const commitsMap = /* @__PURE__ */ new Map();
|
|
773
|
+
try {
|
|
774
|
+
const { stdout } = await run("git", [
|
|
775
|
+
"log",
|
|
776
|
+
"--name-only",
|
|
777
|
+
"--format=%H",
|
|
778
|
+
`${from}^..${to}`
|
|
779
|
+
], { nodeOptions: {
|
|
780
|
+
cwd: workspaceRoot,
|
|
781
|
+
stdio: "pipe"
|
|
782
|
+
} });
|
|
783
|
+
const lines = stdout.trim().split("\n").filter((line) => line.trim() !== "");
|
|
784
|
+
let currentSha = null;
|
|
785
|
+
const HASH_REGEX = /^[0-9a-f]{40}$/i;
|
|
786
|
+
for (const line of lines) {
|
|
787
|
+
const trimmedLine = line.trim();
|
|
788
|
+
if (HASH_REGEX.test(trimmedLine)) {
|
|
789
|
+
currentSha = trimmedLine;
|
|
790
|
+
commitsMap.set(currentSha, []);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (currentSha === null) continue;
|
|
794
|
+
commitsMap.get(currentSha).push(trimmedLine);
|
|
795
|
+
}
|
|
796
|
+
return commitsMap;
|
|
797
|
+
} catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/core/changelog.ts
|
|
804
|
+
const excludeAuthors = [
|
|
805
|
+
/\[bot\]/i,
|
|
806
|
+
/dependabot/i,
|
|
807
|
+
/\(bot\)/i
|
|
808
|
+
];
|
|
809
|
+
async function generateChangelogEntry(options) {
|
|
810
|
+
const { packageName, version, previousVersion, date, commits, owner, repo, types, template, githubClient } = options;
|
|
811
|
+
const templateData = {
|
|
241
812
|
packageName,
|
|
242
813
|
version,
|
|
243
814
|
previousVersion,
|
|
244
|
-
|
|
245
|
-
repo
|
|
815
|
+
date,
|
|
816
|
+
compareUrl: previousVersion ? `https://github.com/${owner}/${repo}/compare/${packageName}@${previousVersion}...${packageName}@${version}` : void 0,
|
|
817
|
+
owner,
|
|
818
|
+
repo,
|
|
819
|
+
groups: buildTemplateGroups({
|
|
820
|
+
commits,
|
|
821
|
+
owner,
|
|
822
|
+
repo,
|
|
823
|
+
types,
|
|
824
|
+
commitAuthors: await resolveCommitAuthors(commits, githubClient)
|
|
825
|
+
})
|
|
246
826
|
};
|
|
827
|
+
const eta = new Eta();
|
|
828
|
+
const templateToUse = template || DEFAULT_CHANGELOG_TEMPLATE;
|
|
829
|
+
return eta.renderString(templateToUse, templateData).trim();
|
|
247
830
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
831
|
+
async function updateChangelog(options) {
|
|
832
|
+
const { version, previousVersion, commits, date, normalizedOptions, workspacePackage, githubClient } = options;
|
|
833
|
+
const changelogPath = join(workspacePackage.path, "CHANGELOG.md");
|
|
834
|
+
const changelogRelativePath = relative(normalizedOptions.workspaceRoot, join(workspacePackage.path, "CHANGELOG.md"));
|
|
835
|
+
const existingContent = await readFileFromGit(normalizedOptions.workspaceRoot, normalizedOptions.branch.default, changelogRelativePath);
|
|
836
|
+
logger.verbose("Existing content found: ", Boolean(existingContent));
|
|
837
|
+
const newEntry = await generateChangelogEntry({
|
|
838
|
+
packageName: workspacePackage.name,
|
|
839
|
+
version,
|
|
840
|
+
previousVersion,
|
|
841
|
+
date,
|
|
842
|
+
commits,
|
|
843
|
+
owner: normalizedOptions.owner,
|
|
844
|
+
repo: normalizedOptions.repo,
|
|
845
|
+
types: normalizedOptions.types,
|
|
846
|
+
template: normalizedOptions.changelog?.template,
|
|
847
|
+
githubClient
|
|
848
|
+
});
|
|
849
|
+
let updatedContent;
|
|
850
|
+
if (!existingContent) {
|
|
851
|
+
updatedContent = `# ${workspacePackage.name}\n\n${newEntry}\n`;
|
|
852
|
+
await writeFile(changelogPath, updatedContent, "utf-8");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const parsed = parseChangelog(existingContent);
|
|
856
|
+
const lines = existingContent.split("\n");
|
|
857
|
+
const existingVersionIndex = parsed.versions.findIndex((v) => v.version === version);
|
|
858
|
+
if (existingVersionIndex !== -1) {
|
|
859
|
+
const existingVersion = parsed.versions[existingVersionIndex];
|
|
860
|
+
const before = lines.slice(0, existingVersion.lineStart);
|
|
861
|
+
const after = lines.slice(existingVersion.lineEnd + 1);
|
|
862
|
+
updatedContent = [
|
|
863
|
+
...before,
|
|
864
|
+
newEntry,
|
|
865
|
+
...after
|
|
866
|
+
].join("\n");
|
|
867
|
+
} else {
|
|
868
|
+
const insertAt = parsed.headerLineEnd + 1;
|
|
869
|
+
const before = lines.slice(0, insertAt);
|
|
870
|
+
const after = lines.slice(insertAt);
|
|
871
|
+
if (before.length > 0 && before[before.length - 1] !== "") before.push("");
|
|
872
|
+
updatedContent = [
|
|
873
|
+
...before,
|
|
874
|
+
newEntry,
|
|
875
|
+
"",
|
|
876
|
+
...after
|
|
877
|
+
].join("\n");
|
|
878
|
+
}
|
|
879
|
+
await writeFile(changelogPath, updatedContent, "utf-8");
|
|
880
|
+
}
|
|
881
|
+
async function resolveCommitAuthors(commits, githubClient) {
|
|
882
|
+
const authorMap = /* @__PURE__ */ new Map();
|
|
883
|
+
const commitAuthors = /* @__PURE__ */ new Map();
|
|
884
|
+
for (const commit of commits) {
|
|
885
|
+
const authorsForCommit = [];
|
|
886
|
+
commit.authors.forEach((author, idx) => {
|
|
887
|
+
if (!author.email || !author.name) return;
|
|
888
|
+
if (excludeAuthors.some((re) => re.test(author.name))) return;
|
|
889
|
+
if (!authorMap.has(author.email)) authorMap.set(author.email, {
|
|
890
|
+
commits: [],
|
|
891
|
+
name: author.name,
|
|
892
|
+
email: author.email
|
|
268
893
|
});
|
|
894
|
+
const info = authorMap.get(author.email);
|
|
895
|
+
if (idx === 0) info.commits.push(commit.shortHash);
|
|
896
|
+
authorsForCommit.push(info);
|
|
897
|
+
});
|
|
898
|
+
commitAuthors.set(commit.hash, authorsForCommit);
|
|
899
|
+
}
|
|
900
|
+
const authors = Array.from(authorMap.values());
|
|
901
|
+
await Promise.all(authors.map((info) => githubClient.resolveAuthorInfo(info)));
|
|
902
|
+
return commitAuthors;
|
|
903
|
+
}
|
|
904
|
+
function parseChangelog(content) {
|
|
905
|
+
const lines = content.split("\n");
|
|
906
|
+
let packageName = null;
|
|
907
|
+
let headerLineEnd = -1;
|
|
908
|
+
const versions = [];
|
|
909
|
+
for (let i = 0; i < lines.length; i++) {
|
|
910
|
+
const line = lines[i].trim();
|
|
911
|
+
if (line.startsWith("# ")) {
|
|
912
|
+
packageName = line.slice(2).trim();
|
|
913
|
+
headerLineEnd = i;
|
|
914
|
+
break;
|
|
269
915
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
283
|
-
for (const pkg of packages) {
|
|
284
|
-
nameToPackage.set(pkg.name, pkg);
|
|
285
|
-
adjacency.set(pkg.name, /* @__PURE__ */ new Set());
|
|
286
|
-
inDegree.set(pkg.name, 0);
|
|
287
|
-
}
|
|
288
|
-
for (const pkg of packages) {
|
|
289
|
-
const deps = new Set([...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies]);
|
|
290
|
-
for (const depName of deps) {
|
|
291
|
-
if (!nameToPackage.has(depName)) continue;
|
|
292
|
-
adjacency.get(depName)?.add(pkg.name);
|
|
293
|
-
inDegree.set(pkg.name, (inDegree.get(pkg.name) ?? 0) + 1);
|
|
916
|
+
}
|
|
917
|
+
for (let i = headerLineEnd + 1; i < lines.length; i++) {
|
|
918
|
+
const line = lines[i].trim();
|
|
919
|
+
if (line.startsWith("## ")) {
|
|
920
|
+
const versionMatch = line.match(/##\s+(?:<small>)?\[?([^\](\s<]+)/);
|
|
921
|
+
if (versionMatch) {
|
|
922
|
+
const version = versionMatch[1];
|
|
923
|
+
const lineStart = i;
|
|
924
|
+
let lineEnd = lines.length - 1;
|
|
925
|
+
for (let j = i + 1; j < lines.length; j++) if (lines[j].trim().startsWith("## ")) {
|
|
926
|
+
lineEnd = j - 1;
|
|
927
|
+
break;
|
|
294
928
|
}
|
|
929
|
+
const versionContent = lines.slice(lineStart, lineEnd + 1).join("\n");
|
|
930
|
+
versions.push({
|
|
931
|
+
version,
|
|
932
|
+
lineStart,
|
|
933
|
+
lineEnd,
|
|
934
|
+
content: versionContent
|
|
935
|
+
});
|
|
295
936
|
}
|
|
296
|
-
return {
|
|
297
|
-
nameToPackage,
|
|
298
|
-
adjacency,
|
|
299
|
-
inDegree
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
function topologicalOrder(packages) {
|
|
303
|
-
return Effect.gen(function* () {
|
|
304
|
-
const { nameToPackage, adjacency, inDegree } = buildGraph(packages);
|
|
305
|
-
const queue = [];
|
|
306
|
-
const levels = /* @__PURE__ */ new Map();
|
|
307
|
-
for (const [name, degree] of inDegree) if (degree === 0) {
|
|
308
|
-
queue.push(name);
|
|
309
|
-
levels.set(name, 0);
|
|
310
|
-
}
|
|
311
|
-
let queueIndex = 0;
|
|
312
|
-
const ordered = [];
|
|
313
|
-
while (queueIndex < queue.length) {
|
|
314
|
-
const current = queue[queueIndex++];
|
|
315
|
-
const currentLevel = levels.get(current) ?? 0;
|
|
316
|
-
const pkg = nameToPackage.get(current);
|
|
317
|
-
if (pkg) ordered.push({
|
|
318
|
-
package: pkg,
|
|
319
|
-
level: currentLevel
|
|
320
|
-
});
|
|
321
|
-
for (const neighbor of adjacency.get(current) ?? []) {
|
|
322
|
-
const nextLevel = currentLevel + 1;
|
|
323
|
-
if (nextLevel > (levels.get(neighbor) ?? 0)) levels.set(neighbor, nextLevel);
|
|
324
|
-
const newDegree = (inDegree.get(neighbor) ?? 0) - 1;
|
|
325
|
-
inDegree.set(neighbor, newDegree);
|
|
326
|
-
if (newDegree === 0) queue.push(neighbor);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (ordered.length !== packages.length) {
|
|
330
|
-
const processed = new Set(ordered.map((o) => o.package.name));
|
|
331
|
-
const unprocessed = packages.filter((p) => !processed.has(p.name)).map((p) => p.name);
|
|
332
|
-
return yield* Effect.fail(/* @__PURE__ */ new Error(`Cycle detected in workspace dependencies. Packages involved: ${unprocessed.join(", ")}`));
|
|
333
|
-
}
|
|
334
|
-
return ordered;
|
|
335
|
-
});
|
|
336
937
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
var GitCommandError = class extends Data.TaggedError("GitCommandError") {};
|
|
345
|
-
var ExternalCommitParserError = class extends Data.TaggedError("ExternalCommitParserError") {};
|
|
346
|
-
var WorkspaceError = class extends Data.TaggedError("WorkspaceError") {};
|
|
347
|
-
var GitHubError = class extends Data.TaggedError("GitHubError") {};
|
|
348
|
-
var VersionCalculationError = class extends Data.TaggedError("VersionCalculationError") {};
|
|
349
|
-
var OverridesLoadError = class extends Data.TaggedError("OverridesLoadError") {};
|
|
350
|
-
var NPMError = class extends Data.TaggedError("NPMError") {};
|
|
351
|
-
var PublishError = class extends Data.TaggedError("PublishError") {};
|
|
352
|
-
var TagError = class extends Data.TaggedError("TagError") {};
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
packageName,
|
|
941
|
+
versions,
|
|
942
|
+
headerLineEnd
|
|
943
|
+
};
|
|
944
|
+
}
|
|
353
945
|
|
|
354
946
|
//#endregion
|
|
355
|
-
//#region src/
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
947
|
+
//#region src/core/github.ts
|
|
948
|
+
var GitHubClient = class {
|
|
949
|
+
owner;
|
|
950
|
+
repo;
|
|
951
|
+
githubToken;
|
|
952
|
+
apiBase = "https://api.github.com";
|
|
953
|
+
constructor({ owner, repo, githubToken }) {
|
|
954
|
+
this.owner = owner;
|
|
955
|
+
this.repo = repo;
|
|
956
|
+
this.githubToken = githubToken;
|
|
957
|
+
}
|
|
958
|
+
async request(path, init = {}) {
|
|
959
|
+
const url = path.startsWith("http") ? path : `${this.apiBase}${path}`;
|
|
960
|
+
const res = await fetch(url, {
|
|
961
|
+
...init,
|
|
962
|
+
headers: {
|
|
963
|
+
...init.headers,
|
|
964
|
+
"Accept": "application/vnd.github.v3+json",
|
|
965
|
+
"Authorization": `token ${this.githubToken}`,
|
|
966
|
+
"User-Agent": "ucdjs-release-scripts (+https://github.com/ucdjs/ucdjs-release-scripts)"
|
|
967
|
+
}
|
|
375
968
|
});
|
|
376
|
-
|
|
377
|
-
|
|
969
|
+
if (!res.ok) {
|
|
970
|
+
const errorText = await res.text();
|
|
971
|
+
throw new Error(`GitHub API request failed with status ${res.status}: ${errorText || "No response body"}`);
|
|
378
972
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
973
|
+
if (res.status === 204) return;
|
|
974
|
+
return res.json();
|
|
975
|
+
}
|
|
976
|
+
async getExistingPullRequest(branch) {
|
|
977
|
+
const head = branch.includes(":") ? branch : `${this.owner}:${branch}`;
|
|
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)
|
|
392
1012
|
});
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
function forcePushChanges(branch, remote = "origin") {
|
|
428
|
-
return execGitCommandIfNotDry([
|
|
429
|
-
"push",
|
|
430
|
-
"--force-with-lease",
|
|
431
|
-
remote,
|
|
432
|
-
branch
|
|
433
|
-
]);
|
|
434
|
-
}
|
|
435
|
-
function readFile(filePath, ref = "HEAD") {
|
|
436
|
-
return execGitCommand(["show", `${ref}:${filePath}`]);
|
|
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}`);
|
|
437
1047
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
]).pipe(Effect.map((tags) => {
|
|
445
|
-
return tags.trim().split("\n").map((tag) => tag.trim()).filter((tag) => tag.length > 0)[0] || null;
|
|
446
|
-
}), Effect.flatMap((tag) => {
|
|
447
|
-
if (tag === null) return Effect.succeed(null);
|
|
448
|
-
return execGitCommand(["rev-parse", tag]).pipe(Effect.map((sha) => ({
|
|
449
|
-
name: tag,
|
|
450
|
-
sha: sha.trim()
|
|
451
|
-
})));
|
|
452
|
-
}));
|
|
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}`);
|
|
453
1054
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
message: `commit-parser getCommits`,
|
|
490
|
-
cause: e instanceof Error ? e.message : String(e)
|
|
491
|
-
})
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
function filesChangesBetweenRefs(from, to) {
|
|
495
|
-
const commitsMap = /* @__PURE__ */ new Map();
|
|
496
|
-
return execGitCommand([
|
|
497
|
-
"log",
|
|
498
|
-
"--name-only",
|
|
499
|
-
"--format=%H",
|
|
500
|
-
`${from}^..${to}`
|
|
501
|
-
]).pipe(Effect.map((output) => {
|
|
502
|
-
const lines = output.trim().split("\n").filter((line) => line.trim() !== "");
|
|
503
|
-
let currentSha = null;
|
|
504
|
-
const HASH_REGEX = /^[0-9a-f]{40}$/i;
|
|
505
|
-
for (const line of lines) {
|
|
506
|
-
const trimmedLine = line.trim();
|
|
507
|
-
if (HASH_REGEX.test(trimmedLine)) {
|
|
508
|
-
currentSha = trimmedLine;
|
|
509
|
-
commitsMap.set(currentSha, []);
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
if (currentSha === null) continue;
|
|
513
|
-
commitsMap.get(currentSha).push(trimmedLine);
|
|
514
|
-
}
|
|
515
|
-
return commitsMap;
|
|
516
|
-
}));
|
|
517
|
-
}
|
|
518
|
-
const assertWorkspaceReady = Effect.gen(function* () {
|
|
519
|
-
if (!(yield* isWithinRepository)) return yield* Effect.fail(/* @__PURE__ */ new Error("Not within a Git repository."));
|
|
520
|
-
if (!(yield* isWorkingDirectoryClean)) return yield* Effect.fail(/* @__PURE__ */ new Error("Working directory is not clean."));
|
|
521
|
-
return true;
|
|
522
|
-
});
|
|
523
|
-
return {
|
|
524
|
-
branches: {
|
|
525
|
-
list: listBranches,
|
|
526
|
-
exists: doesBranchExist,
|
|
527
|
-
create: createBranch,
|
|
528
|
-
checkout: checkoutBranch,
|
|
529
|
-
rebase: rebaseBranch,
|
|
530
|
-
get: getBranch
|
|
531
|
-
},
|
|
532
|
-
commits: {
|
|
533
|
-
stage: stageChanges,
|
|
534
|
-
write: writeCommit,
|
|
535
|
-
writeEmpty: writeEmptyCommit,
|
|
536
|
-
push: pushChanges,
|
|
537
|
-
forcePush: forcePushChanges,
|
|
538
|
-
get: getCommits,
|
|
539
|
-
filesChangesBetweenRefs
|
|
540
|
-
},
|
|
541
|
-
tags: {
|
|
542
|
-
mostRecentForPackage: getMostRecentPackageTag,
|
|
543
|
-
create: createTag,
|
|
544
|
-
push: pushTag
|
|
545
|
-
},
|
|
546
|
-
workspace: {
|
|
547
|
-
readFile,
|
|
548
|
-
isWithinRepository,
|
|
549
|
-
isWorkingDirectoryClean,
|
|
550
|
-
assertWorkspaceReady
|
|
1055
|
+
return info;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
function createGitHubClient(options) {
|
|
1059
|
+
return new GitHubClient(options);
|
|
1060
|
+
}
|
|
1061
|
+
function toGitHubError(operation, error) {
|
|
1062
|
+
return {
|
|
1063
|
+
type: "github",
|
|
1064
|
+
operation,
|
|
1065
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async function wrapGitHub(operation, fn) {
|
|
1069
|
+
try {
|
|
1070
|
+
return ok(await fn());
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
return err(toGitHubError(operation, error));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function createGitHubOperations(options, overrides = {}) {
|
|
1076
|
+
const client = createGitHubClient(options);
|
|
1077
|
+
return {
|
|
1078
|
+
getExistingPullRequest: (branch) => wrapGitHub("getExistingPullRequest", async () => {
|
|
1079
|
+
if (overrides.getExistingPullRequest) return overrides.getExistingPullRequest(branch);
|
|
1080
|
+
return client.getExistingPullRequest(branch);
|
|
1081
|
+
}),
|
|
1082
|
+
upsertPullRequest: (input) => wrapGitHub("upsertPullRequest", async () => {
|
|
1083
|
+
if (overrides.upsertPullRequest) return overrides.upsertPullRequest(input);
|
|
1084
|
+
return client.upsertPullRequest(input);
|
|
1085
|
+
}),
|
|
1086
|
+
setCommitStatus: (input) => wrapGitHub("setCommitStatus", async () => {
|
|
1087
|
+
if (overrides.setCommitStatus) {
|
|
1088
|
+
await overrides.setCommitStatus(input);
|
|
1089
|
+
return;
|
|
551
1090
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1091
|
+
await client.setCommitStatus(input);
|
|
1092
|
+
}),
|
|
1093
|
+
resolveAuthorInfo: (info) => wrapGitHub("resolveAuthorInfo", async () => {
|
|
1094
|
+
if (overrides.resolveAuthorInfo) return overrides.resolveAuthorInfo(info);
|
|
1095
|
+
return client.resolveAuthorInfo(info);
|
|
1096
|
+
})
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
function dedentString(str) {
|
|
1100
|
+
const lines = str.split("\n");
|
|
1101
|
+
const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity);
|
|
1102
|
+
return lines.map((line) => minIndent === Infinity ? line : line.slice(minIndent)).join("\n").trim();
|
|
1103
|
+
}
|
|
1104
|
+
function generatePullRequestBody(updates, body) {
|
|
1105
|
+
const eta = new Eta();
|
|
1106
|
+
const bodyTemplate = body ? dedentString(body) : DEFAULT_PR_BODY_TEMPLATE;
|
|
1107
|
+
return eta.renderString(bodyTemplate, { packages: updates.map((u) => ({
|
|
1108
|
+
name: u.package.name,
|
|
1109
|
+
currentVersion: u.currentVersion,
|
|
1110
|
+
newVersion: u.newVersion,
|
|
1111
|
+
bumpType: u.bumpType,
|
|
1112
|
+
hasDirectChanges: u.hasDirectChanges
|
|
1113
|
+
})) });
|
|
1114
|
+
}
|
|
556
1115
|
|
|
557
1116
|
//#endregion
|
|
558
|
-
//#region src/
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
const RepositoryInfoSchema = Schema.Struct({
|
|
597
|
-
owner: Schema.String,
|
|
598
|
-
repo: Schema.String
|
|
599
|
-
});
|
|
600
|
-
var GitHubService = class extends Effect.Service()("@ucdjs/release-scripts/GitHubService", {
|
|
601
|
-
effect: Effect.gen(function* () {
|
|
602
|
-
const config = yield* ReleaseScriptsOptions;
|
|
603
|
-
function makeRequest(endpoint, schema, options = {}) {
|
|
604
|
-
const url = `https://api.github.com/repos/${config.owner}/${config.repo}/${endpoint}`;
|
|
605
|
-
return Effect.tryPromise({
|
|
606
|
-
try: async () => {
|
|
607
|
-
const res = await fetch(url, {
|
|
608
|
-
...options,
|
|
609
|
-
headers: {
|
|
610
|
-
"Authorization": `token ${config.githubToken}`,
|
|
611
|
-
"Accept": "application/vnd.github.v3+json",
|
|
612
|
-
"Content-Type": "application/json",
|
|
613
|
-
"User-Agent": "ucdjs-release-scripts (https://github.com/ucdjs/release-scripts)",
|
|
614
|
-
...options.headers
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
if (!res.ok) {
|
|
618
|
-
const text = await res.text();
|
|
619
|
-
throw new Error(`GitHub API request failed with status ${res.status}: ${text}`);
|
|
620
|
-
}
|
|
621
|
-
if (res.status === 204) return;
|
|
622
|
-
return res.json();
|
|
623
|
-
},
|
|
624
|
-
catch: (e) => new GitHubError({
|
|
625
|
-
message: String(e),
|
|
626
|
-
operation: "request",
|
|
627
|
-
cause: e
|
|
628
|
-
})
|
|
629
|
-
}).pipe(Effect.flatMap((json) => json === void 0 ? Effect.succeed(void 0) : Schema.decodeUnknown(schema)(json).pipe(Effect.mapError((e) => new GitHubError({
|
|
630
|
-
message: "Failed to decode GitHub response",
|
|
631
|
-
operation: "request",
|
|
632
|
-
cause: e
|
|
633
|
-
})))));
|
|
634
|
-
}
|
|
635
|
-
function getPullRequestByBranch(branch) {
|
|
636
|
-
const head = branch.includes(":") ? branch : `${config.owner}:${branch}`;
|
|
637
|
-
return makeRequest(`pulls?state=open&head=${encodeURIComponent(head)}`, Schema.Array(PullRequestSchema)).pipe(Effect.map((pulls) => pulls.length > 0 ? pulls[0] : null), Effect.mapError((e) => new GitHubError({
|
|
638
|
-
message: e.message,
|
|
639
|
-
operation: "getPullRequestByBranch",
|
|
640
|
-
cause: e.cause
|
|
641
|
-
})));
|
|
642
|
-
}
|
|
643
|
-
function setCommitStatus(sha, status) {
|
|
644
|
-
return makeRequest(`statuses/${sha}`, Schema.Unknown, {
|
|
645
|
-
method: "POST",
|
|
646
|
-
body: JSON.stringify(status)
|
|
647
|
-
}).pipe(Effect.map(() => status), Effect.catchAll((e) => Effect.fail(new GitHubError({
|
|
648
|
-
message: e.message,
|
|
649
|
-
operation: "setCommitStatus",
|
|
650
|
-
cause: e.cause
|
|
651
|
-
}))));
|
|
652
|
-
}
|
|
653
|
-
function updatePullRequest(number, options) {
|
|
654
|
-
return makeRequest(`pulls/${number}`, PullRequestSchema, {
|
|
655
|
-
method: "PATCH",
|
|
656
|
-
body: JSON.stringify(options)
|
|
657
|
-
}).pipe(Effect.mapError((e) => new GitHubError({
|
|
658
|
-
message: e.message,
|
|
659
|
-
operation: "updatePullRequest",
|
|
660
|
-
cause: e.cause
|
|
661
|
-
})));
|
|
662
|
-
}
|
|
663
|
-
function createPullRequest(options) {
|
|
664
|
-
return makeRequest("pulls", PullRequestSchema, {
|
|
665
|
-
method: "POST",
|
|
666
|
-
body: JSON.stringify(options)
|
|
667
|
-
}).pipe(Effect.mapError((e) => new GitHubError({
|
|
668
|
-
message: e.message,
|
|
669
|
-
operation: "createPullRequest",
|
|
670
|
-
cause: e.cause
|
|
671
|
-
})));
|
|
672
|
-
}
|
|
673
|
-
const prBodyTemplate = `## Release Summary
|
|
674
|
-
|
|
675
|
-
This PR prepares the release of <%= it.count %> package<%= it.count === 1 ? "" : "s" %>:
|
|
676
|
-
|
|
677
|
-
<% for (const release of it.releases) { %>
|
|
678
|
-
- **<%= release.packageName %>**: \`<%= release.previousVersion %>\` → \`<%= release.version %>\`
|
|
679
|
-
<% } %>
|
|
1117
|
+
//#region src/operations/branch.ts
|
|
1118
|
+
async function prepareReleaseBranch(options) {
|
|
1119
|
+
const { git, workspaceRoot, releaseBranch, defaultBranch } = options;
|
|
1120
|
+
const currentBranch = await git.getCurrentBranch(workspaceRoot);
|
|
1121
|
+
if (!currentBranch.ok) return currentBranch;
|
|
1122
|
+
if (currentBranch.value !== defaultBranch) return err({
|
|
1123
|
+
type: "git",
|
|
1124
|
+
operation: "validateBranch",
|
|
1125
|
+
message: `Current branch is '${currentBranch.value}'. Please switch to '${defaultBranch}'.`
|
|
1126
|
+
});
|
|
1127
|
+
const branchExists = await git.doesBranchExist(releaseBranch, workspaceRoot);
|
|
1128
|
+
if (!branchExists.ok) return branchExists;
|
|
1129
|
+
if (!branchExists.value) {
|
|
1130
|
+
const created = await git.createBranch(releaseBranch, defaultBranch, workspaceRoot);
|
|
1131
|
+
if (!created.ok) return created;
|
|
1132
|
+
}
|
|
1133
|
+
const checkedOut = await git.checkoutBranch(releaseBranch, workspaceRoot);
|
|
1134
|
+
if (!checkedOut.ok) return checkedOut;
|
|
1135
|
+
if (branchExists.value) {
|
|
1136
|
+
const pulled = await git.pullLatestChanges(releaseBranch, workspaceRoot);
|
|
1137
|
+
if (!pulled.ok) return pulled;
|
|
1138
|
+
if (!pulled.value) logger.warn("Failed to pull latest changes, continuing anyway.");
|
|
1139
|
+
}
|
|
1140
|
+
const rebased = await git.rebaseBranch(defaultBranch, workspaceRoot);
|
|
1141
|
+
if (!rebased.ok) return rebased;
|
|
1142
|
+
return ok(void 0);
|
|
1143
|
+
}
|
|
1144
|
+
async function syncReleaseChanges(options) {
|
|
1145
|
+
const { git, workspaceRoot, releaseBranch, commitMessage, hasChanges } = options;
|
|
1146
|
+
const committed = hasChanges ? await git.commitChanges(commitMessage, workspaceRoot) : ok(false);
|
|
1147
|
+
if (!committed.ok) return committed;
|
|
1148
|
+
const isAhead = await git.isBranchAheadOfRemote(releaseBranch, workspaceRoot);
|
|
1149
|
+
if (!isAhead.ok) return isAhead;
|
|
1150
|
+
if (!committed.value && !isAhead.value) return ok(false);
|
|
1151
|
+
const pushed = await git.pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
|
|
1152
|
+
if (!pushed.ok) return pushed;
|
|
1153
|
+
return ok(true);
|
|
1154
|
+
}
|
|
680
1155
|
|
|
681
|
-
|
|
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
|
+
}
|
|
682
1186
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
+
}
|
|
703
1209
|
|
|
704
1210
|
//#endregion
|
|
705
|
-
//#region src/
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
})
|
|
724
|
-
})
|
|
725
|
-
});
|
|
726
|
-
var NPMService = class extends Effect.Service()("@ucdjs/release-scripts/NPMService", {
|
|
727
|
-
effect: Effect.gen(function* () {
|
|
728
|
-
const executor = yield* CommandExecutor.CommandExecutor;
|
|
729
|
-
const config = yield* ReleaseScriptsOptions;
|
|
730
|
-
const fetchPackument = (packageName) => Effect.tryPromise({
|
|
731
|
-
try: async () => {
|
|
732
|
-
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
|
733
|
-
if (response.status === 404) return null;
|
|
734
|
-
if (!response.ok) throw new Error(`Failed to fetch packument: ${response.statusText}`);
|
|
735
|
-
return await response.json();
|
|
736
|
-
},
|
|
737
|
-
catch: (error) => {
|
|
738
|
-
return new NPMError({
|
|
739
|
-
message: error instanceof Error ? error.message : String(error),
|
|
740
|
-
operation: "fetchPackument"
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
}).pipe(Effect.flatMap((data) => {
|
|
744
|
-
if (data === null) return Effect.succeed(null);
|
|
745
|
-
return Schema.decodeUnknown(PackumentSchema)(data).pipe(Effect.mapError((error) => new NPMError({
|
|
746
|
-
message: `Failed to parse packument: ${error}`,
|
|
747
|
-
operation: "fetchPackument"
|
|
748
|
-
})));
|
|
749
|
-
}));
|
|
750
|
-
const versionExists = (packageName, version) => fetchPackument(packageName).pipe(Effect.map((packument) => {
|
|
751
|
-
if (!packument) return false;
|
|
752
|
-
return version in packument.versions;
|
|
753
|
-
}));
|
|
754
|
-
const getLatestVersion = (packageName) => fetchPackument(packageName).pipe(Effect.map((packument) => {
|
|
755
|
-
if (!packument) return null;
|
|
756
|
-
return packument["dist-tags"].latest || null;
|
|
757
|
-
}));
|
|
758
|
-
const publish = (options) => Effect.gen(function* () {
|
|
759
|
-
const args = ["publish"];
|
|
760
|
-
if (options.tagName) args.push("--tag", options.tagName);
|
|
761
|
-
if (options.otp) args.push("--otp", options.otp);
|
|
762
|
-
if (options.provenance !== false) args.push("--provenance");
|
|
763
|
-
if (options.dryRun ?? config.dryRun) args.push("--dry-run");
|
|
764
|
-
const command = Command.make("pnpm", ...args).pipe(Command.workingDirectory(options.packagePath));
|
|
765
|
-
return (yield* executor.string(command).pipe(Effect.mapError((err) => new PublishError({
|
|
766
|
-
message: `Failed to publish package at ${options.packagePath}: ${err.message}`,
|
|
767
|
-
cause: err
|
|
768
|
-
})))).trim();
|
|
1211
|
+
//#region src/versioning/commits.ts
|
|
1212
|
+
/**
|
|
1213
|
+
* Get commits grouped by workspace package.
|
|
1214
|
+
* For each package, retrieves all commits since its last release tag that affect that package.
|
|
1215
|
+
*
|
|
1216
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
1217
|
+
* @param {WorkspacePackage[]} packages - Array of workspace packages to analyze
|
|
1218
|
+
* @returns {Promise<Map<string, GitCommit[]>>} A map of package names to their commits since their last release
|
|
1219
|
+
*/
|
|
1220
|
+
async function getWorkspacePackageGroupedCommits(workspaceRoot, packages) {
|
|
1221
|
+
const changedPackages = /* @__PURE__ */ new Map();
|
|
1222
|
+
const promises = packages.map(async (pkg) => {
|
|
1223
|
+
const lastTag = await getMostRecentPackageTag(workspaceRoot, pkg.name);
|
|
1224
|
+
const allCommits = await getCommits({
|
|
1225
|
+
from: lastTag,
|
|
1226
|
+
to: "HEAD",
|
|
1227
|
+
cwd: workspaceRoot,
|
|
1228
|
+
folder: pkg.path
|
|
769
1229
|
});
|
|
1230
|
+
logger.verbose(`Found ${farver.cyan(allCommits.length)} commits for package ${farver.bold(pkg.name)} since tag ${farver.cyan(lastTag ?? "N/A")}`);
|
|
770
1231
|
return {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
getLatestVersion,
|
|
774
|
-
publish
|
|
1232
|
+
pkgName: pkg.name,
|
|
1233
|
+
commits: allCommits
|
|
775
1234
|
};
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1235
|
+
});
|
|
1236
|
+
const results = await Promise.all(promises);
|
|
1237
|
+
for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
|
|
1238
|
+
return changedPackages;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Check if a file path touches any package folder.
|
|
1242
|
+
* @param file - The file path to check
|
|
1243
|
+
* @param packagePaths - Set of normalized package paths
|
|
1244
|
+
* @param workspaceRoot - The workspace root for path normalization
|
|
1245
|
+
* @returns true if the file is inside a package folder
|
|
1246
|
+
*/
|
|
1247
|
+
function fileMatchesPackageFolder(file, packagePaths, workspaceRoot) {
|
|
1248
|
+
const normalizedFile = file.startsWith("./") ? file.slice(2) : file;
|
|
1249
|
+
for (const pkgPath of packagePaths) {
|
|
1250
|
+
const normalizedPkgPath = pkgPath.startsWith(workspaceRoot) ? pkgPath.slice(workspaceRoot.length + 1) : pkgPath;
|
|
1251
|
+
if (normalizedFile.startsWith(`${normalizedPkgPath}/`) || normalizedFile === normalizedPkgPath) return true;
|
|
1252
|
+
}
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Check if a commit is a "global" commit (doesn't touch any package folder).
|
|
1257
|
+
* @param workspaceRoot - The workspace root
|
|
1258
|
+
* @param files - Array of files changed in the commit
|
|
1259
|
+
* @param packagePaths - Set of normalized package paths
|
|
1260
|
+
* @returns true if this is a global commit
|
|
1261
|
+
*/
|
|
1262
|
+
function isGlobalCommit(workspaceRoot, files, packagePaths) {
|
|
1263
|
+
if (!files || files.length === 0) return false;
|
|
1264
|
+
return !files.some((file) => fileMatchesPackageFolder(file, packagePaths, workspaceRoot));
|
|
1265
|
+
}
|
|
1266
|
+
const DEPENDENCY_FILES = [
|
|
1267
|
+
"package.json",
|
|
1268
|
+
"pnpm-lock.yaml",
|
|
1269
|
+
"pnpm-workspace.yaml",
|
|
1270
|
+
"yarn.lock",
|
|
1271
|
+
"package-lock.json"
|
|
1272
|
+
];
|
|
1273
|
+
/**
|
|
1274
|
+
* Find the oldest and newest commits across all packages.
|
|
1275
|
+
* @param packageCommits - Map of package commits
|
|
1276
|
+
* @returns Object with oldest and newest commit SHAs, or null if no commits
|
|
1277
|
+
*/
|
|
1278
|
+
function findCommitRange(packageCommits) {
|
|
1279
|
+
let oldestCommit = null;
|
|
1280
|
+
let newestCommit = null;
|
|
1281
|
+
for (const commits of packageCommits.values()) {
|
|
1282
|
+
if (commits.length === 0) continue;
|
|
1283
|
+
const firstCommit = commits[0].shortHash;
|
|
1284
|
+
const lastCommit = commits[commits.length - 1].shortHash;
|
|
1285
|
+
if (!newestCommit) newestCommit = firstCommit;
|
|
1286
|
+
oldestCommit = lastCommit;
|
|
1287
|
+
}
|
|
1288
|
+
if (!oldestCommit || !newestCommit) return null;
|
|
1289
|
+
return {
|
|
1290
|
+
oldest: oldestCommit,
|
|
1291
|
+
newest: newestCommit
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Get global commits for each package based on their individual commit timelines.
|
|
1296
|
+
* This solves the problem where packages with different release histories need different global commits.
|
|
1297
|
+
*
|
|
1298
|
+
* A "global commit" is a commit that doesn't touch any package folder but may affect all packages
|
|
1299
|
+
* (e.g., root package.json, CI config, README).
|
|
1300
|
+
*
|
|
1301
|
+
* Performance: Makes ONE batched git call to get files for all commits across all packages.
|
|
1302
|
+
*
|
|
1303
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
1304
|
+
* @param packageCommits - Map of package name to their commits (from getWorkspacePackageCommits)
|
|
1305
|
+
* @param allPackages - All workspace packages (used to identify package folders)
|
|
1306
|
+
* @param mode - Filter mode: false (disabled), "all" (all global commits), or "dependencies" (only dependency-related)
|
|
1307
|
+
* @returns Map of package name to their global commits
|
|
1308
|
+
*/
|
|
1309
|
+
async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPackages, mode) {
|
|
1310
|
+
const result = /* @__PURE__ */ new Map();
|
|
1311
|
+
if (!mode) {
|
|
1312
|
+
logger.verbose("Global commits mode disabled");
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
logger.verbose(`Computing global commits per-package (mode: ${farver.cyan(mode)})`);
|
|
1316
|
+
const commitRange = findCommitRange(packageCommits);
|
|
1317
|
+
if (!commitRange) {
|
|
1318
|
+
logger.verbose("No commits found across packages");
|
|
1319
|
+
return result;
|
|
1320
|
+
}
|
|
1321
|
+
logger.verbose("Fetching files for commits range", `${farver.cyan(commitRange.oldest)}..${farver.cyan(commitRange.newest)}`);
|
|
1322
|
+
const commitFilesMap = await getGroupedFilesByCommitSha(workspaceRoot, commitRange.oldest, commitRange.newest);
|
|
1323
|
+
if (!commitFilesMap) {
|
|
1324
|
+
logger.warn("Failed to get commit file list, returning empty global commits");
|
|
1325
|
+
return result;
|
|
1326
|
+
}
|
|
1327
|
+
logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.size)} commits in ONE git call`);
|
|
1328
|
+
const packagePaths = new Set(allPackages.map((p) => p.path));
|
|
1329
|
+
for (const [pkgName, commits] of packageCommits) {
|
|
1330
|
+
const globalCommitsAffectingPackage = [];
|
|
1331
|
+
logger.verbose("Filtering global commits for package", `${farver.bold(pkgName)} from ${farver.cyan(commits.length)} commits`);
|
|
1332
|
+
for (const commit of commits) {
|
|
1333
|
+
const files = commitFilesMap.get(commit.shortHash);
|
|
1334
|
+
if (!files) continue;
|
|
1335
|
+
if (isGlobalCommit(workspaceRoot, files, packagePaths)) globalCommitsAffectingPackage.push(commit);
|
|
865
1336
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
else if (Array.isArray(config.packages)) {
|
|
871
|
-
workspaceOptions = {
|
|
872
|
-
excludePrivate: false,
|
|
873
|
-
include: config.packages
|
|
874
|
-
};
|
|
875
|
-
explicitPackages = config.packages;
|
|
876
|
-
} else {
|
|
877
|
-
workspaceOptions = config.packages;
|
|
878
|
-
if (config.packages.include) explicitPackages = config.packages.include;
|
|
879
|
-
}
|
|
880
|
-
const workspacePackages = yield* findWorkspacePackages(workspaceOptions);
|
|
881
|
-
if (explicitPackages) {
|
|
882
|
-
const foundNames = new Set(workspacePackages.map((p) => p.name));
|
|
883
|
-
const missing = explicitPackages.filter((p) => !foundNames.has(p));
|
|
884
|
-
if (missing.length > 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`));
|
|
885
|
-
}
|
|
886
|
-
return workspacePackages;
|
|
887
|
-
});
|
|
888
|
-
function findWorkspacePackages(options) {
|
|
889
|
-
return workspacePackageListOutput.pipe(Effect.flatMap((rawProjects) => {
|
|
890
|
-
const allPackageNames = new Set(rawProjects.map((p) => p.name));
|
|
891
|
-
return Effect.all(rawProjects.map((rawProject) => readPackageJson(rawProject.path).pipe(Effect.flatMap((packageJson) => {
|
|
892
|
-
if (!shouldIncludePackage(packageJson, options)) return Effect.succeed(null);
|
|
893
|
-
const version = packageJson.version ?? rawProject.version;
|
|
894
|
-
if (!version) return Effect.logWarning(`Skipping package ${rawProject.name} without version`).pipe(Effect.as(null));
|
|
895
|
-
const pkg = {
|
|
896
|
-
name: rawProject.name,
|
|
897
|
-
version,
|
|
898
|
-
path: rawProject.path,
|
|
899
|
-
packageJson,
|
|
900
|
-
workspaceDependencies: Object.keys(rawProject.dependencies || {}).filter((dep) => allPackageNames.has(dep)),
|
|
901
|
-
workspaceDevDependencies: Object.keys(rawProject.devDependencies || {}).filter((dep) => allPackageNames.has(dep))
|
|
902
|
-
};
|
|
903
|
-
return Schema.decodeUnknown(WorkspacePackageSchema)(pkg).pipe(Effect.mapError((e) => new WorkspaceError({
|
|
904
|
-
message: `Invalid workspace package structure for ${rawProject.name}`,
|
|
905
|
-
cause: e,
|
|
906
|
-
operation: "findWorkspacePackages"
|
|
907
|
-
})));
|
|
908
|
-
}), Effect.catchAll(() => {
|
|
909
|
-
return Effect.logWarning(`Skipping invalid package ${rawProject.name}`).pipe(Effect.as(null));
|
|
910
|
-
})))).pipe(Effect.map((packages) => packages.filter((pkg) => pkg !== null)));
|
|
911
|
-
}));
|
|
1337
|
+
logger.verbose("Package global commits found", `${farver.bold(pkgName)}: ${farver.cyan(globalCommitsAffectingPackage.length)} global commits`);
|
|
1338
|
+
if (mode === "all") {
|
|
1339
|
+
result.set(pkgName, globalCommitsAffectingPackage);
|
|
1340
|
+
continue;
|
|
912
1341
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (
|
|
917
|
-
|
|
1342
|
+
const dependencyCommits = [];
|
|
1343
|
+
for (const commit of globalCommitsAffectingPackage) {
|
|
1344
|
+
const files = commitFilesMap.get(commit.shortHash);
|
|
1345
|
+
if (!files) continue;
|
|
1346
|
+
if (files.some((file) => DEPENDENCY_FILES.includes(file.startsWith("./") ? file.slice(2) : file))) {
|
|
1347
|
+
logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`);
|
|
1348
|
+
dependencyCommits.push(commit);
|
|
918
1349
|
}
|
|
919
|
-
if (options.exclude?.includes(pkg.name)) return false;
|
|
920
|
-
return true;
|
|
921
|
-
}
|
|
922
|
-
function findPackageByName(packageName) {
|
|
923
|
-
return discoverWorkspacePackages.pipe(Effect.map((packages) => packages.find((pkg) => pkg.name === packageName) || null));
|
|
924
1350
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
findPackageByName
|
|
931
|
-
};
|
|
932
|
-
}),
|
|
933
|
-
dependencies: []
|
|
934
|
-
}) {};
|
|
1351
|
+
logger.verbose("Global commits affect dependencies", `${farver.bold(pkgName)}: ${farver.cyan(dependencyCommits.length)} global commits affect dependencies`);
|
|
1352
|
+
result.set(pkgName, dependencyCommits);
|
|
1353
|
+
}
|
|
1354
|
+
return result;
|
|
1355
|
+
}
|
|
935
1356
|
|
|
936
1357
|
//#endregion
|
|
937
|
-
//#region src/
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (
|
|
946
|
-
throw new Error(`Cannot update range "${oldRange}" to version ${newVersion}: new version is outside the existing range. Complex range updating is not yet implemented.`);
|
|
947
|
-
}
|
|
948
|
-
return `${workspacePrefix}${raw.startsWith("^") || raw.startsWith("~") ? raw[0] : ""}${newVersion}`;
|
|
949
|
-
}
|
|
950
|
-
function updateDependencyRecord(record, releaseMap) {
|
|
951
|
-
if (!record) return {
|
|
952
|
-
updated: false,
|
|
953
|
-
next: void 0
|
|
954
|
-
};
|
|
955
|
-
let changed = false;
|
|
956
|
-
const next = { ...record };
|
|
957
|
-
for (const [dep, currentRange] of Object.entries(record)) {
|
|
958
|
-
const bumped = releaseMap.get(dep);
|
|
959
|
-
if (!bumped) continue;
|
|
960
|
-
const updatedRange = nextRange(currentRange, bumped);
|
|
961
|
-
if (updatedRange !== currentRange) {
|
|
962
|
-
next[dep] = updatedRange;
|
|
963
|
-
changed = true;
|
|
964
|
-
}
|
|
1358
|
+
//#region src/operations/version.ts
|
|
1359
|
+
function determineHighestBump(commits) {
|
|
1360
|
+
if (commits.length === 0) return "none";
|
|
1361
|
+
let highestBump = "none";
|
|
1362
|
+
for (const commit of commits) {
|
|
1363
|
+
const bump = determineBumpType(commit);
|
|
1364
|
+
if (bump === "major") return "major";
|
|
1365
|
+
if (bump === "minor") highestBump = "minor";
|
|
1366
|
+
else if (bump === "patch" && highestBump === "none") highestBump = "patch";
|
|
965
1367
|
}
|
|
1368
|
+
return highestBump;
|
|
1369
|
+
}
|
|
1370
|
+
function createVersionUpdate(pkg, bump, hasDirectChanges) {
|
|
1371
|
+
const newVersion = getNextVersion(pkg.version, bump);
|
|
966
1372
|
return {
|
|
967
|
-
|
|
968
|
-
|
|
1373
|
+
package: pkg,
|
|
1374
|
+
currentVersion: pkg.version,
|
|
1375
|
+
newVersion,
|
|
1376
|
+
bumpType: bump,
|
|
1377
|
+
hasDirectChanges
|
|
969
1378
|
};
|
|
970
1379
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const workspace = yield* WorkspaceService;
|
|
974
|
-
function applyReleases(allPackages, releases) {
|
|
975
|
-
const releaseMap = /* @__PURE__ */ new Map();
|
|
976
|
-
for (const release of releases) releaseMap.set(release.package.name, release.newVersion);
|
|
977
|
-
return Effect.all(allPackages.map((pkg) => Effect.gen(function* () {
|
|
978
|
-
const releaseVersion = releaseMap.get(pkg.name);
|
|
979
|
-
const nextJson = { ...pkg.packageJson };
|
|
980
|
-
let updated = false;
|
|
981
|
-
if (releaseVersion && pkg.packageJson.version !== releaseVersion) {
|
|
982
|
-
nextJson.version = releaseVersion;
|
|
983
|
-
updated = true;
|
|
984
|
-
}
|
|
985
|
-
const depsResult = updateDependencyRecord(pkg.packageJson.dependencies, releaseMap);
|
|
986
|
-
if (depsResult.updated) {
|
|
987
|
-
nextJson.dependencies = depsResult.next;
|
|
988
|
-
updated = true;
|
|
989
|
-
}
|
|
990
|
-
const devDepsResult = updateDependencyRecord(pkg.packageJson.devDependencies, releaseMap);
|
|
991
|
-
if (devDepsResult.updated) {
|
|
992
|
-
nextJson.devDependencies = devDepsResult.next;
|
|
993
|
-
updated = true;
|
|
994
|
-
}
|
|
995
|
-
const peerDepsResult = updateDependencyRecord(pkg.packageJson.peerDependencies, releaseMap);
|
|
996
|
-
if (peerDepsResult.updated) {
|
|
997
|
-
nextJson.peerDependencies = peerDepsResult.next;
|
|
998
|
-
updated = true;
|
|
999
|
-
}
|
|
1000
|
-
if (!updated) return "skipped";
|
|
1001
|
-
return yield* workspace.writePackageJson(pkg.path, nextJson).pipe(Effect.map(() => "written"));
|
|
1002
|
-
})));
|
|
1003
|
-
}
|
|
1004
|
-
return { applyReleases };
|
|
1005
|
-
}),
|
|
1006
|
-
dependencies: [WorkspaceService.Default]
|
|
1007
|
-
}) {};
|
|
1008
|
-
|
|
1009
|
-
//#endregion
|
|
1010
|
-
//#region src/services/version-calculator.service.ts
|
|
1011
|
-
const BUMP_PRIORITY = {
|
|
1012
|
-
none: 0,
|
|
1013
|
-
patch: 1,
|
|
1014
|
-
minor: 2,
|
|
1015
|
-
major: 3
|
|
1016
|
-
};
|
|
1017
|
-
function maxBump(current, incoming) {
|
|
1018
|
-
return (BUMP_PRIORITY[incoming] ?? 0) > (BUMP_PRIORITY[current] ?? 0) ? incoming : current;
|
|
1019
|
-
}
|
|
1020
|
-
function bumpFromCommit(commit) {
|
|
1380
|
+
function determineBumpType(commit) {
|
|
1381
|
+
if (!commit.isConventional) return "none";
|
|
1021
1382
|
if (commit.isBreaking) return "major";
|
|
1022
1383
|
if (commit.type === "feat") return "minor";
|
|
1023
1384
|
if (commit.type === "fix" || commit.type === "perf") return "patch";
|
|
1024
1385
|
return "none";
|
|
1025
1386
|
}
|
|
1026
|
-
function determineBump(commits) {
|
|
1027
|
-
return commits.reduce((acc, commit) => maxBump(acc, bumpFromCommit(commit)), "none");
|
|
1028
|
-
}
|
|
1029
|
-
var VersionCalculatorService = class extends Effect.Service()("@ucdjs/release-scripts/VersionCalculatorService", {
|
|
1030
|
-
effect: Effect.gen(function* () {
|
|
1031
|
-
function calculateBumps(packages, overrides) {
|
|
1032
|
-
return Effect.all(packages.map((pkg) => Effect.gen(function* () {
|
|
1033
|
-
const bumpType = determineBump([...pkg.commits, ...pkg.globalCommits]);
|
|
1034
|
-
const hasDirectChanges = pkg.commits.length > 0;
|
|
1035
|
-
let nextVersion = null;
|
|
1036
|
-
const override = overrides[pkg.name];
|
|
1037
|
-
if (override) {
|
|
1038
|
-
if (!semver.valid(override)) return yield* Effect.fail(new VersionCalculationError({
|
|
1039
|
-
message: `Invalid override version for ${pkg.name}: ${override}`,
|
|
1040
|
-
packageName: pkg.name
|
|
1041
|
-
}));
|
|
1042
|
-
nextVersion = override;
|
|
1043
|
-
}
|
|
1044
|
-
if (nextVersion === null) if (bumpType === "none") nextVersion = pkg.version;
|
|
1045
|
-
else {
|
|
1046
|
-
const bumped = semver.inc(pkg.version, bumpType);
|
|
1047
|
-
if (!bumped) return yield* Effect.fail(new VersionCalculationError({
|
|
1048
|
-
message: `Failed to bump version for ${pkg.name} using bump type ${bumpType}`,
|
|
1049
|
-
packageName: pkg.name
|
|
1050
|
-
}));
|
|
1051
|
-
nextVersion = bumped;
|
|
1052
|
-
}
|
|
1053
|
-
return {
|
|
1054
|
-
package: {
|
|
1055
|
-
name: pkg.name,
|
|
1056
|
-
version: pkg.version,
|
|
1057
|
-
path: pkg.path,
|
|
1058
|
-
packageJson: pkg.packageJson,
|
|
1059
|
-
workspaceDependencies: pkg.workspaceDependencies,
|
|
1060
|
-
workspaceDevDependencies: pkg.workspaceDevDependencies
|
|
1061
|
-
},
|
|
1062
|
-
currentVersion: pkg.version,
|
|
1063
|
-
newVersion: nextVersion,
|
|
1064
|
-
bumpType,
|
|
1065
|
-
hasDirectChanges
|
|
1066
|
-
};
|
|
1067
|
-
})), { concurrency: 10 });
|
|
1068
|
-
}
|
|
1069
|
-
return { calculateBumps };
|
|
1070
|
-
}),
|
|
1071
|
-
dependencies: []
|
|
1072
|
-
}) {};
|
|
1073
1387
|
|
|
1074
1388
|
//#endregion
|
|
1075
|
-
//#region src/
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const scope = commit.scope ? `(${commit.scope})` : "";
|
|
1093
|
-
const description = commit.isConventional ? commit.description : commit.message.split("\n")[0] ?? commit.message;
|
|
1094
|
-
const line = `${commit.shortHash} ${commit.type.padEnd(12)}${scope.padEnd(10)}: ${description}`;
|
|
1095
|
-
return isGreyed ? `${GREY}${line}${RESET}` : line;
|
|
1096
|
-
}
|
|
1097
|
-
function formatCommits(commits) {
|
|
1098
|
-
if (commits.length === 0) return " No commits since the last version";
|
|
1099
|
-
const lines = commits.slice(0, 10).map((c) => ` ${formatCommit(c)}`);
|
|
1100
|
-
if (commits.length > 10) lines.push(` ${GREY}... and ${commits.length - 10} more${RESET}`);
|
|
1101
|
-
return lines.join("\n");
|
|
1102
|
-
}
|
|
1103
|
-
function getPrereleaseInfo(version) {
|
|
1104
|
-
const parsed = semver.parse(version);
|
|
1105
|
-
if (!parsed) return null;
|
|
1106
|
-
if (parsed.prerelease.length === 0) return null;
|
|
1107
|
-
return {
|
|
1108
|
-
identifier: String(parsed.prerelease[0]),
|
|
1109
|
-
baseVersion: `${parsed.major}.${parsed.minor}.${parsed.patch}`
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
function generateVersionOptions(currentVersion, conventionalBump, prereleaseInfo) {
|
|
1113
|
-
const options = [];
|
|
1114
|
-
const majorVersion = semver.inc(currentVersion, "major");
|
|
1115
|
-
const minorVersion = semver.inc(currentVersion, "minor");
|
|
1116
|
-
const patchVersion = semver.inc(currentVersion, "patch");
|
|
1117
|
-
if (majorVersion) options.push({
|
|
1118
|
-
title: `major ${majorVersion}`,
|
|
1119
|
-
value: {
|
|
1120
|
-
version: majorVersion,
|
|
1121
|
-
bumpType: "major"
|
|
1122
|
-
}
|
|
1123
|
-
});
|
|
1124
|
-
if (minorVersion) options.push({
|
|
1125
|
-
title: `minor ${minorVersion}`,
|
|
1126
|
-
value: {
|
|
1127
|
-
version: minorVersion,
|
|
1128
|
-
bumpType: "minor"
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
if (patchVersion) options.push({
|
|
1132
|
-
title: `patch ${patchVersion}`,
|
|
1133
|
-
value: {
|
|
1134
|
-
version: patchVersion,
|
|
1135
|
-
bumpType: "patch"
|
|
1136
|
-
}
|
|
1137
|
-
});
|
|
1138
|
-
if (prereleaseInfo) {
|
|
1139
|
-
const nextPrerelease = semver.inc(currentVersion, "prerelease", prereleaseInfo.identifier);
|
|
1140
|
-
if (nextPrerelease) options.push({
|
|
1141
|
-
title: `next ${nextPrerelease}`,
|
|
1142
|
-
value: {
|
|
1143
|
-
version: nextPrerelease,
|
|
1144
|
-
bumpType: "patch"
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1389
|
+
//#region src/versioning/package.ts
|
|
1390
|
+
/**
|
|
1391
|
+
* Build a dependency graph from workspace packages
|
|
1392
|
+
*
|
|
1393
|
+
* Creates a bidirectional graph that maps:
|
|
1394
|
+
* - packages: Map of package name → WorkspacePackage
|
|
1395
|
+
* - dependents: Map of package name → Set of packages that depend on it
|
|
1396
|
+
*
|
|
1397
|
+
* @param packages - All workspace packages
|
|
1398
|
+
* @returns Dependency graph with packages and dependents maps
|
|
1399
|
+
*/
|
|
1400
|
+
function buildPackageDependencyGraph(packages) {
|
|
1401
|
+
const packagesMap = /* @__PURE__ */ new Map();
|
|
1402
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
1403
|
+
for (const pkg of packages) {
|
|
1404
|
+
packagesMap.set(pkg.name, pkg);
|
|
1405
|
+
dependents.set(pkg.name, /* @__PURE__ */ new Set());
|
|
1147
1406
|
}
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
bumpType: conventionalBump
|
|
1407
|
+
for (const pkg of packages) {
|
|
1408
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
1409
|
+
for (const dep of allDeps) {
|
|
1410
|
+
const depSet = dependents.get(dep);
|
|
1411
|
+
if (depSet) depSet.add(pkg.name);
|
|
1154
1412
|
}
|
|
1155
|
-
});
|
|
1156
|
-
if (prereleaseInfo) {
|
|
1157
|
-
const prePatch = semver.inc(currentVersion, "prepatch", prereleaseInfo.identifier);
|
|
1158
|
-
const preMinor = semver.inc(currentVersion, "preminor", prereleaseInfo.identifier);
|
|
1159
|
-
const preMajor = semver.inc(currentVersion, "premajor", prereleaseInfo.identifier);
|
|
1160
|
-
if (prePatch) options.push({
|
|
1161
|
-
title: `pre-patch ${prePatch}`,
|
|
1162
|
-
value: {
|
|
1163
|
-
version: prePatch,
|
|
1164
|
-
bumpType: "patch"
|
|
1165
|
-
}
|
|
1166
|
-
});
|
|
1167
|
-
if (preMinor) options.push({
|
|
1168
|
-
title: `pre-minor ${preMinor}`,
|
|
1169
|
-
value: {
|
|
1170
|
-
version: preMinor,
|
|
1171
|
-
bumpType: "minor"
|
|
1172
|
-
}
|
|
1173
|
-
});
|
|
1174
|
-
if (preMajor) options.push({
|
|
1175
|
-
title: `pre-major ${preMajor}`,
|
|
1176
|
-
value: {
|
|
1177
|
-
version: preMajor,
|
|
1178
|
-
bumpType: "major"
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
} else {
|
|
1182
|
-
const betaPatch = semver.inc(currentVersion, "prepatch", "beta");
|
|
1183
|
-
const betaMinor = semver.inc(currentVersion, "preminor", "beta");
|
|
1184
|
-
const betaMajor = semver.inc(currentVersion, "premajor", "beta");
|
|
1185
|
-
if (betaPatch) options.push({
|
|
1186
|
-
title: `pre-patch ${betaPatch}`,
|
|
1187
|
-
value: {
|
|
1188
|
-
version: betaPatch,
|
|
1189
|
-
bumpType: "patch"
|
|
1190
|
-
}
|
|
1191
|
-
});
|
|
1192
|
-
if (betaMinor) options.push({
|
|
1193
|
-
title: `pre-minor ${betaMinor}`,
|
|
1194
|
-
value: {
|
|
1195
|
-
version: betaMinor,
|
|
1196
|
-
bumpType: "minor"
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
if (betaMajor) options.push({
|
|
1200
|
-
title: `pre-major ${betaMajor}`,
|
|
1201
|
-
value: {
|
|
1202
|
-
version: betaMajor,
|
|
1203
|
-
bumpType: "major"
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1206
1413
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
bumpType: "none"
|
|
1212
|
-
}
|
|
1213
|
-
});
|
|
1214
|
-
options.push({
|
|
1215
|
-
title: "custom ...",
|
|
1216
|
-
value: {
|
|
1217
|
-
version: "custom",
|
|
1218
|
-
bumpType: "none"
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
return options;
|
|
1222
|
-
}
|
|
1223
|
-
function findDefaultIndex(options, conventionalBump) {
|
|
1224
|
-
if (conventionalBump === "none") return 0;
|
|
1225
|
-
const conventionalIndex = options.findIndex((o) => o.title.startsWith("conventional"));
|
|
1226
|
-
return conventionalIndex >= 0 ? conventionalIndex : 0;
|
|
1227
|
-
}
|
|
1228
|
-
async function promptForCustomVersion(currentVersion) {
|
|
1229
|
-
return (await prompts({
|
|
1230
|
-
type: "text",
|
|
1231
|
-
name: "version",
|
|
1232
|
-
message: `Enter custom version (current: ${currentVersion})`,
|
|
1233
|
-
validate: (input) => {
|
|
1234
|
-
if (!semver.valid(input)) return "Please enter a valid semver version (e.g., 1.2.3)";
|
|
1235
|
-
return true;
|
|
1236
|
-
}
|
|
1237
|
-
})).version || null;
|
|
1238
|
-
}
|
|
1239
|
-
var VersionPromptService = class extends Effect.Service()("@ucdjs/release-scripts/VersionPromptService", {
|
|
1240
|
-
effect: Effect.gen(function* () {
|
|
1241
|
-
const config = yield* ReleaseScriptsOptions;
|
|
1242
|
-
let applyToAllRemainingChoice = null;
|
|
1243
|
-
let isCancelled = false;
|
|
1244
|
-
function promptForVersion(pkg, conventionalBump, remainingCount) {
|
|
1245
|
-
return Effect.async((resume) => {
|
|
1246
|
-
if (isCancelled) {
|
|
1247
|
-
resume(Effect.succeed({
|
|
1248
|
-
newVersion: pkg.version,
|
|
1249
|
-
bumpType: "none",
|
|
1250
|
-
applyToAllRemaining: false,
|
|
1251
|
-
cancelled: true
|
|
1252
|
-
}));
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
const allCommits = [...pkg.commits, ...pkg.globalCommits];
|
|
1256
|
-
const prereleaseInfo = getPrereleaseInfo(pkg.version);
|
|
1257
|
-
const commitCount = allCommits.length;
|
|
1258
|
-
console.log("");
|
|
1259
|
-
console.log(`${commitCount} commit${commitCount === 1 ? "" : "s"} since the last version:`);
|
|
1260
|
-
console.log(formatCommits(allCommits));
|
|
1261
|
-
console.log("");
|
|
1262
|
-
if (applyToAllRemainingChoice) {
|
|
1263
|
-
const result = {
|
|
1264
|
-
newVersion: applyToAllRemainingChoice.version === "custom" ? pkg.version : applyToAllRemainingChoice.version,
|
|
1265
|
-
bumpType: applyToAllRemainingChoice.bumpType,
|
|
1266
|
-
applyToAllRemaining: false,
|
|
1267
|
-
cancelled: false
|
|
1268
|
-
};
|
|
1269
|
-
resume(Effect.succeed(result));
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
const options = generateVersionOptions(pkg.version, conventionalBump, prereleaseInfo);
|
|
1273
|
-
const defaultIndex = findDefaultIndex(options, conventionalBump);
|
|
1274
|
-
if (remainingCount > 1) options.push({
|
|
1275
|
-
title: "apply-to-all ›",
|
|
1276
|
-
value: {
|
|
1277
|
-
version: "apply-to-all",
|
|
1278
|
-
bumpType: "none"
|
|
1279
|
-
}
|
|
1280
|
-
});
|
|
1281
|
-
prompts({
|
|
1282
|
-
type: "select",
|
|
1283
|
-
name: "choice",
|
|
1284
|
-
message: `Current version ${pkg.version}`,
|
|
1285
|
-
choices: options.map((o) => ({
|
|
1286
|
-
title: o.title,
|
|
1287
|
-
value: o.value
|
|
1288
|
-
})),
|
|
1289
|
-
initial: defaultIndex,
|
|
1290
|
-
hint: "Use arrow keys to navigate, enter to select"
|
|
1291
|
-
}).then(async (response) => {
|
|
1292
|
-
if (!response.choice) {
|
|
1293
|
-
isCancelled = true;
|
|
1294
|
-
const result = {
|
|
1295
|
-
newVersion: pkg.version,
|
|
1296
|
-
bumpType: "none",
|
|
1297
|
-
applyToAllRemaining: false,
|
|
1298
|
-
cancelled: true
|
|
1299
|
-
};
|
|
1300
|
-
resume(Effect.succeed(result));
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
if (response.choice.version === "apply-to-all") {
|
|
1304
|
-
const applyOptions = options.filter((o) => o.value.version !== "custom" && o.value.version !== "apply-to-all");
|
|
1305
|
-
const applyResponse = await prompts({
|
|
1306
|
-
type: "select",
|
|
1307
|
-
name: "choice",
|
|
1308
|
-
message: `Apply to all ${remainingCount} remaining packages`,
|
|
1309
|
-
choices: applyOptions.map((o) => ({
|
|
1310
|
-
title: o.title,
|
|
1311
|
-
value: o.value
|
|
1312
|
-
})),
|
|
1313
|
-
initial: findDefaultIndex(applyOptions, conventionalBump)
|
|
1314
|
-
});
|
|
1315
|
-
if (!applyResponse.choice) {
|
|
1316
|
-
isCancelled = true;
|
|
1317
|
-
resume(Effect.succeed({
|
|
1318
|
-
newVersion: pkg.version,
|
|
1319
|
-
bumpType: "none",
|
|
1320
|
-
applyToAllRemaining: false,
|
|
1321
|
-
cancelled: true
|
|
1322
|
-
}));
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
if (applyResponse.choice.version === "custom") {
|
|
1326
|
-
const customVersion = await promptForCustomVersion(pkg.version);
|
|
1327
|
-
if (!customVersion) {
|
|
1328
|
-
isCancelled = true;
|
|
1329
|
-
resume(Effect.succeed({
|
|
1330
|
-
newVersion: pkg.version,
|
|
1331
|
-
bumpType: "none",
|
|
1332
|
-
applyToAllRemaining: false,
|
|
1333
|
-
cancelled: true
|
|
1334
|
-
}));
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
applyToAllRemainingChoice = {
|
|
1338
|
-
version: customVersion,
|
|
1339
|
-
bumpType: applyResponse.choice.bumpType
|
|
1340
|
-
};
|
|
1341
|
-
} else applyToAllRemainingChoice = applyResponse.choice;
|
|
1342
|
-
const result = {
|
|
1343
|
-
newVersion: applyToAllRemainingChoice?.version || pkg.version,
|
|
1344
|
-
bumpType: applyToAllRemainingChoice?.bumpType || "none",
|
|
1345
|
-
applyToAllRemaining: true,
|
|
1346
|
-
cancelled: false
|
|
1347
|
-
};
|
|
1348
|
-
resume(Effect.succeed(result));
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
let selectedVersion = response.choice.version;
|
|
1352
|
-
const selectedBumpType = response.choice.bumpType;
|
|
1353
|
-
if (selectedVersion === "custom") {
|
|
1354
|
-
const customVersion = await promptForCustomVersion(pkg.version);
|
|
1355
|
-
if (!customVersion) {
|
|
1356
|
-
isCancelled = true;
|
|
1357
|
-
resume(Effect.succeed({
|
|
1358
|
-
newVersion: pkg.version,
|
|
1359
|
-
bumpType: "none",
|
|
1360
|
-
applyToAllRemaining: false,
|
|
1361
|
-
cancelled: true
|
|
1362
|
-
}));
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
selectedVersion = customVersion;
|
|
1366
|
-
}
|
|
1367
|
-
const result = {
|
|
1368
|
-
newVersion: selectedVersion,
|
|
1369
|
-
bumpType: selectedBumpType,
|
|
1370
|
-
applyToAllRemaining: false,
|
|
1371
|
-
cancelled: false
|
|
1372
|
-
};
|
|
1373
|
-
resume(Effect.succeed(result));
|
|
1374
|
-
});
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
return {
|
|
1378
|
-
promptForVersion,
|
|
1379
|
-
isEnabled: config.prompts.versions,
|
|
1380
|
-
resetApplyToAll: () => {
|
|
1381
|
-
applyToAllRemainingChoice = null;
|
|
1382
|
-
isCancelled = false;
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
1385
|
-
}),
|
|
1386
|
-
dependencies: []
|
|
1387
|
-
}) {};
|
|
1388
|
-
|
|
1389
|
-
//#endregion
|
|
1390
|
-
//#region src/utils/helpers.ts
|
|
1391
|
-
function loadOverrides(options) {
|
|
1392
|
-
return Effect.gen(function* () {
|
|
1393
|
-
return yield* (yield* GitService).workspace.readFile(options.overridesPath, options.sha).pipe(Effect.flatMap((content) => Effect.try({
|
|
1394
|
-
try: () => JSON.parse(content),
|
|
1395
|
-
catch: (err) => new OverridesLoadError({
|
|
1396
|
-
message: "Failed to parse overrides file.",
|
|
1397
|
-
cause: err
|
|
1398
|
-
})
|
|
1399
|
-
})), Effect.catchAll(() => Effect.succeed({})));
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
const GitCommitSchema = Schema.Struct({
|
|
1403
|
-
isConventional: Schema.Boolean,
|
|
1404
|
-
isBreaking: Schema.Boolean,
|
|
1405
|
-
type: Schema.String,
|
|
1406
|
-
scope: Schema.Union(Schema.String, Schema.Undefined),
|
|
1407
|
-
description: Schema.String,
|
|
1408
|
-
references: Schema.Array(Schema.Struct({
|
|
1409
|
-
type: Schema.Union(Schema.Literal("issue"), Schema.Literal("pull-request")),
|
|
1410
|
-
value: Schema.String
|
|
1411
|
-
})),
|
|
1412
|
-
authors: Schema.Array(Schema.Struct({
|
|
1413
|
-
name: Schema.String,
|
|
1414
|
-
email: Schema.String,
|
|
1415
|
-
profile: Schema.optional(Schema.String)
|
|
1416
|
-
})),
|
|
1417
|
-
hash: Schema.String,
|
|
1418
|
-
shortHash: Schema.String,
|
|
1419
|
-
body: Schema.String,
|
|
1420
|
-
message: Schema.String,
|
|
1421
|
-
date: Schema.String
|
|
1422
|
-
});
|
|
1423
|
-
const WorkspacePackageWithCommitsSchema = Schema.Struct({
|
|
1424
|
-
...WorkspacePackageSchema.fields,
|
|
1425
|
-
commits: Schema.Array(GitCommitSchema),
|
|
1426
|
-
globalCommits: Schema.Array(GitCommitSchema).pipe(Schema.propertySignature, Schema.withConstructorDefault(() => []))
|
|
1427
|
-
});
|
|
1428
|
-
function mergePackageCommitsIntoPackages(packages) {
|
|
1429
|
-
return Effect.gen(function* () {
|
|
1430
|
-
const git = yield* GitService;
|
|
1431
|
-
return yield* Effect.forEach(packages, (pkg) => Effect.gen(function* () {
|
|
1432
|
-
const lastTag = yield* git.tags.mostRecentForPackage(pkg.name);
|
|
1433
|
-
const commits = yield* git.commits.get({
|
|
1434
|
-
from: lastTag?.name || void 0,
|
|
1435
|
-
to: "HEAD",
|
|
1436
|
-
folder: pkg.path
|
|
1437
|
-
});
|
|
1438
|
-
const withCommits = {
|
|
1439
|
-
...pkg,
|
|
1440
|
-
commits,
|
|
1441
|
-
globalCommits: []
|
|
1442
|
-
};
|
|
1443
|
-
return yield* Schema.decode(WorkspacePackageWithCommitsSchema)(withCommits).pipe(Effect.mapError((e) => /* @__PURE__ */ new Error(`Failed to decode package with commits for ${pkg.name}: ${e}`)));
|
|
1444
|
-
}));
|
|
1445
|
-
});
|
|
1414
|
+
return {
|
|
1415
|
+
packages: packagesMap,
|
|
1416
|
+
dependents
|
|
1417
|
+
};
|
|
1446
1418
|
}
|
|
1447
1419
|
/**
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
* This function handles an important edge case in monorepo releases:
|
|
1451
|
-
* When pkg-a is released, then a global change is made, and then pkg-b is released,
|
|
1452
|
-
* we need to ensure that the global change is only attributed to pkg-a's release,
|
|
1453
|
-
* not re-counted for pkg-b.
|
|
1420
|
+
* Get all packages affected by changes (including transitive dependents)
|
|
1454
1421
|
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
* 3. For each package, filter commits based on its last tag cutoff
|
|
1459
|
-
* 4. Apply mode-specific filtering for global commits
|
|
1422
|
+
* Uses graph traversal to find all packages that need updates:
|
|
1423
|
+
* - Packages with direct changes
|
|
1424
|
+
* - All packages that depend on changed packages (transitively)
|
|
1460
1425
|
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1426
|
+
* @param graph - Dependency graph
|
|
1427
|
+
* @param changedPackages - Set of package names with direct changes
|
|
1428
|
+
* @returns Set of all package names that need updates
|
|
1429
|
+
*/
|
|
1430
|
+
function getAllAffectedPackages(graph, changedPackages) {
|
|
1431
|
+
const affected = /* @__PURE__ */ new Set();
|
|
1432
|
+
function visitDependents(pkgName) {
|
|
1433
|
+
if (affected.has(pkgName)) return;
|
|
1434
|
+
affected.add(pkgName);
|
|
1435
|
+
const dependents = graph.dependents.get(pkgName);
|
|
1436
|
+
if (dependents) for (const dependent of dependents) visitDependents(dependent);
|
|
1437
|
+
}
|
|
1438
|
+
for (const pkg of changedPackages) visitDependents(pkg);
|
|
1439
|
+
return affected;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Create version updates for all packages affected by dependency changes
|
|
1469
1443
|
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
* - "none": No global commits (returns empty map)
|
|
1473
|
-
* - "all": All commits that touch files outside any package directory
|
|
1474
|
-
* - "dependencies": Only commits that touch dependency-related files (package.json, lock files, etc.)
|
|
1444
|
+
* When a package is updated, all packages that depend on it should also be updated.
|
|
1445
|
+
* This function calculates which additional packages need patch bumps due to dependency changes.
|
|
1475
1446
|
*
|
|
1476
|
-
* @
|
|
1447
|
+
* @param graph - Dependency graph
|
|
1448
|
+
* @param workspacePackages - All workspace packages
|
|
1449
|
+
* @param directUpdates - Packages with direct code changes
|
|
1450
|
+
* @returns All updates including dependent packages that need patch bumps
|
|
1477
1451
|
*/
|
|
1478
|
-
function
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1452
|
+
function createDependentUpdates(graph, workspacePackages, directUpdates) {
|
|
1453
|
+
const allUpdates = [...directUpdates];
|
|
1454
|
+
const directUpdateMap = new Map(directUpdates.map((u) => [u.package.name, u]));
|
|
1455
|
+
const affectedPackages = getAllAffectedPackages(graph, new Set(directUpdates.map((u) => u.package.name)));
|
|
1456
|
+
for (const pkgName of affectedPackages) {
|
|
1457
|
+
logger.verbose(`Processing affected package: ${pkgName}`);
|
|
1458
|
+
if (directUpdateMap.has(pkgName)) {
|
|
1459
|
+
logger.verbose(`Skipping ${pkgName}, already has a direct update`);
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
const pkg = workspacePackages.find((p) => p.name === pkgName);
|
|
1463
|
+
if (!pkg) continue;
|
|
1464
|
+
allUpdates.push(createVersionUpdate(pkg, "patch", false));
|
|
1465
|
+
}
|
|
1466
|
+
return allUpdates;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/versioning/version.ts
|
|
1471
|
+
const messageColorMap = {
|
|
1472
|
+
feat: farver.green,
|
|
1473
|
+
feature: farver.green,
|
|
1474
|
+
refactor: farver.cyan,
|
|
1475
|
+
style: farver.cyan,
|
|
1476
|
+
docs: farver.blue,
|
|
1477
|
+
doc: farver.blue,
|
|
1478
|
+
types: farver.blue,
|
|
1479
|
+
type: farver.blue,
|
|
1480
|
+
chore: farver.gray,
|
|
1481
|
+
ci: farver.gray,
|
|
1482
|
+
build: farver.gray,
|
|
1483
|
+
deps: farver.gray,
|
|
1484
|
+
dev: farver.gray,
|
|
1485
|
+
fix: farver.yellow,
|
|
1486
|
+
test: farver.yellow,
|
|
1487
|
+
perf: farver.magenta,
|
|
1488
|
+
revert: farver.red,
|
|
1489
|
+
breaking: farver.red
|
|
1490
|
+
};
|
|
1491
|
+
function formatCommitsForDisplay(commits) {
|
|
1492
|
+
if (commits.length === 0) return farver.dim("No commits found");
|
|
1493
|
+
const maxCommitsToShow = 10;
|
|
1494
|
+
const commitsToShow = commits.slice(0, maxCommitsToShow);
|
|
1495
|
+
const hasMore = commits.length > maxCommitsToShow;
|
|
1496
|
+
const typeLength = commits.map(({ type }) => type.length).reduce((a, b) => Math.max(a, b), 0);
|
|
1497
|
+
const scopeLength = commits.map(({ scope }) => scope?.length).reduce((a, b) => Math.max(a || 0, b || 0), 0) || 0;
|
|
1498
|
+
const formattedCommits = commitsToShow.map((commit) => {
|
|
1499
|
+
let color = messageColorMap[commit.type] || ((c) => c);
|
|
1500
|
+
if (commit.isBreaking) color = (s) => farver.inverse.red(s);
|
|
1501
|
+
const paddedType = commit.type.padStart(typeLength + 1, " ");
|
|
1502
|
+
const paddedScope = !commit.scope ? " ".repeat(scopeLength ? scopeLength + 2 : 0) : farver.dim("(") + commit.scope + farver.dim(")") + " ".repeat(scopeLength - commit.scope.length);
|
|
1503
|
+
return [
|
|
1504
|
+
farver.dim(commit.shortHash),
|
|
1505
|
+
" ",
|
|
1506
|
+
color === farver.gray ? color(paddedType) : farver.bold(color(paddedType)),
|
|
1507
|
+
" ",
|
|
1508
|
+
paddedScope,
|
|
1509
|
+
farver.dim(":"),
|
|
1510
|
+
" ",
|
|
1511
|
+
color === farver.gray ? color(commit.description) : commit.description
|
|
1512
|
+
].join("");
|
|
1513
|
+
}).join("\n");
|
|
1514
|
+
if (hasMore) return `${formattedCommits}\n ${farver.dim(`... and ${commits.length - maxCommitsToShow} more commits`)}`;
|
|
1515
|
+
return formattedCommits;
|
|
1516
|
+
}
|
|
1517
|
+
async function calculateVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage, overrides: initialOverrides = {} }) {
|
|
1518
|
+
const versionUpdates = [];
|
|
1519
|
+
const processedPackages = /* @__PURE__ */ new Set();
|
|
1520
|
+
const newOverrides = { ...initialOverrides };
|
|
1521
|
+
const bumpRanks = {
|
|
1522
|
+
major: 3,
|
|
1523
|
+
minor: 2,
|
|
1524
|
+
patch: 1,
|
|
1525
|
+
none: 0
|
|
1526
|
+
};
|
|
1527
|
+
logger.verbose(`Starting version inference for ${packageCommits.size} packages with commits`);
|
|
1528
|
+
for (const [pkgName, pkgCommits] of packageCommits) {
|
|
1529
|
+
const pkg = workspacePackages.find((p) => p.name === pkgName);
|
|
1530
|
+
if (!pkg) {
|
|
1531
|
+
logger.error(`Package ${pkgName} not found in workspace packages, skipping`);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
processedPackages.add(pkgName);
|
|
1535
|
+
const globalCommits = globalCommitsPerPackage.get(pkgName) || [];
|
|
1536
|
+
const allCommitsForPackage = [...pkgCommits, ...globalCommits];
|
|
1537
|
+
const determinedBump = determineHighestBump(allCommitsForPackage);
|
|
1538
|
+
const override = newOverrides[pkgName];
|
|
1539
|
+
const effectiveBump = override?.type || determinedBump;
|
|
1540
|
+
if (effectiveBump === "none") continue;
|
|
1541
|
+
let newVersion = override?.version || getNextVersion(pkg.version, effectiveBump);
|
|
1542
|
+
let finalBumpType = effectiveBump;
|
|
1543
|
+
if (!isCI && showPrompt) {
|
|
1544
|
+
logger.clearScreen();
|
|
1545
|
+
logger.section(`📝 Commits for ${farver.cyan(pkg.name)}`);
|
|
1546
|
+
formatCommitsForDisplay(allCommitsForPackage).split("\n").forEach((line) => logger.item(line));
|
|
1547
|
+
logger.emptyLine();
|
|
1548
|
+
const selectedVersion = await selectVersionPrompt(workspaceRoot, pkg, pkg.version, newVersion);
|
|
1549
|
+
if (selectedVersion === null) continue;
|
|
1550
|
+
const userBump = calculateBumpType(pkg.version, selectedVersion);
|
|
1551
|
+
finalBumpType = userBump;
|
|
1552
|
+
if (bumpRanks[userBump] < bumpRanks[determinedBump]) {
|
|
1553
|
+
newOverrides[pkgName] = {
|
|
1554
|
+
type: userBump,
|
|
1555
|
+
version: selectedVersion
|
|
1556
|
+
};
|
|
1557
|
+
logger.info(`Version override recorded for ${pkgName}: ${determinedBump} → ${userBump}`);
|
|
1558
|
+
} else if (newOverrides[pkgName] && bumpRanks[userBump] >= bumpRanks[determinedBump]) {
|
|
1559
|
+
delete newOverrides[pkgName];
|
|
1560
|
+
logger.info(`Version override removed for ${pkgName}.`);
|
|
1505
1561
|
}
|
|
1506
|
-
|
|
1562
|
+
newVersion = selectedVersion;
|
|
1507
1563
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1564
|
+
versionUpdates.push({
|
|
1565
|
+
package: pkg,
|
|
1566
|
+
currentVersion: pkg.version,
|
|
1567
|
+
newVersion,
|
|
1568
|
+
bumpType: finalBumpType,
|
|
1569
|
+
hasDirectChanges: allCommitsForPackage.length > 0
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (!isCI && showPrompt) for (const pkg of workspacePackages) {
|
|
1573
|
+
if (processedPackages.has(pkg.name)) continue;
|
|
1574
|
+
logger.clearScreen();
|
|
1575
|
+
logger.section(`📦 Package: ${pkg.name}`);
|
|
1576
|
+
logger.item("No direct commits found");
|
|
1577
|
+
const newVersion = await selectVersionPrompt(workspaceRoot, pkg, pkg.version, pkg.version);
|
|
1578
|
+
if (newVersion === null) break;
|
|
1579
|
+
if (newVersion !== pkg.version) {
|
|
1580
|
+
const bumpType = calculateBumpType(pkg.version, newVersion);
|
|
1581
|
+
versionUpdates.push({
|
|
1582
|
+
package: pkg,
|
|
1583
|
+
currentVersion: pkg.version,
|
|
1584
|
+
newVersion,
|
|
1585
|
+
bumpType,
|
|
1586
|
+
hasDirectChanges: false
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return {
|
|
1591
|
+
updates: versionUpdates,
|
|
1592
|
+
overrides: newOverrides
|
|
1593
|
+
};
|
|
1513
1594
|
}
|
|
1514
1595
|
/**
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
* @param files - List of files changed in the commit
|
|
1518
|
-
* @param packagePaths - Set of package directory paths
|
|
1519
|
-
* @returns true if at least one file is outside all package directories
|
|
1596
|
+
* Calculate version updates and prepare dependent updates
|
|
1597
|
+
* Returns both the updates and a function to apply them
|
|
1520
1598
|
*/
|
|
1521
|
-
function
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1599
|
+
async function calculateAndPrepareVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage, overrides }) {
|
|
1600
|
+
const { updates: directUpdates, overrides: newOverrides } = await calculateVersionUpdates({
|
|
1601
|
+
workspacePackages,
|
|
1602
|
+
packageCommits,
|
|
1603
|
+
workspaceRoot,
|
|
1604
|
+
showPrompt,
|
|
1605
|
+
globalCommitsPerPackage,
|
|
1606
|
+
overrides
|
|
1526
1607
|
});
|
|
1608
|
+
const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, directUpdates);
|
|
1609
|
+
const applyUpdates = async () => {
|
|
1610
|
+
await Promise.all(allUpdates.map(async (update) => {
|
|
1611
|
+
const depUpdates = getDependencyUpdates(update.package, allUpdates);
|
|
1612
|
+
await updatePackageJson(update.package, update.newVersion, depUpdates);
|
|
1613
|
+
}));
|
|
1614
|
+
};
|
|
1615
|
+
return {
|
|
1616
|
+
allUpdates,
|
|
1617
|
+
applyUpdates,
|
|
1618
|
+
overrides: newOverrides
|
|
1619
|
+
};
|
|
1527
1620
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1621
|
+
async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
|
|
1622
|
+
const packageJsonPath = join(pkg.path, "package.json");
|
|
1623
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
1624
|
+
const packageJson = JSON.parse(content);
|
|
1625
|
+
packageJson.version = newVersion;
|
|
1626
|
+
function updateDependency(deps, depName, depVersion, isPeerDependency = false) {
|
|
1627
|
+
if (!deps) return;
|
|
1628
|
+
const oldVersion = deps[depName];
|
|
1629
|
+
if (!oldVersion) return;
|
|
1630
|
+
if (oldVersion === "workspace:*") {
|
|
1631
|
+
logger.verbose(` - Skipping workspace:* dependency: ${depName}`);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (isPeerDependency) {
|
|
1635
|
+
const majorVersion = depVersion.split(".")[0];
|
|
1636
|
+
deps[depName] = `>=${depVersion} <${Number(majorVersion) + 1}.0.0`;
|
|
1637
|
+
} else deps[depName] = `^${depVersion}`;
|
|
1638
|
+
logger.verbose(` - Updated dependency ${depName}: ${oldVersion} → ${deps[depName]}`);
|
|
1639
|
+
}
|
|
1640
|
+
for (const [depName, depVersion] of dependencyUpdates) {
|
|
1641
|
+
updateDependency(packageJson.dependencies, depName, depVersion);
|
|
1642
|
+
updateDependency(packageJson.devDependencies, depName, depVersion);
|
|
1643
|
+
updateDependency(packageJson.peerDependencies, depName, depVersion, true);
|
|
1644
|
+
}
|
|
1645
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
|
|
1646
|
+
logger.verbose(` - Successfully wrote updated package.json`);
|
|
1548
1647
|
}
|
|
1549
1648
|
/**
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
* This establishes the overall time range we need to analyze for global commits.
|
|
1553
|
-
*
|
|
1554
|
-
* @param packages - Array of packages with their commits
|
|
1555
|
-
* @returns Tuple of [oldestCommitSha, newestCommitSha], or [null, null] if no commits found
|
|
1649
|
+
* Get all dependency updates needed for a package
|
|
1556
1650
|
*/
|
|
1557
|
-
function
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
for (const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
if (newestCommit == null || new Date(lastCommit.date) > new Date(newestCommit.date)) newestCommit = lastCommit;
|
|
1567
|
-
if (oldestCommit == null || new Date(firstCommit.date) < new Date(oldestCommit.date)) oldestCommit = firstCommit;
|
|
1651
|
+
function getDependencyUpdates(pkg, allUpdates) {
|
|
1652
|
+
const updates = /* @__PURE__ */ new Map();
|
|
1653
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
1654
|
+
for (const dep of allDeps) {
|
|
1655
|
+
const update = allUpdates.find((u) => u.package.name === dep);
|
|
1656
|
+
if (update) {
|
|
1657
|
+
logger.verbose(` - Dependency ${dep} will be updated: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`);
|
|
1658
|
+
updates.set(dep, update.newVersion);
|
|
1659
|
+
}
|
|
1568
1660
|
}
|
|
1569
|
-
if (
|
|
1570
|
-
return
|
|
1661
|
+
if (updates.size === 0) logger.verbose(` - No dependency updates needed`);
|
|
1662
|
+
return updates;
|
|
1571
1663
|
}
|
|
1572
1664
|
|
|
1573
1665
|
//#endregion
|
|
1574
|
-
//#region src/
|
|
1575
|
-
function
|
|
1576
|
-
return
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1666
|
+
//#region src/versioning/operations.ts
|
|
1667
|
+
function createVersioningOperations() {
|
|
1668
|
+
return {
|
|
1669
|
+
getWorkspacePackageGroupedCommits: async (workspaceRoot, packages) => {
|
|
1670
|
+
try {
|
|
1671
|
+
return {
|
|
1672
|
+
ok: true,
|
|
1673
|
+
value: await getWorkspacePackageGroupedCommits(workspaceRoot, packages)
|
|
1674
|
+
};
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
return {
|
|
1677
|
+
ok: false,
|
|
1678
|
+
error: {
|
|
1679
|
+
type: "git",
|
|
1680
|
+
operation: "getWorkspacePackageGroupedCommits",
|
|
1681
|
+
message: String(error)
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
getGlobalCommitsPerPackage: async (workspaceRoot, packageCommits, packages, mode) => {
|
|
1687
|
+
try {
|
|
1688
|
+
return {
|
|
1689
|
+
ok: true,
|
|
1690
|
+
value: await getGlobalCommitsPerPackage(workspaceRoot, packageCommits, packages, mode)
|
|
1691
|
+
};
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
return {
|
|
1694
|
+
ok: false,
|
|
1695
|
+
error: {
|
|
1696
|
+
type: "git",
|
|
1697
|
+
operation: "getGlobalCommitsPerPackage",
|
|
1698
|
+
message: String(error)
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
calculateAndPrepareVersionUpdates: async (payload) => {
|
|
1704
|
+
try {
|
|
1705
|
+
return {
|
|
1706
|
+
ok: true,
|
|
1707
|
+
value: await calculateAndPrepareVersionUpdates(payload)
|
|
1708
|
+
};
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
return {
|
|
1711
|
+
ok: false,
|
|
1712
|
+
error: {
|
|
1713
|
+
type: "git",
|
|
1714
|
+
operation: "calculateAndPrepareVersionUpdates",
|
|
1715
|
+
message: String(error)
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1599
1719
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
//#endregion
|
|
1724
|
+
//#region src/workflows/prepare.ts
|
|
1725
|
+
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
|
+
if (options.safeguards) {
|
|
1735
|
+
const clean = await gitOps.isWorkingDirectoryClean(options.workspaceRoot);
|
|
1736
|
+
if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
1737
|
+
}
|
|
1738
|
+
const discovered = await workspaceOps.discoverWorkspacePackages(options.workspaceRoot, options);
|
|
1739
|
+
if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
|
|
1740
|
+
const ensured = ensureHasPackages(discovered.value);
|
|
1741
|
+
if (!ensured.ok) {
|
|
1742
|
+
logger.warn(ensured.error.message);
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
const workspacePackages = ensured.value;
|
|
1746
|
+
logger.section("📦 Workspace Packages");
|
|
1747
|
+
logger.item(`Found ${workspacePackages.length} packages`);
|
|
1748
|
+
for (const pkg of workspacePackages) {
|
|
1749
|
+
logger.item(`${farver.cyan(pkg.name)} (${farver.bold(pkg.version)})`);
|
|
1750
|
+
logger.item(` ${farver.gray("→")} ${farver.gray(pkg.path)}`);
|
|
1751
|
+
}
|
|
1752
|
+
logger.emptyLine();
|
|
1753
|
+
const prepareBranchResult = await prepareReleaseBranch({
|
|
1754
|
+
git: gitOps,
|
|
1755
|
+
workspaceRoot: options.workspaceRoot,
|
|
1756
|
+
releaseBranch: options.branch.release,
|
|
1757
|
+
defaultBranch: options.branch.default
|
|
1758
|
+
});
|
|
1759
|
+
if (!prepareBranchResult.ok) exitWithError(prepareBranchResult.error.message);
|
|
1760
|
+
const overridesPath = join(options.workspaceRoot, ucdjsReleaseOverridesPath);
|
|
1761
|
+
let existingOverrides = {};
|
|
1762
|
+
try {
|
|
1763
|
+
const overridesContent = await readFile(overridesPath, "utf-8");
|
|
1764
|
+
existingOverrides = JSON.parse(overridesContent);
|
|
1765
|
+
logger.info("Found existing version overrides file.");
|
|
1766
|
+
} catch {
|
|
1767
|
+
logger.info("No existing version overrides file found. Continuing...");
|
|
1768
|
+
}
|
|
1769
|
+
const updatesResult = await calculateUpdates({
|
|
1770
|
+
versioning: versioningOps,
|
|
1771
|
+
workspacePackages,
|
|
1772
|
+
workspaceRoot: options.workspaceRoot,
|
|
1773
|
+
showPrompt: options.prompts?.versions !== false,
|
|
1774
|
+
globalCommitMode: options.globalCommitMode === "none" ? false : options.globalCommitMode,
|
|
1775
|
+
overrides: existingOverrides
|
|
1776
|
+
});
|
|
1777
|
+
if (!updatesResult.ok) exitWithError(updatesResult.error.message);
|
|
1778
|
+
const { allUpdates, applyUpdates, overrides: newOverrides } = updatesResult.value;
|
|
1779
|
+
if (Object.keys(newOverrides).length > 0) {
|
|
1780
|
+
logger.info("Writing version overrides file...");
|
|
1781
|
+
try {
|
|
1782
|
+
await mkdir(join(options.workspaceRoot, ".github"), { recursive: true });
|
|
1783
|
+
await writeFile(overridesPath, JSON.stringify(newOverrides, null, 2), "utf-8");
|
|
1784
|
+
logger.success("Successfully wrote version overrides file.");
|
|
1785
|
+
} catch (e) {
|
|
1786
|
+
logger.error("Failed to write version overrides file:", e);
|
|
1604
1787
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
yield* dependencyGraph.topologicalOrder(packages);
|
|
1615
|
-
const releases = [];
|
|
1616
|
-
if (versionPrompt.isEnabled) {
|
|
1617
|
-
yield* Console.log("\nInteractive version selection enabled.\n");
|
|
1618
|
-
versionPrompt.resetApplyToAll();
|
|
1619
|
-
for (let i = 0; i < packages.length; i++) {
|
|
1620
|
-
const pkg = packages[i];
|
|
1621
|
-
const conventionalBump = determineBump([...pkg.commits, ...pkg.globalCommits]);
|
|
1622
|
-
const remainingCount = packages.length - i;
|
|
1623
|
-
const override = overrides[pkg.name];
|
|
1624
|
-
if (override) {
|
|
1625
|
-
if (!semver.valid(override)) return yield* Effect.fail(/* @__PURE__ */ new Error(`Invalid override version for ${pkg.name}: ${override}`));
|
|
1626
|
-
releases.push({
|
|
1627
|
-
package: {
|
|
1628
|
-
name: pkg.name,
|
|
1629
|
-
version: pkg.version,
|
|
1630
|
-
path: pkg.path,
|
|
1631
|
-
packageJson: pkg.packageJson,
|
|
1632
|
-
workspaceDependencies: pkg.workspaceDependencies,
|
|
1633
|
-
workspaceDevDependencies: pkg.workspaceDevDependencies
|
|
1634
|
-
},
|
|
1635
|
-
currentVersion: pkg.version,
|
|
1636
|
-
newVersion: override,
|
|
1637
|
-
bumpType: "none",
|
|
1638
|
-
hasDirectChanges: pkg.commits.length > 0
|
|
1639
|
-
});
|
|
1640
|
-
continue;
|
|
1641
|
-
}
|
|
1642
|
-
const result = yield* versionPrompt.promptForVersion(pkg, conventionalBump, remainingCount);
|
|
1643
|
-
if (result.cancelled) {
|
|
1644
|
-
yield* Console.log("\nCancelled by user.");
|
|
1645
|
-
if (startingBranch !== (yield* git.branches.get)) {
|
|
1646
|
-
yield* git.branches.checkout(startingBranch);
|
|
1647
|
-
yield* Console.log(`Switched back to "${startingBranch}".`);
|
|
1648
|
-
}
|
|
1649
|
-
return yield* Effect.fail(/* @__PURE__ */ new Error("Release preparation cancelled."));
|
|
1788
|
+
}
|
|
1789
|
+
if (Object.keys(newOverrides).length === 0 && Object.keys(existingOverrides).length > 0) {
|
|
1790
|
+
let shouldRemoveOverrides = false;
|
|
1791
|
+
for (const update of allUpdates) {
|
|
1792
|
+
const overriddenVersion = existingOverrides[update.package.name];
|
|
1793
|
+
if (overriddenVersion) {
|
|
1794
|
+
if (compare(update.newVersion, overriddenVersion.version) > 0) {
|
|
1795
|
+
shouldRemoveOverrides = true;
|
|
1796
|
+
break;
|
|
1650
1797
|
}
|
|
1651
|
-
releases.push({
|
|
1652
|
-
package: {
|
|
1653
|
-
name: pkg.name,
|
|
1654
|
-
version: pkg.version,
|
|
1655
|
-
path: pkg.path,
|
|
1656
|
-
packageJson: pkg.packageJson,
|
|
1657
|
-
workspaceDependencies: pkg.workspaceDependencies,
|
|
1658
|
-
workspaceDevDependencies: pkg.workspaceDevDependencies
|
|
1659
|
-
},
|
|
1660
|
-
currentVersion: pkg.version,
|
|
1661
|
-
newVersion: result.newVersion,
|
|
1662
|
-
bumpType: result.bumpType,
|
|
1663
|
-
hasDirectChanges: pkg.commits.length > 0
|
|
1664
|
-
});
|
|
1665
1798
|
}
|
|
1666
|
-
} else {
|
|
1667
|
-
const calculatedReleases = yield* versionCalculator.calculateBumps(packages, overrides);
|
|
1668
|
-
releases.push(...calculatedReleases);
|
|
1669
1799
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
for (const release of releases) {
|
|
1679
|
-
const pkg = packages.find((p) => p.name === release.package.name);
|
|
1680
|
-
if (!pkg || !pkg.commits) continue;
|
|
1681
|
-
const result = yield* changelog.generateChangelog(pkg, release.newVersion, pkg.commits);
|
|
1682
|
-
yield* Effect.tryPromise({
|
|
1683
|
-
try: async () => {
|
|
1684
|
-
await (await import("node:fs/promises")).writeFile(result.filePath, result.markdown, "utf-8");
|
|
1685
|
-
},
|
|
1686
|
-
catch: (e) => /* @__PURE__ */ new Error(`Failed to write changelog: ${String(e)}`)
|
|
1687
|
-
});
|
|
1688
|
-
changelogFiles.push(result.filePath);
|
|
1800
|
+
if (shouldRemoveOverrides) {
|
|
1801
|
+
logger.info("Removing obsolete version overrides file...");
|
|
1802
|
+
try {
|
|
1803
|
+
await rm(overridesPath);
|
|
1804
|
+
logger.success("Successfully removed obsolete version overrides file.");
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
logger.error("Failed to remove obsolete version overrides file:", e);
|
|
1807
|
+
}
|
|
1689
1808
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1809
|
+
}
|
|
1810
|
+
if (allUpdates.filter((u) => u.hasDirectChanges).length === 0) logger.warn("No packages have changes requiring a release");
|
|
1811
|
+
logger.section("🔄 Version Updates");
|
|
1812
|
+
logger.item(`Updating ${allUpdates.length} packages (including dependents)`);
|
|
1813
|
+
for (const update of allUpdates) logger.item(`${update.package.name}: ${update.currentVersion} → ${update.newVersion}`);
|
|
1814
|
+
await applyUpdates();
|
|
1815
|
+
if (options.changelog?.enabled) {
|
|
1816
|
+
logger.step("Updating changelogs");
|
|
1817
|
+
const groupedPackageCommits = await versioningOps.getWorkspacePackageGroupedCommits(options.workspaceRoot, workspacePackages);
|
|
1818
|
+
if (!groupedPackageCommits.ok) exitWithError(groupedPackageCommits.error.message);
|
|
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);
|
|
1821
|
+
const changelogPromises = allUpdates.map((update) => {
|
|
1822
|
+
const pkgCommits = groupedPackageCommits.value.get(update.package.name) || [];
|
|
1823
|
+
const globalCommits = globalCommitsPerPackage.value.get(update.package.name) || [];
|
|
1824
|
+
const allCommits = [...pkgCommits, ...globalCommits];
|
|
1825
|
+
if (allCommits.length === 0) {
|
|
1826
|
+
logger.verbose(`No commits for ${update.package.name}, skipping changelog`);
|
|
1827
|
+
return Promise.resolve();
|
|
1828
|
+
}
|
|
1829
|
+
logger.verbose(`Updating changelog for ${farver.cyan(update.package.name)}`);
|
|
1830
|
+
return updateChangelog({
|
|
1831
|
+
normalizedOptions: {
|
|
1832
|
+
...options,
|
|
1833
|
+
workspaceRoot: options.workspaceRoot
|
|
1834
|
+
},
|
|
1835
|
+
githubClient: createGitHubClient({
|
|
1836
|
+
owner: options.owner,
|
|
1837
|
+
repo: options.repo,
|
|
1838
|
+
githubToken: options.githubToken
|
|
1839
|
+
}),
|
|
1840
|
+
workspacePackage: update.package,
|
|
1841
|
+
version: update.newVersion,
|
|
1842
|
+
previousVersion: update.currentVersion !== "0.0.0" ? update.currentVersion : void 0,
|
|
1843
|
+
commits: allCommits,
|
|
1844
|
+
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1718
1845
|
});
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1846
|
+
}).filter((p) => p != null);
|
|
1847
|
+
const updates = await Promise.all(changelogPromises);
|
|
1848
|
+
logger.success(`Updated ${updates.length} changelog(s)`);
|
|
1849
|
+
}
|
|
1850
|
+
const hasChangesToPush = await syncReleaseChanges({
|
|
1851
|
+
git: gitOps,
|
|
1852
|
+
workspaceRoot: options.workspaceRoot,
|
|
1853
|
+
releaseBranch: options.branch.release,
|
|
1854
|
+
commitMessage: "chore: update release versions",
|
|
1855
|
+
hasChanges: true
|
|
1856
|
+
});
|
|
1857
|
+
if (!hasChangesToPush.ok) exitWithError(hasChangesToPush.error.message);
|
|
1858
|
+
if (!hasChangesToPush.value) {
|
|
1859
|
+
const prResult = await syncPullRequest({
|
|
1860
|
+
github: githubOps,
|
|
1861
|
+
releaseBranch: options.branch.release,
|
|
1862
|
+
defaultBranch: options.branch.default,
|
|
1863
|
+
pullRequestTitle: options.pullRequest?.title,
|
|
1864
|
+
pullRequestBody: options.pullRequest?.body,
|
|
1865
|
+
updates: allUpdates
|
|
1866
|
+
});
|
|
1867
|
+
if (!prResult.ok) exitWithError(prResult.error.message);
|
|
1868
|
+
if (prResult.value.pullRequest) {
|
|
1869
|
+
logger.item("No updates needed, PR is already up to date");
|
|
1870
|
+
return {
|
|
1871
|
+
updates: allUpdates,
|
|
1872
|
+
prUrl: prResult.value.pullRequest.html_url,
|
|
1873
|
+
created: prResult.value.created
|
|
1874
|
+
};
|
|
1729
1875
|
}
|
|
1876
|
+
logger.error("No changes to commit, and no existing PR. Nothing to do.");
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
const prResult = await syncPullRequest({
|
|
1880
|
+
github: githubOps,
|
|
1881
|
+
releaseBranch: options.branch.release,
|
|
1882
|
+
defaultBranch: options.branch.default,
|
|
1883
|
+
pullRequestTitle: options.pullRequest?.title,
|
|
1884
|
+
pullRequestBody: options.pullRequest?.body,
|
|
1885
|
+
updates: allUpdates
|
|
1730
1886
|
});
|
|
1887
|
+
if (!prResult.ok) exitWithError(prResult.error.message);
|
|
1888
|
+
if (prResult.value.pullRequest?.html_url) {
|
|
1889
|
+
logger.section("🚀 Pull Request");
|
|
1890
|
+
logger.success(`Pull request ${prResult.value.created ? "created" : "updated"}: ${prResult.value.pullRequest.html_url}`);
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
updates: allUpdates,
|
|
1894
|
+
prUrl: prResult.value.pullRequest?.html_url,
|
|
1895
|
+
created: prResult.value.created
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
//#endregion
|
|
1900
|
+
//#region src/prepare.ts
|
|
1901
|
+
async function release(options) {
|
|
1902
|
+
return prepareWorkflow(options);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
//#endregion
|
|
1906
|
+
//#region src/workflows/publish.ts
|
|
1907
|
+
async function publishWorkflow(options) {
|
|
1908
|
+
logger.warn("Publish workflow is not implemented yet.");
|
|
1909
|
+
logger.verbose("Publish options:", options);
|
|
1731
1910
|
}
|
|
1732
1911
|
|
|
1733
1912
|
//#endregion
|
|
1734
1913
|
//#region src/publish.ts
|
|
1735
|
-
function
|
|
1736
|
-
|
|
1737
|
-
return parsed !== null && parsed.prerelease.length > 0;
|
|
1738
|
-
}
|
|
1739
|
-
function getDistTag(version) {
|
|
1740
|
-
return isPrerelease(version) ? "next" : "latest";
|
|
1741
|
-
}
|
|
1742
|
-
function buildPackage(packagePath) {
|
|
1743
|
-
return Effect.gen(function* () {
|
|
1744
|
-
const executor = yield* CommandExecutor.CommandExecutor;
|
|
1745
|
-
const command = Command.make("pnpm", "run", "build").pipe(Command.workingDirectory(packagePath));
|
|
1746
|
-
return (yield* executor.string(command).pipe(Effect.mapError((err) => /* @__PURE__ */ new Error(`Failed to build package at ${packagePath}: ${err.message}`)))).trim();
|
|
1747
|
-
});
|
|
1914
|
+
async function publish(options) {
|
|
1915
|
+
return publishWorkflow(options);
|
|
1748
1916
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const dependencyGraph = yield* DependencyGraphService;
|
|
1755
|
-
yield* git.workspace.assertWorkspaceReady;
|
|
1756
|
-
const currentBranch = yield* git.branches.get;
|
|
1757
|
-
if (currentBranch !== config.branch.default) return yield* Effect.fail(/* @__PURE__ */ new Error(`Publish must be run on the default branch "${config.branch.default}". Current branch: "${currentBranch}"`));
|
|
1758
|
-
yield* Console.log(`On default branch "${config.branch.default}".`);
|
|
1759
|
-
const publicPackages = (yield* workspace.discoverWorkspacePackages).filter((pkg) => !pkg.packageJson.private);
|
|
1760
|
-
yield* Console.log(`Found ${publicPackages.length} public package${publicPackages.length === 1 ? "" : "s"} to check.`);
|
|
1761
|
-
const orderedPackages = yield* dependencyGraph.topologicalOrder(publicPackages);
|
|
1762
|
-
const results = [];
|
|
1763
|
-
for (const updateOrder of orderedPackages) {
|
|
1764
|
-
const pkg = updateOrder.package;
|
|
1765
|
-
const version = pkg.version;
|
|
1766
|
-
const tagName = `${pkg.name}@${version}`;
|
|
1767
|
-
if (yield* npm.versionExists(pkg.name, version)) {
|
|
1768
|
-
yield* Console.log(`Skipping ${pkg.name}@${version} - already published.`);
|
|
1769
|
-
results.push({
|
|
1770
|
-
packageName: pkg.name,
|
|
1771
|
-
version,
|
|
1772
|
-
status: "skipped",
|
|
1773
|
-
reason: "Already published to npm"
|
|
1774
|
-
});
|
|
1775
|
-
continue;
|
|
1776
|
-
}
|
|
1777
|
-
yield* Console.log(`Building ${pkg.name}...`);
|
|
1778
|
-
yield* buildPackage(pkg.path);
|
|
1779
|
-
yield* Console.log(`Build complete for ${pkg.name}.`);
|
|
1780
|
-
const distTag = getDistTag(version);
|
|
1781
|
-
yield* Console.log(`Publishing ${pkg.name}@${version} with tag "${distTag}"...`);
|
|
1782
|
-
const publishResult = yield* npm.publish({
|
|
1783
|
-
packagePath: pkg.path,
|
|
1784
|
-
tagName: distTag,
|
|
1785
|
-
otp: config.npm.otp,
|
|
1786
|
-
provenance: config.npm.provenance,
|
|
1787
|
-
dryRun: config.dryRun
|
|
1788
|
-
}).pipe(Effect.map(() => ({ success: true })), Effect.catchAll((err) => Effect.succeed({
|
|
1789
|
-
success: false,
|
|
1790
|
-
error: err
|
|
1791
|
-
})));
|
|
1792
|
-
if (publishResult.success) {
|
|
1793
|
-
yield* Console.log(`Published ${pkg.name}@${version}.`);
|
|
1794
|
-
if (!config.dryRun) {
|
|
1795
|
-
yield* Console.log(`Creating tag ${tagName}...`);
|
|
1796
|
-
yield* git.tags.create(tagName, `Release ${tagName}`);
|
|
1797
|
-
yield* git.tags.push(tagName);
|
|
1798
|
-
yield* Console.log(`Tag ${tagName} created and pushed.`);
|
|
1799
|
-
} else yield* Console.log(`[Dry Run] Would create and push tag ${tagName}.`);
|
|
1800
|
-
results.push({
|
|
1801
|
-
packageName: pkg.name,
|
|
1802
|
-
version,
|
|
1803
|
-
status: "published"
|
|
1804
|
-
});
|
|
1805
|
-
} else {
|
|
1806
|
-
const error = publishResult.error;
|
|
1807
|
-
yield* Console.log(`Failed to publish ${pkg.name}@${version}: ${error.message}`);
|
|
1808
|
-
results.push({
|
|
1809
|
-
packageName: pkg.name,
|
|
1810
|
-
version,
|
|
1811
|
-
status: "failed",
|
|
1812
|
-
reason: error.message
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
const published = results.filter((r) => r.status === "published");
|
|
1817
|
-
const skipped = results.filter((r) => r.status === "skipped");
|
|
1818
|
-
const failed = results.filter((r) => r.status === "failed");
|
|
1819
|
-
yield* Console.log("\nPublish Summary:");
|
|
1820
|
-
yield* Console.log(` Published: ${published.length}`);
|
|
1821
|
-
yield* Console.log(` Skipped: ${skipped.length}`);
|
|
1822
|
-
yield* Console.log(` Failed: ${failed.length}`);
|
|
1823
|
-
if (failed.length > 0) {
|
|
1824
|
-
yield* Console.log("\nFailed packages:");
|
|
1825
|
-
for (const f of failed) yield* Console.log(` - ${f.packageName}@${f.version}: ${f.reason}`);
|
|
1826
|
-
return yield* Effect.fail(/* @__PURE__ */ new Error("Some packages failed to publish."));
|
|
1827
|
-
}
|
|
1828
|
-
if (published.length === 0 && skipped.length > 0) yield* Console.log("\nAll packages were already published.");
|
|
1829
|
-
else if (published.length > 0) yield* Console.log("\nPublish complete!");
|
|
1830
|
-
});
|
|
1917
|
+
|
|
1918
|
+
//#endregion
|
|
1919
|
+
//#region src/operations/discover.ts
|
|
1920
|
+
async function discoverPackages({ workspace, workspaceRoot, options }) {
|
|
1921
|
+
return workspace.discoverWorkspacePackages(workspaceRoot, options);
|
|
1831
1922
|
}
|
|
1832
1923
|
|
|
1833
1924
|
//#endregion
|
|
1834
|
-
//#region src/verify.ts
|
|
1835
|
-
function
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
try: () => JSON.parse(content),
|
|
1842
|
-
catch: (e) => /* @__PURE__ */ new Error(`Failed to parse package.json for ${pkg.name} at ${ref}: ${String(e)}`)
|
|
1843
|
-
})));
|
|
1925
|
+
//#region src/workflows/verify.ts
|
|
1926
|
+
async function verifyWorkflow(options) {
|
|
1927
|
+
const gitOps = createGitOperations();
|
|
1928
|
+
const githubOps = createGitHubOperations({
|
|
1929
|
+
owner: options.owner,
|
|
1930
|
+
repo: options.repo,
|
|
1931
|
+
githubToken: options.githubToken
|
|
1844
1932
|
});
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1933
|
+
const workspaceOps = createWorkspaceOperations();
|
|
1934
|
+
if (options.safeguards) {
|
|
1935
|
+
const clean = await gitOps.isWorkingDirectoryClean(options.workspaceRoot);
|
|
1936
|
+
if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
1937
|
+
}
|
|
1938
|
+
const releaseBranch = options.branch.release;
|
|
1939
|
+
const defaultBranch = options.branch.default;
|
|
1940
|
+
const releasePr = await githubOps.getExistingPullRequest(releaseBranch);
|
|
1941
|
+
if (!releasePr.ok) exitWithError(releasePr.error.message);
|
|
1942
|
+
if (!releasePr.value || !releasePr.value.head) {
|
|
1943
|
+
logger.warn(`No open release pull request found for branch "${releaseBranch}". Nothing to verify.`);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
logger.info(`Found release PR #${releasePr.value.number}. Verifying against default branch "${defaultBranch}"...`);
|
|
1947
|
+
const originalBranch = await gitOps.getCurrentBranch(options.workspaceRoot);
|
|
1948
|
+
if (!originalBranch.ok) exitWithError(originalBranch.error.message);
|
|
1949
|
+
if (originalBranch.value !== defaultBranch) {
|
|
1950
|
+
const checkout = await gitOps.checkoutBranch(defaultBranch, options.workspaceRoot);
|
|
1951
|
+
if (!checkout.ok || !checkout.value) exitWithError(`Failed to checkout branch: ${defaultBranch}`);
|
|
1952
|
+
}
|
|
1953
|
+
let existingOverrides = {};
|
|
1954
|
+
try {
|
|
1955
|
+
const overridesContent = await gitOps.readFileFromGit(options.workspaceRoot, releasePr.value.head.sha, ucdjsReleaseOverridesPath);
|
|
1956
|
+
if (overridesContent.ok && overridesContent.value) {
|
|
1957
|
+
existingOverrides = JSON.parse(overridesContent.value);
|
|
1958
|
+
logger.info("Found existing version overrides file on release branch.");
|
|
1858
1959
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1960
|
+
} catch {
|
|
1961
|
+
logger.info("No version overrides file found on release branch. Continuing...");
|
|
1962
|
+
}
|
|
1963
|
+
const discovered = await discoverPackages({
|
|
1964
|
+
workspace: workspaceOps,
|
|
1965
|
+
workspaceRoot: options.workspaceRoot,
|
|
1966
|
+
options
|
|
1967
|
+
});
|
|
1968
|
+
if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
|
|
1969
|
+
const ensured = ensureHasPackages(discovered.value);
|
|
1970
|
+
if (!ensured.ok) {
|
|
1971
|
+
logger.warn(ensured.error.message);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const mainPackages = ensured.value;
|
|
1975
|
+
const updatesResult = await calculateUpdates({
|
|
1976
|
+
versioning: createVersioningOperations(),
|
|
1977
|
+
workspacePackages: mainPackages,
|
|
1978
|
+
workspaceRoot: options.workspaceRoot,
|
|
1979
|
+
showPrompt: false,
|
|
1980
|
+
globalCommitMode: options.globalCommitMode === "none" ? false : options.globalCommitMode,
|
|
1981
|
+
overrides: existingOverrides
|
|
1982
|
+
});
|
|
1983
|
+
if (!updatesResult.ok) exitWithError(updatesResult.error.message);
|
|
1984
|
+
const expectedUpdates = updatesResult.value.allUpdates;
|
|
1985
|
+
const expectedVersionMap = new Map(expectedUpdates.map((u) => [u.package.name, u.newVersion]));
|
|
1986
|
+
const prVersionMap = /* @__PURE__ */ new Map();
|
|
1987
|
+
for (const pkg of mainPackages) {
|
|
1988
|
+
const pkgJsonPath = relative(options.workspaceRoot, join(pkg.path, "package.json"));
|
|
1989
|
+
const pkgJsonContent = await gitOps.readFileFromGit(options.workspaceRoot, releasePr.value.head.sha, pkgJsonPath);
|
|
1990
|
+
if (pkgJsonContent.ok && pkgJsonContent.value) {
|
|
1991
|
+
const pkgJson = JSON.parse(pkgJsonContent.value);
|
|
1992
|
+
prVersionMap.set(pkg.name, pkgJson.version);
|
|
1865
1993
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
});
|
|
1994
|
+
}
|
|
1995
|
+
if (originalBranch.value !== defaultBranch) await gitOps.checkoutBranch(originalBranch.value, options.workspaceRoot);
|
|
1996
|
+
let isOutOfSync = false;
|
|
1997
|
+
for (const [pkgName, expectedVersion] of expectedVersionMap.entries()) {
|
|
1998
|
+
const prVersion = prVersionMap.get(pkgName);
|
|
1999
|
+
if (!prVersion) {
|
|
2000
|
+
logger.warn(`Package "${pkgName}" found in default branch but not in release branch. Skipping.`);
|
|
1873
2001
|
continue;
|
|
1874
2002
|
}
|
|
1875
|
-
if (
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
});
|
|
1879
|
-
for (const section of [
|
|
1880
|
-
"dependencies",
|
|
1881
|
-
"devDependencies",
|
|
1882
|
-
"peerDependencies"
|
|
1883
|
-
]) {
|
|
1884
|
-
const deps = snapshot[section];
|
|
1885
|
-
if (!deps || typeof deps !== "object") continue;
|
|
1886
|
-
for (const [depName, range] of Object.entries(deps)) {
|
|
1887
|
-
const bumpedVersion = releaseVersionByName.get(depName);
|
|
1888
|
-
if (!bumpedVersion) continue;
|
|
1889
|
-
if (typeof range !== "string") {
|
|
1890
|
-
reasons.push({
|
|
1891
|
-
packageName: pkg.name,
|
|
1892
|
-
reason: `${section}.${depName} is not a string range`
|
|
1893
|
-
});
|
|
1894
|
-
continue;
|
|
1895
|
-
}
|
|
1896
|
-
if (!satisfiesRange(range, bumpedVersion)) reasons.push({
|
|
1897
|
-
packageName: pkg.name,
|
|
1898
|
-
reason: `${section}.${depName} does not include ${bumpedVersion}`
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
2003
|
+
if (gt(expectedVersion, prVersion)) {
|
|
2004
|
+
logger.error(`Package "${pkgName}" is out of sync. Expected version >= ${expectedVersion}, but PR has ${prVersion}.`);
|
|
2005
|
+
isOutOfSync = true;
|
|
2006
|
+
} else logger.success(`Package "${pkgName}" is up to date (PR version: ${prVersion}, Expected: ${expectedVersion})`);
|
|
1902
2007
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
const versionCalculator = yield* VersionCalculatorService;
|
|
1911
|
-
const workspace = yield* WorkspaceService;
|
|
1912
|
-
yield* git.workspace.assertWorkspaceReady;
|
|
1913
|
-
const releasePullRequest = yield* github.getPullRequestByBranch(config.branch.release);
|
|
1914
|
-
if (!releasePullRequest || !releasePullRequest.head) return yield* Effect.fail(/* @__PURE__ */ new Error(`Release pull request for branch "${config.branch.release}" does not exist.`));
|
|
1915
|
-
yield* Console.log(`Release pull request #${releasePullRequest.number} exists.`);
|
|
1916
|
-
if ((yield* git.branches.get) !== config.branch.default) {
|
|
1917
|
-
yield* git.branches.checkout(config.branch.default);
|
|
1918
|
-
yield* Console.log(`Checked out to default branch "${config.branch.default}".`);
|
|
1919
|
-
}
|
|
1920
|
-
const overrides = yield* loadOverrides({
|
|
1921
|
-
sha: releasePullRequest.head.sha,
|
|
1922
|
-
overridesPath: ".github/ucdjs-release.overrides.json"
|
|
2008
|
+
const statusContext = "ucdjs/release-verify";
|
|
2009
|
+
if (isOutOfSync) {
|
|
2010
|
+
await githubOps.setCommitStatus({
|
|
2011
|
+
sha: releasePr.value.head.sha,
|
|
2012
|
+
state: "failure",
|
|
2013
|
+
context: statusContext,
|
|
2014
|
+
description: "Release PR is out of sync with the default branch. Please re-run the release process."
|
|
1923
2015
|
});
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
const ordered = yield* dependencyGraph.topologicalOrder(packages);
|
|
1929
|
-
yield* Console.log("Calculated releases:", releases);
|
|
1930
|
-
yield* Console.log("Release order:", ordered);
|
|
1931
|
-
const releaseHeadSha = releasePullRequest.head.sha;
|
|
1932
|
-
const branchSnapshots = /* @__PURE__ */ new Map();
|
|
1933
|
-
for (const pkg of packages) {
|
|
1934
|
-
const snapshot = yield* snapshotPackageJson(pkg, releaseHeadSha).pipe(Effect.catchAll((err) => Effect.succeed(err instanceof Error ? err : new Error(String(err)))));
|
|
1935
|
-
branchSnapshots.set(pkg.name, snapshot);
|
|
1936
|
-
}
|
|
1937
|
-
const drift = findDrift(packages, releases, branchSnapshots);
|
|
1938
|
-
if (drift.length === 0) yield* Console.log("Release branch is in sync with expected releases.");
|
|
1939
|
-
else yield* Console.log("Release branch is out of sync:", drift);
|
|
1940
|
-
const status = drift.length === 0 ? {
|
|
2016
|
+
logger.error("Verification failed. Commit status set to 'failure'.");
|
|
2017
|
+
} else {
|
|
2018
|
+
await githubOps.setCommitStatus({
|
|
2019
|
+
sha: releasePr.value.head.sha,
|
|
1941
2020
|
state: "success",
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2021
|
+
context: statusContext,
|
|
2022
|
+
description: "Release PR is up to date.",
|
|
2023
|
+
targetUrl: `https://github.com/${options.owner}/${options.repo}/pull/${releasePr.value.number}`
|
|
2024
|
+
});
|
|
2025
|
+
logger.success("Verification successful. Commit status set to 'success'.");
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
//#endregion
|
|
2030
|
+
//#region src/verify.ts
|
|
2031
|
+
async function verify(options) {
|
|
2032
|
+
return verifyWorkflow(options);
|
|
1952
2033
|
}
|
|
1953
2034
|
|
|
1954
2035
|
//#endregion
|
|
1955
2036
|
//#region src/index.ts
|
|
1956
2037
|
async function createReleaseScripts(options) {
|
|
1957
|
-
const
|
|
1958
|
-
const AppLayer = Layer.mergeAll(ChangelogService.Default, GitService.Default, GitHubService.Default, DependencyGraphService.Default, NPMService.Default, PackageUpdaterService.Default, VersionCalculatorService.Default, VersionPromptService.Default, WorkspaceService.Default).pipe(Layer.provide(Layer.succeed(ReleaseScriptsOptions, config)), Layer.provide(NodeCommandExecutor.layer), Layer.provide(NodeFileSystem.layer));
|
|
1959
|
-
const runProgram = (program) => {
|
|
1960
|
-
const provided = program.pipe(Effect.provide(AppLayer));
|
|
1961
|
-
return Effect.runPromise(provided);
|
|
1962
|
-
};
|
|
1963
|
-
const safeguardProgram = Effect.gen(function* () {
|
|
1964
|
-
return yield* (yield* GitService).workspace.assertWorkspaceReady;
|
|
1965
|
-
});
|
|
1966
|
-
try {
|
|
1967
|
-
await runProgram(safeguardProgram);
|
|
1968
|
-
} catch (err) {
|
|
1969
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1970
|
-
await Effect.runPromise(Console.error(`❌ Initialization failed: ${message}`));
|
|
1971
|
-
throw err;
|
|
1972
|
-
}
|
|
2038
|
+
const normalizedOptions = normalizeReleaseScriptsOptions(options);
|
|
1973
2039
|
return {
|
|
1974
2040
|
async verify() {
|
|
1975
|
-
return
|
|
2041
|
+
return verify(normalizedOptions);
|
|
1976
2042
|
},
|
|
1977
2043
|
async prepare() {
|
|
1978
|
-
return
|
|
2044
|
+
return release(normalizedOptions);
|
|
1979
2045
|
},
|
|
1980
2046
|
async publish() {
|
|
1981
|
-
return
|
|
2047
|
+
return publish(normalizedOptions);
|
|
1982
2048
|
},
|
|
1983
2049
|
packages: {
|
|
1984
2050
|
async list() {
|
|
1985
|
-
return
|
|
1986
|
-
return yield* (yield* WorkspaceService).discoverWorkspacePackages;
|
|
1987
|
-
}));
|
|
2051
|
+
return discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
|
|
1988
2052
|
},
|
|
1989
2053
|
async get(packageName) {
|
|
1990
|
-
return
|
|
1991
|
-
return (yield* (yield* WorkspaceService).findPackageByName(packageName)) || null;
|
|
1992
|
-
}));
|
|
2054
|
+
return (await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions)).find((p) => p.name === packageName);
|
|
1993
2055
|
}
|
|
1994
2056
|
}
|
|
1995
2057
|
};
|