@varlock/bumpy 0.0.1 → 0.0.2

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.
Files changed (38) hide show
  1. package/dist/add-BjyVIUlr.mjs +175 -0
  2. package/dist/{ai-B8ZL2x8z.mjs → ai-CQhUyHAG.mjs} +2 -2
  3. package/dist/{apply-release-plan-DtU3rVyL.mjs → apply-release-plan-D6TSrcwX.mjs} +11 -6
  4. package/dist/changelog-github-Du62krXi.mjs +193 -0
  5. package/dist/{changeset-ClCYsChu.mjs → changeset-UCZdSRDv.mjs} +36 -3
  6. package/dist/{check-CkRubvuk.mjs → check-jIwike9F.mjs} +5 -11
  7. package/dist/ci-D6LQbR38.mjs +585 -0
  8. package/dist/ci-setup-C6FlOfW5.mjs +211 -0
  9. package/dist/clack-CDRCHrC-.mjs +1216 -0
  10. package/dist/cli.mjs +20 -16
  11. package/dist/{config-CJ2orhTL.mjs → config-BkwIEaQg.mjs} +2 -2
  12. package/dist/{fs-DbNNEyzq.mjs → fs-0AtnPUUe.mjs} +1 -1
  13. package/dist/{generate-oOFD9ABC.mjs → generate-Btrsn1qi.mjs} +28 -9
  14. package/dist/git-CGHVXXKw.mjs +78 -0
  15. package/dist/index.d.mts +10 -2
  16. package/dist/index.mjs +8 -8
  17. package/dist/{init-Blw2GfC_.mjs → init-B0q3wEQW.mjs} +2 -2
  18. package/dist/logger-C2dEe5Su.mjs +135 -0
  19. package/dist/{migrate-DvOrXSw0.mjs → migrate-CfQNwD0T.mjs} +18 -11
  20. package/dist/{names-C-u50ofE.mjs → names-Ck8cun7B.mjs} +2 -1
  21. package/dist/package-manager-DcI5TdDE.mjs +80 -0
  22. package/dist/{publish-DZ3m7qkX.mjs → publish-D_7RqEYL.mjs} +74 -20
  23. package/dist/{publish-pipeline-1M5GmbdP.mjs → publish-pipeline-ChnqW8nR.mjs} +40 -54
  24. package/dist/{release-plan-CFnutSHD.mjs → release-plan-BEzwApuK.mjs} +2 -2
  25. package/dist/{semver-DWO6NFKN.mjs → semver-BTzYh8vc.mjs} +1 -1
  26. package/dist/shell-Dj7JRD_q.mjs +92 -0
  27. package/dist/{status-DRpq_Mha.mjs → status--Q8yAxQ4.mjs} +7 -7
  28. package/dist/{version-CJwf8XIA.mjs → version-cAUkfYPx.mjs} +60 -21
  29. package/dist/workspace-CxEKakDm.mjs +107 -0
  30. package/package.json +4 -2
  31. package/dist/add-u5V9V3L7.mjs +0 -131
  32. package/dist/changelog-github-n-3zV1p9.mjs +0 -59
  33. package/dist/ci-8KWWhjXl.mjs +0 -224
  34. package/dist/logger-ZqggsyGZ.mjs +0 -176
  35. package/dist/prompt-BP8toAOI.mjs +0 -46
  36. package/dist/shell-DPlltpzb.mjs +0 -44
  37. package/dist/workspace-mVjawG8g.mjs +0 -183
  38. /package/dist/{dep-graph-DiLeAhl9.mjs → dep-graph-E-9-eQ2J.mjs} +0 -0
