Skip to content

Promote next channel to stable (@varlock/bumpy 1.14.0)#110

Merged
theoephraim merged 8 commits into
mainfrom
next
Jun 13, 2026
Merged

Promote next channel to stable (@varlock/bumpy 1.14.0)#110
theoephraim merged 8 commits into
mainfrom
next

Conversation

@theoephraim

Copy link
Copy Markdown
Member

Promotes the next prerelease channel to stable. This merge carries the cycle's accumulated bump files (in .bumpy/next/) into main — versions never diverged, so the diff is the feature work plus file moves.

On merge, main's release workflow will open the ordinary stable version PR: @varlock/bumpy1.14.0, with a consolidated changelog entry built from the cycle's bump files. The @next dist-tag has shipped 1.14.0-rc.0 and 1.14.0-rc.1 through this cycle.

What's in the cycle:

  • Prerelease channels (#104) — branch-based prerelease lines; versions derived at publish time, never committed.
  • Deterministic channel release PR titles (#107) — wildcard rc.x counters in PR titles/bodies/commits so they can't drift from the registry; package count for multi-package cycles. Validated live on #109.
  • Docs (#106) — environment deployment-branch allowances for channel branches with trusted publishing.

theoephraim and others added 6 commits June 12, 2026 13:56
…nt on next-targeted PRs)

pull_request_target reads the workflow from the base branch, so this must
live on next for #104's own check to use it. The check-local job builds
the PR head, so next not yet having the feature source is fine.
…ions) (#104)

## Summary

Branch-based **prerelease channels** — design doc *and* full
implementation. This PR started as an RFC; after a few design revisions
(see commit history) it now ships the feature, tests, and docs.

The TL;DR of the model:

- **Branch = channel.** Nominate a `next` / `beta` / etc. branch in
`.bumpy/_config.json`. Pushing to it produces `-rc.N` / `-beta.N`
versions on the matching dist-tag, using the same `ci release` flow as
`main`.
- **Prerelease versions are never committed to git.** Every
`package.json` on a channel branch keeps the last stable version,
identical to `main`. Targets are derived from bump files, counters from
the registry (`max published -preid.N + 1`), idempotency from npm's
`gitHead` metadata. No suffix to strip at promotion, no counter state to
corrupt, no version conflicts on `main` ↔ channel syncs, and abandoned
cycles can't cause registry collisions.
- **Bump file location is the only state.** Pending at `.bumpy/*.md`,
shipped at `.bumpy/<channel>/*.md`. The "release PR" on a channel is a
pure file-move PR — computed versions appear in its title and merge
commit message as narrative; the registry wins at publish time. The
general pending rule ("pending unless in *this* context's dir") gives
alpha → beta graduation for free.
- **The cycle moves as one.** Every publish recomputes the full cycle;
on channels, range satisfaction is checked against the prerelease
version (which never satisfies a stable range), so all dependents join
at proportional bump levels and inter-cycle deps are exact-pinned in the
published artifacts. The channel dist-tag always points at one coherent
set.
- **Promotion is just a merge.** Main treats channel-dir files as
pending, so the ordinary stable flow consumes them, bumps
stable-to-stable, and writes one consolidated `CHANGELOG.md` entry. Zero
special promotion code.

## Why not just match changesets' pre mode?

Changesets' [own
docs](https://github.com/changesets/changesets/blob/main/docs/prereleases.md)
describe their prerelease mode as *"very complicated"* with *"mistakes
that can lead to repository and publish states that are very hard to
fix."*
[docs/prereleases.md](https://github.com/dmno-dev/bumpy/blob/docs/prerelease-channels/docs/prereleases.md)
surveys the recurring complaints
([#239](changesets/changesets#239),
[#381](changesets/changesets#381),
[#729](changesets/changesets#729),
[#786](changesets/changesets#786),
[#960](changesets/changesets#960)) and designs
them out rather than re-importing them — see the side-by-side comparison
table at the bottom of the doc.

## Implementation

- `src/core/channels.ts` — config resolution + validation, branch
detection (prefers `GITHUB_REF_NAME` for detached CI checkouts),
`--channel` override.
- `src/core/prerelease.ts` — registry-floor counters, per-package
published-from-`HEAD` skip (`gitHead` on npm; git tags for
custom-publish packages), and the transient in-place rewrite: computed
versions + exact pins are written to the working tree so pack/build see
them, then restored in a `finally`.
- `src/core/release-plan.ts` — new `prereleasePreid` mode: Phase A
checks ranges against `<target>-<preid>.0`, producing the required
wide-but-proportional cascade (channel-only; stable plans are
unchanged).
- Command flows: `version` on a channel only moves files; `publish`
derives + rewrites + publishes to the channel dist-tag (and the stable
path refuses suffixed versions when channels are configured); `ci
release` publishes when the triggering push moved files into
`.bumpy/<channel>/` and maintains the file-move release PR (with
`versionPr.automerge` support); `status` shows the cycle with
registry-derived counters; `check` skips channel branches and gains
`--base`; unknown branches make `ci release` error instead of guessing.
GitHub releases for prereleases are marked `--prerelease`.
- Config schema (`config-schema.json`) updated; `preid` is optional in
the schema to leave room for future stable/maintenance channels.

## Out of scope (deliberately)

- Ephemeral per-PR/per-commit previews —
[pkg.pr.new](https://pkg.pr.new)'s job; the doc draws the line
explicitly.
- Workflow-dispatch one-off prereleases — nearly free under this
architecture (same compute-and-publish from any SHA); planned as a
fast-follow.
- Stable/maintenance channels (`1.x` branches) — future; config shape
already accommodates.
- Per-bump-file `channel:` frontmatter — not planned; channels stay
branch-derived.

## Test plan

- [x] 285 tests pass (27 new: channel config resolution/validation,
registry-floor counter math, exact-pin rewrite + restore, channel-dir
bump file reading/moves/duplicate detection, prerelease cascade behavior
incl. `workspace:*` opt-out and transitivity)
- [x] `tsc --noEmit`, oxlint, oxfmt clean
- [x] End-to-end smoke test in a scratch workspace: bump file → channel
`version` (file move only, no version writes) → `status` (correct `rc.0`
counters) → `publish --dry-run` (correct versions, `--tag next`, exact
pins) → merge to `main` → stable `version` (channel dir consumed,
consolidated changelog, no spurious cascade on main)
- [ ] Dogfood on a real channel branch in this repo once merged (first
real `bumpy ci release` run on a `next` push)
<a href="https://bumpy.varlock.dev"><img
src="https://raw.githubusercontent.com/dmno-dev/bumpy/main/images/frog-clipboard.png"
alt="bumpy-frog" width="60" align="left" style="image-rendering:
pixelated;" title="Hi! I'm bumpy!" /></a>

This PR was created and will be kept in sync by
[bumpy](https://bumpy.varlock.dev) based on your bump files (in
`.bumpy/`). Merge it when you are ready to release the packages listed
below:
<br clear="left" />

> 🔀 **Prerelease channel `next`** — merging this PR publishes the
versions below to the `@next` dist-tag.
> The diff only moves bump files into `.bumpy/next/` — prerelease
versions are derived at publish time and never committed. Version
numbers shown here are estimates; the registry wins at publish.

### <a href="https://bumpy.varlock.dev" title="Minor releases"><img
src="https://raw.githubusercontent.com/dmno-dev/bumpy/main/images/frog-minor.png"
alt="minor" width="52" style="image-rendering: pixelated;" align="right"
/></a> Minor releases

#### `@varlock/bumpy` 1.13.2 → **1.14.0-rc.0**
<sub>[CHANGELOG.md](https://github.com/dmno-dev/bumpy/pull/105/changes#diff-4172fac00a078eb9cef75fbac6f1df8320cd5bf3d45ed733e055f7567d8d0e29)</sub>

- Add prerelease channels — branch-based prerelease lines (e.g. `next` →
`@next` dist-tag) where prerelease versions are never committed to git.
Targets derive from bump files, counters from the registry; shipped bump
files are tracked by moving them into `.bumpy/<channel>/`. Includes
channel-aware `version` / `publish` / `status` / `ci release` flows,
exact-pinned lockstep cycle publishes, and promotion-by-merge to stable.
([bump
file](https://github.com/dmno-dev/bumpy/pull/105/changes#diff-cd102124258af740299a22752ba5caa925bab64cdec60ef8d060beddc4687c75))

Co-authored-by: bumpy-bot <276066384+bumpy-bot@users.noreply.github.com>
…106)

When the publish job runs in a GitHub Environment with deployment branch
restrictions (our recommended hardening restricts it to `main`),
prerelease channel branches can't enter the environment — with trusted
publishing this means OIDC token requests are rejected and channel
publishes fail.

Adds reminders in both places users would hit this:

- **docs/prereleases.md** — setup step 3 (adding the channel branch to
the release workflow) now calls out updating the environment's allowed
deployment branches.
- **docs/github-actions.md** — the "restrict deployment branches to
`main`" hardening bullet now notes channel branches must be added to the
allowed list.
Channel release PR titles previously showed registry-derived prerelease
counters, which could be out of sync with what actually publishes (the
registry is re-queried at publish time and wins). They also degraded
poorly in multi-package cycles: an arbitrary alphabetical lead package
plus `(+N more)`.

Titles, PR bodies, and merge commit messages now only claim what's
derivable from committed state:

- **Wildcard counter** — versions display as `1.2.0-rc.x`; the target
comes from bump files (deterministic), the `.x` is assigned from the
registry at publish. The title can no longer drift, by construction.
- **Package count for multi-package cycles** — `🐸 Versioned release
(next): 4 packages` instead of an arbitrary lead + `(+N more)`.
Single-package cycles keep `name@1.2.0-rc.x`.
- **No registry call in the version-PR job** — the `forDisplay` fetch
(and its offline `-rc.?` fallback) is gone from the release PR path; one
less network dependency and failure mode. `status` and `ci plan` keep
live registry-derived counters (`.?` when offline) since they're
interactive/live output.
- **Consistent wildcard** — the PR check comment and channel `version`
output now use `.x` too (they never queried the registry; `.?`
previously implied a failed lookup rather than "assigned later").

Docs updated where they described the title as "advisory narrative;
registry wins" — the new story is simpler: the title only shows
deterministic state.

All 292 tests pass, including new coverage for `channelDisplayPlan`
(wildcard mapping + unpublishable-package filtering).
<a href="https://bumpy.varlock.dev"><img
src="https://raw.githubusercontent.com/dmno-dev/bumpy/main/images/frog-clipboard.png"
alt="bumpy-frog" width="60" align="left" style="image-rendering:
pixelated;" title="Hi! I'm bumpy!" /></a>

This PR was created and will be kept in sync by
[bumpy](https://bumpy.varlock.dev) based on your bump files (in
`.bumpy/`). Merge it when you are ready to release the packages listed
below:
<br clear="left" />

> 🔀 **Prerelease channel `next`** — merging this PR publishes the
versions below to the `@next` dist-tag.
> The diff only moves bump files into `.bumpy/next/` — prerelease
versions are derived at publish time and never committed. The `.x`
counter is assigned from the registry at publish time.

### <a href="https://bumpy.varlock.dev" title="Minor releases"><img
src="https://raw.githubusercontent.com/dmno-dev/bumpy/main/images/frog-minor.png"
alt="minor" width="52" style="image-rendering: pixelated;" align="right"
/></a> Minor releases

#### `@varlock/bumpy` 1.13.2 → **1.14.0-rc.x**
<sub>[CHANGELOG.md](https://github.com/dmno-dev/bumpy/pull/109/changes#diff-4172fac00a078eb9cef75fbac6f1df8320cd5bf3d45ed733e055f7567d8d0e29)</sub>

- Add prerelease channels — branch-based prerelease lines (e.g. `next` →
`@next` dist-tag) where prerelease versions are never committed to git.
Targets derive from bump files, counters from the registry; shipped bump
files are tracked by moving them into `.bumpy/<channel>/`. Includes
channel-aware `version` / `publish` / `status` / `ci release` flows,
exact-pinned lockstep cycle publishes, and promotion-by-merge to stable.
([bump
file](https://github.com/dmno-dev/bumpy/pull/109/changes#diff-cd102124258af740299a22752ba5caa925bab64cdec60ef8d060beddc4687c75))
- Channel release PR titles and bodies now show deterministic versions:
targets with a wildcard counter (`1.2.0-rc.x`) derived purely from
committed state, instead of registry-derived counters that could drift
between PR creation and publish. Multi-package cycles show a package
count in the title instead of an arbitrary lead package. The PR check
comment and `version` output use the same `.x` wildcard; `status` / `ci
plan` still show live registry-derived counters (`.?` when offline).
([bump
file](https://github.com/dmno-dev/bumpy/pull/109/changes#diff-66048fddb3a0d7725cd37a4ec20bd6f474c6cbb0e3cfea8d9137e2dd8416e521))

Co-authored-by: bumpy-bot <276066384+bumpy-bot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

bumpy-frog

This PR promotes the next prerelease cycle to a stable release. The changes below that already shipped to the @next dist-tag will be consolidated into the next stable version bump.

minor Minor releases

  • @varlock/bumpy 1.13.2 → 1.14.0

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

Found via the promotion PR
[#110](#110): its check failed and
commented "Merging this PR will not cause a version bump" even though
the cycle has two pending bump files.

**Root cause:** `ciCheckCommand` called `readBumpFiles(rootDir)` without
the `channels` option, so files in `.bumpy/next/` were never parsed. On
a promotion PR those are exactly the pending files (the diff vs main
lists them), so the diff-based filter matched nothing → "no bump files"
comment + exit 1.

**Fix:**
- `ci check` reads channel dirs like every other channel-aware path.
This is safe for all PR shapes: feature PRs never have shipped channel
files in their diff vs the PR base; promotion (channel → main) and
graduation (channel → channel) PRs do, and there they genuinely are
pending.
- Channel-dir bump files render in the comment as `next/feature.md` with
subdir-aware view-diff/edit links (previously they'd have linked to the
nonexistent root path).

Deliberately **not** changed: the local `bumpy check` and `bumpy add`
commands also read bump files without channels, but they always diff
against `config.baseBranch` — making them channel-aware without
base-branch awareness would wrongly count shipped channel files as
branch coverage on feature branches off `next`. Separate issue.

Tests: promotion-PR comment rendering + the channel-path/basename id
contract that the diff filter relies on. 295 pass.

After this merges to next, re-triggering the check on #110 should
produce the proper stable-promotion comment (`1.13.2 → 1.14.0`).
Follow-up from [#110](#110
comment looking identical to an ordinary feature PR's. A promotion PR is
the highest-stakes merge in the channel flow — it ends the cycle,
consolidates the changelog, and ships to `@latest` — so the comment now
says so.

**Detection:** stable-targeted PR (no channel match on the base)
carrying bump files with `channel` set. Covers the canonical `next` →
`main` PR and partial promotions from branches cut off a channel.
Channel-targeted PRs keep their existing banner.

**Headline** becomes:

> **This PR promotes the `next` prerelease cycle to a stable release.**
The changes below that already shipped to the `@next` dist-tag will be
consolidated into the next stable version bump.

**File list** annotates shipped files, so mixed PRs (shipped cycle +
never-rc'd fixes) read at a glance:

> - `next/prerelease-channels.md` _(shipped on `@next`)_
> - `ci-check-channel-bump-files.md`

The shipped annotation also appears on graduation PRs (e.g.
`.bumpy/alpha/` files on a beta-targeted PR), where it's equally
accurate.

297 tests pass. Once merged, #110's comment should update itself with
the promotion callout (next moves forward → synchronize → check-local
rebuilds).
@theoephraim theoephraim merged commit 60e1f7a into main Jun 13, 2026
8 checks passed
theoephraim added a commit that referenced this pull request Jun 13, 2026
Dogfood finding from the first promotion: after merging
[#110](#110), the channel release
PR [#112](#112) lingered open,
offering to publish another rc of a cycle that had already moved to its
stable release ([#114](#114)). I
closed it manually; this makes bumpy do it.

**When:** as part of creating/updating the destination's version PR —
- stable version PR on main whose plan includes channel-dir bump files
(promotion), and
- channel release PR creation that moved files from *other* channels
(graduation, e.g. `alpha` → `beta`).

**What:** for each promoted source channel, look up an open PR on its
`versionPr.branch` head and close it with a comment explaining why,
noting a fresh release PR appears if more work lands on the channel.
Failures are warn-only — a lingering PR is annoying, not dangerous. The
current channel's own PR is explicitly never touched.

Targeting `main` (rather than `next`) deliberately: this path executes
via the published bumpy in the release workflow, so shipping it in
1.14.0 activates it for the next cycle's promotion — and `next` is due
for its post-promotion reset anyway. The bump file will fold into
[#114](#114).

Docs updated in the promotion section. 297 tests pass.

---

**Update — race fix (b13e94d):** the first cut closed the source
channel's release PR whenever its bump files were *pending* on the
destination, but promoted files stay pending until the destination's
version/release PR merges (potentially days). If a user started a fresh
`next` cycle in that window, an unrelated merge to `main` would close
its legitimate release PR — then the next push to `next` recreates it,
and the cycle repeats (close → recreate ping-pong).

Now the close is gated on the **triggering push** actually adding files
under `.bumpy/<channel>/` (its `before..after` range — the same
detection the channel publish trigger uses), so it fires exactly once,
on the promotion/graduation merge itself. Moved both call sites to after
the checkout-back so the non-CI fallback (`HEAD^..HEAD`) diffs the
triggering commit from the right branch.

_Thanks to @theoephraim for spotting the restarted-cycle race._
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants