Skip to content

feat: prerelease channels (branch-based, no committed prerelease versions)#104

Merged
theoephraim merged 11 commits into
nextfrom
docs/prerelease-channels
Jun 12, 2026
Merged

feat: prerelease channels (branch-based, no committed prerelease versions)#104
theoephraim merged 11 commits into
nextfrom
docs/prerelease-channels

Conversation

@theoephraim

@theoephraim theoephraim commented Jun 9, 2026

Copy link
Copy Markdown
Member

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 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 surveys the recurring complaints (#239, #381, #729, #786, #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'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

  • 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)
  • tsc --noEmit, oxlint, oxfmt clean
  • 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)

Draft design doc for branch-based prerelease support. Models channels as
long-lived branches (next, beta, etc.) with bump files tracked by directory
location — pending at .bumpy/*, shipped at .bumpy/<channel>/, consolidated
on promotion to main. Documents the comparison with changesets' pre mode
and what's deliberately out of scope.

Not yet implemented — sharing for user feedback before building.
Channels are for long-lived release lines (next/beta/rc), not per-PR
previews. Add a "when to use this" section and point users at pkg.pr.new
for ephemeral PR packages.
…dination

Rewrite the dependency section around exact-pin-within-cycle, with cycle
membership coming from explicit declarations (bump files, linked/fixed,
cascadeTo) rather than automatic propagation. Drop the propagation
config knob — matches bumpy's existing "explicit > implicit" stance for
stable releases.

Add a "Coordinating multi-package prereleases" section with the
stranded-prerelease failure mode and the four ways to fix it.

Update the changesets comparison table to reflect the new approach.
Re-thinking: semver only resolves a prerelease against a range when
major.minor.patch matches exactly. That means every prerelease of an
upstream breaks every dependent's range — not occasionally, but always.
"Suppressing" cascade would just produce prereleases that consumers
can't install together without manual overrides.

So the right model is: Phase A/B/C run unchanged on channels, producing
a wide cascade by nature of how prerelease semver matches. The user
complaints behind changesets #960 are about bump-level policy (peer
deps jumping to major), not about whether to cascade — and bumpy's
proportional rules already address that.

Drop the "Coordinating multi-package prereleases" stranded-failure
section (no longer applies). Replace with a "Limiting cascade scope"
note pointing at the existing ignore/include/managed controls. Update
the changesets comparison row to reflect the actual difference.
After reviewing realistic prerelease workflows: between bumpy channels
(managed long-running release lines) and pkg.pr.new (ephemeral previews,
canaries, branch snapshots), real-world use cases are covered. No need
for an in-bumpy snapshot mechanism that would compete with the
recommended tool.

Expand the rule-of-thumb table to cover per-commit canaries and ad-hoc
branch snapshots. Reinforce the pkg.pr.new pointer in the day-to-day
workflow section so readers see it at the natural moment ("I want to
install this PR before merging").
…les, and tags

Major revision of the channels design around one principle: git carries
inputs (bump files) and stable outputs only. Prerelease versions exist
solely in the registry and git tags.

- package.json stays at the last stable version on channel branches
- release PR becomes a pure file-move PR; computed versions go in the
  PR title and merge commit message (advisory; registry wins at publish)
- counters derived from registry floor (max published + 1) — immune to
  abandoned cycles, force-resets, and re-runs
- every publish recomputes the full cycle; lockstep republish + exact
  pins make the @next set coherent by construction
- promotion is now just a merge; no suffix stripping, no rc versions
  ever on main, no version conflicts on main <-> channel syncs
- generalized pending rule enables channel graduation (alpha -> beta)
- no CHANGELOG.md on channels; GitHub releases + render-on-demand via
  bumpy status; consolidated stable entry written once at promotion
- publish mechanics specified: trigger via channel-dir diff since last
  tag, tag-on-SHA idempotency, gitHead-based partial-failure resume
- document build-time version baking caveat (rewrite before build)
- reserve schema room for future stable/maintenance channels
Branch-based prerelease lines where prerelease versions are never
committed to git — derived at publish time from bump files (targets),
the registry (counters), and git tags/gitHead (idempotency).

Core:
- channels config block (+ JSON schema): branch, preid, tag, versionPr
  with automerge; names validated as .bumpy/ subdirectory-safe
- bump file location is the only channel state: pending at .bumpy/ root
  (or other channels' dirs), shipped in .bumpy/<channel>/; the general
  pending rule gives alpha -> beta graduation for free
- release plan gains a prerelease mode: range satisfaction is checked
  against <target>-<preid>.0, so every dependent joins the cycle at
  proportional bump levels (prereleases never satisfy stable ranges)
- prerelease versions: registry-floor counters (max published + 1),
  per-package gitHead skip for re-runs/resume, exact-pinned in-cycle
  deps written transiently into the working tree and restored after

Commands:
- version: on a channel, only moves pending files into .bumpy/<channel>/
- publish: channel flow computes + rewrites + publishes to the channel
  dist-tag; stable flow refuses suffixed versions when channels exist
- ci release: channel branches get publish-on-move-detection (push
  range diff) + file-move release PRs with computed versions in the
  title/commit message; unknown branches are refused
- ci plan/check, status, check: channel-aware (status shows the cycle;
  check skips channel branches and gains --base)
- promotion needs no special mode: main consumes channel dirs as
  pending, bumps stable-to-stable, writes the consolidated changelog
- GitHub releases for prerelease versions are marked --prerelease

Docs updated to match (banner removed, trigger/notes/check rows
aligned); changesets comparison moved to Implemented.
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

bumpy-frog

The changes in this PR will be included in the next 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.

@theoephraim theoephraim changed the title docs: proposed prerelease channels design (RFC) feat: prerelease channels (branch-based, no committed prerelease versions) Jun 12, 2026
- add to Features and Documentation sections
- add to changesets pain-points comparison
- remove from Roadmap (now implemented)
- add channels.next (preid rc, @next dist-tag) to .bumpy/_config.json
- add next to the release workflow push triggers
- make the release concurrency group per-branch
@theoephraim theoephraim changed the base branch from main to next June 12, 2026 20:44
When a PR targets a prerelease channel branch, the bumpy ci check
comment now says so explicitly instead of implying a stable release:
- headline: 'This PR targets the <name> prerelease channel — merging
  ships these as a prerelease to @<tag>, not a stable release'
- versions display the derived '-<preid>.?' suffix (counter comes from
  the registry at publish time)
- a note with the dist-tag install hint and how to promote to stable
- plan uses the prerelease preid so the wider channel cascade is shown

Export formatReleasePlanComment and cover both stable and channel
comment shapes with tests.
Split the PR check into two mutually-exclusive jobs:
- check-published: fork PRs run published @latest (never executes
  untrusted code with the pull_request_target write token)
- check-local: non-fork PRs build + run this repo's local bumpy, so
  unreleased behavior (e.g. channel-aware comments) is dogfooded on our
  own PRs before it ships to @latest

Lets PR #104 (internal, targets the next channel) show the real
channel-aware check comment now instead of waiting for promotion.
theoephraim added a commit that referenced this pull request Jun 12, 2026
…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.
@theoephraim theoephraim reopened this Jun 12, 2026
@theoephraim theoephraim force-pushed the docs/prerelease-channels branch from fd20b8f to 573780a Compare June 12, 2026 21:03
@theoephraim theoephraim merged commit 1380f72 into next Jun 12, 2026
8 checks passed
theoephraim added a commit that referenced this pull request Jun 13, 2026
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/bumpy` → **1.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](#104)) — branch-based
prerelease lines; versions derived at publish time, never committed.
- **Deterministic channel release PR titles**
([#107](#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](#109).
- **Docs** ([#106](#106)) —
environment deployment-branch allowances for channel branches with
trusted publishing.

---------

Co-authored-by: bumpy 🐸 <bumpy.bot@varlock.dev>
Co-authored-by: bumpy-bot <276066384+bumpy-bot@users.noreply.github.com>
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.

1 participant