@varlock/bumpy 0.0.1 → 1.0.0

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