@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/index.mjs CHANGED
@@ -1,45 +1,377 @@
1
- import { t as Eta } from "./eta-BV8TCRDW.mjs";
2
- import { Console, Context, Data, Effect, Layer, Schema } from "effect";
3
- import process from "node:process";
4
- import path from "node:path";
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/options.ts
13
- const DEFAULT_PR_BODY_TEMPLATE = `## Summary\n\nThis PR contains the following changes:\n\n- Updated package versions\n- Updated changelogs\n\n## Packages\n\nThe following packages will be released:\n\n{{packages}}`;
14
- const DEFAULT_CHANGELOG_TEMPLATE = `# Changelog\n\n{{releases}}`;
15
- const DEFAULT_TYPES = {
16
- feat: {
17
- title: "🚀 Features",
18
- color: "green"
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
- fix: {
21
- title: "🐞 Bug Fixes",
22
- color: "red"
163
+ emptyLine: () => {
164
+ console.log();
23
165
  },
24
- refactor: {
25
- title: "🔧 Code Refactoring",
26
- color: "blue"
166
+ item: (message, ...args) => {
167
+ console.log(` ${message}`, ...args);
27
168
  },
28
- perf: {
29
- title: "🏎 Performance",
30
- color: "orange"
169
+ step: (message) => {
170
+ console.log(` ${farver.blue("→")} ${message}`);
31
171
  },
32
- docs: {
33
- title: "📚 Documentation",
34
- color: "purple"
172
+ success: (message) => {
173
+ console.log(` ${farver.green("✓")} ${message}`);
35
174
  },
36
- style: {
37
- title: "🎨 Styles",
38
- color: "pink"
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 = {}, dryRun = false, npm = {}, prompts = {} } = options;
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: options.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: { versions: prompts.versions ?? !isCI }
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/utils/changelog-formatters.ts
90
- const eta$1 = new Eta();
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
- * Pure function to parse commits into changelog entries
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 parseCommits(commits) {
95
- return commits.filter((commit) => commit.isConventional).filter((commit) => commit.type !== "chore").map((commit) => ({
96
- type: commit.type || "other",
97
- scope: commit.scope,
98
- description: commit.description,
99
- breaking: commit.isBreaking || false,
100
- hash: commit.hash,
101
- shortHash: commit.shortHash,
102
- references: commit.references.map((ref) => ({
103
- type: ref.type,
104
- value: ref.value
105
- })),
106
- authors: commit.authors.map((author) => ({
107
- name: author.name,
108
- email: author.email,
109
- profile: author.profile
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
- * Pure function to group changelog entries by type
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 groupByType(entries) {
117
- const groups = /* @__PURE__ */ new Map();
118
- for (const entry of entries) {
119
- const type = entry.breaking ? "breaking" : entry.type;
120
- if (!groups.has(type)) groups.set(type, []);
121
- groups.get(type).push(entry);
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
- * Changelog template for Eta rendering
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
- const CHANGELOG_ENTRY_TEMPLATE = `## <%= it.version %>
129
-
130
- <% if (it.entries.length === 0) { %>
131
- *No conventional commits found.*
132
- <% } else { %>
133
- <% const groups = it.groupedEntries; %>
134
- <% const typeOrder = ["breaking", "feat", "fix", "perf", "docs", "style", "refactor", "test", "build", "ci", "chore"]; %>
135
- <% const typeLabels = {
136
- breaking: "💥 Breaking Changes",
137
- feat: "🚀 Features",
138
- fix: "🐞 Bug Fixes",
139
- perf: " Performance",
140
- docs: "Documentation",
141
- style: "Styling",
142
- refactor: "Refactoring",
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 + " &nbsp;-&nbsp; by " + authorText + hashPart;
168
- }; %>
169
-
170
- <% for (const type of typeOrder) { %>
171
- <% const entries = groups.get(type); %>
172
- <% if (entries && entries.length > 0) { %>
173
- ### &nbsp;&nbsp;&nbsp;<%= 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
- ### &nbsp;&nbsp;&nbsp;<%= 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
- ##### &nbsp;&nbsp;&nbsp;&nbsp;[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
- * Pure function to format changelog as markdown
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 formatChangelogEntryMarkdown(changelog) {
213
- const groups = groupByType(changelog.entries);
214
- return eta$1.renderString(CHANGELOG_ENTRY_TEMPLATE, {
215
- packageName: changelog.packageName,
216
- version: changelog.version,
217
- previousVersion: changelog.previousVersion,
218
- entries: changelog.entries,
219
- groupedEntries: groups,
220
- repo: changelog.repo
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 appendChangelogEntry(existingContent, changelogEntry, packageName) {
224
- const entry = changelogEntry.trim();
225
- if (!entry) return existingContent ?? `# ${packageName}\n`;
226
- if (!existingContent || existingContent.trim() === "") return `# ${packageName}\n\n${entry}\n`;
227
- const lines = existingContent.split("\n");
228
- const firstLine = lines[0]?.trim() ?? "";
229
- if (!firstLine.startsWith("# ")) return `# ${packageName}\n\n${entry}\n\n${existingContent.trim()}\n`;
230
- let insertIndex = 1;
231
- while (insertIndex < lines.length && lines[insertIndex]?.trim() === "") insertIndex++;
232
- const rest = lines.slice(insertIndex).join("\n").trim();
233
- if (rest) return `${firstLine}\n\n${entry}\n\n${rest}\n`;
234
- return `${firstLine}\n\n${entry}\n`;
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
- * Pure function to create a changelog object
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 createChangelog(packageName, version, previousVersion, commits, repo) {
240
- return {
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
- entries: parseCommits(commits),
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
- //#endregion
250
- //#region src/services/changelog.service.ts
251
- var ChangelogService = class extends Effect.Service()("@ucdjs/release-scripts/ChangelogService", {
252
- effect: Effect.gen(function* () {
253
- const config = yield* ReleaseScriptsOptions;
254
- function generateChangelog(pkg, newVersion, commits) {
255
- return Effect.gen(function* () {
256
- const changelog = createChangelog(pkg.name, newVersion, pkg.version, commits, `${config.owner}/${config.repo}`);
257
- const entryMarkdown = formatChangelogEntryMarkdown(changelog);
258
- return {
259
- changelog,
260
- markdown: appendChangelogEntry(yield* Effect.tryPromise({
261
- try: async () => {
262
- return await (await import("node:fs/promises")).readFile(`${pkg.path}/CHANGELOG.md`, "utf-8");
263
- },
264
- catch: (err) => err
265
- }).pipe(Effect.catchAll(() => Effect.succeed(""))), entryMarkdown, pkg.name),
266
- filePath: `${pkg.path}/CHANGELOG.md`
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
- return { generateChangelog };
271
- }),
272
- dependencies: []
273
- }) {};
274
-
275
- //#endregion
276
- //#region src/services/dependency-graph.service.ts
277
- var DependencyGraphService = class extends Effect.Service()("@ucdjs/release-scripts/DependencyGraphService", {
278
- effect: Effect.gen(function* () {
279
- function buildGraph(packages) {
280
- const nameToPackage = /* @__PURE__ */ new Map();
281
- const adjacency = /* @__PURE__ */ new Map();
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
- return { topologicalOrder };
338
- }),
339
- dependencies: []
340
- }) {};
341
-
342
- //#endregion
343
- //#region src/errors.ts
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/services/git.service.ts
356
- var GitService = class extends Effect.Service()("@ucdjs/release-scripts/GitService", {
357
- effect: Effect.gen(function* () {
358
- const executor = yield* CommandExecutor.CommandExecutor;
359
- const config = yield* ReleaseScriptsOptions;
360
- const execGitCommand = (args) => executor.string(Command.make("git", ...args).pipe(Command.workingDirectory(config.workspaceRoot))).pipe(Effect.mapError((err) => {
361
- return new GitCommandError({
362
- command: `git ${args.join(" ")}`,
363
- stderr: err.message
364
- });
365
- }));
366
- const execGitCommandIfNotDry = config.dryRun ? (args) => Effect.succeed(`Dry run mode: skipping git command "git ${args.join(" ")}"`) : execGitCommand;
367
- const isWithinRepository = Effect.gen(function* () {
368
- return (yield* execGitCommand(["rev-parse", "--is-inside-work-tree"]).pipe(Effect.catchAll(() => Effect.succeed("false")))).trim() === "true";
369
- });
370
- const listBranches = Effect.gen(function* () {
371
- return (yield* execGitCommand(["branch", "--list"])).trim().split("\n").filter((line) => line.length > 0).map((line) => line.replace(/^\* /, "").trim()).map((line) => line.trim());
372
- });
373
- const isWorkingDirectoryClean = Effect.gen(function* () {
374
- return (yield* execGitCommand(["status", "--porcelain"])).trim().length === 0;
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
- function doesBranchExist(branch) {
377
- return listBranches.pipe(Effect.map((branches) => branches.includes(branch)));
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
- function createBranch(branch, base = config.branch.default) {
380
- return execGitCommandIfNotDry([
381
- "branch",
382
- branch,
383
- base
384
- ]);
385
- }
386
- const getBranch = Effect.gen(function* () {
387
- return (yield* execGitCommand([
388
- "rev-parse",
389
- "--abbrev-ref",
390
- "HEAD"
391
- ])).trim();
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
- function checkoutBranch(branch) {
394
- return execGitCommand(["checkout", branch]);
395
- }
396
- function rebaseBranch(onto) {
397
- return execGitCommandIfNotDry(["rebase", onto]);
398
- }
399
- function stageChanges(files) {
400
- return Effect.gen(function* () {
401
- if (files.length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("No files to stage."));
402
- return yield* execGitCommandIfNotDry(["add", ...files]);
403
- });
404
- }
405
- function writeCommit(message) {
406
- return execGitCommandIfNotDry([
407
- "commit",
408
- "-m",
409
- message
410
- ]);
411
- }
412
- function writeEmptyCommit(message) {
413
- return execGitCommandIfNotDry([
414
- "commit",
415
- "--allow-empty",
416
- "-m",
417
- message
418
- ]);
419
- }
420
- function pushChanges(branch, remote = "origin") {
421
- return execGitCommandIfNotDry([
422
- "push",
423
- remote,
424
- branch
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
- function getMostRecentPackageTag(packageName) {
439
- return execGitCommand([
440
- "tag",
441
- "--list",
442
- "--sort=-version:refname",
443
- `${packageName}@*`
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
- function createTag(name, message) {
455
- return execGitCommandIfNotDry(message ? [
456
- "tag",
457
- "-a",
458
- name,
459
- "-m",
460
- message
461
- ] : ["tag", name]).pipe(Effect.mapError((err) => new TagError({
462
- message: `Failed to create tag "${name}"`,
463
- tagName: name,
464
- operation: "create",
465
- cause: err
466
- })));
467
- }
468
- function pushTag(name, remote = "origin") {
469
- return execGitCommandIfNotDry([
470
- "push",
471
- remote,
472
- name
473
- ]).pipe(Effect.mapError((err) => new TagError({
474
- message: `Failed to push tag "${name}" to ${remote}`,
475
- tagName: name,
476
- operation: "push",
477
- cause: err
478
- })));
479
- }
480
- function getCommits(options) {
481
- return Effect.tryPromise({
482
- try: async () => CommitParser.getCommits({
483
- from: options?.from,
484
- to: options?.to,
485
- folder: options?.folder,
486
- cwd: config.workspaceRoot
487
- }),
488
- catch: (e) => new ExternalCommitParserError({
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
- dependencies: [NodeCommandExecutor.layer]
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/services/github.service.ts
559
- const eta = new Eta();
560
- const PullRequestSchema = Schema.Struct({
561
- number: Schema.Number,
562
- title: Schema.String,
563
- body: Schema.String,
564
- head: Schema.Struct({
565
- ref: Schema.String,
566
- sha: Schema.String
567
- }),
568
- base: Schema.Struct({
569
- ref: Schema.String,
570
- sha: Schema.String
571
- }),
572
- state: Schema.Literal("open", "closed", "merged"),
573
- draft: Schema.Boolean,
574
- mergeable: Schema.NullOr(Schema.Boolean),
575
- url: Schema.String,
576
- html_url: Schema.String
577
- });
578
- const CreatePullRequestOptionsSchema = Schema.Struct({
579
- title: Schema.String,
580
- body: Schema.NullOr(Schema.String),
581
- head: Schema.String,
582
- base: Schema.String,
583
- draft: Schema.optional(Schema.Boolean)
584
- });
585
- const UpdatePullRequestOptionsSchema = Schema.Struct({
586
- title: Schema.optional(Schema.String),
587
- body: Schema.optional(Schema.String),
588
- state: Schema.optional(Schema.Literal("open", "closed"))
589
- });
590
- const CommitStatusSchema = Schema.Struct({
591
- state: Schema.Literal("pending", "success", "error", "failure"),
592
- target_url: Schema.optional(Schema.String),
593
- description: Schema.optional(Schema.String),
594
- context: Schema.String
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
- ## Changes
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
- See individual package changelogs for details.
684
- `;
685
- function generateReleasePRBody(releases) {
686
- return Effect.gen(function* () {
687
- return eta.renderString(prBodyTemplate, {
688
- count: releases.length,
689
- releases
690
- });
691
- });
692
- }
693
- return {
694
- getPullRequestByBranch,
695
- setCommitStatus,
696
- createPullRequest,
697
- updatePullRequest,
698
- generateReleasePRBody
699
- };
700
- }),
701
- dependencies: []
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/services/npm.service.ts
706
- const PackumentSchema = Schema.Struct({
707
- "name": Schema.String,
708
- "dist-tags": Schema.Record({
709
- key: Schema.String,
710
- value: Schema.String
711
- }),
712
- "versions": Schema.Record({
713
- key: Schema.String,
714
- value: Schema.Struct({
715
- name: Schema.String,
716
- version: Schema.String,
717
- description: Schema.optional(Schema.String),
718
- dist: Schema.Struct({
719
- tarball: Schema.String,
720
- shasum: Schema.String,
721
- integrity: Schema.optional(Schema.String)
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
- fetchPackument,
772
- versionExists,
773
- getLatestVersion,
774
- publish
1232
+ pkgName: pkg.name,
1233
+ commits: allCommits
775
1234
  };
776
- }),
777
- dependencies: [NodeCommandExecutor.layer]
778
- }) {};
779
-
780
- //#endregion
781
- //#region src/services/workspace.service.ts
782
- const DependencyRecordSchema = Schema.Record({
783
- key: Schema.String,
784
- value: Schema.String
785
- });
786
- const PnpmDependencySchema = Schema.Record({
787
- key: Schema.String,
788
- value: Schema.Struct({
789
- from: Schema.String,
790
- version: Schema.String,
791
- resolved: Schema.optional(Schema.String),
792
- path: Schema.optional(Schema.String)
793
- })
794
- });
795
- const PackageJsonSchema = Schema.Struct({
796
- name: Schema.String,
797
- private: Schema.optional(Schema.Boolean),
798
- version: Schema.optional(Schema.String),
799
- dependencies: Schema.optional(DependencyRecordSchema),
800
- devDependencies: Schema.optional(DependencyRecordSchema),
801
- peerDependencies: Schema.optional(DependencyRecordSchema)
802
- });
803
- const WorkspacePackageSchema = Schema.Struct({
804
- name: Schema.String,
805
- version: Schema.String,
806
- path: Schema.String,
807
- packageJson: PackageJsonSchema,
808
- workspaceDependencies: Schema.Array(Schema.String),
809
- workspaceDevDependencies: Schema.Array(Schema.String)
810
- });
811
- const WorkspaceListSchema = Schema.Array(Schema.Struct({
812
- name: Schema.String,
813
- path: Schema.String,
814
- version: Schema.optional(Schema.String),
815
- private: Schema.optional(Schema.Boolean),
816
- dependencies: Schema.optional(PnpmDependencySchema),
817
- devDependencies: Schema.optional(PnpmDependencySchema),
818
- peerDependencies: Schema.optional(PnpmDependencySchema)
819
- }));
820
- var WorkspaceService = class extends Effect.Service()("@ucdjs/release-scripts/WorkspaceService", {
821
- effect: Effect.gen(function* () {
822
- const executor = yield* CommandExecutor.CommandExecutor;
823
- const config = yield* ReleaseScriptsOptions;
824
- const workspacePackageListOutput = yield* executor.string(Command.make("pnpm", "-r", "ls", "--json").pipe(Command.workingDirectory(config.workspaceRoot))).pipe(Effect.flatMap((stdout) => Effect.try({
825
- try: () => JSON.parse(stdout),
826
- catch: (e) => new WorkspaceError({
827
- message: "Failed to parse pnpm JSON output",
828
- operation: "discover",
829
- cause: e
830
- })
831
- })), Effect.flatMap((json) => Schema.decodeUnknown(WorkspaceListSchema)(json).pipe(Effect.mapError((e) => new WorkspaceError({
832
- message: "Failed to decode pnpm output",
833
- operation: "discover",
834
- cause: e
835
- })))), Effect.cached);
836
- function readPackageJson(pkgPath) {
837
- return Effect.tryPromise({
838
- try: async () => JSON.parse(await fs.readFile(path.join(pkgPath, "package.json"), "utf8")),
839
- catch: (e) => new WorkspaceError({
840
- message: `Failed to read package.json for ${pkgPath}`,
841
- cause: e,
842
- operation: "readPackageJson"
843
- })
844
- }).pipe(Effect.flatMap((json) => Schema.decodeUnknown(PackageJsonSchema)(json).pipe(Effect.mapError((e) => new WorkspaceError({
845
- message: `Invalid package.json for ${pkgPath}`,
846
- cause: e,
847
- operation: "readPackageJson"
848
- })), Effect.map((validated) => ({
849
- ...json,
850
- ...validated
851
- })))));
852
- }
853
- function writePackageJson(pkgPath, json) {
854
- const fullPath = path.join(pkgPath, "package.json");
855
- const content = `${JSON.stringify(json, null, 2)}\n`;
856
- if (config.dryRun) return Effect.succeed(`Dry run: skip writing ${fullPath}`);
857
- return Effect.tryPromise({
858
- try: async () => await fs.writeFile(fullPath, content, "utf8"),
859
- catch: (e) => new WorkspaceError({
860
- message: `Failed to write package.json for ${pkgPath}`,
861
- cause: e,
862
- operation: "writePackageJson"
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
- const discoverWorkspacePackages = Effect.gen(function* () {
867
- let workspaceOptions;
868
- let explicitPackages;
869
- if (config.packages == null || config.packages === true) workspaceOptions = { excludePrivate: false };
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
- function shouldIncludePackage(pkg, options) {
914
- if (!options) return true;
915
- if (options.excludePrivate && pkg.private) return false;
916
- if (options.include && options.include.length > 0) {
917
- if (!options.include.includes(pkg.name)) return false;
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
- return {
926
- readPackageJson,
927
- writePackageJson,
928
- findWorkspacePackages,
929
- discoverWorkspacePackages,
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/services/package-updater.service.ts
938
- const DASH_RE = / - /;
939
- const RANGE_OPERATION_RE = /^(?:>=|<=|[><=])/;
940
- function nextRange(oldRange, newVersion) {
941
- const workspacePrefix = oldRange.startsWith("workspace:") ? "workspace:" : "";
942
- const raw = workspacePrefix ? oldRange.slice(10) : oldRange;
943
- if (raw === "*" || raw === "latest") return `${workspacePrefix}${raw}`;
944
- if (raw.includes("||") || DASH_RE.test(raw) || RANGE_OPERATION_RE.test(raw) || raw.includes(" ") && !DASH_RE.test(raw)) {
945
- if (semver.satisfies(newVersion, raw)) return `${workspacePrefix}${raw}`;
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
- updated: changed,
968
- next: changed ? next : record
1373
+ package: pkg,
1374
+ currentVersion: pkg.version,
1375
+ newVersion,
1376
+ bumpType: bump,
1377
+ hasDirectChanges
969
1378
  };
970
1379
  }
971
- var PackageUpdaterService = class extends Effect.Service()("@ucdjs/release-scripts/PackageUpdaterService", {
972
- effect: Effect.gen(function* () {
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/services/version-prompt.service.ts
1076
- const GREY = "\x1B[90m";
1077
- const RESET = "\x1B[0m";
1078
- const NON_VERSIONING_TYPES = new Set([
1079
- "chore",
1080
- "docs",
1081
- "style",
1082
- "test",
1083
- "ci",
1084
- "build",
1085
- "refactor"
1086
- ]);
1087
- function isVersioningCommit(commit) {
1088
- return !NON_VERSIONING_TYPES.has(commit.type) || commit.isBreaking;
1089
- }
1090
- function formatCommit(commit) {
1091
- const isGreyed = !isVersioningCommit(commit);
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 conventionalVersion = conventionalBump !== "none" ? semver.inc(currentVersion, conventionalBump) : null;
1149
- if (conventionalVersion) options.push({
1150
- title: `conventional ${conventionalVersion}`,
1151
- value: {
1152
- version: conventionalVersion,
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
- options.push({
1208
- title: `as-is ${currentVersion}`,
1209
- value: {
1210
- version: currentVersion,
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
- * Retrieves global commits that affect all packages in a monorepo.
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
- * Algorithm:
1456
- * 1. Find the overall commit range across all packages
1457
- * 2. Fetch all commits and file changes once for this range
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
- * Example scenario:
1462
- * - pkg-a: last released at commit A
1463
- * - global change at commit B (after A)
1464
- * - pkg-b: last released at commit C (after B)
1465
- *
1466
- * Result:
1467
- * - For pkg-a: includes commits from A to HEAD (including B)
1468
- * - For pkg-b: includes commits from C to HEAD (excluding B, since it was already in pkg-b's release range)
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
- * @param packages - Array of workspace packages with their associated commits
1471
- * @param mode - Determines which global commits to include:
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
- * @returns A map of package names to their relevant global commits
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 mergeCommitsAffectingGloballyIntoPackage(packages, mode) {
1479
- return Effect.gen(function* () {
1480
- const git = yield* GitService;
1481
- if (mode === "none") return packages;
1482
- const [oldestCommitSha, newestCommitSha] = findCommitRange(packages);
1483
- if (oldestCommitSha == null || newestCommitSha == null) return packages;
1484
- const allCommits = yield* git.commits.get({
1485
- from: oldestCommitSha,
1486
- to: newestCommitSha,
1487
- folder: "."
1488
- });
1489
- const affectedFilesPerCommit = yield* git.commits.filesChangesBetweenRefs(oldestCommitSha, newestCommitSha);
1490
- const commitTimestamps = new Map(allCommits.map((c) => [c.hash, new Date(c.date).getTime()]));
1491
- const packagePaths = new Set(packages.map((p) => p.path));
1492
- const result = /* @__PURE__ */ new Map();
1493
- for (const pkg of packages) {
1494
- const lastTag = yield* git.tags.mostRecentForPackage(pkg.name);
1495
- const cutoffTimestamp = lastTag ? commitTimestamps.get(lastTag.sha) ?? 0 : 0;
1496
- const globalCommits = [];
1497
- for (const commit of allCommits) {
1498
- const commitTimestamp = commitTimestamps.get(commit.hash);
1499
- if (commitTimestamp == null || commitTimestamp <= cutoffTimestamp) continue;
1500
- const files = affectedFilesPerCommit.get(commit.hash);
1501
- if (!files) continue;
1502
- if (isGlobalCommit(files, packagePaths)) if (mode === "dependencies") {
1503
- if (files.some((file) => isDependencyFile(file))) globalCommits.push(commit);
1504
- } else globalCommits.push(commit);
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
- result.set(pkg.name, globalCommits);
1562
+ newVersion = selectedVersion;
1507
1563
  }
1508
- return yield* Effect.succeed(packages.map((pkg) => ({
1509
- ...pkg,
1510
- globalCommits: result.get(pkg.name) || []
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
- * Determines if a commit is "global" (affects files outside any package directory).
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 isGlobalCommit(files, packagePaths) {
1522
- return files.some((file) => {
1523
- const normalized = file.startsWith("./") ? file.slice(2) : file;
1524
- for (const pkgPath of packagePaths) if (normalized === pkgPath || normalized.startsWith(`${pkgPath}/`)) return false;
1525
- return true;
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
- * Files that are considered dependency-related in a monorepo.
1530
- */
1531
- const DEPENDENCY_FILES = new Set([
1532
- "package.json",
1533
- "pnpm-lock.yaml",
1534
- "yarn.lock",
1535
- "package-lock.json",
1536
- "pnpm-workspace.yaml"
1537
- ]);
1538
- /**
1539
- * Determines if a file is dependency-related.
1540
- *
1541
- * @param file - File path to check
1542
- * @returns true if the file is a dependency file (package.json, lock files, etc.)
1543
- */
1544
- function isDependencyFile(file) {
1545
- const normalized = file.startsWith("./") ? file.slice(2) : file;
1546
- if (DEPENDENCY_FILES.has(normalized)) return true;
1547
- return Array.from(DEPENDENCY_FILES).some((dep) => normalized.endsWith(`/${dep}`));
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
- * Finds the oldest and newest commits across all packages.
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 findCommitRange(packages) {
1558
- let oldestCommit = null;
1559
- let newestCommit = null;
1560
- for (const pkg of packages) {
1561
- if (pkg.commits.length === 0) continue;
1562
- const firstCommit = pkg.commits[0];
1563
- if (!firstCommit) throw new Error(`No commits found for package ${pkg.name}`);
1564
- const lastCommit = pkg.commits[pkg.commits.length - 1];
1565
- if (!lastCommit) throw new Error(`No commits found for package ${pkg.name}`);
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 (oldestCommit == null || newestCommit == null) return [null, null];
1570
- return [oldestCommit.hash, newestCommit.hash];
1661
+ if (updates.size === 0) logger.verbose(` - No dependency updates needed`);
1662
+ return updates;
1571
1663
  }
1572
1664
 
1573
1665
  //#endregion
1574
- //#region src/prepare.ts
1575
- function constructPrepareProgram(config) {
1576
- return Effect.gen(function* () {
1577
- const changelog = yield* ChangelogService;
1578
- const git = yield* GitService;
1579
- const github = yield* GitHubService;
1580
- const dependencyGraph = yield* DependencyGraphService;
1581
- const packageUpdater = yield* PackageUpdaterService;
1582
- const versionCalculator = yield* VersionCalculatorService;
1583
- const versionPrompt = yield* VersionPromptService;
1584
- const workspace = yield* WorkspaceService;
1585
- yield* git.workspace.assertWorkspaceReady;
1586
- const startingBranch = yield* git.branches.get;
1587
- if (startingBranch !== config.branch.default) return yield* Effect.fail(/* @__PURE__ */ new Error(`Prepare must be run on the default branch "${config.branch.default}". Current branch: "${startingBranch}"`));
1588
- let releasePullRequest = yield* github.getPullRequestByBranch(config.branch.release);
1589
- const isNewRelease = !releasePullRequest;
1590
- const branchExists = yield* git.branches.exists(config.branch.release);
1591
- if (!branchExists) {
1592
- yield* Console.log(`Creating release branch "${config.branch.release}" from "${config.branch.default}"...`);
1593
- yield* git.branches.create(config.branch.release, config.branch.default);
1594
- yield* Console.log(`Release branch created.`);
1595
- }
1596
- if ((yield* git.branches.get) !== config.branch.release) {
1597
- yield* git.branches.checkout(config.branch.release);
1598
- yield* Console.log(`Checked out to release branch "${config.branch.release}".`);
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
- if (!isNewRelease || branchExists) {
1601
- yield* Console.log(`Rebasing "${config.branch.release}" onto "${config.branch.default}"...`);
1602
- yield* git.branches.rebase(config.branch.default);
1603
- yield* Console.log(`Rebase complete.`);
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
- const overrides = yield* loadOverrides({
1606
- sha: config.branch.default,
1607
- overridesPath: ".github/ucdjs-release.overrides.json"
1608
- });
1609
- if (Object.keys(overrides).length > 0) yield* Console.log("Loaded version overrides:", overrides);
1610
- const originalBranch = yield* git.branches.get;
1611
- yield* git.branches.checkout(config.branch.default);
1612
- const packages = yield* workspace.discoverWorkspacePackages.pipe(Effect.flatMap(mergePackageCommitsIntoPackages), Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)));
1613
- yield* Console.log(`Discovered ${packages.length} packages with commits.`);
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
- const releasesCount = releases.length;
1671
- yield* Console.log(`\n${releasesCount} package${releasesCount === 1 ? "" : "s"} will be released.`);
1672
- yield* git.branches.checkout(originalBranch);
1673
- yield* Console.log("Updating package.json files...");
1674
- yield* packageUpdater.applyReleases(packages, releases);
1675
- yield* Console.log("package.json files updated.");
1676
- yield* Console.log("Generating changelogs...");
1677
- const changelogFiles = [];
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
- yield* Console.log(`Generated ${changelogFiles.length} changelog file${changelogFiles.length === 1 ? "" : "s"}.`);
1691
- const filesToStage = [...releases.map((r) => `${r.package.path}/package.json`), ...changelogFiles];
1692
- yield* Console.log(`Staging ${filesToStage.length} file${filesToStage.length === 1 ? "" : "s"}...`);
1693
- yield* git.commits.stage(filesToStage);
1694
- const commitMessage = `chore(release): prepare release
1695
-
1696
- ${releasesCount} package${releasesCount === 1 ? "" : "s"} updated:
1697
- ${releases.map((r) => ` - ${r.package.name}@${r.newVersion}`).join("\n")}`;
1698
- yield* Console.log("Creating commit...");
1699
- yield* git.commits.write(commitMessage);
1700
- yield* Console.log("Commit created.");
1701
- yield* Console.log(`Pushing to "${config.branch.release}"...`);
1702
- if (isNewRelease && !branchExists) yield* git.commits.push(config.branch.release);
1703
- else yield* git.commits.forcePush(config.branch.release);
1704
- yield* Console.log(`Push complete.`);
1705
- const prBody = yield* github.generateReleasePRBody(releases.map((r) => ({
1706
- packageName: r.package.name,
1707
- version: r.newVersion,
1708
- previousVersion: r.package.version
1709
- })));
1710
- if (isNewRelease) {
1711
- yield* Console.log("Creating release pull request...");
1712
- releasePullRequest = yield* github.createPullRequest({
1713
- title: config.pullRequest.title,
1714
- body: prBody,
1715
- head: config.branch.release,
1716
- base: config.branch.default,
1717
- draft: true
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
- yield* Console.log(`Release pull request #${releasePullRequest.number} created.`);
1720
- } else {
1721
- yield* Console.log("Updating pull request...");
1722
- yield* github.updatePullRequest(releasePullRequest.number, { body: prBody });
1723
- yield* Console.log("Pull request updated.");
1724
- }
1725
- yield* Console.log(`\nRelease preparation complete! View PR: #${releasePullRequest.number}`);
1726
- if (startingBranch !== (yield* git.branches.get)) {
1727
- yield* git.branches.checkout(startingBranch);
1728
- yield* Console.log(`Switched back to "${startingBranch}".`);
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 isPrerelease(version) {
1736
- const parsed = semver.parse(version);
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
- function constructPublishProgram(config) {
1750
- return Effect.gen(function* () {
1751
- const git = yield* GitService;
1752
- const npm = yield* NPMService;
1753
- const workspace = yield* WorkspaceService;
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 satisfiesRange(range, version) {
1836
- return semver.satisfies(version, range, { includePrerelease: true });
1837
- }
1838
- function snapshotPackageJson(pkg, ref) {
1839
- return Effect.gen(function* () {
1840
- return yield* (yield* GitService).workspace.readFile(`${pkg.path}/package.json`, ref).pipe(Effect.flatMap((content) => Effect.try({
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
- function findDrift(packages, releases, branchSnapshots) {
1847
- const releaseVersionByName = /* @__PURE__ */ new Map();
1848
- for (const rel of releases) releaseVersionByName.set(rel.package.name, rel.newVersion);
1849
- const reasons = [];
1850
- for (const pkg of packages) {
1851
- const snapshot = branchSnapshots.get(pkg.name);
1852
- if (snapshot == null) {
1853
- reasons.push({
1854
- packageName: pkg.name,
1855
- reason: "package.json missing on release branch"
1856
- });
1857
- continue;
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
- if (snapshot instanceof Error) {
1860
- reasons.push({
1861
- packageName: pkg.name,
1862
- reason: snapshot.message
1863
- });
1864
- continue;
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
- const expectedVersion = releaseVersionByName.get(pkg.name) ?? pkg.version;
1867
- const branchVersion = typeof snapshot.version === "string" ? snapshot.version : void 0;
1868
- if (!branchVersion) {
1869
- reasons.push({
1870
- packageName: pkg.name,
1871
- reason: "package.json on release branch lacks version"
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 (branchVersion !== expectedVersion) reasons.push({
1876
- packageName: pkg.name,
1877
- reason: `version mismatch: expected ${expectedVersion}, found ${branchVersion}`
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
- return reasons;
1904
- }
1905
- function constructVerifyProgram(config) {
1906
- return Effect.gen(function* () {
1907
- const git = yield* GitService;
1908
- const github = yield* GitHubService;
1909
- const dependencyGraph = yield* DependencyGraphService;
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
- yield* Console.log("Loaded overrides:", overrides);
1925
- const packages = yield* workspace.discoverWorkspacePackages.pipe(Effect.flatMap(mergePackageCommitsIntoPackages), Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)));
1926
- yield* Console.log("Discovered packages with commits and global commits:", packages);
1927
- const releases = yield* versionCalculator.calculateBumps(packages, overrides);
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
- description: "Release artifacts in sync",
1943
- context: "release/verify"
1944
- } : {
1945
- state: "failure",
1946
- description: "Release branch out of sync",
1947
- context: "release/verify"
1948
- };
1949
- yield* github.setCommitStatus(releaseHeadSha, status);
1950
- if (drift.length > 0) return yield* Effect.fail(/* @__PURE__ */ new Error("Release branch is out of sync."));
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 config = normalizeReleaseScriptsOptions(options);
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 runProgram(constructVerifyProgram(config));
2041
+ return verify(normalizedOptions);
1976
2042
  },
1977
2043
  async prepare() {
1978
- return runProgram(constructPrepareProgram(config));
2044
+ return release(normalizedOptions);
1979
2045
  },
1980
2046
  async publish() {
1981
- return runProgram(constructPublishProgram(config));
2047
+ return publish(normalizedOptions);
1982
2048
  },
1983
2049
  packages: {
1984
2050
  async list() {
1985
- return runProgram(Effect.gen(function* () {
1986
- return yield* (yield* WorkspaceService).discoverWorkspacePackages;
1987
- }));
2051
+ return discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
1988
2052
  },
1989
2053
  async get(packageName) {
1990
- return runProgram(Effect.gen(function* () {
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
  };