@@ -0,0 +1,585 @@
1
+ import { n as log, t as colorize } from "./logger-C2dEe5Su.mjs";
2
+ import { a as loadConfig } from "./config-BkwIEaQg.mjs";
3
+ import { t as detectPackageManager } from "./package-manager-DcI5TdDE.mjs";
4
+ import { n as discoverWorkspace } from "./workspace-CxEKakDm.mjs";
5
+ import { t as DependencyGraph } from "./dep-graph-E-9-eQ2J.mjs";
6
+ import { n as runArgsAsync, o as tryRunArgs, t as runArgs } from "./shell-Dj7JRD_q.mjs";
7
+ import { r as readChangesets } from "./changeset-UCZdSRDv.mjs";
8
+ import { t as assembleReleasePlan } from "./release-plan-BEzwApuK.mjs";
9
+ import { n as getChangedFiles } from "./git-CGHVXXKw.mjs";
10
+ import { t as randomName } from "./names-Ck8cun7B.mjs";
11
+ //#region src/commands/ci.ts
12
+ /** Validate a git branch name to prevent injection */
13
+ function validateBranchName(name) {
14
+ if (!/^[a-zA-Z0-9_./-]+$/.test(name)) throw new Error(`Invalid branch name: ${name}`);
15
+ return name;
16
+ }
17
+ /** Validate a PR number is numeric */
18
+ function validatePrNumber(pr) {
19
+ if (!/^\d+$/.test(pr)) throw new Error(`Invalid PR number: ${pr}`);
20
+ return pr;
21
+ }
22
+ /** Configure git identity for CI commits if not already set */
23
+ function ensureGitIdentity(rootDir, config) {
24
+ if (!tryRunArgs([
25
+ "git",
26
+ "config",
27
+ "user.name"
28
+ ], { cwd: rootDir })) {
29
+ const { name: gitName, email: gitEmail } = config.gitUser;
30
+ runArgs([
31
+ "git",
32
+ "config",
33
+ "user.name",
34
+ gitName
35
+ ], { cwd: rootDir });
36
+ runArgs([
37
+ "git",
38
+ "config",
39
+ "user.email",
40
+ gitEmail
41
+ ], { cwd: rootDir });
42
+ log.dim(` Using git identity: ${gitName} <${gitEmail}>`);
43
+ }
44
+ }
45
+ /**
46
+ * CI check: report on pending changesets.
47
+ * Designed for PR workflows — shows what would be released and optionally comments on the PR.
48
+ */
49
+ async function ciCheckCommand(rootDir, opts) {
50
+ const config = await loadConfig(rootDir);
51
+ const { packages } = await discoverWorkspace(rootDir, config);
52
+ const depGraph = new DependencyGraph(packages);
53
+ const allChangesets = await readChangesets(rootDir);
54
+ if (detectPrBranch(rootDir) === config.versionPr.branch) {
55
+ log.dim(" Skipping — this is the version PR branch.");
56
+ return;
57
+ }
58
+ const inCI = !!process.env.CI;
59
+ const shouldComment = opts.comment ?? inCI;
60
+ const prNumber = detectPrNumber();
61
+ const pm = await detectPackageManager(rootDir);
62
+ const changedFiles = getChangedFiles(rootDir, config.baseBranch);
63
+ const prChangesetIds = new Set(changedFiles.filter((f) => /^\.bumpy\/.*\.md$/.test(f) && !f.endsWith("README.md")).map((f) => f.replace(/^\.bumpy\//, "").replace(/\.md$/, "")));
64
+ const prChangesets = allChangesets.filter((cs) => prChangesetIds.has(cs.id));
65
+ if (prChangesets.length === 0) {
66
+ log.info("No changesets found in this PR.");
67
+ if (shouldComment && prNumber) await postOrUpdatePrComment(prNumber, formatNoChangesetsComment(detectPrBranch(rootDir), pm), rootDir);
68
+ if (opts.failOnMissing) process.exit(1);
69
+ return;
70
+ }
71
+ const plan = assembleReleasePlan(prChangesets, packages, depGraph, config);
72
+ log.bold(`${prChangesets.length} changeset(s) → ${plan.releases.length} package(s) to release\n`);
73
+ for (const r of plan.releases) {
74
+ const tag = r.isDependencyBump ? " (dep)" : r.isCascadeBump ? " (cascade)" : "";
75
+ console.log(` ${r.name}: ${r.oldVersion} → ${colorize(r.newVersion, "cyan")}${tag}`);
76
+ }
77
+ if (shouldComment && prNumber) await postOrUpdatePrComment(prNumber, formatReleasePlanComment(plan, prChangesets, prNumber, detectPrBranch(rootDir), pm), rootDir);
78
+ }
79
+ /**
80
+ * CI release: either auto-publish or create a version PR.
81
+ * Designed for merge-to-main workflows.
82
+ */
83
+ async function ciReleaseCommand(rootDir, opts) {
84
+ const config = await loadConfig(rootDir);
85
+ ensureGitIdentity(rootDir, config);
86
+ const { packages } = await discoverWorkspace(rootDir, config);
87
+ const depGraph = new DependencyGraph(packages);
88
+ const changesets = await readChangesets(rootDir);
89
+ if (changesets.length === 0) {
90
+ log.info("No pending changesets — checking for unpublished packages...");
91
+ const { publishCommand } = await import("./publish-D_7RqEYL.mjs");
92
+ await publishCommand(rootDir, { tag: opts.tag });
93
+ return;
94
+ }
95
+ const plan = assembleReleasePlan(changesets, packages, depGraph, config);
96
+ if (plan.releases.length === 0) {
97
+ log.info("Changesets found but no packages would be released.");
98
+ return;
99
+ }
100
+ if (opts.mode === "auto-publish") await autoPublish(rootDir, config, opts.tag);
101
+ else await createVersionPr(rootDir, plan, config, new Map([...packages.values()].map((p) => [p.name, p.relativeDir])), opts.branch);
102
+ }
103
+ async function autoPublish(rootDir, config, tag) {
104
+ log.step("Running bumpy version...");
105
+ const { versionCommand } = await import("./version-cAUkfYPx.mjs");
106
+ await versionCommand(rootDir);
107
+ log.step("Committing version changes...");
108
+ runArgs([
109
+ "git",
110
+ "add",
111
+ "-A"
112
+ ], { cwd: rootDir });
113
+ if (tryRunArgs([
114
+ "git",
115
+ "status",
116
+ "--porcelain"
117
+ ], { cwd: rootDir })) {
118
+ runArgs([
119
+ "git",
120
+ "commit",
121
+ "-m",
122
+ "Version packages"
123
+ ], { cwd: rootDir });
124
+ runArgs(["git", "push"], { cwd: rootDir });
125
+ }
126
+ log.step("Running bumpy publish...");
127
+ const { publishCommand } = await import("./publish-D_7RqEYL.mjs");
128
+ await publishCommand(rootDir, { tag });
129
+ }
130
+ /**
131
+ * Push a branch to origin, optionally using a custom GitHub token.
132
+ *
133
+ * When `BUMPY_GH_TOKEN` is set, the remote URL is temporarily rewritten to
134
+ * include the token. Pushes made with a PAT or GitHub App token bypass
135
+ * GitHub's anti-recursion guard, allowing `pull_request` workflows to fire
136
+ * on the version PR — no extra CI configuration required.
137
+ *
138
+ * When only the default `GITHUB_TOKEN` is available the push still succeeds,
139
+ * but PR workflows won't be triggered automatically.
140
+ */
141
+ function pushWithToken(rootDir, branch) {
142
+ const token = process.env.BUMPY_GH_TOKEN;
143
+ const repo = process.env.GITHUB_REPOSITORY;
144
+ const server = process.env.GITHUB_SERVER_URL || "https://github.com";
145
+ if (token && repo) {
146
+ const authedUrl = `${server.replace("://", `://x-access-token:${token}@`)}/${repo}.git`;
147
+ const originalUrl = tryRunArgs([
148
+ "git",
149
+ "remote",
150
+ "get-url",
151
+ "origin"
152
+ ], { cwd: rootDir });
153
+ const extraHeaderKey = `http.${server}/.extraheader`;
154
+ const savedHeader = tryRunArgs([
155
+ "git",
156
+ "config",
157
+ "--local",
158
+ extraHeaderKey
159
+ ], { cwd: rootDir });
160
+ const includeIfRaw = tryRunArgs([
161
+ "git",
162
+ "config",
163
+ "--local",
164
+ "--get-regexp",
165
+ "^includeIf\\.gitdir:"
166
+ ], { cwd: rootDir });
167
+ const savedIncludeIfs = [];
168
+ if (includeIfRaw) for (const line of includeIfRaw.split("\n").filter(Boolean)) {
169
+ const spaceIdx = line.indexOf(" ");
170
+ if (spaceIdx > 0) savedIncludeIfs.push({
171
+ key: line.slice(0, spaceIdx),
172
+ value: line.slice(spaceIdx + 1)
173
+ });
174
+ }
175
+ try {
176
+ if (savedHeader) runArgs([
177
+ "git",
178
+ "config",
179
+ "--local",
180
+ "--unset-all",
181
+ extraHeaderKey
182
+ ], { cwd: rootDir });
183
+ for (const entry of savedIncludeIfs) tryRunArgs([
184
+ "git",
185
+ "config",
186
+ "--local",
187
+ "--unset",
188
+ entry.key
189
+ ], { cwd: rootDir });
190
+ runArgs([
191
+ "git",
192
+ "remote",
193
+ "set-url",
194
+ "origin",
195
+ authedUrl
196
+ ], { cwd: rootDir });
197
+ runArgs([
198
+ "git",
199
+ "push",
200
+ "-u",
201
+ "origin",
202
+ branch,
203
+ "--force"
204
+ ], { cwd: rootDir });
205
+ } finally {
206
+ if (originalUrl) runArgs([
207
+ "git",
208
+ "remote",
209
+ "set-url",
210
+ "origin",
211
+ originalUrl
212
+ ], { cwd: rootDir });
213
+ if (savedHeader) runArgs([
214
+ "git",
215
+ "config",
216
+ "--local",
217
+ extraHeaderKey,
218
+ savedHeader
219
+ ], { cwd: rootDir });
220
+ for (const entry of savedIncludeIfs) tryRunArgs([
221
+ "git",
222
+ "config",
223
+ "--local",
224
+ entry.key,
225
+ entry.value
226
+ ], { cwd: rootDir });
227
+ }
228
+ log.dim(" Pushed with custom token — PR workflows will be triggered");
229
+ } else {
230
+ runArgs([
231
+ "git",
232
+ "push",
233
+ "-u",
234
+ "origin",
235
+ branch,
236
+ "--force"
237
+ ], { cwd: rootDir });
238
+ if (!token && repo) log.warn("BUMPY_GH_TOKEN is not set — PR checks will not trigger automatically.\n Run `bumpy ci setup` for help.");
239
+ }
240
+ }
241
+ async function createVersionPr(rootDir, plan, config, packageDirs, branchName) {
242
+ const branch = validateBranchName(branchName || config.versionPr.branch);
243
+ const baseBranch = validateBranchName(tryRunArgs([
244
+ "git",
245
+ "rev-parse",
246
+ "--abbrev-ref",
247
+ "HEAD"
248
+ ], { cwd: rootDir }) || "main");
249
+ const existingPr = tryRunArgs([
250
+ "gh",
251
+ "pr",
252
+ "list",
253
+ "--head",
254
+ branch,
255
+ "--json",
256
+ "number",
257
+ "--jq",
258
+ ".[0].number"
259
+ ], { cwd: rootDir });
260
+ log.step(`Creating branch ${branch}...`);
261
+ if (tryRunArgs([
262
+ "git",
263
+ "rev-parse",
264
+ "--verify",
265
+ branch
266
+ ], { cwd: rootDir }) !== null) {
267
+ runArgs([
268
+ "git",
269
+ "checkout",
270
+ branch
271
+ ], { cwd: rootDir });
272
+ runArgs([
273
+ "git",
274
+ "reset",
275
+ "--hard",
276
+ baseBranch
277
+ ], { cwd: rootDir });
278
+ } else runArgs([
279
+ "git",
280
+ "checkout",
281
+ "-b",
282
+ branch
283
+ ], { cwd: rootDir });
284
+ log.step("Running bumpy version...");
285
+ const { versionCommand } = await import("./version-cAUkfYPx.mjs");
286
+ await versionCommand(rootDir);
287
+ runArgs([
288
+ "git",
289
+ "add",
290
+ "-A"
291
+ ], { cwd: rootDir });
292
+ if (!tryRunArgs([
293
+ "git",
294
+ "status",
295
+ "--porcelain"
296
+ ], { cwd: rootDir })) {
297
+ log.info("No version changes to commit.");
298
+ runArgs([
299
+ "git",
300
+ "checkout",
301
+ baseBranch
302
+ ], { cwd: rootDir });
303
+ return;
304
+ }
305
+ runArgs([
306
+ "git",
307
+ "commit",
308
+ "-F",
309
+ "-"
310
+ ], {
311
+ cwd: rootDir,
312
+ input: [
313
+ "Version packages",
314
+ "",
315
+ ...plan.releases.map((r) => `${r.name}@${r.newVersion}`)
316
+ ].join("\n")
317
+ });
318
+ pushWithToken(rootDir, branch);
319
+ const prBody = formatVersionPrBody(plan, config.versionPr.preamble, packageDirs);
320
+ if (existingPr) {
321
+ const validPr = validatePrNumber(existingPr);
322
+ log.step(`Updating existing PR #${validPr}...`);
323
+ await runArgsAsync([
324
+ "gh",
325
+ "pr",
326
+ "edit",
327
+ validPr,
328
+ "--title",
329
+ config.versionPr.title,
330
+ "--body-file",
331
+ "-"
332
+ ], {
333
+ cwd: rootDir,
334
+ input: prBody
335
+ });
336
+ log.success(`Updated PR #${validPr}`);
337
+ } else {
338
+ log.step("Creating version PR...");
339
+ const prTitle = config.versionPr.title;
340
+ const result = await runArgsAsync([
341
+ "gh",
342
+ "pr",
343
+ "create",
344
+ "--title",
345
+ prTitle,
346
+ "--body-file",
347
+ "-",
348
+ "--base",
349
+ baseBranch,
350
+ "--head",
351
+ branch
352
+ ], {
353
+ cwd: rootDir,
354
+ input: prBody
355
+ });
356
+ log.success(`Created PR: ${result}`);
357
+ }
358
+ runArgs([
359
+ "git",
360
+ "checkout",
361
+ baseBranch
362
+ ], { cwd: rootDir });
363
+ }
364
+ const FROG_IMG_BASE = "https://raw.githubusercontent.com/dmno-dev/bumpy/main/images";
365
+ function buildAddChangesetLink(prBranch) {
366
+ if (!prBranch) return null;
367
+ const repo = process.env.GITHUB_REPOSITORY;
368
+ if (!repo) return null;
369
+ const template = [
370
+ "---",
371
+ "\"package-name\": patch",
372
+ "---",
373
+ "",
374
+ "Description of the change",
375
+ ""
376
+ ].join("\n");
377
+ const filename = `.bumpy/${randomName()}.md`;
378
+ return `https://github.com/${repo}/new/${prBranch}?filename=${encodeURIComponent(filename)}&value=${encodeURIComponent(template)}`;
379
+ }
380
+ function pmRunCommand(pm) {
381
+ if (pm === "bun") return "bunx bumpy";
382
+ if (pm === "pnpm") return "pnpm exec bumpy";
383
+ if (pm === "yarn") return "yarn bumpy";
384
+ return "npx bumpy";
385
+ }
386
+ function formatReleasePlanComment(plan, changesets, prNumber, prBranch, pm) {
387
+ const repo = process.env.GITHUB_REPOSITORY;
388
+ const lines = [];
389
+ const preamble = [
390
+ `<a href="https://github.com/dmno-dev/bumpy"><img src="${FROG_IMG_BASE}/frog-talking.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>`,
391
+ "",
392
+ "**The changes in this PR will be included in the next version bump.**",
393
+ "<br clear=\"left\" />"
394
+ ].join("\n");
395
+ lines.push(preamble);
396
+ lines.push("");
397
+ const groups = {
398
+ major: [],
399
+ minor: [],
400
+ patch: []
401
+ };
402
+ for (const r of plan.releases) groups[r.type]?.push(r);
403
+ for (const type of [
404
+ "major",
405
+ "minor",
406
+ "patch"
407
+ ]) {
408
+ const releases = groups[type];
409
+ if (!releases || releases.length === 0) continue;
410
+ lines.push(bumpSectionHeader(type));
411
+ lines.push("");
412
+ for (const r of releases) {
413
+ const suffix = r.isDependencyBump ? " _(dep)_" : r.isCascadeBump ? " _(cascade)_" : "";
414
+ lines.push(`- \`${r.name}\` ${r.oldVersion} → **${r.newVersion}**${suffix}`);
415
+ }
416
+ lines.push("");
417
+ }
418
+ lines.push(`#### Changesets in this PR`);
419
+ lines.push("");
420
+ for (const cs of changesets) {
421
+ const filename = `${cs.id}.md`;
422
+ const parts = [`\`${filename}\``];
423
+ if (repo) {
424
+ parts.push(`([view diff](https://github.com/${repo}/pull/${prNumber}/files#diff-.bumpy/${filename}))`);
425
+ if (prBranch) parts.push(`([edit](https://github.com/${repo}/edit/${prBranch}/.bumpy/${filename}))`);
426
+ }
427
+ lines.push(`- ${parts.join(" ")}`);
428
+ }
429
+ lines.push("");
430
+ const addLink = buildAddChangesetLink(prBranch);
431
+ if (addLink) lines.push(`[Click here if you want to add another changeset to this PR](${addLink})\n`);
432
+ else lines.push(`To add another changeset, run \`${pmRunCommand(pm)} add\`\n`);
433
+ lines.push("---");
434
+ lines.push(`_This comment is maintained by [bumpy](https://github.com/dmno-dev/bumpy)._`);
435
+ return lines.join("\n");
436
+ }
437
+ function formatNoChangesetsComment(prBranch, pm) {
438
+ const runCmd = pmRunCommand(pm);
439
+ const lines = [
440
+ `<a href="https://github.com/dmno-dev/bumpy"><img src="${FROG_IMG_BASE}/frog-neutral.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>`,
441
+ "",
442
+ "Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**",
443
+ "<br clear=\"left\" />\n",
444
+ "You can add a changeset by running:\n",
445
+ "```bash",
446
+ `${runCmd} add`,
447
+ "```"
448
+ ];
449
+ const addLink = buildAddChangesetLink(prBranch);
450
+ if (addLink) {
451
+ lines.push("");
452
+ lines.push(`Or [click here to add a changeset](${addLink}) directly on GitHub.`);
453
+ }
454
+ lines.push("\n---");
455
+ lines.push(`_This comment is maintained by [bumpy](https://github.com/dmno-dev/bumpy)._`);
456
+ return lines.join("\n");
457
+ }
458
+ function bumpSectionHeader(type) {
459
+ return `### ${`<img src="${FROG_IMG_BASE}/frog-${type}.png" alt="${type}" width="52" style="image-rendering: pixelated;" align="right" />`} ${type.charAt(0).toUpperCase() + type.slice(1)} releases`;
460
+ }
461
+ /** Build inline diff links for a package's changed files in the PR */
462
+ function buildDiffLinks(pkgDir) {
463
+ const pkgJsonPath = `${pkgDir}/package.json`;
464
+ const changelogPath = `${pkgDir}/CHANGELOG.md`;
465
+ return ` <sub>${[`[package.json](#diff-${sha256Hex(pkgJsonPath)})`, `[CHANGELOG.md](#diff-${sha256Hex(changelogPath)})`].join(" · ")}</sub>`;
466
+ }
467
+ function sha256Hex(input) {
468
+ const encoder = new TextEncoder();
469
+ const hasher = new Bun.CryptoHasher("sha256");
470
+ hasher.update(encoder.encode(input));
471
+ return hasher.digest("hex");
472
+ }
473
+ function formatVersionPrBody(plan, preamble, packageDirs) {
474
+ const lines = [];
475
+ lines.push(preamble);
476
+ lines.push("");
477
+ const groups = {
478
+ major: [],
479
+ minor: [],
480
+ patch: []
481
+ };
482
+ for (const r of plan.releases) groups[r.type]?.push(r);
483
+ for (const type of [
484
+ "major",
485
+ "minor",
486
+ "patch"
487
+ ]) {
488
+ const releases = groups[type];
489
+ if (!releases || releases.length === 0) continue;
490
+ lines.push(bumpSectionHeader(type));
491
+ lines.push("");
492
+ for (const r of releases) {
493
+ const suffix = r.isDependencyBump ? " _(dep)_" : r.isCascadeBump ? " _(cascade)_" : "";
494
+ const pkgDir = packageDirs.get(r.name);
495
+ const diffLinks = pkgDir ? buildDiffLinks(pkgDir) : "";
496
+ lines.push(`#### \`${r.name}\` ${r.oldVersion} → **${r.newVersion}**${suffix}${diffLinks}`);
497
+ lines.push("");
498
+ const relevantChangesets = plan.changesets.filter((cs) => r.changesets.includes(cs.id));
499
+ if (relevantChangesets.length > 0) {
500
+ for (const cs of relevantChangesets) if (cs.summary) {
501
+ const csLink = ` ([changeset](#diff-${sha256Hex(`.bumpy/${cs.id}.md`)}))`;
502
+ const summaryLines = cs.summary.split("\n");
503
+ lines.push(`- ${summaryLines[0]}${csLink}`);
504
+ for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${summaryLines[i]}`);
505
+ }
506
+ } else if (r.isDependencyBump) lines.push("- Updated dependencies");
507
+ else if (r.isCascadeBump) lines.push("- Version bump via cascade rule");
508
+ lines.push("");
509
+ }
510
+ }
511
+ return lines.join("\n");
512
+ }
513
+ const COMMENT_MARKER = "<!-- bumpy-release-plan -->";
514
+ async function postOrUpdatePrComment(prNumber, body, rootDir) {
515
+ const validPr = validatePrNumber(prNumber);
516
+ const markedBody = `${COMMENT_MARKER}\n${body}`;
517
+ try {
518
+ const commentId = tryRunArgs([
519
+ "gh",
520
+ "pr",
521
+ "view",
522
+ validPr,
523
+ "--json",
524
+ "comments",
525
+ "--jq",
526
+ `.comments[] | select(.body | startswith("${COMMENT_MARKER}")) | .url | capture("issuecomment-(?<id>[0-9]+)$") | .id`
527
+ ], { cwd: rootDir })?.split("\n")[0]?.trim();
528
+ if (commentId) {
529
+ await runArgsAsync([
530
+ "gh",
531
+ "api",
532
+ `repos/{owner}/{repo}/issues/comments/${commentId}`,
533
+ "-X",
534
+ "PATCH",
535
+ "-F",
536
+ "body=@-"
537
+ ], {
538
+ cwd: rootDir,
539
+ input: markedBody
540
+ });
541
+ log.dim(" Updated PR comment");
542
+ } else {
543
+ await runArgsAsync([
544
+ "gh",
545
+ "pr",
546
+ "comment",
547
+ validPr,
548
+ "--body-file",
549
+ "-"
550
+ ], {
551
+ cwd: rootDir,
552
+ input: markedBody
553
+ });
554
+ log.dim(" Posted PR comment");
555
+ }
556
+ } catch (err) {
557
+ log.warn(` Failed to comment on PR: ${err instanceof Error ? err.message : err}`);
558
+ }
559
+ }
560
+ function detectPrBranch(rootDir) {
561
+ if (process.env.GITHUB_HEAD_REF) return process.env.GITHUB_HEAD_REF;
562
+ return tryRunArgs([
563
+ "gh",
564
+ "pr",
565
+ "view",
566
+ "--json",
567
+ "headRefName",
568
+ "--jq",
569
+ ".headRefName"
570
+ ], { cwd: rootDir })?.trim() || null;
571
+ }
572
+ function detectPrNumber() {
573
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
574
+ const match = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\//);
575
+ if (match) return match[1];
576
+ }
577
+ const envPr = process.env.BUMPY_PR_NUMBER || process.env.PR_NUMBER || null;
578
+ if (envPr && !/^\d+$/.test(envPr)) {
579
+ log.warn(`Ignoring invalid PR number from environment: ${envPr}`);
580
+ return null;
581
+ }
582
+ return envPr;
583
+ }
584
+ //#endregion
585
+ export { ciCheckCommand, ciReleaseCommand };