You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: prerelease channels (branch-based, no committed prerelease versions) (#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)
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.
Copy file name to clipboardExpand all lines: README.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,6 +54,7 @@ Fixed locale fallback logic in utils.
54
54
-**Flexible package management** - include/exclude any package individually via per-package config, glob patterns, or `privatePackages` setting
55
55
-**Non-interactive CLI** - `bumpy add` works fully non-interactively for CI/CD and AI-assisted development
56
56
-**Aggregated GitHub releases** - optionally create a single consolidated release instead of one per package
57
+
-**Prerelease channels** - branch-based `@next` / `@beta` release lines where prerelease versions are derived at publish time, never committed to git (see [prerelease channels docs](https://github.com/dmno-dev/bumpy/blob/main/docs/prereleases.md))
57
58
-**Auto-generate from commits** - `bumpy generate` creates bump files from branch commits - works with any commit style, with enhanced detection for conventional commits
58
59
-**Pluggable changelog formatters** - built-in `"default"` and `"github"` formatters, or write your own
59
60
-**Zero runtime dependencies** - dependencies are minimal and bundled at release time
@@ -119,6 +120,7 @@ The skill teaches the AI to examine git changes, identify affected packages, cho
119
120
-[CLI reference](https://github.com/dmno-dev/bumpy/blob/main/docs/cli.md) - every command with flags and examples
120
121
-[GitHub Actions setup](https://github.com/dmno-dev/bumpy/blob/main/docs/github-actions.md) - CI workflows, token setup, trusted publishing
121
122
-[Version propagation](https://github.com/dmno-dev/bumpy/blob/main/docs/version-propagation.md) - how dependency bumps cascade through your graph
@@ -133,6 +135,7 @@ Bumpy is built as a successor to [🦋changesets](https://github.com/changesets/
133
135
-**Custom publish commands** - changesets is hardcoded to `npm publish`. Bumpy supports per-package custom publish for VSCode extensions, Docker images, JSR, etc.
134
136
-**Flexible package management** - changesets treats all private packages the same. Bumpy lets you include/exclude any package individually.
135
137
-**CI without a separate action or bot** - changesets requires installing a [GitHub App](https://github.com/apps/changeset-bot)_and_ using a [separate GitHub Action](https://github.com/changesets/action). Bumpy replaces both with two CLI commands (`bumpy ci check` + `bumpy ci release`) that run directly in your workflows - no extra repos to trust, no app installation requiring org admin approval.
138
+
-**Prerelease channels that don't corrupt state** - changesets' prerelease mode is described in [their own docs](https://github.com/changesets/changesets/blob/main/docs/prereleases.md) as "very complicated" with states "very hard to fix." Bumpy uses [branch-based channels](https://github.com/dmno-dev/bumpy/blob/main/docs/prereleases.md) where prerelease versions are never committed - no global mode file to poison unrelated releases.
136
139
-**Automatic migration** - `bumpy init` detects `.changeset/`, renames it to `.bumpy/`, migrates config, keeps pending files, and offers to uninstall `@changesets/cli`.
Copy file name to clipboardExpand all lines: docs/cli.md
+29-15Lines changed: 29 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -53,9 +53,12 @@ bumpy status --verbose
53
53
|`--bump <types>`| Filter by bump type, e.g. `"major"` or `"minor,patch"`|
54
54
|`--filter <names>`| Filter by package name or glob |
55
55
|`--verbose`| Show bump file details and summaries |
56
+
|`--channel <name>`| Channel override (default: inferred from the current branch) |
56
57
57
58
Exits with code `0` if releases are pending, `1` if none.
58
59
60
+
On a [prerelease channel](prereleases.md) branch, status shows the cycle instead: shipped vs pending bump files and the derived `-<preid>.N` versions (counters come from the registry; offline they render as `.?`).
61
+
59
62
## `bumpy version`
60
63
61
64
Consume all pending bump files and apply the release plan:
|`--channel <name>`| Channel override (default: inferred from the current branch) |
82
+
83
+
On a [prerelease channel](prereleases.md) branch, `bumpy version` does something much smaller: it **moves pending bump files into `.bumpy/<channel>/`** and writes no versions and no changelogs — prerelease versions are derived at publish time and never committed.
|`--no-push`| Skip pushing git tags to the remote |
101
+
|`--filter <names>`| Only publish matching packages (supports globs) |
102
+
|`--channel <name>`| Channel override (default: inferred from the current branch) |
103
+
104
+
On a [prerelease channel](prereleases.md) branch, publish derives prerelease versions (targets from the cycle's bump files, counters from the registry), transiently writes them into the working tree so pack/build see them, publishes the whole cycle to the channel's dist-tag with exact-pinned inter-cycle deps, then restores the files. Nothing version-shaped is ever committed.
|`--strict`| Fail if any changed package is not covered by a bump file |
129
+
|`--no-fail`| Warn only, never exit non-zero (useful for advisory hooks) |
130
+
|`--hook pre-commit`| Only count staged + committed bump files |
131
+
|`--hook pre-push`| Only count committed bump files |
132
+
|`--base <branch>`| Branch to compare against (default: `baseBranch`) — use the channel branch for feature branches targeting a [channel](prereleases.md)|
133
+
134
+
The check is skipped automatically on channel branches and release PR branches (they move/consume bump files by design).
123
135
124
136
### Hook context
125
137
@@ -240,6 +252,8 @@ bumpy ci release --auto-publish --tag beta
240
252
241
253
Requires `GH_TOKEN`. When `BUMPY_GH_TOKEN` is set, it is automatically used to push the version branch and create/edit the PR so that PR workflows trigger (see [GitHub Actions setup](github-actions.md#token-setup)).
242
254
255
+
**Channel branches:** when run on a branch configured as a [prerelease channel](prereleases.md), `ci release` switches to the channel flow — it publishes the cycle when the triggering push moved bump files into `.bumpy/<channel>/` (a release PR merge), and creates/updates the file-move release PR when pending bump files exist. When channels are configured and the branch is neither `baseBranch` nor a channel branch, the command exits with an error instead of guessing.
256
+
243
257
## `bumpy ci setup`
244
258
245
259
Interactive guide to set up `BUMPY_GH_TOKEN` for CI. Walks through creating a fine-grained PAT or GitHub App token and storing it as a repository secret.
"automerge":false, // enable auto-merge on the release PR
118
+
},
119
+
},
120
+
},
121
+
}
122
+
```
123
+
124
+
Channel names become `.bumpy/<name>/` subdirectories (holding bump files that shipped on the channel), so they must be filesystem-safe and can't start with `_` or collide with reserved entries.
125
+
102
126
## Per-package config
103
127
104
128
Per-package settings can be defined in two places:
Changesets' prerelease mode is described in their own docs as "very complicated" with "mistakes that can lead to repository and publish states that are very hard to fix." Key problems: global mode state poisons unrelated merges, exiting pre bumps ALL packages, counters require committed state, dist-tags can't be controlled.
153
+
154
+
Bumpy replaces the mode with **branch-based channels** ([docs/prereleases.md](./prereleases.md)): a long-lived branch (e.g. `next`) maps to a prerelease line. Bump file location (`.bumpy/<channel>/`) is the only state; prerelease versions are never committed — targets derive from bump files, counters from the registry. Promotion to stable is just a merge.
155
+
156
+
-[changesets#729](https://github.com/changesets/changesets/issues/729) — exiting pre mode bumps all versions (14 comments)
157
+
-[changesets#786](https://github.com/changesets/changesets/issues/786) — can't control dist-tag in pre mode (13 comments)
-[changesets#381](https://github.com/changesets/changesets/issues/381) — prerelease counters require committed state
161
+
150
162
### Local bump file verification
151
163
152
164
`bumpy check` verifies that changed packages on the current branch have corresponding bump files. Compares your branch to the base branch, maps changed files to packages. By default it only fails if no bump files exist at all (matching changesets behavior). Use `--strict` to require every changed package to be covered, `--no-fail` for advisory-only mode, or `--hook pre-commit`/`--hook pre-push` to control which bump files count based on their git status. No GitHub API needed.
@@ -157,15 +169,6 @@ Changesets has no built-in equivalent — users rely on the CI bot comment to ca
157
169
158
170
## Planned / Not Yet Implemented
159
171
160
-
### Prerelease mode that actually works
161
-
162
-
Changesets' prerelease mode is described in their own docs as "very complicated" with "mistakes that can lead to repository and publish states that are very hard to fix." Key problems: no target on bump files, multi-branch corruption, exiting pre bumps ALL packages, bad interactions with linked/fixed groups.
163
-
164
-
-[changesets#729](https://github.com/changesets/changesets/issues/729) — exiting pre mode bumps all versions (14 comments)
165
-
-[changesets#786](https://github.com/changesets/changesets/issues/786) — can't control dist-tag in pre mode (13 comments)
0 commit comments