@varlock/bumpy 1.13.2 → 1.14.0-rc.1

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 (28) hide show
  1. package/README.md +3 -1
  2. package/config-schema.json +43 -0
  3. package/dist/{add-Dt1hddMt.mjs → add-C9rU_89s.mjs} +4 -4
  4. package/dist/{apply-release-plan-DD2R7SL2.mjs → apply-release-plan-DxTsUSqa.mjs} +11 -2
  5. package/dist/{bump-file-B7hmXZlB.mjs → bump-file-mRJeReRJ.mjs} +43 -8
  6. package/dist/{changelog-CbaET5V6.mjs → changelog-DuFhnJRO.mjs} +3 -3
  7. package/dist/{changelog-github-DXDnWkrB.mjs → changelog-github-jLOtwuWj.mjs} +2 -2
  8. package/dist/channels-CFXZkyGd.mjs +75 -0
  9. package/dist/{check-Dvi0DIqC.mjs → check-DIl9Dz68.mjs} +18 -6
  10. package/dist/{ci-B7gF6CFP.mjs → ci-ChYmDuwy.mjs} +376 -23
  11. package/dist/cli.mjs +30 -15
  12. package/dist/{config-D_4GYDJi.mjs → config-0we4ISZX.mjs} +5 -1
  13. package/dist/{generate-DohUlhu3.mjs → generate-B2OMt_64.mjs} +3 -3
  14. package/dist/{git-BWPimLgc.mjs → git-DAWj8LyV.mjs} +12 -1
  15. package/dist/index.d.mts +46 -4
  16. package/dist/index.mjs +7 -7
  17. package/dist/prerelease-B2PVfXkm.mjs +205 -0
  18. package/dist/{publish-VYBhDYFM.mjs → publish-Ak6jmwi_.mjs} +105 -14
  19. package/dist/{publish-pipeline-BPedWvKS.mjs → publish-pipeline-BD8mLbL9.mjs} +2 -2
  20. package/dist/{release-plan-mK7iGeGq.mjs → release-plan-C84pcBi-.mjs} +12 -17
  21. package/dist/status-DIzi-Iai.mjs +232 -0
  22. package/dist/{types-Bkh-igOJ.mjs → types-lpiG-Zxh.mjs} +1 -0
  23. package/dist/version-CMJUopVj.mjs +192 -0
  24. package/package.json +1 -1
  25. package/dist/status-DxzKPM8d.mjs +0 -129
  26. package/dist/version-Cm0nRAFF.mjs +0 -123
  27. /package/dist/{commit-message-CSWVKPJ-.mjs → commit-message-BwsowSds.mjs} +0 -0
  28. /package/dist/{init-BCkm6Nfa.mjs → init-DkAY5hjc.mjs} +0 -0
@@ -1,13 +1,15 @@
1
1
  import { n as log, t as colorize } from "./logger-BgksGFuf.mjs";
2
- import { a as loadConfig } from "./config-D_4GYDJi.mjs";
2
+ import { a as loadConfig } from "./config-0we4ISZX.mjs";
3
3
  import { n as detectPackageManager } from "./package-manager-Db_vTztt.mjs";
