@ucdjs/release-scripts 0.1.0-beta.3 → 0.1.0-beta.30

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,835 +1,1544 @@
1
+ import { t as Eta } from "./eta-BV8TCRDW.mjs";
2
+ import { Console, Context, Data, Effect, Layer, Schema } from "effect";
3
+ import path from "node:path";
4
+ import { Command, CommandExecutor } from "@effect/platform";
5
+ import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node";
6
+ import * as CommitParser from "commit-parser";
1
7
  import process from "node:process";
2
- import { getCommits } from "commit-parser";
3
- import createDebug from "debug";
4
- import farver from "farver";
5
- import { exec } from "tinyexec";
6
- import { readFile, writeFile } from "node:fs/promises";
7
- import { join } from "node:path";
8
- import prompts from "prompts";
8
+ import semver from "semver";
9
+ import fs from "node:fs/promises";
9
10
 
10
- //#region src/logger.ts
11
- function createDebugger(namespace) {
12
- const debug$2 = createDebug(namespace);
13
- if (debug$2.enabled) return debug$2;
14
- }
15
-
16
- //#endregion
17
- //#region src/utils.ts
18
- const globalOptions = { dryRun: false };
19
- async function run(bin, args, opts = {}) {
20
- return exec(bin, args, {
21
- throwOnError: true,
22
- ...opts,
23
- nodeOptions: {
24
- stdio: "inherit",
25
- ...opts.nodeOptions
26
- }
27
- });
28
- }
29
- async function dryRun(bin, args, opts) {
30
- return console.log(farver.blue(`[dryrun] ${bin} ${args.join(" ")}`), opts || "");
31
- }
32
- const runIfNotDry = globalOptions.dryRun ? dryRun : run;
33
-
34
- //#endregion
35
- //#region src/commits.ts
36
- const debug$1 = createDebugger("ucdjs:release-scripts:commits");
37
- async function getLastPackageTag(packageName, workspaceRoot) {
38
- const { stdout } = await run("git", ["tag", "--list"], { nodeOptions: {
39
- cwd: workspaceRoot,
40
- stdio: "pipe"
41
- } });
42
- return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse().find((tag) => tag.startsWith(`${packageName}@`));
43
- }
44
- function determineHighestBump(commits) {
45
- if (commits.length === 0) return "none";
46
- let highestBump = "none";
47
- for (const commit of commits) {
48
- const bump = determineBumpType(commit);
49
- if (bump === "major") return "major";
50
- if (bump === "minor") highestBump = "minor";
51
- else if (bump === "patch" && highestBump === "none") highestBump = "patch";
52
- }
53
- return highestBump;
54
- }
55
- async function getPackageCommits(pkg, workspaceRoot) {
56
- const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
57
- const allCommits = getCommits({
58
- from: lastTag,
59
- to: "HEAD"
60
- });
61
- debug$1?.(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
62
- const touchedCommitHashes = await getCommitsTouchingPackage(lastTag || "HEAD", "HEAD", pkg.path, workspaceRoot);
63
- const touchedSet = new Set(touchedCommitHashes);
64
- const packageCommits = allCommits.filter((commit) => touchedSet.has(commit.shortHash));
65
- debug$1?.(`${packageCommits.length} commits affect ${pkg.name}`);
66
- return packageCommits;
67
- }
68
- async function analyzePackageCommits(pkg, workspaceRoot) {
69
- return determineHighestBump(await getPackageCommits(pkg, workspaceRoot));
70
- }
71
- function determineBumpType(commit) {
72
- if (commit.isBreaking) return "major";
73
- if (!commit.isConventional || !commit.type) return "none";
74
- switch (commit.type) {
75
- case "feat": return "minor";
76
- case "fix":
77
- case "perf": return "patch";
78
- case "docs":
79
- case "style":
80
- case "refactor":
81
- case "test":
82
- case "build":
83
- case "ci":
84
- case "chore":
85
- case "revert": return "none";
86
- default: return "none";
87
- }
88
- }
89
- async function getCommitsTouchingPackage(from, to, packagePath, workspaceRoot) {
90
- try {
91
- const { stdout } = await run("git", [
92
- "log",
93
- "--pretty=format:%h",
94
- from === "HEAD" ? "HEAD" : `${from}...${to}`,
95
- "--",
96
- packagePath
97
- ], { nodeOptions: {
98
- cwd: workspaceRoot,
99
- stdio: "pipe"
100
- } });
101
- return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
102
- } catch (error) {
103
- debug$1?.(`Error getting commits touching package: ${error}`);
104
- return [];
105
- }
106
- }
107
-
108
- //#endregion
109
- //#region src/validation.ts
11
+ //#region src/utils/changelog-formatters.ts
12
+ const eta$1 = new Eta();
110
13
  /**
111
- * Validation utilities for release scripts
14
+ * Pure function to parse commits into changelog entries
112
15
  */
113
- function isValidSemver(version) {
114
- return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
115
- }
116
- function validateSemver(version) {
117
- if (!isValidSemver(version)) throw new Error(`Invalid semver version: ${version}`);
16
+ function parseCommits(commits) {
17
+ return commits.filter((commit) => commit.isConventional).map((commit) => ({
18
+ type: commit.type || "other",
19
+ scope: commit.scope,
20
+ description: commit.description,
21
+ breaking: commit.isBreaking || false,
22
+ hash: commit.hash,
23
+ shortHash: commit.shortHash,
24
+ references: commit.references.map((ref) => ({
25
+ type: ref.type,
26
+ value: ref.value
27
+ }))
28
+ }));
118
29
  }
119
-
120
- //#endregion
121
- //#region src/version.ts
122
30
  /**
123
- * Calculate the new version based on current version and bump type
124
- * Pure function - no side effects, easily testable
31
+ * Pure function to group changelog entries by type
125
32
  */
126
- function calculateNewVersion(currentVersion, bump) {
127
- if (bump === "none") return currentVersion;
128
- validateSemver(currentVersion);
129
- const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
130
- if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
131
- const [, major, minor, patch, suffix] = match;
132
- let newMajor = Number.parseInt(major, 10);
133
- let newMinor = Number.parseInt(minor, 10);
134
- let newPatch = Number.parseInt(patch, 10);
135
- switch (bump) {
136
- case "major":
137
- newMajor += 1;
138
- newMinor = 0;
139
- newPatch = 0;
140
- break;
141
- case "minor":
142
- newMinor += 1;
143
- newPatch = 0;
144
- break;
145
- case "patch":
146
- newPatch += 1;
147
- break;
33
+ function groupByType(entries) {
34
+ const groups = /* @__PURE__ */ new Map();
35
+ for (const entry of entries) {
36
+ const type = entry.breaking ? "breaking" : entry.type;
37
+ if (!groups.has(type)) groups.set(type, []);
38
+ groups.get(type).push(entry);
148
39
  }
149
- return `${newMajor}.${newMinor}.${newPatch}`;
40
+ return groups;
150
41
  }
151
42
  /**
152
- * Create a version update object
43
+ * Changelog template for Eta rendering
153
44
  */
154
- function createVersionUpdate(pkg, bump, hasDirectChanges) {
155
- const newVersion = calculateNewVersion(pkg.version, bump);
156
- return {
157
- package: pkg,
158
- currentVersion: pkg.version,
159
- newVersion,
160
- bumpType: bump,
161
- hasDirectChanges
162
- };
163
- }
45
+ const CHANGELOG_TEMPLATE = `# <%= it.packageName %> v<%= it.version %>
46
+
47
+ **Previous version**: \`<%= it.previousVersion %>\`
48
+ **New version**: \`<%= it.version %>\`
49
+
50
+ <% if (it.entries.length === 0) { %>
51
+ *No conventional commits found.*
52
+ <% } else { %>
53
+ <% const groups = it.groupedEntries; %>
54
+ <% const typeOrder = ["breaking", "feat", "fix", "perf", "docs", "style", "refactor", "test", "build", "ci", "chore"]; %>
55
+ <% const typeLabels = {
56
+ breaking: "💥 Breaking Changes",
57
+ feat: "✨ Features",
58
+ fix: "🐛 Bug Fixes",
59
+ perf: "⚡ Performance",
60
+ docs: "📝 Documentation",
61
+ style: "💄 Styling",
62
+ refactor: "♻️ Refactoring",
63
+ test: "✅ Tests",
64
+ build: "📦 Build",
65
+ ci: "👷 CI",
66
+ chore: "🔧 Chores"
67
+ }; %>
68
+
69
+ <% for (const type of typeOrder) { %>
70
+ <% const entries = groups.get(type); %>
71
+ <% if (entries && entries.length > 0) { %>
72
+ ## <%= typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1) %>
73
+
74
+ <% for (const entry of entries) { %>
75
+ - <% if (entry.scope) { %>**<%= entry.scope %>**: <% } %><%= entry.description %><% if (entry.references.length > 0) { %> (<%= entry.references.map(r => "#" + r.value).join(", ") %>)<% } %> (\`<%= entry.shortHash %>\`)
76
+ <% } %>
77
+
78
+ <% } %>
79
+ <% } %>
80
+
81
+ <% for (const [type, entries] of groups) { %>
82
+ <% if (!typeOrder.includes(type)) { %>
83
+ ## <%= type.charAt(0).toUpperCase() + type.slice(1) %>
84
+
85
+ <% for (const entry of entries) { %>
86
+ - <% if (entry.scope) { %>**<%= entry.scope %>**: <% } %><%= entry.description %> (\`<%= entry.shortHash %>\`)
87
+ <% } %>
88
+
89
+ <% } %>
90
+ <% } %>
91
+ <% } %>`;
164
92
  /**
165
- * Update a package.json file with new version and dependency versions
93
+ * Pure function to format changelog as markdown
166
94
  */
167
- async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
168
- const packageJsonPath = join(pkg.path, "package.json");
169
- const content = await readFile(packageJsonPath, "utf-8");
170
- const packageJson = JSON.parse(content);
171
- packageJson.version = newVersion;
172
- for (const [depName, depVersion] of dependencyUpdates) {
173
- if (packageJson.dependencies?.[depName]) {
174
- if (packageJson.dependencies[depName] === "workspace:*") continue;
175
- packageJson.dependencies[depName] = `^${depVersion}`;
176
- }
177
- if (packageJson.devDependencies?.[depName]) {
178
- if (packageJson.devDependencies[depName] === "workspace:*") continue;
179
- packageJson.devDependencies[depName] = `^${depVersion}`;
180
- }
181
- if (packageJson.peerDependencies?.[depName]) {
182
- if (packageJson.peerDependencies[depName] === "workspace:*") continue;
183
- packageJson.peerDependencies[depName] = `^${depVersion}`;
184
- }
185
- }
186
- await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
95
+ function formatChangelogMarkdown(changelog) {
96
+ const groups = groupByType(changelog.entries);
97
+ return eta$1.renderString(CHANGELOG_TEMPLATE, {
98
+ packageName: changelog.packageName,
99
+ version: changelog.version,
100
+ previousVersion: changelog.previousVersion,
101
+ entries: changelog.entries,
102
+ groupedEntries: groups
103
+ });
187
104
  }
188
105
  /**
189
- * Get all dependency updates needed for a package
106
+ * Pure function to create a changelog object
190
107
  */
191
- function getDependencyUpdates(pkg, allUpdates) {
192
- const updates = /* @__PURE__ */ new Map();
193
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
194
- for (const dep of allDeps) {
195
- const update = allUpdates.find((u) => u.package.name === dep);
196
- if (update) updates.set(dep, update.newVersion);
197
- }
198
- return updates;
108
+ function createChangelog(packageName, version, previousVersion, commits) {
109
+ return {
110
+ packageName,
111
+ version,
112
+ previousVersion,
113
+ entries: parseCommits(commits)
114
+ };
199
115
  }
200
116
 
201
117
  //#endregion
202
- //#region src/dependencies.ts
203
- /**
204
- * Pure function: Determine which packages need updates due to dependency changes
205
- *
206
- * When a package is updated, all packages that depend on it should also be updated.
207
- * This function calculates which additional packages need patch bumps.
208
- *
209
- * @param updateOrder - Packages in topological order with their dependency levels
210
- * @param directUpdates - Packages with direct code changes
211
- * @returns All updates including dependent packages
212
- */
213
- function createDependentUpdates(updateOrder, directUpdates) {
214
- const allUpdates = [...directUpdates];
215
- const updatedPackages = new Set(directUpdates.map((u) => u.package.name));
216
- for (const { package: pkg } of updateOrder) {
217
- if (updatedPackages.has(pkg.name)) continue;
218
- if (hasUpdatedDependencies(pkg, updatedPackages)) {
219
- allUpdates.push(createVersionUpdate(pkg, "patch", false));
220
- updatedPackages.add(pkg.name);
118
+ //#region src/services/changelog.service.ts
119
+ var ChangelogService = class extends Effect.Service()("@ucdjs/release-scripts/ChangelogService", {
120
+ effect: Effect.gen(function* () {
121
+ function generateChangelog(pkg, newVersion, commits) {
122
+ return Effect.gen(function* () {
123
+ const changelog = createChangelog(pkg.name, newVersion, pkg.version, commits);
124
+ return {
125
+ changelog,
126
+ markdown: formatChangelogMarkdown(changelog),
127
+ filePath: `${pkg.path}/CHANGELOG.md`
128
+ };
129
+ });
130
+ }
131
+ return { generateChangelog };
132
+ }),
133
+ dependencies: []
134
+ }) {};
135
+
136
+ //#endregion
137
+ //#region src/services/dependency-graph.service.ts
138
+ var DependencyGraphService = class extends Effect.Service()("@ucdjs/release-scripts/DependencyGraphService", {
139
+ effect: Effect.gen(function* () {
140
+ function buildGraph(packages) {
141
+ const nameToPackage = /* @__PURE__ */ new Map();
142
+ const adjacency = /* @__PURE__ */ new Map();
143
+ const inDegree = /* @__PURE__ */ new Map();
144
+ for (const pkg of packages) {
145
+ nameToPackage.set(pkg.name, pkg);
146
+ adjacency.set(pkg.name, /* @__PURE__ */ new Set());
147
+ inDegree.set(pkg.name, 0);
148
+ }
149
+ for (const pkg of packages) {
150
+ const deps = new Set([...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies]);
151
+ for (const depName of deps) {
152
+ if (!nameToPackage.has(depName)) continue;
153
+ adjacency.get(depName)?.add(pkg.name);
154
+ inDegree.set(pkg.name, (inDegree.get(pkg.name) ?? 0) + 1);
155
+ }
156
+ }
157
+ return {
158
+ nameToPackage,
159
+ adjacency,
160
+ inDegree
161
+ };
221
162
  }
163
+ function topologicalOrder(packages) {
164
+ return Effect.gen(function* () {
165
+ const { nameToPackage, adjacency, inDegree } = buildGraph(packages);
166
+ const queue = [];
167
+ const levels = /* @__PURE__ */ new Map();
168
+ for (const [name, degree] of inDegree) if (degree === 0) {
169
+ queue.push(name);
170
+ levels.set(name, 0);
171
+ }
172
+ let queueIndex = 0;
173
+ const ordered = [];
174
+ while (queueIndex < queue.length) {
175
+ const current = queue[queueIndex++];
176
+ const currentLevel = levels.get(current) ?? 0;
177
+ const pkg = nameToPackage.get(current);
178
+ if (pkg) ordered.push({
179
+ package: pkg,
180
+ level: currentLevel
181
+ });
182
+ for (const neighbor of adjacency.get(current) ?? []) {
183
+ const nextLevel = currentLevel + 1;
184
+ if (nextLevel > (levels.get(neighbor) ?? 0)) levels.set(neighbor, nextLevel);
185
+ const newDegree = (inDegree.get(neighbor) ?? 0) - 1;
186
+ inDegree.set(neighbor, newDegree);
187
+ if (newDegree === 0) queue.push(neighbor);
188
+ }
189
+ }
190
+ if (ordered.length !== packages.length) {
191
+ const processed = new Set(ordered.map((o) => o.package.name));
192
+ const unprocessed = packages.filter((p) => !processed.has(p.name)).map((p) => p.name);
193
+ return yield* Effect.fail(/* @__PURE__ */ new Error(`Cycle detected in workspace dependencies. Packages involved: ${unprocessed.join(", ")}`));
194
+ }
195
+ return ordered;
196
+ });
197
+ }
198
+ return { topologicalOrder };
199
+ }),
200
+ dependencies: []
201
+ }) {};
202
+
203
+ //#endregion
204
+ //#region src/errors.ts
205
+ var GitCommandError = class extends Data.TaggedError("GitCommandError") {};
206
+ var ExternalCommitParserError = class extends Data.TaggedError("ExternalCommitParserError") {};
207
+ var WorkspaceError = class extends Data.TaggedError("WorkspaceError") {};
208
+ var GitHubError = class extends Data.TaggedError("GitHubError") {};
209
+ var VersionCalculationError = class extends Data.TaggedError("VersionCalculationError") {};
210
+ var OverridesLoadError = class extends Data.TaggedError("OverridesLoadError") {};
211
+ var NPMError = class extends Data.TaggedError("NPMError") {};
212
+ var PublishError = class extends Data.TaggedError("PublishError") {};
213
+ var TagError = class extends Data.TaggedError("TagError") {};
214
+
215
+ //#endregion
216
+ //#region src/options.ts
217
+ 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}}`;
218
+ const DEFAULT_CHANGELOG_TEMPLATE = `# Changelog\n\n{{releases}}`;
219
+ const DEFAULT_TYPES = {
220
+ feat: {
221
+ title: "🚀 Features",
222
+ color: "green"
223
+ },
224
+ fix: {
225
+ title: "🐞 Bug Fixes",
226
+ color: "red"
227
+ },
228
+ refactor: {
229
+ title: "🔧 Code Refactoring",
230
+ color: "blue"
231
+ },
232
+ perf: {
233
+ title: "🏎 Performance",
234
+ color: "orange"
235
+ },
236
+ docs: {
237
+ title: "📚 Documentation",
238
+ color: "purple"
239
+ },
240
+ style: {
241
+ title: "🎨 Styles",
242
+ color: "pink"
222
243
  }
223
- return allUpdates;
224
- }
225
- /**
226
- * Pure function: Check if a package has any updated dependencies
227
- */
228
- function hasUpdatedDependencies(pkg, updatedPackages) {
229
- return [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies].some((dep) => updatedPackages.has(dep));
244
+ };
245
+ function normalizeReleaseScriptsOptions(options) {
246
+ const { workspaceRoot = process.cwd(), githubToken = "", repo: fullRepo, packages = true, branch = {}, globalCommitMode = "dependencies", pullRequest = {}, changelog = {}, types = {}, dryRun = false, npm = {} } = options;
247
+ const token = githubToken.trim();
248
+ if (!token) throw new Error("GitHub token is required. Pass it in via options.");
249
+ if (!fullRepo || !fullRepo.trim() || !fullRepo.includes("/")) throw new Error("Repository (repo) is required. Specify in 'owner/repo' format (e.g., 'octocat/hello-world').");
250
+ const [owner, repo] = fullRepo.split("/");
251
+ if (!owner || !repo) throw new Error(`Invalid repo format: "${fullRepo}". Expected format: "owner/repo" (e.g., "octocat/hello-world").`);
252
+ return {
253
+ dryRun,
254
+ workspaceRoot,
255
+ githubToken: token,
256
+ owner,
257
+ repo,
258
+ packages: typeof packages === "object" && !Array.isArray(packages) ? {
259
+ exclude: packages.exclude ?? [],
260
+ include: packages.include ?? [],
261
+ excludePrivate: packages.excludePrivate ?? false
262
+ } : packages,
263
+ branch: {
264
+ release: branch.release ?? "release/next",
265
+ default: branch.default ?? "main"
266
+ },
267
+ globalCommitMode,
268
+ pullRequest: {
269
+ title: pullRequest.title ?? "chore: release new version",
270
+ body: pullRequest.body ?? DEFAULT_PR_BODY_TEMPLATE
271
+ },
272
+ changelog: {
273
+ enabled: changelog.enabled ?? true,
274
+ template: changelog.template ?? DEFAULT_CHANGELOG_TEMPLATE,
275
+ emojis: changelog.emojis ?? true
276
+ },
277
+ types: options.types ? {
278
+ ...DEFAULT_TYPES,
279
+ ...types
280
+ } : DEFAULT_TYPES,
281
+ npm: {
282
+ otp: npm.otp,
283
+ provenance: npm.provenance ?? true
284
+ }
285
+ };
230
286
  }
287
+ var ReleaseScriptsOptions = class extends Context.Tag("@ucdjs/release-scripts/ReleaseScriptsOptions")() {};
231
288
 
232
289
  //#endregion
233
- //#region src/git.ts
234
- /**
235
- * Check if the working directory is clean (no uncommitted changes)
236
- * @param {string} workspaceRoot - The root directory of the workspace
237
- * @returns {Promise<boolean>} A Promise resolving to true if clean, false otherwise
238
- */
239
- async function isWorkingDirectoryClean(workspaceRoot) {
240
- try {
241
- if ((await run("git", ["status", "--porcelain"], { nodeOptions: {
242
- cwd: workspaceRoot,
243
- stdio: "pipe"
244
- } })).stdout.trim() !== "") return false;
245
- return true;
246
- } catch (err) {
247
- console.error("Error checking git status:", err);
248
- return false;
290
+ //#region src/services/git.service.ts
291
+ var GitService = class extends Effect.Service()("@ucdjs/release-scripts/GitService", {
292
+ effect: Effect.gen(function* () {
293
+ const executor = yield* CommandExecutor.CommandExecutor;
294
+ const config = yield* ReleaseScriptsOptions;
295
+ const execGitCommand = (args) => executor.string(Command.make("git", ...args).pipe(Command.workingDirectory(config.workspaceRoot))).pipe(Effect.mapError((err) => {
296
+ return new GitCommandError({
297
+ command: `git ${args.join(" ")}`,
298
+ stderr: err.message
299
+ });
300
+ }));
301
+ const execGitCommandIfNotDry = config.dryRun ? (args) => Effect.succeed(`Dry run mode: skipping git command "git ${args.join(" ")}"`) : execGitCommand;
302
+ const isWithinRepository = Effect.gen(function* () {
303
+ return (yield* execGitCommand(["rev-parse", "--is-inside-work-tree"]).pipe(Effect.catchAll(() => Effect.succeed("false")))).trim() === "true";
304
+ });
305
+ const listBranches = Effect.gen(function* () {
306
+ return (yield* execGitCommand(["branch", "--list"])).trim().split("\n").filter((line) => line.length > 0).map((line) => line.replace(/^\* /, "").trim()).map((line) => line.trim());
307
+ });
308
+ const isWorkingDirectoryClean = Effect.gen(function* () {
309
+ return (yield* execGitCommand(["status", "--porcelain"])).trim().length === 0;
310
+ });
311
+ function doesBranchExist(branch) {
312
+ return listBranches.pipe(Effect.map((branches) => branches.includes(branch)));
313
+ }
314
+ function createBranch(branch, base = config.branch.default) {
315
+ return execGitCommandIfNotDry([
316
+ "branch",
317
+ branch,
318
+ base
319
+ ]);
320
+ }
321
+ const getBranch = Effect.gen(function* () {
322
+ return (yield* execGitCommand([
323
+ "rev-parse",
324
+ "--abbrev-ref",
325
+ "HEAD"
326
+ ])).trim();
327
+ });
328
+ function checkoutBranch(branch) {
329
+ return execGitCommand(["checkout", branch]);
330
+ }
331
+ function rebaseBranch(onto) {
332
+ return execGitCommandIfNotDry(["rebase", onto]);
333
+ }
334
+ function stageChanges(files) {
335
+ return Effect.gen(function* () {
336
+ if (files.length === 0) return yield* Effect.fail(/* @__PURE__ */ new Error("No files to stage."));
337
+ return yield* execGitCommandIfNotDry(["add", ...files]);
338
+ });
339
+ }
340
+ function writeCommit(message) {
341
+ return execGitCommandIfNotDry([
342
+ "commit",
343
+ "-m",
344
+ message
345
+ ]);
346
+ }
347
+ function writeEmptyCommit(message) {
348
+ return execGitCommandIfNotDry([
349
+ "commit",
350
+ "--allow-empty",
351
+ "-m",
352
+ message
353
+ ]);
354
+ }
355
+ function pushChanges(branch, remote = "origin") {
356
+ return execGitCommandIfNotDry([
357
+ "push",
358
+ remote,
359
+ branch
360
+ ]);
361
+ }
362
+ function forcePushChanges(branch, remote = "origin") {
363
+ return execGitCommandIfNotDry([
364
+ "push",
365
+ "--force-with-lease",
366
+ remote,
367
+ branch
368
+ ]);
369
+ }
370
+ function readFile(filePath, ref = "HEAD") {
371
+ return execGitCommand(["show", `${ref}:${filePath}`]);
372
+ }
373
+ function getMostRecentPackageTag(packageName) {
374
+ return execGitCommand([
375
+ "tag",
376
+ "--list",
377
+ "--sort=-version:refname",
378
+ `${packageName}@*`
379
+ ]).pipe(Effect.map((tags) => {
380
+ return tags.trim().split("\n").map((tag) => tag.trim()).filter((tag) => tag.length > 0)[0] || null;
381
+ }), Effect.flatMap((tag) => {
382
+ if (tag === null) return Effect.succeed(null);
383
+ return execGitCommand(["rev-parse", tag]).pipe(Effect.map((sha) => ({
384
+ name: tag,
385
+ sha: sha.trim()
386
+ })));
387
+ }));
388
+ }
389
+ function createTag(name, message) {
390
+ return execGitCommandIfNotDry(message ? [
391
+ "tag",
392
+ "-a",
393
+ name,
394
+ "-m",
395
+ message
396
+ ] : ["tag", name]).pipe(Effect.mapError((err) => new TagError({
397
+ message: `Failed to create tag "${name}"`,
398
+ tagName: name,
399
+ operation: "create",
400
+ cause: err
401
+ })));
402
+ }
403
+ function pushTag(name, remote = "origin") {
404
+ return execGitCommandIfNotDry([
405
+ "push",
406
+ remote,
407
+ name
408
+ ]).pipe(Effect.mapError((err) => new TagError({
409
+ message: `Failed to push tag "${name}" to ${remote}`,
410
+ tagName: name,
411
+ operation: "push",
412
+ cause: err
413
+ })));
414
+ }
415
+ function getCommits(options) {
416
+ return Effect.tryPromise({
417
+ try: async () => CommitParser.getCommits({
418
+ from: options?.from,
419
+ to: options?.to,
420
+ folder: options?.folder,
421
+ cwd: config.workspaceRoot
422
+ }),
423
+ catch: (e) => new ExternalCommitParserError({
424
+ message: `commit-parser getCommits`,
425
+ cause: e instanceof Error ? e.message : String(e)
426
+ })
427
+ });
428
+ }
429
+ function filesChangesBetweenRefs(from, to) {
430
+ const commitsMap = /* @__PURE__ */ new Map();
431
+ return execGitCommand([
432
+ "log",
433
+ "--name-only",
434
+ "--format=%H",
435
+ `${from}^..${to}`
436
+ ]).pipe(Effect.map((output) => {
437
+ const lines = output.trim().split("\n").filter((line) => line.trim() !== "");
438
+ let currentSha = null;
439
+ const HASH_REGEX = /^[0-9a-f]{40}$/i;
440
+ for (const line of lines) {
441
+ const trimmedLine = line.trim();
442
+ if (HASH_REGEX.test(trimmedLine)) {
443
+ currentSha = trimmedLine;
444
+ commitsMap.set(currentSha, []);
445
+ continue;
446
+ }
447
+ if (currentSha === null) continue;
448
+ commitsMap.get(currentSha).push(trimmedLine);
449
+ }
450
+ return commitsMap;
451
+ }));
452
+ }
453
+ const assertWorkspaceReady = Effect.gen(function* () {
454
+ if (!(yield* isWithinRepository)) return yield* Effect.fail(/* @__PURE__ */ new Error("Not within a Git repository."));
455
+ if (!(yield* isWorkingDirectoryClean)) return yield* Effect.fail(/* @__PURE__ */ new Error("Working directory is not clean."));
456
+ return true;
457
+ });
458
+ return {
459
+ branches: {
460
+ list: listBranches,
461
+ exists: doesBranchExist,
462
+ create: createBranch,
463
+ checkout: checkoutBranch,
464
+ rebase: rebaseBranch,
465
+ get: getBranch
466
+ },
467
+ commits: {
468
+ stage: stageChanges,
469
+ write: writeCommit,
470
+ writeEmpty: writeEmptyCommit,
471
+ push: pushChanges,
472
+ forcePush: forcePushChanges,
473
+ get: getCommits,
474
+ filesChangesBetweenRefs
475
+ },
476
+ tags: {
477
+ mostRecentForPackage: getMostRecentPackageTag,
478
+ create: createTag,
479
+ push: pushTag
480
+ },
481
+ workspace: {
482
+ readFile,
483
+ isWithinRepository,
484
+ isWorkingDirectoryClean,
485
+ assertWorkspaceReady
486
+ }
487
+ };
488
+ }),
489
+ dependencies: [NodeCommandExecutor.layer]
490
+ }) {};
491
+
492
+ //#endregion
493
+ //#region src/services/github.service.ts
494
+ const eta = new Eta();
495
+ const PullRequestSchema = Schema.Struct({
496
+ number: Schema.Number,
497
+ title: Schema.String,
498
+ body: Schema.String,
499
+ head: Schema.Struct({
500
+ ref: Schema.String,
501
+ sha: Schema.String
502
+ }),
503
+ base: Schema.Struct({
504
+ ref: Schema.String,
505
+ sha: Schema.String
506
+ }),
507
+ state: Schema.Literal("open", "closed", "merged"),
508
+ draft: Schema.Boolean,
509
+ mergeable: Schema.NullOr(Schema.Boolean),
510
+ url: Schema.String,
511
+ html_url: Schema.String
512
+ });
513
+ const CreatePullRequestOptionsSchema = Schema.Struct({
514
+ title: Schema.String,
515
+ body: Schema.NullOr(Schema.String),
516
+ head: Schema.String,
517
+ base: Schema.String,
518
+ draft: Schema.optional(Schema.Boolean)
519
+ });
520
+ const UpdatePullRequestOptionsSchema = Schema.Struct({
521
+ title: Schema.optional(Schema.String),
522
+ body: Schema.optional(Schema.String),
523
+ state: Schema.optional(Schema.Literal("open", "closed"))
524
+ });
525
+ const CommitStatusSchema = Schema.Struct({
526
+ state: Schema.Literal("pending", "success", "error", "failure"),
527
+ target_url: Schema.optional(Schema.String),
528
+ description: Schema.optional(Schema.String),
529
+ context: Schema.String
530
+ });
531
+ const RepositoryInfoSchema = Schema.Struct({
532
+ owner: Schema.String,
533
+ repo: Schema.String
534
+ });
535
+ var GitHubService = class extends Effect.Service()("@ucdjs/release-scripts/GitHubService", {
536
+ effect: Effect.gen(function* () {
537
+ const config = yield* ReleaseScriptsOptions;
538
+ function makeRequest(endpoint, schema, options = {}) {
539
+ const url = `https://api.github.com/repos/${config.owner}/${config.repo}/${endpoint}`;
540
+ return Effect.tryPromise({
541
+ try: async () => {
542
+ const res = await fetch(url, {
543
+ ...options,
544
+ headers: {
545
+ "Authorization": `token ${config.githubToken}`,
546
+ "Accept": "application/vnd.github.v3+json",
547
+ "Content-Type": "application/json",
548
+ "User-Agent": "ucdjs-release-scripts (https://github.com/ucdjs/release-scripts)",
549
+ ...options.headers
550
+ }
551
+ });
552
+ if (!res.ok) {
553
+ const text = await res.text();
554
+ throw new Error(`GitHub API request failed with status ${res.status}: ${text}`);
555
+ }
556
+ if (res.status === 204) return;
557
+ return res.json();
558
+ },
559
+ catch: (e) => new GitHubError({
560
+ message: String(e),
561
+ operation: "request",
562
+ cause: e
563
+ })
564
+ }).pipe(Effect.flatMap((json) => json === void 0 ? Effect.succeed(void 0) : Schema.decodeUnknown(schema)(json).pipe(Effect.mapError((e) => new GitHubError({
565
+ message: "Failed to decode GitHub response",
566
+ operation: "request",
567
+ cause: e
568
+ })))));
569
+ }
570
+ function getPullRequestByBranch(branch) {
571
+ const head = branch.includes(":") ? branch : `${config.owner}:${branch}`;
572
+ 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({
573
+ message: e.message,
574
+ operation: "getPullRequestByBranch",
575
+ cause: e.cause
576
+ })));
577
+ }
578
+ function setCommitStatus(sha, status) {
579
+ return makeRequest(`statuses/${sha}`, Schema.Unknown, {
580
+ method: "POST",
581
+ body: JSON.stringify(status)
582
+ }).pipe(Effect.map(() => status), Effect.catchAll((e) => Effect.fail(new GitHubError({
583
+ message: e.message,
584
+ operation: "setCommitStatus",
585
+ cause: e.cause
586
+ }))));
587
+ }
588
+ function updatePullRequest(number, options) {
589
+ return makeRequest(`pulls/${number}`, PullRequestSchema, {
590
+ method: "PATCH",
591
+ body: JSON.stringify(options)
592
+ }).pipe(Effect.mapError((e) => new GitHubError({
593
+ message: e.message,
594
+ operation: "updatePullRequest",
595
+ cause: e.cause
596
+ })));
597
+ }
598
+ function createPullRequest(options) {
599
+ return makeRequest("pulls", PullRequestSchema, {
600
+ method: "POST",
601
+ body: JSON.stringify(options)
602
+ }).pipe(Effect.mapError((e) => new GitHubError({
603
+ message: e.message,
604
+ operation: "createPullRequest",
605
+ cause: e.cause
606
+ })));
607
+ }
608
+ const prBodyTemplate = `## Release Summary
609
+
610
+ This PR prepares the release of <%= it.count %> package<%= it.count === 1 ? "" : "s" %>:
611
+
612
+ <% for (const release of it.releases) { %>
613
+ - **<%= release.packageName %>**: \`<%= release.previousVersion %>\` → \`<%= release.version %>\`
614
+ <% } %>
615
+
616
+ ## Changes
617
+
618
+ See individual package changelogs for details.
619
+ `;
620
+ function generateReleasePRBody(releases) {
621
+ return Effect.gen(function* () {
622
+ return eta.renderString(prBodyTemplate, {
623
+ count: releases.length,
624
+ releases
625
+ });
626
+ });
627
+ }
628
+ return {
629
+ getPullRequestByBranch,
630
+ setCommitStatus,
631
+ createPullRequest,
632
+ updatePullRequest,
633
+ generateReleasePRBody
634
+ };
635
+ }),
636
+ dependencies: []
637
+ }) {};
638
+
639
+ //#endregion
640
+ //#region src/services/npm.service.ts
641
+ const PackumentSchema = Schema.Struct({
642
+ "name": Schema.String,
643
+ "dist-tags": Schema.Record({
644
+ key: Schema.String,
645
+ value: Schema.String
646
+ }),
647
+ "versions": Schema.Record({
648
+ key: Schema.String,
649
+ value: Schema.Struct({
650
+ name: Schema.String,
651
+ version: Schema.String,
652
+ description: Schema.optional(Schema.String),
653
+ dist: Schema.Struct({
654
+ tarball: Schema.String,
655
+ shasum: Schema.String,
656
+ integrity: Schema.optional(Schema.String)
657
+ })
658
+ })
659
+ })
660
+ });
661
+ var NPMService = class extends Effect.Service()("@ucdjs/release-scripts/NPMService", {
662
+ effect: Effect.gen(function* () {
663
+ const executor = yield* CommandExecutor.CommandExecutor;
664
+ const config = yield* ReleaseScriptsOptions;
665
+ const fetchPackument = (packageName) => Effect.tryPromise({
666
+ try: async () => {
667
+ const response = await fetch(`https://registry.npmjs.org/${packageName}`);
668
+ if (response.status === 404) return null;
669
+ if (!response.ok) throw new Error(`Failed to fetch packument: ${response.statusText}`);
670
+ return await response.json();
671
+ },
672
+ catch: (error) => {
673
+ return new NPMError({
674
+ message: error instanceof Error ? error.message : String(error),
675
+ operation: "fetchPackument"
676
+ });
677
+ }
678
+ }).pipe(Effect.flatMap((data) => {
679
+ if (data === null) return Effect.succeed(null);
680
+ return Schema.decodeUnknown(PackumentSchema)(data).pipe(Effect.mapError((error) => new NPMError({
681
+ message: `Failed to parse packument: ${error}`,
682
+ operation: "fetchPackument"
683
+ })));
684
+ }));
685
+ const versionExists = (packageName, version) => fetchPackument(packageName).pipe(Effect.map((packument) => {
686
+ if (!packument) return false;
687
+ return version in packument.versions;
688
+ }));
689
+ const getLatestVersion = (packageName) => fetchPackument(packageName).pipe(Effect.map((packument) => {
690
+ if (!packument) return null;
691
+ return packument["dist-tags"].latest || null;
692
+ }));
693
+ const publish = (options) => Effect.gen(function* () {
694
+ const args = ["publish"];
695
+ if (options.tagName) args.push("--tag", options.tagName);
696
+ if (options.otp) args.push("--otp", options.otp);
697
+ if (options.provenance !== false) args.push("--provenance");
698
+ if (options.dryRun ?? config.dryRun) args.push("--dry-run");
699
+ const command = Command.make("pnpm", ...args).pipe(Command.workingDirectory(options.packagePath));
700
+ return (yield* executor.string(command).pipe(Effect.mapError((err) => new PublishError({
701
+ message: `Failed to publish package at ${options.packagePath}: ${err.message}`,
702
+ cause: err
703
+ })))).trim();
704
+ });
705
+ return {
706
+ fetchPackument,
707
+ versionExists,
708
+ getLatestVersion,
709
+ publish
710
+ };
711
+ }),
712
+ dependencies: [NodeCommandExecutor.layer]
713
+ }) {};
714
+
715
+ //#endregion
716
+ //#region src/services/workspace.service.ts
717
+ const DependencyObjectSchema = Schema.Record({
718
+ key: Schema.String,
719
+ value: Schema.String
720
+ });
721
+ const PackageJsonSchema = Schema.Struct({
722
+ name: Schema.String,
723
+ private: Schema.optional(Schema.Boolean),
724
+ version: Schema.optional(Schema.String),
725
+ dependencies: Schema.optional(DependencyObjectSchema),
726
+ devDependencies: Schema.optional(DependencyObjectSchema),
727
+ peerDependencies: Schema.optional(DependencyObjectSchema)
728
+ });
729
+ const WorkspacePackageSchema = Schema.Struct({
730
+ name: Schema.String,
731
+ version: Schema.String,
732
+ path: Schema.String,
733
+ packageJson: PackageJsonSchema,
734
+ workspaceDependencies: Schema.Array(Schema.String),
735
+ workspaceDevDependencies: Schema.Array(Schema.String)
736
+ });
737
+ const WorkspaceListSchema = Schema.Array(Schema.Struct({
738
+ name: Schema.String,
739
+ path: Schema.String,
740
+ version: Schema.optional(Schema.String),
741
+ private: Schema.optional(Schema.Boolean),
742
+ dependencies: Schema.optional(DependencyObjectSchema),
743
+ devDependencies: Schema.optional(DependencyObjectSchema),
744
+ peerDependencies: Schema.optional(DependencyObjectSchema)
745
+ }));
746
+ var WorkspaceService = class extends Effect.Service()("@ucdjs/release-scripts/WorkspaceService", {
747
+ effect: Effect.gen(function* () {
748
+ const executor = yield* CommandExecutor.CommandExecutor;
749
+ const config = yield* ReleaseScriptsOptions;
750
+ const workspacePackageListOutput = yield* executor.string(Command.make("pnpm", "-r", "ls", "--json").pipe(Command.workingDirectory(config.workspaceRoot))).pipe(Effect.flatMap((stdout) => Effect.try({
751
+ try: () => JSON.parse(stdout),
752
+ catch: (e) => new WorkspaceError({
753
+ message: "Failed to parse pnpm JSON output",
754
+ operation: "discover",
755
+ cause: e
756
+ })
757
+ })), Effect.flatMap((json) => Schema.decodeUnknown(WorkspaceListSchema)(json).pipe(Effect.mapError((e) => new WorkspaceError({
758
+ message: "Failed to decode pnpm output",
759
+ operation: "discover",
760
+ cause: e
761
+ })))), Effect.cached);
762
+ function readPackageJson(pkgPath) {
763
+ return Effect.tryPromise({
764
+ try: async () => JSON.parse(await fs.readFile(path.join(pkgPath, "package.json"), "utf8")),
765
+ catch: (e) => new WorkspaceError({
766
+ message: `Failed to read package.json for ${pkgPath}`,
767
+ cause: e,
768
+ operation: "readPackageJson"
769
+ })
770
+ }).pipe(Effect.flatMap((json) => Schema.decodeUnknown(PackageJsonSchema)(json).pipe(Effect.mapError((e) => new WorkspaceError({
771
+ message: `Invalid package.json for ${pkgPath}`,
772
+ cause: e,
773
+ operation: "readPackageJson"
774
+ })))));
775
+ }
776
+ function writePackageJson(pkgPath, json) {
777
+ const fullPath = path.join(pkgPath, "package.json");
778
+ const content = `${JSON.stringify(json, null, 2)}\n`;
779
+ if (config.dryRun) return Effect.succeed(`Dry run: skip writing ${fullPath}`);
780
+ return Effect.tryPromise({
781
+ try: async () => await fs.writeFile(fullPath, content, "utf8"),
782
+ catch: (e) => new WorkspaceError({
783
+ message: `Failed to write package.json for ${pkgPath}`,
784
+ cause: e,
785
+ operation: "writePackageJson"
786
+ })
787
+ });
788
+ }
789
+ const discoverWorkspacePackages = Effect.gen(function* () {
790
+ let workspaceOptions;
791
+ let explicitPackages;
792
+ if (config.packages == null || config.packages === true) workspaceOptions = { excludePrivate: false };
793
+ else if (Array.isArray(config.packages)) {
794
+ workspaceOptions = {
795
+ excludePrivate: false,
796
+ include: config.packages
797
+ };
798
+ explicitPackages = config.packages;
799
+ } else {
800
+ workspaceOptions = config.packages;
801
+ if (config.packages.include) explicitPackages = config.packages.include;
802
+ }
803
+ const workspacePackages = yield* findWorkspacePackages(workspaceOptions);
804
+ if (explicitPackages) {
805
+ const foundNames = new Set(workspacePackages.map((p) => p.name));
806
+ const missing = explicitPackages.filter((p) => !foundNames.has(p));
807
+ if (missing.length > 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`));
808
+ }
809
+ return workspacePackages;
810
+ });
811
+ function findWorkspacePackages(options) {
812
+ return workspacePackageListOutput.pipe(Effect.flatMap((rawProjects) => {
813
+ const allPackageNames = new Set(rawProjects.map((p) => p.name));
814
+ return Effect.all(rawProjects.map((rawProject) => readPackageJson(rawProject.path).pipe(Effect.flatMap((packageJson) => {
815
+ if (!shouldIncludePackage(packageJson, options)) return Effect.succeed(null);
816
+ const pkg = {
817
+ name: rawProject.name,
818
+ version: rawProject.version,
819
+ path: rawProject.path,
820
+ packageJson,
821
+ workspaceDependencies: Object.keys(rawProject.dependencies || {}).filter((dep) => allPackageNames.has(dep)),
822
+ workspaceDevDependencies: Object.keys(rawProject.devDependencies || {}).filter((dep) => allPackageNames.has(dep))
823
+ };
824
+ return Schema.decodeUnknown(WorkspacePackageSchema)(pkg).pipe(Effect.mapError((e) => new WorkspaceError({
825
+ message: `Invalid workspace package structure for ${rawProject.name}`,
826
+ cause: e,
827
+ operation: "findWorkspacePackages"
828
+ })));
829
+ }), Effect.catchAll(() => {
830
+ return Effect.logWarning(`Skipping invalid package ${rawProject.name}`).pipe(Effect.as(null));
831
+ })))).pipe(Effect.map((packages) => packages.filter((pkg) => pkg !== null)));
832
+ }));
833
+ }
834
+ function shouldIncludePackage(pkg, options) {
835
+ if (!options) return true;
836
+ if (options.excludePrivate && pkg.private) return false;
837
+ if (options.include && options.include.length > 0) {
838
+ if (!options.include.includes(pkg.name)) return false;
839
+ }
840
+ if (options.exclude?.includes(pkg.name)) return false;
841
+ return true;
842
+ }
843
+ function findPackageByName(packageName) {
844
+ return discoverWorkspacePackages.pipe(Effect.map((packages) => packages.find((pkg) => pkg.name === packageName) || null));
845
+ }
846
+ return {
847
+ readPackageJson,
848
+ writePackageJson,
849
+ findWorkspacePackages,
850
+ discoverWorkspacePackages,
851
+ findPackageByName
852
+ };
853
+ }),
854
+ dependencies: []
855
+ }) {};
856
+
857
+ //#endregion
858
+ //#region src/services/package-updater.service.ts
859
+ const DASH_RE = / - /;
860
+ const RANGE_OPERATION_RE = /^(?:>=|<=|[><=])/;
861
+ function nextRange(oldRange, newVersion) {
862
+ const workspacePrefix = oldRange.startsWith("workspace:") ? "workspace:" : "";
863
+ const raw = workspacePrefix ? oldRange.slice(10) : oldRange;
864
+ if (raw === "*" || raw === "latest") return `${workspacePrefix}${raw}`;
865
+ if (raw.includes("||") || DASH_RE.test(raw) || RANGE_OPERATION_RE.test(raw) || raw.includes(" ") && !DASH_RE.test(raw)) {
866
+ if (semver.satisfies(newVersion, raw)) return `${workspacePrefix}${raw}`;
867
+ throw new Error(`Cannot update range "${oldRange}" to version ${newVersion}: new version is outside the existing range. Complex range updating is not yet implemented.`);
249
868
  }
869
+ return `${workspacePrefix}${raw.startsWith("^") || raw.startsWith("~") ? raw[0] : ""}${newVersion}`;
250
870
  }
251
- /**
252
- * Check if a git branch exists locally
253
- * @param {string} branch - The branch name to check
254
- * @param {string} workspaceRoot - The root directory of the workspace
255
- * @returns {Promise<boolean>} Promise resolving to true if branch exists, false otherwise
256
- */
257
- async function doesBranchExist(branch, workspaceRoot) {
258
- try {
259
- await run("git", [
260
- "rev-parse",
261
- "--verify",
262
- branch
263
- ], { nodeOptions: {
264
- cwd: workspaceRoot,
265
- stdio: "pipe"
266
- } });
267
- return true;
268
- } catch {
269
- return false;
871
+ function updateDependencyRecord(record, releaseMap) {
872
+ if (!record) return {
873
+ updated: false,
874
+ next: void 0
875
+ };
876
+ let changed = false;
877
+ const next = { ...record };
878
+ for (const [dep, currentRange] of Object.entries(record)) {
879
+ const bumped = releaseMap.get(dep);
880
+ if (!bumped) continue;
881
+ const updatedRange = nextRange(currentRange, bumped);
882
+ if (updatedRange !== currentRange) {
883
+ next[dep] = updatedRange;
884
+ changed = true;
885
+ }
270
886
  }
887
+ return {
888
+ updated: changed,
889
+ next: changed ? next : record
890
+ };
271
891
  }
272
- /**
273
- * Pull latest changes from remote branch
274
- * @param branch - The branch name to pull from
275
- * @param workspaceRoot - The root directory of the workspace
276
- * @returns Promise resolving to true if pull succeeded, false otherwise
277
- */
278
- async function pullLatestChanges(branch, workspaceRoot) {
279
- try {
280
- await run("git", [
281
- "pull",
282
- "origin",
283
- branch
284
- ], { nodeOptions: {
285
- cwd: workspaceRoot,
286
- stdio: "pipe"
287
- } });
288
- return true;
289
- } catch {
290
- return false;
291
- }
892
+ var PackageUpdaterService = class extends Effect.Service()("@ucdjs/release-scripts/PackageUpdaterService", {
893
+ effect: Effect.gen(function* () {
894
+ const workspace = yield* WorkspaceService;
895
+ function applyReleases(allPackages, releases) {
896
+ const releaseMap = /* @__PURE__ */ new Map();
897
+ for (const release of releases) releaseMap.set(release.package.name, release.newVersion);
898
+ return Effect.all(allPackages.map((pkg) => Effect.gen(function* () {
899
+ const releaseVersion = releaseMap.get(pkg.name);
900
+ const nextJson = { ...pkg.packageJson };
901
+ let updated = false;
902
+ if (releaseVersion && pkg.packageJson.version !== releaseVersion) {
903
+ nextJson.version = releaseVersion;
904
+ updated = true;
905
+ }
906
+ const depsResult = updateDependencyRecord(pkg.packageJson.dependencies, releaseMap);
907
+ if (depsResult.updated) {
908
+ nextJson.dependencies = depsResult.next;
909
+ updated = true;
910
+ }
911
+ const devDepsResult = updateDependencyRecord(pkg.packageJson.devDependencies, releaseMap);
912
+ if (devDepsResult.updated) {
913
+ nextJson.devDependencies = devDepsResult.next;
914
+ updated = true;
915
+ }
916
+ const peerDepsResult = updateDependencyRecord(pkg.packageJson.peerDependencies, releaseMap);
917
+ if (peerDepsResult.updated) {
918
+ nextJson.peerDependencies = peerDepsResult.next;
919
+ updated = true;
920
+ }
921
+ if (!updated) return "skipped";
922
+ return yield* workspace.writePackageJson(pkg.path, nextJson).pipe(Effect.map(() => "written"));
923
+ })));
924
+ }
925
+ return { applyReleases };
926
+ }),
927
+ dependencies: [WorkspaceService.Default]
928
+ }) {};
929
+
930
+ //#endregion
931
+ //#region src/services/version-calculator.service.ts
932
+ const BUMP_PRIORITY = {
933
+ none: 0,
934
+ patch: 1,
935
+ minor: 2,
936
+ major: 3
937
+ };
938
+ function maxBump(current, incoming) {
939
+ return (BUMP_PRIORITY[incoming] ?? 0) > (BUMP_PRIORITY[current] ?? 0) ? incoming : current;
292
940
  }
293
- /**
294
- * Create a new git branch
295
- * @param branch - The new branch name
296
- * @param base - The base branch to create from
297
- * @param workspaceRoot - The root directory of the workspace
298
- */
299
- async function createBranch(branch, base, workspaceRoot) {
300
- await runIfNotDry("git", [
301
- "checkout",
302
- "-b",
303
- branch,
304
- base
305
- ], { nodeOptions: { cwd: workspaceRoot } });
941
+ function bumpFromCommit(commit) {
942
+ if (commit.isBreaking) return "major";
943
+ if (commit.type === "feat") return "minor";
944
+ if (commit.type === "fix" || commit.type === "perf") return "patch";
945
+ return "none";
306
946
  }
307
- /**
308
- * Checkout a git branch
309
- * @param branch - The branch name to checkout
310
- * @param workspaceRoot - The root directory of the workspace
311
- * @returns Promise resolving to true if checkout succeeded, false otherwise
312
- */
313
- async function checkoutBranch(branch, workspaceRoot) {
314
- try {
315
- await run("git", ["checkout", branch], { nodeOptions: { cwd: workspaceRoot } });
316
- return true;
317
- } catch {
318
- return false;
319
- }
947
+ function determineBump(commits) {
948
+ return commits.reduce((acc, commit) => maxBump(acc, bumpFromCommit(commit)), "none");
320
949
  }
321
- /**
322
- * Get the current branch name
323
- * @param workspaceRoot - The root directory of the workspace
324
- * @returns Promise resolving to the current branch name
325
- */
326
- async function getCurrentBranch(workspaceRoot) {
327
- return (await run("git", [
328
- "rev-parse",
329
- "--abbrev-ref",
330
- "HEAD"
331
- ], { nodeOptions: {
332
- cwd: workspaceRoot,
333
- stdio: "pipe"
334
- } })).stdout.trim();
950
+ var VersionCalculatorService = class extends Effect.Service()("@ucdjs/release-scripts/VersionCalculatorService", {
951
+ effect: Effect.gen(function* () {
952
+ function calculateBumps(packages, overrides) {
953
+ return Effect.all(packages.map((pkg) => Effect.gen(function* () {
954
+ const bumpType = determineBump([...pkg.commits, ...pkg.globalCommits]);
955
+ const hasDirectChanges = pkg.commits.length > 0;
956
+ let nextVersion = null;
957
+ const override = overrides[pkg.name];
958
+ if (override) {
959
+ if (!semver.valid(override)) return yield* Effect.fail(new VersionCalculationError({
960
+ message: `Invalid override version for ${pkg.name}: ${override}`,
961
+ packageName: pkg.name
962
+ }));
963
+ nextVersion = override;
964
+ }
965
+ if (nextVersion === null) if (bumpType === "none") nextVersion = pkg.version;
966
+ else {
967
+ const bumped = semver.inc(pkg.version, bumpType);
968
+ if (!bumped) return yield* Effect.fail(new VersionCalculationError({
969
+ message: `Failed to bump version for ${pkg.name} using bump type ${bumpType}`,
970
+ packageName: pkg.name
971
+ }));
972
+ nextVersion = bumped;
973
+ }
974
+ return {
975
+ package: {
976
+ name: pkg.name,
977
+ version: pkg.version,
978
+ path: pkg.path,
979
+ packageJson: pkg.packageJson,
980
+ workspaceDependencies: pkg.workspaceDependencies,
981
+ workspaceDevDependencies: pkg.workspaceDevDependencies
982
+ },
983
+ currentVersion: pkg.version,
984
+ newVersion: nextVersion,
985
+ bumpType,
986
+ hasDirectChanges
987
+ };
988
+ })), { concurrency: 10 });
989
+ }
990
+ return { calculateBumps };
991
+ }),
992
+ dependencies: []
993
+ }) {};
994
+
995
+ //#endregion
996
+ //#region src/utils/helpers.ts
997
+ function loadOverrides(options) {
998
+ return Effect.gen(function* () {
999
+ return yield* (yield* GitService).workspace.readFile(options.overridesPath, options.sha).pipe(Effect.flatMap((content) => Effect.try({
1000
+ try: () => JSON.parse(content),
1001
+ catch: (err) => new OverridesLoadError({
1002
+ message: "Failed to parse overrides file.",
1003
+ cause: err
1004
+ })
1005
+ })), Effect.catchAll(() => Effect.succeed({})));
1006
+ });
1007
+ }
1008
+ const GitCommitSchema = Schema.Struct({
1009
+ isConventional: Schema.Boolean,
1010
+ isBreaking: Schema.Boolean,
1011
+ type: Schema.String,
1012
+ scope: Schema.Union(Schema.String, Schema.Undefined),
1013
+ description: Schema.String,
1014
+ references: Schema.Array(Schema.Struct({
1015
+ type: Schema.Union(Schema.Literal("issue"), Schema.Literal("pull-request")),
1016
+ value: Schema.String
1017
+ })),
1018
+ authors: Schema.Array(Schema.Struct({
1019
+ name: Schema.String,
1020
+ email: Schema.String,
1021
+ profile: Schema.optional(Schema.String)
1022
+ })),
1023
+ hash: Schema.String,
1024
+ shortHash: Schema.String,
1025
+ body: Schema.String,
1026
+ message: Schema.String,
1027
+ date: Schema.String
1028
+ });
1029
+ const WorkspacePackageWithCommitsSchema = Schema.Struct({
1030
+ ...WorkspacePackageSchema.fields,
1031
+ commits: Schema.Array(GitCommitSchema),
1032
+ globalCommits: Schema.Array(GitCommitSchema).pipe(Schema.propertySignature, Schema.withConstructorDefault(() => []))
1033
+ });
1034
+ function mergePackageCommitsIntoPackages(packages) {
1035
+ return Effect.gen(function* () {
1036
+ const git = yield* GitService;
1037
+ return yield* Effect.forEach(packages, (pkg) => Effect.gen(function* () {
1038
+ const lastTag = yield* git.tags.mostRecentForPackage(pkg.name);
1039
+ const commits = yield* git.commits.get({
1040
+ from: lastTag?.name || void 0,
1041
+ to: "HEAD",
1042
+ folder: pkg.path
1043
+ });
1044
+ const withCommits = {
1045
+ ...pkg,
1046
+ commits,
1047
+ globalCommits: []
1048
+ };
1049
+ return yield* Schema.decode(WorkspacePackageWithCommitsSchema)(withCommits).pipe(Effect.mapError((e) => /* @__PURE__ */ new Error(`Failed to decode package with commits for ${pkg.name}: ${e}`)));
1050
+ }));
1051
+ });
335
1052
  }
336
1053
  /**
337
- * Rebase current branch onto another branch
338
- * @param ontoBranch - The target branch to rebase onto
339
- * @param workspaceRoot - The root directory of the workspace
1054
+ * Retrieves global commits that affect all packages in a monorepo.
1055
+ *
1056
+ * This function handles an important edge case in monorepo releases:
1057
+ * When pkg-a is released, then a global change is made, and then pkg-b is released,
1058
+ * we need to ensure that the global change is only attributed to pkg-a's release,
1059
+ * not re-counted for pkg-b.
1060
+ *
1061
+ * Algorithm:
1062
+ * 1. Find the overall commit range across all packages
1063
+ * 2. Fetch all commits and file changes once for this range
1064
+ * 3. For each package, filter commits based on its last tag cutoff
1065
+ * 4. Apply mode-specific filtering for global commits
1066
+ *
1067
+ * Example scenario:
1068
+ * - pkg-a: last released at commit A
1069
+ * - global change at commit B (after A)
1070
+ * - pkg-b: last released at commit C (after B)
1071
+ *
1072
+ * Result:
1073
+ * - For pkg-a: includes commits from A to HEAD (including B)
1074
+ * - For pkg-b: includes commits from C to HEAD (excluding B, since it was already in pkg-b's release range)
1075
+ *
1076
+ * @param packages - Array of workspace packages with their associated commits
1077
+ * @param mode - Determines which global commits to include:
1078
+ * - "none": No global commits (returns empty map)
1079
+ * - "all": All commits that touch files outside any package directory
1080
+ * - "dependencies": Only commits that touch dependency-related files (package.json, lock files, etc.)
1081
+ *
1082
+ * @returns A map of package names to their relevant global commits
340
1083
  */
341
- async function rebaseBranch(ontoBranch, workspaceRoot) {
342
- await run("git", ["rebase", ontoBranch], { nodeOptions: { cwd: workspaceRoot } });
1084
+ function mergeCommitsAffectingGloballyIntoPackage(packages, mode) {
1085
+ return Effect.gen(function* () {
1086
+ const git = yield* GitService;
1087
+ if (mode === "none") return packages;
1088
+ const [oldestCommitSha, newestCommitSha] = findCommitRange(packages);
1089
+ if (oldestCommitSha == null || newestCommitSha == null) return packages;
1090
+ const allCommits = yield* git.commits.get({
1091
+ from: oldestCommitSha,
1092
+ to: newestCommitSha,
1093
+ folder: "."
1094
+ });
1095
+ const affectedFilesPerCommit = yield* git.commits.filesChangesBetweenRefs(oldestCommitSha, newestCommitSha);
1096
+ const commitTimestamps = new Map(allCommits.map((c) => [c.hash, new Date(c.date).getTime()]));
1097
+ const packagePaths = new Set(packages.map((p) => p.path));
1098
+ const result = /* @__PURE__ */ new Map();
1099
+ for (const pkg of packages) {
1100
+ const lastTag = yield* git.tags.mostRecentForPackage(pkg.name);
1101
+ const cutoffTimestamp = lastTag ? commitTimestamps.get(lastTag.sha) ?? 0 : 0;
1102
+ const globalCommits = [];
1103
+ for (const commit of allCommits) {
1104
+ const commitTimestamp = commitTimestamps.get(commit.hash);
1105
+ if (commitTimestamp == null || commitTimestamp <= cutoffTimestamp) continue;
1106
+ const files = affectedFilesPerCommit.get(commit.hash);
1107
+ if (!files) continue;
1108
+ if (isGlobalCommit(files, packagePaths)) if (mode === "dependencies") {
1109
+ if (files.some((file) => isDependencyFile(file))) globalCommits.push(commit);
1110
+ } else globalCommits.push(commit);
1111
+ }
1112
+ result.set(pkg.name, globalCommits);
1113
+ }
1114
+ return yield* Effect.succeed(packages.map((pkg) => ({
1115
+ ...pkg,
1116
+ globalCommits: result.get(pkg.name) || []
1117
+ })));
1118
+ });
343
1119
  }
344
1120
  /**
345
- * Check if local branch is ahead of remote (has commits to push)
346
- * @param branch - The branch name to check
347
- * @param workspaceRoot - The root directory of the workspace
348
- * @returns Promise resolving to true if local is ahead, false otherwise
1121
+ * Determines if a commit is "global" (affects files outside any package directory).
1122
+ *
1123
+ * @param files - List of files changed in the commit
1124
+ * @param packagePaths - Set of package directory paths
1125
+ * @returns true if at least one file is outside all package directories
349
1126
  */
350
- async function isBranchAheadOfRemote(branch, workspaceRoot) {
351
- try {
352
- const result = await run("git", [
353
- "rev-list",
354
- `origin/${branch}..${branch}`,
355
- "--count"
356
- ], { nodeOptions: {
357
- cwd: workspaceRoot,
358
- stdio: "pipe"
359
- } });
360
- return Number.parseInt(result.stdout.trim(), 10) > 0;
361
- } catch {
1127
+ function isGlobalCommit(files, packagePaths) {
1128
+ return files.some((file) => {
1129
+ const normalized = file.startsWith("./") ? file.slice(2) : file;
1130
+ for (const pkgPath of packagePaths) if (normalized === pkgPath || normalized.startsWith(`${pkgPath}/`)) return false;
362
1131
  return true;
363
- }
1132
+ });
364
1133
  }
365
1134
  /**
366
- * Check if there are any changes to commit (staged or unstaged)
367
- * @param workspaceRoot - The root directory of the workspace
368
- * @returns Promise resolving to true if there are changes, false otherwise
1135
+ * Files that are considered dependency-related in a monorepo.
369
1136
  */
370
- async function hasChangesToCommit(workspaceRoot) {
371
- return (await run("git", ["status", "--porcelain"], { nodeOptions: {
372
- cwd: workspaceRoot,
373
- stdio: "pipe"
374
- } })).stdout.trim() !== "";
375
- }
1137
+ const DEPENDENCY_FILES = new Set([
1138
+ "package.json",
1139
+ "pnpm-lock.yaml",
1140
+ "yarn.lock",
1141
+ "package-lock.json",
1142
+ "pnpm-workspace.yaml"
1143
+ ]);
376
1144
  /**
377
- * Commit changes with a message
378
- * @param message - The commit message
379
- * @param workspaceRoot - The root directory of the workspace
380
- * @returns Promise resolving to true if commit was made, false if there were no changes
1145
+ * Determines if a file is dependency-related.
1146
+ *
1147
+ * @param file - File path to check
1148
+ * @returns true if the file is a dependency file (package.json, lock files, etc.)
381
1149
  */
382
- async function commitChanges(message, workspaceRoot) {
383
- await run("git", ["add", "."], { nodeOptions: { cwd: workspaceRoot } });
384
- if (!await hasChangesToCommit(workspaceRoot)) return false;
385
- await run("git", [
386
- "commit",
387
- "-m",
388
- message
389
- ], { nodeOptions: { cwd: workspaceRoot } });
390
- return true;
1150
+ function isDependencyFile(file) {
1151
+ const normalized = file.startsWith("./") ? file.slice(2) : file;
1152
+ if (DEPENDENCY_FILES.has(normalized)) return true;
1153
+ return Array.from(DEPENDENCY_FILES).some((dep) => normalized.endsWith(`/${dep}`));
391
1154
  }
392
1155
  /**
393
- * Push branch to remote
394
- * @param branch - The branch name to push
395
- * @param workspaceRoot - The root directory of the workspace
396
- * @param options - Push options
397
- * @param options.force - Force push (overwrite remote)
398
- * @param options.forceWithLease - Force push with safety check (won't overwrite unexpected changes)
1156
+ * Finds the oldest and newest commits across all packages.
1157
+ *
1158
+ * This establishes the overall time range we need to analyze for global commits.
1159
+ *
1160
+ * @param packages - Array of packages with their commits
1161
+ * @returns Tuple of [oldestCommitSha, newestCommitSha], or [null, null] if no commits found
399
1162
  */
400
- async function pushBranch(branch, workspaceRoot, options) {
401
- const args = [
402
- "push",
403
- "origin",
404
- branch
405
- ];
406
- if (options?.forceWithLease) args.push("--force-with-lease");
407
- else if (options?.force) args.push("--force");
408
- await run("git", args, { nodeOptions: { cwd: workspaceRoot } });
1163
+ function findCommitRange(packages) {
1164
+ let oldestCommit = null;
1165
+ let newestCommit = null;
1166
+ for (const pkg of packages) {
1167
+ if (pkg.commits.length === 0) continue;
1168
+ const firstCommit = pkg.commits[0];
1169
+ if (!firstCommit) throw new Error(`No commits found for package ${pkg.name}`);
1170
+ const lastCommit = pkg.commits[pkg.commits.length - 1];
1171
+ if (!lastCommit) throw new Error(`No commits found for package ${pkg.name}`);
1172
+ if (newestCommit == null || new Date(lastCommit.date) > new Date(newestCommit.date)) newestCommit = lastCommit;
1173
+ if (oldestCommit == null || new Date(firstCommit.date) < new Date(oldestCommit.date)) oldestCommit = firstCommit;
1174
+ }
1175
+ if (oldestCommit == null || newestCommit == null) return [null, null];
1176
+ return [oldestCommit.hash, newestCommit.hash];
409
1177
  }
410
1178
 
411
1179
  //#endregion
412
- //#region src/github.ts
413
- async function getExistingPullRequest({ owner, repo, branch, githubToken }) {
414
- try {
415
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch}`, { headers: {
416
- Accept: "application/vnd.github.v3+json",
417
- Authorization: `token ${githubToken}`
418
- } });
419
- if (!res.ok) throw new Error(`GitHub API request failed with status ${res.status}`);
420
- const pulls = await res.json();
421
- if (pulls == null || !Array.isArray(pulls) || pulls.length === 0) return null;
422
- const firstPullRequest = pulls[0];
423
- 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");
424
- const pullRequest = {
425
- number: firstPullRequest.number,
426
- title: firstPullRequest.title,
427
- body: firstPullRequest.body,
428
- draft: firstPullRequest.draft,
429
- html_url: firstPullRequest.html_url
430
- };
431
- console.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
432
- return pullRequest;
433
- } catch (err) {
434
- console.error("Error fetching pull request:", err);
435
- return null;
436
- }
437
- }
438
- async function upsertPullRequest({ owner, repo, title, body, head, base, pullNumber, githubToken }) {
439
- try {
440
- const isUpdate = pullNumber != null;
441
- const url = isUpdate ? `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}` : `https://api.github.com/repos/${owner}/${repo}/pulls`;
442
- const method = isUpdate ? "PATCH" : "POST";
443
- const requestBody = isUpdate ? {
444
- title,
445
- body
446
- } : {
447
- title,
448
- body,
449
- head,
450
- base
451
- };
452
- const res = await fetch(url, {
453
- method,
454
- headers: {
455
- Accept: "application/vnd.github.v3+json",
456
- Authorization: `token ${githubToken}`
457
- },
458
- body: JSON.stringify(requestBody)
1180
+ //#region src/prepare.ts
1181
+ function constructPrepareProgram(config) {
1182
+ return Effect.gen(function* () {
1183
+ const changelog = yield* ChangelogService;
1184
+ const git = yield* GitService;
1185
+ const github = yield* GitHubService;
1186
+ const dependencyGraph = yield* DependencyGraphService;
1187
+ const packageUpdater = yield* PackageUpdaterService;
1188
+ const versionCalculator = yield* VersionCalculatorService;
1189
+ const workspace = yield* WorkspaceService;
1190
+ yield* git.workspace.assertWorkspaceReady;
1191
+ let releasePullRequest = yield* github.getPullRequestByBranch(config.branch.release);
1192
+ const isNewRelease = !releasePullRequest;
1193
+ const branchExists = yield* git.branches.exists(config.branch.release);
1194
+ if (!branchExists) {
1195
+ yield* Console.log(`🌿 Creating release branch "${config.branch.release}" from "${config.branch.default}"...`);
1196
+ yield* git.branches.create(config.branch.release, config.branch.default);
1197
+ yield* Console.log(`✅ Release branch created.`);
1198
+ }
1199
+ if ((yield* git.branches.get) !== config.branch.release) {
1200
+ yield* git.branches.checkout(config.branch.release);
1201
+ yield* Console.log(`✅ Checked out to release branch "${config.branch.release}".`);
1202
+ }
1203
+ if (!isNewRelease || branchExists) {
1204
+ yield* Console.log(`🔄 Rebasing "${config.branch.release}" onto "${config.branch.default}"...`);
1205
+ yield* git.branches.rebase(config.branch.default);
1206
+ yield* Console.log(`✅ Rebase complete.`);
1207
+ }
1208
+ const overrides = yield* loadOverrides({
1209
+ sha: config.branch.default,
1210
+ overridesPath: ".github/ucdjs-release.overrides.json"
459
1211
  });
460
- if (!res.ok) throw new Error(`GitHub API request failed with status ${res.status}`);
461
- const pr = await res.json();
462
- 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");
463
- const action = isUpdate ? "Updated" : "Created";
464
- console.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
465
- return {
466
- number: pr.number,
467
- title: pr.title,
468
- body: pr.body,
469
- draft: pr.draft,
470
- html_url: pr.html_url
471
- };
472
- } catch (err) {
473
- console.error(`Error upserting pull request:`, err);
474
- throw err;
475
- }
476
- }
477
- function generatePullRequestBody(updates, _body) {
478
- const lines = [];
479
- lines.push("## Packages");
480
- lines.push("");
481
- const directChanges = updates.filter((u) => u.hasDirectChanges);
482
- const dependencyUpdates = updates.filter((u) => !u.hasDirectChanges);
483
- if (directChanges.length > 0) {
484
- lines.push("### Direct Changes");
485
- lines.push("");
486
- for (const update of directChanges) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`);
487
- lines.push("");
488
- }
489
- if (dependencyUpdates.length > 0) {
490
- lines.push("### Dependency Updates");
491
- lines.push("");
492
- for (const update of dependencyUpdates) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (dependencies changed)`);
493
- lines.push("");
494
- }
495
- lines.push("---");
496
- lines.push("");
497
- lines.push("This release PR was automatically generated.");
498
- return lines.join("\n");
1212
+ if (Object.keys(overrides).length > 0) yield* Console.log("📋 Loaded version overrides:", overrides);
1213
+ const originalBranch = yield* git.branches.get;
1214
+ yield* git.branches.checkout(config.branch.default);
1215
+ const packages = yield* workspace.discoverWorkspacePackages.pipe(Effect.flatMap(mergePackageCommitsIntoPackages), Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)));
1216
+ yield* Console.log(`📦 Discovered ${packages.length} packages with commits.`);
1217
+ const releases = yield* versionCalculator.calculateBumps(packages, overrides);
1218
+ yield* dependencyGraph.topologicalOrder(packages);
1219
+ const releasesCount = releases.length;
1220
+ yield* Console.log(`📊 ${releasesCount} package${releasesCount === 1 ? "" : "s"} will be released.`);
1221
+ yield* git.branches.checkout(originalBranch);
1222
+ yield* Console.log("✏️ Updating package.json files...");
1223
+ yield* packageUpdater.applyReleases(packages, releases);
1224
+ yield* Console.log("✅ package.json files updated.");
1225
+ yield* Console.log("📝 Generating changelogs...");
1226
+ const changelogFiles = [];
1227
+ for (const release of releases) {
1228
+ const pkg = packages.find((p) => p.name === release.package.name);
1229
+ if (!pkg || !pkg.commits) continue;
1230
+ const result = yield* changelog.generateChangelog(pkg, release.newVersion, pkg.commits);
1231
+ yield* Effect.tryPromise({
1232
+ try: async () => {
1233
+ await (await import("node:fs/promises")).writeFile(result.filePath, result.markdown, "utf-8");
1234
+ },
1235
+ catch: (e) => /* @__PURE__ */ new Error(`Failed to write changelog: ${String(e)}`)
1236
+ });
1237
+ changelogFiles.push(result.filePath);
1238
+ }
1239
+ yield* Console.log(`✅ Generated ${changelogFiles.length} changelog file${changelogFiles.length === 1 ? "" : "s"}.`);
1240
+ const filesToStage = [...releases.map((r) => `${r.package.path}/package.json`), ...changelogFiles];
1241
+ yield* Console.log(`📌 Staging ${filesToStage.length} file${filesToStage.length === 1 ? "" : "s"}...`);
1242
+ yield* git.commits.stage(filesToStage);
1243
+ const commitMessage = `chore(release): prepare release
1244
+
1245
+ ${releasesCount} package${releasesCount === 1 ? "" : "s"} updated:
1246
+ ${releases.map((r) => ` - ${r.package.name}@${r.newVersion}`).join("\n")}`;
1247
+ yield* Console.log("💾 Creating commit...");
1248
+ yield* git.commits.write(commitMessage);
1249
+ yield* Console.log(" Commit created.");
1250
+ yield* Console.log(`⬆️ Pushing to "${config.branch.release}"...`);
1251
+ if (isNewRelease && !branchExists) yield* git.commits.push(config.branch.release);
1252
+ else yield* git.commits.forcePush(config.branch.release);
1253
+ yield* Console.log(`✅ Push complete.`);
1254
+ const prBody = yield* github.generateReleasePRBody(releases.map((r) => ({
1255
+ packageName: r.package.name,
1256
+ version: r.newVersion,
1257
+ previousVersion: r.package.version
1258
+ })));
1259
+ if (isNewRelease) {
1260
+ yield* Console.log("📋 Creating release pull request...");
1261
+ releasePullRequest = yield* github.createPullRequest({
1262
+ title: config.pullRequest.title,
1263
+ body: prBody,
1264
+ head: config.branch.release,
1265
+ base: config.branch.default,
1266
+ draft: true
1267
+ });
1268
+ yield* Console.log(`✅ Release pull request #${releasePullRequest.number} created.`);
1269
+ } else {
1270
+ yield* Console.log("📄 Updating pull request...");
1271
+ yield* github.updatePullRequest(releasePullRequest.number, { body: prBody });
1272
+ yield* Console.log("✅ Pull request updated.");
1273
+ }
1274
+ yield* Console.log(`\n🎉 Release preparation complete! View PR: #${releasePullRequest.number}`);
1275
+ });
499
1276
  }
500
1277
 
501
1278
  //#endregion
502
- //#region src/prompts.ts
503
- async function promptPackageSelection(packages) {
504
- const response = await prompts({
505
- type: "multiselect",
506
- name: "selectedPackages",
507
- message: "Select packages to release",
508
- choices: packages.map((pkg) => ({
509
- title: `${pkg.name} (${pkg.version})`,
510
- value: pkg.name,
511
- selected: true
512
- })),
513
- min: 1,
514
- hint: "Space to select/deselect. Return to submit."
515
- });
516
- if (!response.selectedPackages || response.selectedPackages.length === 0) throw new Error("No packages selected");
517
- return response.selectedPackages;
1279
+ //#region src/publish.ts
1280
+ function isPrerelease(version) {
1281
+ const parsed = semver.parse(version);
1282
+ return parsed !== null && parsed.prerelease.length > 0;
518
1283
  }
519
- async function promptVersionOverride(packageName, currentVersion, suggestedVersion, suggestedBumpType) {
520
- const choices = [{
521
- title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
522
- value: "suggested"
523
- }];
524
- for (const bumpType of [
525
- "patch",
526
- "minor",
527
- "major"
528
- ]) if (bumpType !== suggestedBumpType) {
529
- const version = calculateNewVersion(currentVersion, bumpType);
530
- choices.push({
531
- title: `${bumpType}: ${version}`,
532
- value: bumpType
533
- });
534
- }
535
- choices.push({
536
- title: "Custom version",
537
- value: "custom"
1284
+ function getDistTag(version) {
1285
+ return isPrerelease(version) ? "next" : "latest";
1286
+ }
1287
+ function buildPackage(packagePath) {
1288
+ return Effect.gen(function* () {
1289
+ const executor = yield* CommandExecutor.CommandExecutor;
1290
+ const command = Command.make("pnpm", "run", "build").pipe(Command.workingDirectory(packagePath));
1291
+ return (yield* executor.string(command).pipe(Effect.mapError((err) => /* @__PURE__ */ new Error(`Failed to build package at ${packagePath}: ${err.message}`)))).trim();
538
1292
  });
539
- const response = await prompts([{
540
- type: "select",
541
- name: "choice",
542
- message: `${packageName} (${currentVersion}):`,
543
- choices,
544
- initial: 0
545
- }, {
546
- type: (prev) => prev === "custom" ? "text" : null,
547
- name: "customVersion",
548
- message: "Enter custom version:",
549
- initial: suggestedVersion,
550
- validate: (value) => {
551
- return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(value) || "Invalid semver version (e.g., 1.0.0)";
552
- }
553
- }]);
554
- if (response.choice === "suggested") return suggestedVersion;
555
- else if (response.choice === "custom") return response.customVersion;
556
- else return calculateNewVersion(currentVersion, response.choice);
557
1293
  }
558
- async function promptVersionOverrides(packages) {
559
- const overrides = /* @__PURE__ */ new Map();
560
- for (const pkg of packages) {
561
- const newVersion = await promptVersionOverride(pkg.name, pkg.currentVersion, pkg.suggestedVersion, pkg.bumpType);
562
- overrides.set(pkg.name, newVersion);
563
- }
564
- return overrides;
1294
+ function constructPublishProgram(config) {
1295
+ return Effect.gen(function* () {
1296
+ const git = yield* GitService;
1297
+ const npm = yield* NPMService;
1298
+ const workspace = yield* WorkspaceService;
1299
+ const dependencyGraph = yield* DependencyGraphService;
1300
+ yield* git.workspace.assertWorkspaceReady;
1301
+ const currentBranch = yield* git.branches.get;
1302
+ 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}"`));
1303
+ yield* Console.log(`✅ On default branch "${config.branch.default}".`);
1304
+ const publicPackages = (yield* workspace.discoverWorkspacePackages).filter((pkg) => !pkg.packageJson.private);
1305
+ yield* Console.log(`📦 Found ${publicPackages.length} public package${publicPackages.length === 1 ? "" : "s"} to check.`);
1306
+ const orderedPackages = yield* dependencyGraph.topologicalOrder(publicPackages);
1307
+ const results = [];
1308
+ for (const updateOrder of orderedPackages) {
1309
+ const pkg = updateOrder.package;
1310
+ const version = pkg.version;
1311
+ const tagName = `${pkg.name}@${version}`;
1312
+ if (yield* npm.versionExists(pkg.name, version)) {
1313
+ yield* Console.log(`⏭️ Skipping ${pkg.name}@${version} - already published.`);
1314
+ results.push({
1315
+ packageName: pkg.name,
1316
+ version,
1317
+ status: "skipped",
1318
+ reason: "Already published to npm"
1319
+ });
1320
+ continue;
1321
+ }
1322
+ yield* Console.log(`🔨 Building ${pkg.name}...`);
1323
+ yield* buildPackage(pkg.path);
1324
+ yield* Console.log(`✅ Build complete for ${pkg.name}.`);
1325
+ const distTag = getDistTag(version);
1326
+ yield* Console.log(`🚀 Publishing ${pkg.name}@${version} with tag "${distTag}"...`);
1327
+ const publishResult = yield* npm.publish({
1328
+ packagePath: pkg.path,
1329
+ tagName: distTag,
1330
+ otp: config.npm.otp,
1331
+ provenance: config.npm.provenance,
1332
+ dryRun: config.dryRun
1333
+ }).pipe(Effect.map(() => ({ success: true })), Effect.catchAll((err) => Effect.succeed({
1334
+ success: false,
1335
+ error: err
1336
+ })));
1337
+ if (publishResult.success) {
1338
+ yield* Console.log(`✅ Published ${pkg.name}@${version}.`);
1339
+ if (!config.dryRun) {
1340
+ yield* Console.log(`🏷️ Creating tag ${tagName}...`);
1341
+ yield* git.tags.create(tagName, `Release ${tagName}`);
1342
+ yield* git.tags.push(tagName);
1343
+ yield* Console.log(`✅ Tag ${tagName} created and pushed.`);
1344
+ } else yield* Console.log(`🏷️ [Dry Run] Would create and push tag ${tagName}.`);
1345
+ results.push({
1346
+ packageName: pkg.name,
1347
+ version,
1348
+ status: "published"
1349
+ });
1350
+ } else {
1351
+ const error = publishResult.error;
1352
+ yield* Console.log(`❌ Failed to publish ${pkg.name}@${version}: ${error.message}`);
1353
+ results.push({
1354
+ packageName: pkg.name,
1355
+ version,
1356
+ status: "failed",
1357
+ reason: error.message
1358
+ });
1359
+ }
1360
+ }
1361
+ const published = results.filter((r) => r.status === "published");
1362
+ const skipped = results.filter((r) => r.status === "skipped");
1363
+ const failed = results.filter((r) => r.status === "failed");
1364
+ yield* Console.log("\n📊 Publish Summary:");
1365
+ yield* Console.log(` Published: ${published.length}`);
1366
+ yield* Console.log(` Skipped: ${skipped.length}`);
1367
+ yield* Console.log(` Failed: ${failed.length}`);
1368
+ if (failed.length > 0) {
1369
+ yield* Console.log("\n❌ Failed packages:");
1370
+ for (const f of failed) yield* Console.log(` - ${f.packageName}@${f.version}: ${f.reason}`);
1371
+ return yield* Effect.fail(/* @__PURE__ */ new Error("Some packages failed to publish."));
1372
+ }
1373
+ if (published.length === 0 && skipped.length > 0) yield* Console.log("\n✅ All packages were already published.");
1374
+ else if (published.length > 0) yield* Console.log("\n🎉 Publish complete!");
1375
+ });
565
1376
  }
566
1377
 
567
1378
  //#endregion
568
- //#region src/workspace.ts
569
- const debug = createDebugger("ucdjs:release-scripts:workspace");
570
- function shouldIncludePackage(pkg, options) {
571
- if (!options) return true;
572
- if (options.excludePrivate && pkg.private) return false;
573
- if (options.included && options.included.length > 0) {
574
- if (!options.included.includes(pkg.name)) return false;
575
- }
576
- if (options.excluded?.includes(pkg.name)) return false;
577
- return true;
578
- }
579
- async function findWorkspacePackages(workspaceRoot, options) {
580
- const result = await run("pnpm", [
581
- "-r",
582
- "ls",
583
- "--json"
584
- ], { nodeOptions: {
585
- cwd: workspaceRoot,
586
- stdio: "pipe"
587
- } });
588
- const rawProjects = JSON.parse(result.stdout);
589
- const packages = [];
590
- const allPackageNames = new Set(rawProjects.map((p) => p.name));
591
- for (const rawProject of rawProjects) {
592
- const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
593
- const packageJson = JSON.parse(content);
594
- if (!shouldIncludePackage(packageJson, options)) {
595
- debug?.(`Excluding package ${rawProject.name}`);
596
- continue;
597
- }
598
- const workspaceDeps = extractWorkspaceDependencies(rawProject.dependencies, allPackageNames);
599
- const workspaceDevDeps = extractWorkspaceDependencies(rawProject.devDependencies, allPackageNames);
600
- packages.push({
601
- name: rawProject.name,
602
- version: rawProject.version,
603
- path: rawProject.path,
604
- packageJson,
605
- workspaceDependencies: workspaceDeps,
606
- workspaceDevDependencies: workspaceDevDeps
607
- });
608
- }
609
- return packages;
1379
+ //#region src/verify.ts
1380
+ function satisfiesRange(range, version) {
1381
+ return semver.satisfies(version, range, { includePrerelease: true });
610
1382
  }
611
- function extractWorkspaceDependencies(dependencies, workspacePackages) {
612
- if (!dependencies) return [];
613
- return Object.keys(dependencies).filter((dep) => {
614
- return workspacePackages.has(dep);
1383
+ function snapshotPackageJson(pkg, ref) {
1384
+ return Effect.gen(function* () {
1385
+ return yield* (yield* GitService).workspace.readFile(`${pkg.path}/package.json`, ref).pipe(Effect.flatMap((content) => Effect.try({
1386
+ try: () => JSON.parse(content),
1387
+ catch: (e) => /* @__PURE__ */ new Error(`Failed to parse package.json for ${pkg.name} at ${ref}: ${String(e)}`)
1388
+ })));
615
1389
  });
616
1390
  }
617
- function buildDependencyGraph(packages) {
618
- const packagesMap = /* @__PURE__ */ new Map();
619
- const dependents = /* @__PURE__ */ new Map();
1391
+ function findDrift(packages, releases, branchSnapshots) {
1392
+ const releaseVersionByName = /* @__PURE__ */ new Map();
1393
+ for (const rel of releases) releaseVersionByName.set(rel.package.name, rel.newVersion);
1394
+ const reasons = [];
620
1395
  for (const pkg of packages) {
621
- packagesMap.set(pkg.name, pkg);
622
- dependents.set(pkg.name, /* @__PURE__ */ new Set());
623
- }
624
- for (const pkg of packages) {
625
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
626
- for (const dep of allDeps) {
627
- const depSet = dependents.get(dep);
628
- if (depSet) depSet.add(pkg.name);
1396
+ const snapshot = branchSnapshots.get(pkg.name);
1397
+ if (snapshot == null) {
1398
+ reasons.push({
1399
+ packageName: pkg.name,
1400
+ reason: "package.json missing on release branch"
1401
+ });
1402
+ continue;
1403
+ }
1404
+ if (snapshot instanceof Error) {
1405
+ reasons.push({
1406
+ packageName: pkg.name,
1407
+ reason: snapshot.message
1408
+ });
1409
+ continue;
1410
+ }
1411
+ const expectedVersion = releaseVersionByName.get(pkg.name) ?? pkg.version;
1412
+ const branchVersion = typeof snapshot.version === "string" ? snapshot.version : void 0;
1413
+ if (!branchVersion) {
1414
+ reasons.push({
1415
+ packageName: pkg.name,
1416
+ reason: "package.json on release branch lacks version"
1417
+ });
1418
+ continue;
1419
+ }
1420
+ if (branchVersion !== expectedVersion) reasons.push({
1421
+ packageName: pkg.name,
1422
+ reason: `version mismatch: expected ${expectedVersion}, found ${branchVersion}`
1423
+ });
1424
+ for (const section of [
1425
+ "dependencies",
1426
+ "devDependencies",
1427
+ "peerDependencies"
1428
+ ]) {
1429
+ const deps = snapshot[section];
1430
+ if (!deps || typeof deps !== "object") continue;
1431
+ for (const [depName, range] of Object.entries(deps)) {
1432
+ const bumpedVersion = releaseVersionByName.get(depName);
1433
+ if (!bumpedVersion) continue;
1434
+ if (typeof range !== "string") {
1435
+ reasons.push({
1436
+ packageName: pkg.name,
1437
+ reason: `${section}.${depName} is not a string range`
1438
+ });
1439
+ continue;
1440
+ }
1441
+ if (!satisfiesRange(range, bumpedVersion)) reasons.push({
1442
+ packageName: pkg.name,
1443
+ reason: `${section}.${depName} does not include ${bumpedVersion}`
1444
+ });
1445
+ }
629
1446
  }
630
1447
  }
631
- return {
632
- packages: packagesMap,
633
- dependents
634
- };
1448
+ return reasons;
635
1449
  }
636
- function getPackageUpdateOrder(graph, changedPackages) {
637
- const result = [];
638
- const visited = /* @__PURE__ */ new Set();
639
- const toUpdate = new Set(changedPackages);
640
- const packagesToProcess = new Set(changedPackages);
641
- for (const pkg of changedPackages) {
642
- const deps = graph.dependents.get(pkg);
643
- if (deps) for (const dep of deps) {
644
- packagesToProcess.add(dep);
645
- toUpdate.add(dep);
1450
+ function constructVerifyProgram(config) {
1451
+ return Effect.gen(function* () {
1452
+ const git = yield* GitService;
1453
+ const github = yield* GitHubService;
1454
+ const dependencyGraph = yield* DependencyGraphService;
1455
+ const versionCalculator = yield* VersionCalculatorService;
1456
+ const workspace = yield* WorkspaceService;
1457
+ yield* git.workspace.assertWorkspaceReady;
1458
+ const releasePullRequest = yield* github.getPullRequestByBranch(config.branch.release);
1459
+ if (!releasePullRequest || !releasePullRequest.head) return yield* Effect.fail(/* @__PURE__ */ new Error(`Release pull request for branch "${config.branch.release}" does not exist.`));
1460
+ yield* Console.log(`✅ Release pull request #${releasePullRequest.number} exists.`);
1461
+ if ((yield* git.branches.get) !== config.branch.default) {
1462
+ yield* git.branches.checkout(config.branch.default);
1463
+ yield* Console.log(`✅ Checked out to default branch "${config.branch.default}".`);
646
1464
  }
647
- }
648
- function visit(pkgName, level) {
649
- if (visited.has(pkgName)) return;
650
- visited.add(pkgName);
651
- const pkg = graph.packages.get(pkgName);
652
- if (!pkg) return;
653
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
654
- let maxDepLevel = level;
655
- for (const dep of allDeps) if (toUpdate.has(dep)) {
656
- visit(dep, level);
657
- const depResult = result.find((r) => r.package.name === dep);
658
- if (depResult && depResult.level >= maxDepLevel) maxDepLevel = depResult.level + 1;
659
- }
660
- result.push({
661
- package: pkg,
662
- level: maxDepLevel
1465
+ const overrides = yield* loadOverrides({
1466
+ sha: releasePullRequest.head.sha,
1467
+ overridesPath: ".github/ucdjs-release.overrides.json"
663
1468
  });
664
- }
665
- for (const pkg of toUpdate) visit(pkg, 0);
666
- result.sort((a, b) => a.level - b.level);
667
- return result;
1469
+ yield* Console.log("Loaded overrides:", overrides);
1470
+ const packages = yield* workspace.discoverWorkspacePackages.pipe(Effect.flatMap(mergePackageCommitsIntoPackages), Effect.flatMap((pkgs) => mergeCommitsAffectingGloballyIntoPackage(pkgs, config.globalCommitMode)));
1471
+ yield* Console.log("Discovered packages with commits and global commits:", packages);
1472
+ const releases = yield* versionCalculator.calculateBumps(packages, overrides);
1473
+ const ordered = yield* dependencyGraph.topologicalOrder(packages);
1474
+ yield* Console.log("Calculated releases:", releases);
1475
+ yield* Console.log("Release order:", ordered);
1476
+ const releaseHeadSha = releasePullRequest.head.sha;
1477
+ const branchSnapshots = /* @__PURE__ */ new Map();
1478
+ for (const pkg of packages) {
1479
+ const snapshot = yield* snapshotPackageJson(pkg, releaseHeadSha).pipe(Effect.catchAll((err) => Effect.succeed(err instanceof Error ? err : new Error(String(err)))));
1480
+ branchSnapshots.set(pkg.name, snapshot);
1481
+ }
1482
+ const drift = findDrift(packages, releases, branchSnapshots);
1483
+ if (drift.length === 0) yield* Console.log("✅ Release branch is in sync with expected releases.");
1484
+ else yield* Console.log("❌ Release branch is out of sync:", drift);
1485
+ const status = drift.length === 0 ? {
1486
+ state: "success",
1487
+ description: "Release artifacts in sync",
1488
+ context: "release/verify"
1489
+ } : {
1490
+ state: "failure",
1491
+ description: "Release branch out of sync",
1492
+ context: "release/verify"
1493
+ };
1494
+ yield* github.setCommitStatus(releaseHeadSha, status);
1495
+ if (drift.length > 0) return yield* Effect.fail(/* @__PURE__ */ new Error("Release branch is out of sync."));
1496
+ });
668
1497
  }
669
1498
 
670
1499
  //#endregion
671
- //#region src/release.ts
672
- const isCI = process.env.CI === "true";
673
- async function release(options) {
674
- const { dryRun: dryRun$1 = false, safeguards = true, workspaceRoot = process.cwd(), releaseBranch = "release/next", githubToken } = options;
675
- globalOptions.dryRun = dryRun$1;
676
- if (githubToken.trim() === "" || githubToken == null) throw new Error("GitHub token is required");
677
- const [owner, repo] = options.repo.split("/");
678
- if (!owner || !repo) throw new Error(`Invalid repo format: ${options.repo}. Expected "owner/repo".`);
679
- if (safeguards && !isWorkingDirectoryClean(workspaceRoot)) {
680
- console.error("Working directory is not clean. Please commit or stash your changes before proceeding.");
681
- return null;
682
- }
683
- const { workspacePackages, packagesToAnalyze: initialPackages } = await discoverPackages(workspaceRoot, options);
684
- if (initialPackages.length === 0) return null;
685
- const isPackagePromptEnabled = options.prompts?.packages !== false;
686
- const isPackagesPreConfigured = Array.isArray(options.packages) || typeof options.packages === "object" && options.packages.included != null;
687
- let packagesToAnalyze = initialPackages;
688
- if (!isCI && isPackagePromptEnabled && !isPackagesPreConfigured) {
689
- const selectedNames = await promptPackageSelection(initialPackages);
690
- packagesToAnalyze = initialPackages.filter((pkg) => selectedNames.includes(pkg.name));
691
- }
692
- const changedPackages = await analyzeCommits(packagesToAnalyze, workspaceRoot);
693
- if (changedPackages.size === 0) throw new Error("No packages have changes requiring a release");
694
- let versionUpdates = calculateVersions(workspacePackages, changedPackages);
695
- const isVersionPromptEnabled = options.prompts?.versions !== false;
696
- if (!isCI && isVersionPromptEnabled) {
697
- const versionOverrides = await promptVersionOverrides(versionUpdates.map((u) => ({
698
- name: u.package.name,
699
- currentVersion: u.currentVersion,
700
- suggestedVersion: u.newVersion,
701
- bumpType: u.bumpType
702
- })));
703
- versionUpdates = versionUpdates.map((update) => {
704
- const overriddenVersion = versionOverrides.get(update.package.name);
705
- if (overriddenVersion && overriddenVersion !== update.newVersion) return {
706
- ...update,
707
- newVersion: overriddenVersion
708
- };
709
- return update;
710
- });
711
- }
712
- const allUpdates = createDependentUpdates(getPackageUpdateOrder(buildDependencyGraph(workspacePackages), new Set(versionUpdates.map((u) => u.package.name))), versionUpdates);
713
- const currentBranch = await getCurrentBranch(workspaceRoot);
714
- const existingPullRequest = await getExistingPullRequest({
715
- owner,
716
- repo,
717
- branch: releaseBranch,
718
- githubToken
1500
+ //#region src/index.ts
1501
+ async function createReleaseScripts(options) {
1502
+ const config = normalizeReleaseScriptsOptions(options);
1503
+ const AppLayer = Layer.mergeAll(ChangelogService.Default, GitService.Default, GitHubService.Default, DependencyGraphService.Default, NPMService.Default, PackageUpdaterService.Default, VersionCalculatorService.Default, WorkspaceService.Default).pipe(Layer.provide(Layer.succeed(ReleaseScriptsOptions, config)), Layer.provide(NodeCommandExecutor.layer), Layer.provide(NodeFileSystem.layer));
1504
+ const runProgram = (program) => {
1505
+ const provided = program.pipe(Effect.provide(AppLayer));
1506
+ return Effect.runPromise(provided);
1507
+ };
1508
+ const safeguardProgram = Effect.gen(function* () {
1509
+ return yield* (yield* GitService).workspace.assertWorkspaceReady;
719
1510
  });
720
- const prExists = !!existingPullRequest;
721
- if (prExists) console.log("Existing pull request found:", existingPullRequest.html_url);
722
- else console.log("No existing pull request found, will create new one");
723
- const branchExists = await doesBranchExist(releaseBranch, workspaceRoot);
724
- if (!branchExists) {
725
- console.log("Creating release branch:", releaseBranch);
726
- await createBranch(releaseBranch, currentBranch, workspaceRoot);
727
- }
728
- if (!await checkoutBranch(releaseBranch, workspaceRoot)) throw new Error(`Failed to checkout branch: ${releaseBranch}`);
729
- if (branchExists) {
730
- console.log("Pulling latest changes from remote");
731
- if (!await pullLatestChanges(releaseBranch, workspaceRoot)) console.log("Warning: Failed to pull latest changes, continuing anyway");
732
- }
733
- console.log("Rebasing release branch onto", currentBranch);
734
- await rebaseBranch(currentBranch, workspaceRoot);
735
- await updatePackageJsonFiles(allUpdates);
736
- const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
737
- const isBranchAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
738
- if (!hasCommitted && !isBranchAhead) {
739
- console.log("No changes to commit and branch is in sync with remote");
740
- await checkoutBranch(currentBranch, workspaceRoot);
741
- if (prExists) {
742
- console.log("No updates needed, PR is already up to date");
743
- return {
744
- updates: allUpdates,
745
- prUrl: existingPullRequest.html_url,
746
- created: false
747
- };
748
- } else {
749
- console.error("No changes to commit, and no existing PR. Nothing to do.");
750
- return null;
751
- }
1511
+ try {
1512
+ await runProgram(safeguardProgram);
1513
+ } catch (err) {
1514
+ const message = err instanceof Error ? err.message : String(err);
1515
+ await Effect.runPromise(Console.error(`❌ Initialization failed: ${message}`));
1516
+ throw err;
752
1517
  }
753
- console.log("Pushing changes to remote");
754
- await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
755
- const prTitle = existingPullRequest?.title || options.pullRequest?.title || "chore: update package versions";
756
- const prBody = generatePullRequestBody(allUpdates, options.pullRequest?.body);
757
- const pullRequest = await upsertPullRequest({
758
- owner,
759
- repo,
760
- pullNumber: existingPullRequest?.number,
761
- title: prTitle,
762
- body: prBody,
763
- head: releaseBranch,
764
- base: currentBranch,
765
- githubToken
766
- });
767
- console.log(prExists ? "Updated pull request:" : "Created pull request:", pullRequest?.html_url);
768
- await checkoutBranch(currentBranch, workspaceRoot);
769
1518
  return {
770
- updates: allUpdates,
771
- prUrl: pullRequest?.html_url,
772
- created: !prExists
773
- };
774
- }
775
- async function discoverPackages(workspaceRoot, options) {
776
- let workspacePackages;
777
- let packagesToAnalyze;
778
- if (typeof options.packages === "boolean" && options.packages === true) {
779
- workspacePackages = await findWorkspacePackages(workspaceRoot, { excludePrivate: false });
780
- packagesToAnalyze = workspacePackages;
781
- return {
782
- workspacePackages,
783
- packagesToAnalyze
784
- };
785
- }
786
- if (Array.isArray(options.packages)) {
787
- const packageNames = options.packages;
788
- workspacePackages = await findWorkspacePackages(workspaceRoot, {
789
- excludePrivate: false,
790
- included: packageNames
791
- });
792
- packagesToAnalyze = workspacePackages.filter((pkg) => packageNames.includes(pkg.name));
793
- if (packagesToAnalyze.length !== packageNames.length) {
794
- const found = new Set(packagesToAnalyze.map((p) => p.name));
795
- const missing = packageNames.filter((p) => !found.has(p));
796
- throw new Error(`Packages not found in workspace: ${missing.join(", ")}`);
1519
+ async verify() {
1520
+ return runProgram(constructVerifyProgram(config));
1521
+ },
1522
+ async prepare() {
1523
+ return runProgram(constructPrepareProgram(config));
1524
+ },
1525
+ async publish() {
1526
+ return runProgram(constructPublishProgram(config));
1527
+ },
1528
+ packages: {
1529
+ async list() {
1530
+ return runProgram(Effect.gen(function* () {
1531
+ return yield* (yield* WorkspaceService).discoverWorkspacePackages;
1532
+ }));
1533
+ },
1534
+ async get(packageName) {
1535
+ return runProgram(Effect.gen(function* () {
1536
+ return (yield* (yield* WorkspaceService).findPackageByName(packageName)) || null;
1537
+ }));
1538
+ }
797
1539
  }
798
- return {
799
- workspacePackages,
800
- packagesToAnalyze
801
- };
802
- }
803
- workspacePackages = await findWorkspacePackages(workspaceRoot, options.packages);
804
- packagesToAnalyze = workspacePackages;
805
- return {
806
- workspacePackages,
807
- packagesToAnalyze
808
1540
  };
809
1541
  }
810
- async function analyzeCommits(packages, workspaceRoot) {
811
- const changedPackages = /* @__PURE__ */ new Map();
812
- for (const pkg of packages) {
813
- const bump = await analyzePackageCommits(pkg, workspaceRoot);
814
- if (bump !== "none") changedPackages.set(pkg.name, bump);
815
- }
816
- return changedPackages;
817
- }
818
- function calculateVersions(allPackages, changedPackages) {
819
- const updates = [];
820
- for (const [pkgName, bump] of changedPackages) {
821
- const pkg = allPackages.find((p) => p.name === pkgName);
822
- if (!pkg) continue;
823
- updates.push(createVersionUpdate(pkg, bump, true));
824
- }
825
- return updates;
826
- }
827
- async function updatePackageJsonFiles(updates) {
828
- await Promise.all(updates.map(async (update) => {
829
- const depUpdates = getDependencyUpdates(update.package, updates);
830
- await updatePackageJson(update.package, update.newVersion, depUpdates);
831
- }));
832
- }
833
1542
 
834
1543
  //#endregion
835
- export { release };
1544
+ export { createReleaseScripts };