4
- import { i as recoverDeletedBumpFiles, r as readBumpFiles, s as discoverWorkspace, t as filterBranchBumpFiles } from "./bump-file-B7hmXZlB.mjs";
5
- import { a as DependencyGraph, t as assembleReleasePlan } from "./release-plan-mK7iGeGq.mjs";
4
+ import { a as recoverDeletedBumpFiles, c as discoverWorkspace, i as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-mRJeReRJ.mjs";
5
+ import { o as DependencyGraph, t as assembleReleasePlan } from "./release-plan-C84pcBi-.mjs";
6
6
  import { n as runArgs, r as runArgsAsync, s as tryRunArgs } from "./shell-C8KgKnMQ.mjs";
7
- import { a as getChangedFiles, f as withGitToken } from "./git-BWPimLgc.mjs";
7
+ import { a as getChangedFiles, p as withGitToken } from "./git-DAWj8LyV.mjs";
8
8
  import { t as randomName } from "./names-COooXAFg.mjs";
9
- import { n as findChangedPackages } from "./check-Dvi0DIqC.mjs";
10
- import { t as resolveCommitMessage } from "./commit-message-CSWVKPJ-.mjs";
9
+ import { n as findChangedPackages } from "./check-DIl9Dz68.mjs";
10
+ import { channelNames, detectReleaseBranch, matchChannelByBranch, resolveChannels } from "./channels-CFXZkyGd.mjs";
11
+ import { n as channelDisplayPlan, r as formatChannelVersionSummary, t as buildChannelReleasePlan } from "./prerelease-B2PVfXkm.mjs";
12
+ import { t as resolveCommitMessage } from "./commit-message-BwsowSds.mjs";
11
13
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
14
  import { createHash } from "node:crypto";
13
15
  //#region src/commands/ci.ts
@@ -76,15 +78,19 @@ async function ciCheckCommand(rootDir, opts) {
76
78
  const { packages } = await discoverWorkspace(rootDir, config);
77
79
  const depGraph = new DependencyGraph(packages);
78
80
  const { bumpFiles: allBumpFiles, errors: parseErrors } = await readBumpFiles(rootDir);
79
- if (detectPrBranch(rootDir) === config.versionPr.branch) {
80
- log.dim(" Skipping this is the version PR branch.");
81
+ const prBranchName = detectPrBranch(rootDir);
82
+ const releasePrBranches = new Set([config.versionPr.branch, ...[...resolveChannels(config).values()].map((c) => c.versionPr.branch)]);
83
+ if (prBranchName && releasePrBranches.has(prBranchName)) {
84
+ log.dim(" Skipping — this is a release PR branch.");
81
85
  return;
82
86
  }
83
87
  const inCI = !!process.env.CI;
84
88
  const shouldComment = opts.comment ?? inCI;
85
89
  const prNumber = detectPrNumber();
86
90
  const pm = await detectPackageManager(rootDir);
87
- const changedFiles = getChangedFiles(rootDir, config.baseBranch);
91
+ const compareBranch = process.env.GITHUB_BASE_REF || config.baseBranch;
92
+ const prChannel = matchChannelByBranch(config, process.env.GITHUB_BASE_REF || null);
93
+ const changedFiles = getChangedFiles(rootDir, compareBranch);
88
94
  const { branchBumpFiles: prBumpFiles, emptyBumpFileIds } = filterBranchBumpFiles(allBumpFiles, changedFiles, rootDir, parseErrors);
89
95
  if (parseErrors.length > 0) for (const err of parseErrors) log.error(err);
90
96
  if (prBumpFiles.length === 0) {
@@ -108,14 +114,15 @@ async function ciCheckCommand(rootDir, opts) {
108
114
  if (willFail) process.exit(1);
109
115
  return;
110
116
  }
111
- const plan = assembleReleasePlan(prBumpFiles, packages, depGraph, config);
112
- log.bold(`${prBumpFiles.length} bump file(s) ${plan.releases.length} package(s) to release\n`);
117
+ const plan = assembleReleasePlan(prBumpFiles, packages, depGraph, config, prChannel ? { prereleasePreid: prChannel.preid } : {});
118
+ const releaseSuffix = prChannel ? `-${prChannel.preid}.x` : "";
119
+ log.bold(`${prBumpFiles.length} bump file(s) → ${plan.releases.length} package(s) to release${prChannel ? ` on the "${prChannel.name}" channel (@${prChannel.tag})` : ""}\n`);
113
120
  for (const r of plan.releases) {
114
121
  const tag = r.isDependencyBump ? " (dep)" : r.isCascadeBump ? " (cascade)" : "";
115
- console.log(` ${r.name}: ${r.oldVersion} → ${colorize(r.newVersion, "cyan")}${tag}`);
122
+ console.log(` ${r.name}: ${r.oldVersion} → ${colorize(`${r.newVersion}${releaseSuffix}`, "cyan")}${tag}`);
116
123
  }
117
124
  if (plan.warnings.length > 0) for (const w of plan.warnings) log.warn(w);
118
- if (shouldComment && prNumber) await postOrUpdatePrComment(prNumber, formatReleasePlanComment(plan, prBumpFiles, prNumber, detectPrBranch(rootDir), pm, plan.warnings, parseErrors, emptyBumpFileIds), rootDir);
125
+ if (shouldComment && prNumber) await postOrUpdatePrComment(prNumber, formatReleasePlanComment(plan, prBumpFiles, prNumber, detectPrBranch(rootDir), pm, plan.warnings, parseErrors, emptyBumpFileIds, prChannel), rootDir);
119
126
  if (parseErrors.length > 0 && !opts.noFail) process.exit(1);
120
127
  const coveredPackages = new Set(plan.releases.map((r) => r.name));
121
128
  for (const bf of prBumpFiles) for (const release of bf.releases) coveredPackages.add(release.name);
@@ -136,11 +143,16 @@ async function ciPlanCommand(rootDir) {
136
143
  const config = await loadConfig(rootDir);
137
144
  const { packages } = await discoverWorkspace(rootDir, config);
138
145
  const depGraph = new DependencyGraph(packages);
139
- const { bumpFiles, errors: parseErrors } = await readBumpFiles(rootDir);
146
+ const { bumpFiles, errors: parseErrors } = await readBumpFiles(rootDir, { channels: channelNames(config) });
140
147
  if (parseErrors.length > 0) {
141
148
  for (const err of parseErrors) log.error(err);
142
149
  throw new Error("Bump file parse errors must be fixed before planning.");
143
150
  }
151
+ const channel = matchChannelByBranch(config, detectReleaseBranch(rootDir));
152
+ if (channel) {
153
+ await ciChannelPlan(rootDir, config, channel, packages, depGraph, bumpFiles);
154
+ return;
155
+ }
144
156
  let output;
145
157
  const plan = bumpFiles.length > 0 ? assembleReleasePlan(bumpFiles, packages, depGraph, config) : null;
146
158
  if (plan && plan.releases.length > 0) output = {
@@ -157,7 +169,7 @@ async function ciPlanCommand(rootDir) {
157
169
  packageNames: plan.releases.map((r) => r.name)
158
170
  };
159
171
  else {
160
- const { findUnpublishedPackages } = await import("./publish-VYBhDYFM.mjs");
172
+ const { findUnpublishedPackages } = await import("./publish-Ak6jmwi_.mjs");
161
173
  const unpublished = await findUnpublishedPackages(packages, config);
162
174
  if (unpublished.length > 0) output = {
163
175
  mode: "publish",
@@ -220,9 +232,16 @@ function writeGitHubOutput(key, value) {
220
232
  async function ciReleaseCommand(rootDir, opts) {
221
233
  const config = await loadConfig(rootDir);
222
234
  ensureGitIdentity(rootDir, config);
235
+ const releaseBranch = detectReleaseBranch(rootDir);
236
+ const channel = matchChannelByBranch(config, releaseBranch);
237
+ if (channel) {
238
+ await ciChannelRelease(rootDir, config, channel, opts);
239
+ return;
240
+ }
241
+ if (Object.keys(config.channels || {}).length > 0 && releaseBranch && releaseBranch !== config.baseBranch) throw new Error(`"bumpy ci release" ran on branch "${releaseBranch}", which is neither the base branch ("${config.baseBranch}") nor a configured channel branch. Refusing to release — add the branch to "channels" in .bumpy/_config.json or fix the workflow trigger.`);
223
242
  const { packages } = await discoverWorkspace(rootDir, config);
224
243
  const depGraph = new DependencyGraph(packages);
225
- const { bumpFiles, errors: releaseParseErrors } = await readBumpFiles(rootDir);
244
+ const { bumpFiles, errors: releaseParseErrors } = await readBumpFiles(rootDir, { channels: channelNames(config) });
226
245
  if (releaseParseErrors.length > 0) {
227
246
  for (const err of releaseParseErrors) log.error(err);
228
247
  throw new Error("Bump file parse errors must be fixed before releasing.");
@@ -234,7 +253,7 @@ async function ciReleaseCommand(rootDir, opts) {
234
253
  const msg = bumpFiles.length === 0 ? "No pending bump files — checking for unpublished packages..." : "Bump files found but no packages would be released — checking for unpublished packages...";
235
254
  log.info(msg);
236
255
  const recoveredBumpFiles = recoverDeletedBumpFiles(rootDir);
237
- const { publishCommand } = await import("./publish-VYBhDYFM.mjs");
256
+ const { publishCommand } = await import("./publish-Ak6jmwi_.mjs");
238
257
  await publishCommand(rootDir, {
239
258
  tag: opts.tag,
240
259
  recoveredBumpFiles
@@ -258,7 +277,7 @@ async function ciReleaseCommand(rootDir, opts) {
258
277
  */
259
278
  async function autoPublish(rootDir, config, plan, tag) {
260
279
  log.step("Running bumpy version...");
261
- const { versionCommand } = await import("./version-Cm0nRAFF.mjs");
280
+ const { versionCommand } = await import("./version-CMJUopVj.mjs");
262
281
  await versionCommand(rootDir);
263
282
  log.step("Committing version changes...");
264
283
  runArgs([
@@ -287,7 +306,7 @@ async function autoPublish(rootDir, config, plan, tag) {
287
306
  ], { cwd: rootDir });
288
307
  }
289
308
  log.step("Running bumpy publish...");
290
- const { publishCommand } = await import("./publish-VYBhDYFM.mjs");
309
+ const { publishCommand } = await import("./publish-Ak6jmwi_.mjs");
291
310
  await publishCommand(rootDir, { tag });
292
311
  }
293
312
  /**
@@ -361,7 +380,7 @@ async function createVersionPr(rootDir, plan, config, packageDirs, branchName) {
361
380
  branch
362
381
  ], { cwd: rootDir });
363
382
  log.step("Running bumpy version...");
364
- const { versionCommand } = await import("./version-Cm0nRAFF.mjs");
383
+ const { versionCommand } = await import("./version-CMJUopVj.mjs");
365
384
  await versionCommand(rootDir);
366
385
  runArgs([
367
386
  "git",
@@ -457,6 +476,332 @@ async function createVersionPr(rootDir, plan, config, packageDirs, branchName) {
457
476
  baseBranch
458
477
  ], { cwd: rootDir });
459
478
  }
479
+ /** Read the push event's before/after range, if running on a GitHub Actions push event */
480
+ function getPushEventRange() {
481
+ if (process.env.GITHUB_EVENT_NAME !== "push") return null;
482
+ const path = process.env.GITHUB_EVENT_PATH;
483
+ if (!path) return null;
484
+ try {
485
+ const payload = JSON.parse(readFileSync(path, "utf-8"));
486
+ if (payload.before && payload.after && !/^0+$/.test(payload.before)) return {
487
+ before: payload.before,
488
+ after: payload.after
489
+ };
490
+ } catch {}
491
+ return null;
492
+ }
493
+ /**
494
+ * Bump file IDs added to `.bumpy/<channel>/` by the push that triggered this run.
495
+ *
496
+ * This is the channel publish trigger: merging the release PR moves files into the
497
+ * channel dir; ordinary feature merges don't touch it. Re-running on the same push is
498
+ * idempotent — packages already published from this commit are skipped via the
499
+ * gitHead recorded on the registry.
500
+ */
501
+ function detectChannelMoves(rootDir, channel) {
502
+ const range = getPushEventRange();
503
+ let diffRange;
504
+ if (range) diffRange = `${range.before}..${range.after}`;
505
+ else {
506
+ if (!tryRunArgs([
507
+ "git",
508
+ "rev-parse",
509
+ "--verify",
510
+ "HEAD^"
511
+ ], { cwd: rootDir })) {
512
+ log.warn("Cannot diff against the previous commit (shallow clone?) — channel publish trigger unavailable.\n Use `fetch-depth: 0` in your checkout step, or run `bumpy publish` manually on the channel branch.");
513
+ return [];
514
+ }
515
+ diffRange = "HEAD^..HEAD";
516
+ }
517
+ const out = tryRunArgs([
518
+ "git",
519
+ "diff",
520
+ "--name-only",
521
+ "--diff-filter=A",
522
+ "--no-renames",
523
+ diffRange,
524
+ "--",
525
+ `.bumpy/${channel.name}/`
526
+ ], { cwd: rootDir });
527
+ if (!out) return [];
528
+ return out.split("\n").filter((f) => f.endsWith(".md") && !f.endsWith("README.md")).map((f) => f.split("/").pop().replace(/\.md$/, ""));
529
+ }
530
+ /**
531
+ * CI release on a channel branch. Two independent steps, both of which can run
532
+ * in the same invocation:
533
+ *
534
+ * 1. **Publish** — if this push moved bump files into `.bumpy/<channel>/` (a release
535
+ * PR merge), publish the cycle as prereleases. Versions are derived (targets from
536
+ * bump files, counters from the registry) and never committed.
537
+ * 2. **Release PR** — if pending bump files exist (root or other channels' dirs),
538
+ * create/update the file-move release PR.
539
+ */
540
+ async function ciChannelRelease(rootDir, config, channel, opts) {
541
+ log.bold(`Channel "${channel.name}" (branch "${channel.branch}")\n`);
542
+ const { packages } = await discoverWorkspace(rootDir, config);
543
+ const { bumpFiles, errors: parseErrors } = await readBumpFiles(rootDir, { channels: channelNames(config) });
544
+ if (parseErrors.length > 0) {
545
+ for (const err of parseErrors) log.error(err);
546
+ throw new Error("Bump file parse errors must be fixed before releasing.");
547
+ }
548
+ const pending = bumpFiles.filter((bf) => bf.channel !== channel.name);
549
+ if (opts.autoPublish) {
550
+ if (pending.length > 0) {
551
+ const { channelVersion } = await import("./version-CMJUopVj.mjs");
552
+ if (await channelVersion(rootDir, config, channel, { commit: true })) runArgs([
553
+ "git",
554
+ "push",
555
+ "--no-verify"
556
+ ], { cwd: rootDir });
557
+ }
558
+ const { publishCommand } = await import("./publish-Ak6jmwi_.mjs");
559
+ await publishCommand(rootDir, {
560
+ channel: channel.name,
561
+ tag: opts.tag
562
+ });
563
+ return;
564
+ }
565
+ const movedIds = detectChannelMoves(rootDir, channel);
566
+ const shouldPublish = movedIds.length > 0 && opts.assertMode !== "version-pr";
567
+ if (shouldPublish) {
568
+ log.step(`Release PR merge detected (${movedIds.map((id) => `${id}.md`).join(", ")}) — publishing prereleases...`);
569
+ const { publishCommand } = await import("./publish-Ak6jmwi_.mjs");
570
+ await publishCommand(rootDir, {
571
+ channel: channel.name,
572
+ tag: opts.tag
573
+ });
574
+ }
575
+ if (opts.assertMode === "publish") {
576
+ if (!shouldPublish) throw new Error("Expected mode \"publish\" but this push did not move bump files into the channel dir. Either remove --expect-mode, or gate this step on the output of \"bumpy ci plan\".");
577
+ return;
578
+ }
579
+ if (pending.length > 0) await createChannelReleasePr(rootDir, config, channel, packages, opts.branch);
580
+ else if (!shouldPublish) log.info(`Nothing to do on channel "${channel.name}" — no pending bump files, no release PR merge in this push.`);
581
+ }
582
+ /**
583
+ * Create or update the channel's release PR. Unlike the stable version PR, its diff
584
+ * is pure file moves (pending bump files → `.bumpy/<channel>/`) — no versions, no
585
+ * changelogs. The PR title/body show targets with a wildcard counter (`1.2.0-rc.x`),
586
+ * derived purely from committed state; the exact counter is assigned at publish time.
587
+ */
588
+ async function createChannelReleasePr(rootDir, config, channel, packages, branchOverride) {
589
+ const branch = validateBranchName(branchOverride || channel.versionPr.branch);
590
+ const baseBranch = validateBranchName(channel.branch);
591
+ const existingPr = tryRunArgs([
592
+ "gh",
593
+ "pr",
594
+ "list",
595
+ "--head",
596
+ branch,
597
+ "--json",
598
+ "number",
599
+ "--jq",
600
+ ".[0].number"
601
+ ], { cwd: rootDir });
602
+ log.step(`Creating branch ${branch}...`);
603
+ if (tryRunArgs([
604
+ "git",
605
+ "rev-parse",
606
+ "--verify",
607
+ branch
608
+ ], { cwd: rootDir }) !== null) {
609
+ runArgs([
610
+ "git",
611
+ "checkout",
612
+ branch
613
+ ], { cwd: rootDir });
614
+ runArgs([
615
+ "git",
616
+ "reset",
617
+ "--hard",
618
+ baseBranch
619
+ ], { cwd: rootDir });
620
+ } else runArgs([
621
+ "git",
622
+ "checkout",
623
+ "-b",
624
+ branch
625
+ ], { cwd: rootDir });
626
+ const { channelVersion } = await import("./version-CMJUopVj.mjs");
627
+ const result = await channelVersion(rootDir, config, channel);
628
+ if (!result) {
629
+ log.info("No pending bump files to move.");
630
+ runArgs([
631
+ "git",
632
+ "checkout",
633
+ baseBranch
634
+ ], { cwd: rootDir });
635
+ return;
636
+ }
637
+ const displayPlan = channelDisplayPlan(result.cyclePlan, channel, packages);
638
+ const versionSummary = formatChannelVersionSummary(displayPlan.releases);
639
+ const prTitle = versionSummary ? `${channel.versionPr.title}: ${versionSummary}` : channel.versionPr.title;
640
+ runArgs([
641
+ "git",
642
+ "add",
643
+ "-A",
644
+ ".bumpy/"
645
+ ], { cwd: rootDir });
646
+ if (!tryRunArgs([
647
+ "git",
648
+ "status",
649
+ "--porcelain"
650
+ ], { cwd: rootDir })) {
651
+ log.info("No changes to commit.");
652
+ runArgs([
653
+ "git",
654
+ "checkout",
655
+ baseBranch
656
+ ], { cwd: rootDir });
657
+ return;
658
+ }
659
+ runArgs([
660
+ "git",
661
+ "commit",
662
+ "-F",
663
+ "-"
664
+ ], {
665
+ cwd: rootDir,
666
+ input: `${prTitle}\n\nShipped: ${result.movedFiles.map((bf) => `${bf.id}.md`).join(", ")}`
667
+ });
668
+ pushWithToken(rootDir, branch, config);
669
+ const repo = process.env.GITHUB_REPOSITORY;
670
+ const noPatWarning = !process.env.BUMPY_GH_TOKEN && !!repo;
671
+ const packageDirs = new Map([...packages.values()].map((p) => [p.name, p.relativeDir]));
672
+ const preamble = buildChannelPrPreamble(config, channel);
673
+ let prNumber = null;
674
+ if (existingPr) {
675
+ prNumber = validatePrNumber(existingPr);
676
+ const prBody = formatVersionPrBody(displayPlan, preamble, packageDirs, repo, prNumber, noPatWarning);
677
+ log.step(`Updating existing PR #${prNumber}...`);
678
+ await withPatToken(() => runArgsAsync([
679
+ "gh",
680
+ "pr",
681
+ "edit",
682
+ prNumber,
683
+ "--title",
684
+ prTitle,
685
+ "--body-file",
686
+ "-"
687
+ ], {
688
+ cwd: rootDir,
689
+ input: prBody
690
+ }));
691
+ log.success(`🐸 Updated PR #${prNumber}`);
692
+ } else {
693
+ log.step("Creating release PR...");
694
+ const prBody = formatVersionPrBody(displayPlan, preamble, packageDirs, repo, null, noPatWarning);
695
+ const createResult = await withPatToken(() => runArgsAsync([
696
+ "gh",
697
+ "pr",
698
+ "create",
699
+ "--title",
700
+ prTitle,
701
+ "--body-file",
702
+ "-",
703
+ "--base",
704
+ baseBranch,
705
+ "--head",
706
+ branch
707
+ ], {
708
+ cwd: rootDir,
709
+ input: prBody
710
+ }));
711
+ log.success(`🐸 Created PR: ${createResult}`);
712
+ prNumber = createResult?.match(/\/pull\/(\d+)/)?.[1] ?? null;
713
+ if (repo && prNumber) {
714
+ const updatedBody = formatVersionPrBody(displayPlan, preamble, packageDirs, repo, prNumber, noPatWarning);
715
+ await withPatToken(() => runArgsAsync([
716
+ "gh",
717
+ "pr",
718
+ "edit",
719
+ prNumber,
720
+ "--body-file",
721
+ "-"
722
+ ], {
723
+ cwd: rootDir,
724
+ input: updatedBody
725
+ }));
726
+ }
727
+ if (!process.env.BUMPY_GH_TOKEN) pushWithToken(rootDir, branch, config);
728
+ }
729
+ if (channel.versionPr.automerge && prNumber) await enableAutoMerge(rootDir, prNumber);
730
+ runArgs([
731
+ "git",
732
+ "checkout",
733
+ baseBranch
734
+ ], { cwd: rootDir });
735
+ }
736
+ function buildChannelPrPreamble(config, channel) {
737
+ return [
738
+ config.versionPr.preamble,
739
+ "",
740
+ `> 🔀 **Prerelease channel \`${channel.name}\`** — merging this PR publishes the versions below to the \`@${channel.tag}\` dist-tag.`,
741
+ `> The diff only moves bump files into \`.bumpy/${channel.name}/\` — prerelease versions are derived at publish time and never committed. The \`.x\` counter is assigned from the registry at publish time.`
742
+ ].join("\n");
743
+ }
744
+ /** Enable GitHub auto-merge on a PR, trying the available merge methods in order */
745
+ async function enableAutoMerge(rootDir, prNumber) {
746
+ const validPr = validatePrNumber(prNumber);
747
+ for (const method of [
748
+ "--squash",
749
+ "--merge",
750
+ "--rebase"
751
+ ]) try {
752
+ await withPatToken(() => runArgsAsync([
753
+ "gh",
754
+ "pr",
755
+ "merge",
756
+ validPr,
757
+ "--auto",
758
+ method
759
+ ], { cwd: rootDir }));
760
+ log.dim(` Auto-merge enabled (${method.slice(2)})`);
761
+ return;
762
+ } catch {}
763
+ log.warn(" Failed to enable auto-merge — check repository merge settings and token permissions.");
764
+ }
765
+ /** Channel-aware `ci plan`: reports what `ci release` would do on this channel branch */
766
+ async function ciChannelPlan(rootDir, config, channel, packages, depGraph, bumpFiles) {
767
+ const pending = bumpFiles.filter((bf) => bf.channel !== channel.name);
768
+ const movedIds = detectChannelMoves(rootDir, channel);
769
+ let mode = "nothing";
770
+ let releases = [];
771
+ if (pending.length > 0 || movedIds.length > 0) {
772
+ mode = pending.length > 0 ? "version-pr" : "publish";
773
+ const stablePlan = assembleReleasePlan(bumpFiles, packages, depGraph, config, { prereleasePreid: channel.preid });
774
+ try {
775
+ releases = (await buildChannelReleasePlan(stablePlan, channel, packages, rootDir, { forDisplay: true })).plan.releases;
776
+ } catch {
777
+ releases = stablePlan.releases.map((r) => ({
778
+ ...r,
779
+ newVersion: `${r.newVersion}-${channel.preid}.?`
780
+ }));
781
+ }
782
+ }
783
+ const output = {
784
+ mode,
785
+ channel: channel.name,
786
+ bumpFiles: bumpFiles.map((bf) => ({
787
+ id: bf.id,
788
+ summary: bf.summary,
789
+ releases: bf.releases.map((r) => ({
790
+ name: r.name,
791
+ type: r.type
792
+ })),
793
+ shipped: bf.channel === channel.name
794
+ })),
795
+ releases: releases.map((r) => formatPlanRelease(r, packages, config)),
796
+ packageNames: releases.map((r) => r.name)
797
+ };
798
+ const json = JSON.stringify(output, null, 2);
799
+ console.log(json);
800
+ writeGitHubOutput("mode", output.mode);
801
+ writeGitHubOutput("channel", channel.name);
802
+ writeGitHubOutput("packages", JSON.stringify(output.packageNames));
803
+ writeGitHubOutput("json", JSON.stringify(output));
804
+ }
460
805
  const FROG_IMG_BASE = "https://raw.githubusercontent.com/dmno-dev/bumpy/main/images";
461
806
  function buildAddBumpFileLink(prBranch) {
462
807
  if (!prBranch) return null;
@@ -479,13 +824,15 @@ function pmRunCommand(pm) {
479
824
  if (pm === "yarn") return "yarn bumpy";
480
825
  return "npx bumpy";
481
826
  }
482
- function formatReleasePlanComment(plan, bumpFiles, prNumber, prBranch, pm, warnings = [], parseErrors = [], emptyBumpFileIds = []) {
827
+ function formatReleasePlanComment(plan, bumpFiles, prNumber, prBranch, pm, warnings = [], parseErrors = [], emptyBumpFileIds = [], channel = null) {
483
828
  const repo = process.env.GITHUB_REPOSITORY;
484
829
  const lines = [];
830
+ const versionSuffix = channel ? `-${channel.preid}.x` : "";
831
+ const headline = channel ? `**This PR targets the \`${channel.name}\` prerelease channel** — merging it ships these packages as a **prerelease** to the \`@${channel.tag}\` dist-tag, not a stable release.` : "**The changes in this PR will be included in the next version bump.**";
485
832
  const preamble = [
486
833
  `<a href="https://bumpy.varlock.dev"><img src="${FROG_IMG_BASE}/frog-clipboard.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>`,
487
834
  "",
488
- "**The changes in this PR will be included in the next version bump.**",
835
+ headline,
489
836
  "<br clear=\"left\" />"
490
837
  ].join("\n");
491
838
  lines.push(preamble);
@@ -507,10 +854,16 @@ function formatReleasePlanComment(plan, bumpFiles, prNumber, prBranch, pm, warni
507
854
  lines.push("");
508
855
  for (const r of releases) {
509
856
  const suffix = r.isDependencyBump ? " _(dep)_" : r.isCascadeBump ? " _(cascade)_" : "";
510
- lines.push(`- \`${r.name}\` ${r.oldVersion} → **${r.newVersion}**${suffix}`);
857
+ lines.push(`- \`${r.name}\` ${r.oldVersion} → **${r.newVersion}${versionSuffix}**${suffix}`);
511
858
  }
512
859
  lines.push("");
513
860
  }
861
+ if (channel) {
862
+ const examplePkg = plan.releases.find((r) => !r.isDependencyBump && !r.isCascadeBump)?.name ?? plan.releases[0]?.name;
863
+ const installHint = examplePkg ? ` (e.g. \`npm i ${examplePkg}@${channel.tag}\`)` : "";
864
+ lines.push(`> 🔀 Published to the \`@${channel.tag}\` dist-tag${installHint}. Prerelease versions are derived at publish time — the \`.x\` counter is filled in from the registry. Promote to a stable release by merging \`${channel.branch}\` into your base branch.`);
865
+ lines.push("");
866
+ }
514
867
  lines.push(`#### Bump files in this PR`);
515
868
  lines.push("");
516
869
  for (const bf of bumpFiles) {
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as log, t as colorize } from "./logger-BgksGFuf.mjs";
3
- import { n as findRoot } from "./config-D_4GYDJi.mjs";
3
+ import { n as findRoot } from "./config-0we4ISZX.mjs";
4
4
  //#region src/cli.ts
5
5
  const args = process.argv.slice(2);
6
6
  const command = args[0];
@@ -25,13 +25,13 @@ async function main() {
25
25
  switch (command) {
26
26
  case "init": {
27
27
  const rootDir = await findRoot();
28
- const { initCommand } = await import("./init-BCkm6Nfa.mjs");
28
+ const { initCommand } = await import("./init-DkAY5hjc.mjs");
29
29
  await initCommand(rootDir, { force: flags.force === true });
30
30
  break;
31
31
  }
32
32
  case "add": {
33
33
  const rootDir = await findRoot();
34
- const { addCommand } = await import("./add-Dt1hddMt.mjs");
34
+ const { addCommand } = await import("./add-C9rU_89s.mjs");
35
35
  await addCommand(rootDir, {
36
36
  packages: flags.packages,
37
37
  message: flags.message,
@@ -43,25 +43,29 @@ async function main() {
43
43
  }
44
44
  case "status": {
45
45
  const rootDir = await findRoot();
46
- const { statusCommand } = await import("./status-DxzKPM8d.mjs");
46
+ const { statusCommand } = await import("./status-DIzi-Iai.mjs");
47
47
  await statusCommand(rootDir, {
48
48
  json: flags.json === true,
49
49
  packagesOnly: flags.packages === true,
50
50
  bumpType: flags.bump,
51
51
  filter: flags.filter,
52
- verbose: flags.verbose === true
52
+ verbose: flags.verbose === true,
53
+ channel: flags.channel
53
54
  });
54
55
  break;
55
56
  }
56
57
  case "version": {
57
58
  const rootDir = await findRoot();
58
- const { versionCommand } = await import("./version-Cm0nRAFF.mjs");
59
- await versionCommand(rootDir, { commit: flags.commit === true });
59
+ const { versionCommand } = await import("./version-CMJUopVj.mjs");
60
+ await versionCommand(rootDir, {
61
+ commit: flags.commit === true,
62
+ channel: flags.channel
63
+ });
60
64
  break;
61
65
  }
62
66
  case "generate": {
63
67
  const rootDir = await findRoot();
64
- const { generateCommand } = await import("./generate-DohUlhu3.mjs");
68
+ const { generateCommand } = await import("./generate-B2OMt_64.mjs");
65
69
  await generateCommand(rootDir, {
66
70
  from: flags.from,
67
71
  dryRun: flags["dry-run"] === true,
@@ -71,7 +75,7 @@ async function main() {
71
75
  }
72
76
  case "check": {
73
77
  const rootDir = await findRoot();
74
- const { checkCommand } = await import("./check-Dvi0DIqC.mjs").then((n) => n.t);
78
+ const { checkCommand } = await import("./check-DIl9Dz68.mjs").then((n) => n.t);
75
79
  const hookValue = flags.hook;
76
80
  if (hookValue && hookValue !== "pre-commit" && hookValue !== "pre-push") {
77
81
  log.error(`Invalid --hook value "${hookValue}". Expected "pre-commit" or "pre-push".`);
@@ -80,7 +84,8 @@ async function main() {
80
84
  await checkCommand(rootDir, {
81
85
  strict: flags.strict === true,
82
86
  noFail: flags["no-fail"] === true,
83
- hook: hookValue
87
+ hook: hookValue,
88
+ base: flags.base
84
89
  });
85
90
  break;
86
91
  }
@@ -89,17 +94,17 @@ async function main() {
89
94
  const subcommand = args[1];
90
95
  const ciFlags = parseFlags(args.slice(2));
91
96
  if (subcommand === "check") {
92
- const { ciCheckCommand } = await import("./ci-B7gF6CFP.mjs");
97
+ const { ciCheckCommand } = await import("./ci-ChYmDuwy.mjs");
93
98
  await ciCheckCommand(rootDir, {
94
99
  comment: ciFlags.comment !== void 0 ? ciFlags.comment === true : void 0,
95
100
  strict: ciFlags.strict === true,
96
101
  noFail: ciFlags["no-fail"] === true
97
102
  });
98
103
  } else if (subcommand === "plan") {
99
- const { ciPlanCommand } = await import("./ci-B7gF6CFP.mjs");
104
+ const { ciPlanCommand } = await import("./ci-ChYmDuwy.mjs");
100
105
  await ciPlanCommand(rootDir);
101
106
  } else if (subcommand === "release") {
102
- const { ciReleaseCommand } = await import("./ci-B7gF6CFP.mjs");
107
+ const { ciReleaseCommand } = await import("./ci-ChYmDuwy.mjs");
103
108
  const expectModeFlag = ciFlags["expect-mode"];
104
109
  const autoPublishFlag = ciFlags["auto-publish"] === true;
105
110
  if (expectModeFlag !== void 0 && expectModeFlag !== "version-pr" && expectModeFlag !== "publish") {
@@ -127,12 +132,13 @@ async function main() {
127
132
  }
128
133
  case "publish": {
129
134
  const rootDir = await findRoot();
130
- const { publishCommand } = await import("./publish-VYBhDYFM.mjs");
135
+ const { publishCommand } = await import("./publish-Ak6jmwi_.mjs");
131
136
  await publishCommand(rootDir, {
132
137
  dryRun: flags["dry-run"] === true,
133
138
  tag: flags.tag,
134
139
  noPush: flags["no-push"] === true,
135
- filter: flags.filter
140
+ filter: flags.filter,
141
+ channel: flags.channel
136
142
  });
137
143
  break;
138
144
  }
@@ -186,8 +192,11 @@ function printHelp() {
186
192
  --strict Fail if any changed package is uncovered (default: only fail if no bump files at all)
187
193
  --no-fail Warn only, never exit 1
188
194
  --hook <context> Hook context: "pre-commit" or "pre-push" (controls which bump files count)
195
+ --base <branch> Branch to compare against (default: baseBranch; use the channel branch for channel PRs)
189
196
  version [--commit] Apply bump files and bump versions
197
+ (on a channel branch: moves pending bump files into .bumpy/<channel>/)
190
198
  publish Publish versioned packages
199
+ (on a channel branch: derives prerelease versions and publishes to the channel dist-tag)
191
200
  ci check PR check — report pending releases, comment on PR
192
201
  ci plan Report what ci release would do (JSON + GitHub Actions outputs)
193
202
  ci release Release — create version PR or auto-publish
@@ -211,12 +220,18 @@ function printHelp() {
211
220
  --bump <types> Filter by bump type (e.g., "major", "minor,patch")
212
221
  --filter <names> Filter by package name/glob (e.g., "@myorg/*")
213
222
  --verbose Show bump file details
223
+ --channel <name> Show channel status (default: inferred from the current branch)
214
224
 
215
225
  Publish options:
216
226
  --dry-run Preview without publishing
217
227
  --tag <tag> npm dist-tag (e.g., "next", "beta")
218
228
  --no-push Skip pushing git tags to remote
219
229
  --filter <names> Publish only matching packages (e.g., "@myorg/*")
230
+ --channel <name> Publish a prerelease channel (default: inferred from the current branch)
231
+
232
+ Version options:
233
+ --commit Create a git commit with the version changes
234
+ --channel <name> Channel override (default: inferred from the current branch)
220
235
 
221
236
  CI check options:
222
237
  --comment Force PR comment on/off (auto-detected in CI)
@@ -1,6 +1,6 @@
1
1
  import { a as __exportAll } from "./logger-BgksGFuf.mjs";
2
2
  import { a as readJson, n as exists, o as readJsonc } from "./fs-CBXKZhoU.mjs";
3
- import { l as normalizeCascadeConfig, r as DEFAULT_CONFIG } from "./types-Bkh-igOJ.mjs";
3
+ import { l as normalizeCascadeConfig, r as DEFAULT_CONFIG } from "./types-lpiG-Zxh.mjs";
4
4
  import { resolve } from "node:path";
5
5
  //#region src/core/config.ts
6
6
  var config_exports = /* @__PURE__ */ __exportAll({
@@ -89,6 +89,10 @@ function mergeConfig(defaults, user) {
89
89
  packages: {
90
90
  ...defaults.packages,
91
91
  ...user.packages
92
+ },
93
+ channels: {
94
+ ...defaults.channels,
95
+ ...user.channels
92
96
  }
93
97
  };
94
98
  }