chore: automate the release-candidate version bump - #287
Merged
StefanSteiner merged 1 commit intoSep 6, 2026
Conversation
Add the three prerelease keys to the "." package in release-please-config.json so the rc counter increments on its own: "prerelease": true, "prerelease-type": "rc", "versioning": "prerelease" Before this, the config had no prerelease keys, so release-please applied the default strategy: it bumped major.minor.patch and carried the -rc.N suffix along unchanged. From 1.0.0-rc.2 a `fix:` computed 1.0.1-rc.2 -- well-formed, publishable, and sorting *above* 1.0.0, so publishing it would have made a later 1.0.0 final a downgrade. Producing the next rc required a Release-As: footer on every single release, in the squash commit body, and forgetting it was the default outcome. See tableau#283. With the keys in place, every releasing prefix (fix:, feat:, feat!:) computes 1.0.0-rc.3 instead. Release-As: still wins when present, so it remains available to pin a specific version. Also remove `bump-minor-pre-major` (both occurrences). release-please only consults it when version.isPreMajor -- defined as major < 1 -- so it has been inert since 1.0.0-rc.1. Dry runs confirmed removing it changes no computed version under either config. This discharges a deferred item from the 1.88 uplift plan. Verified with `release-please@17.6.0 release-pr --dry-run` (17.6.0 is what release-please-action@v5 actually bundles) against real config files on pushed branches, after first reproducing the live 1.0.1-rc.2 that PR tableau#282 computed. Cross-checked on 17.11.2; both agree on every case. The caveat this introduces: the keys must be removed, or `prerelease` flipped to false, when 1.0.0 final ships -- otherwise the next `fix:` computes 1.0.1-rc (verified). That failure is visible in the release PR title and reversible, unlike the silent one it replaces. Documented in the release checklist, next to the bump table, and in a dedicated "Graduating to 1.0.0" section. Refs tableau#283
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds prerelease handling to
release-please-config.jsonso the rc line incrementsautomatically, instead of depending on a maintainer remembering a
Release-As:footer in the squash commit body of every release. Supersedes #283.
Also removes the two inert
bump-minor-pre-majorkeys, and updates the releasedocs that #285 just landed — they document the footer-driven process as the only
mechanism, which this change makes stale.
The config diff
Placement justification
The three keys go on the
.package only, not the top level, and not both.Placement is not a correctness question — all three work. Both the source and the
dry runs say so:
allOf: [ReleaserConfigOptions, {packages, …}], sothe root accepts exactly the same keys a package entry does. All three are
schema-valid in either position.
parseConfigbuildsdefaultConfig = extractReleaserConfig(config)from thetop level, then for each package does
mergeReleaserConfig(defaultConfig, extractReleaserConfig(config.packages[path])).mergeReleaserConfigresolves every field aspathConfig.X ?? defaultConfig.X ?? <built-in default>— includingprerelease,prereleaseType, andversioning.both-places all compute
1.0.0-rc.3.So the choice is about maintainability, and there are two reasons to pick the
package:
silently override it.
1.0.0. The keys have to come out when1.0.0ships (see the caveat below). Mirroring them into both places — the pattern
skip-github-releasecurrently follows — would double the number of placessomeone has to remember to edit. Given the whole point of this change is to
remove a step people forget, adding a redundant copy of the thing that must
later be removed would be working against it.
skip-github-releaseis left duplicated as-is. It is redundant but not inert, andchurning it isn't in scope for a change to the file that drives releases.
On
prereleasebeing overloaded: the schema documents it as "create theGitHub release as prerelease", but
PrereleaseVersioningStrategyalso reads it todecide whether to increment the prerelease suffix or truncate it. Here the
release-creation meaning has no effect at all —
skip-github-release: truemeans release-please never creates a GitHub Release; a maintainer does that by
hand and passes
--prereleasetogh release create. So in this repo the key ispurely a versioning switch.
Verification
npx release-please release-pr --dry-run, against real config files on pushedbranches. Two constraints forced that shape, both verified rather than assumed:
release-prhas no--prereleaseflag — onlygithub-releasedoes — soprerelease: truecannot be tested via CLI flags.--local/--local-pathdoes not read the config from the local clone.Proved it by putting
"release-as": "9.9.9"and a custompull-request-title-patternin a local clone's committed config: both wereignored, output unchanged. Config and manifest always come over the API from
the target branch.
Correction to a prior finding: the docs #285 landed said 17.11.2 is "the
version
release-please-action@v5pins". It isn't. The action'spackage.jsondeclares the range
^17.6.0, and itspackage-lock.jsonand bundleddist/index.jsat thev5tag both resolve to 17.6.0 (dist/index.jsline48369:
exports.VERSION = '17.6.0';, inside release-please's ownx-release-please-start-versionmarkers). So the faithful version is 17.6.0. Iran the whole matrix on 17.6.0 and cross-checked the load-bearing rows on 17.11.2
— both agree on every case, so the finding doesn't depend on the version. This
PR fixes the doc line.
Harness fidelity first
Before varying anything, the harness had to reproduce PR #282's live computation
of
1.0.1-rc.2. Current config, real repo,--target-branch=main:Version, PR title, and branch name all match live PR #282. 17.11.2 reproduced it
too.
Because config variants need pushed branches, the matrix ran on my fork
(
StefanSteiner/hyper-api-rust), which has thev1.0.0-rc.2tag. The fork has noGitHub Releases, so it got its own control before use — a scratch branch
byte-identical to
upstream/mainwith the current config also computed1.0.1-rc.2. Substrate validated.Every case: branch off
upstream/main(so the realfix:#280 is in range) + onechore:commit installing the config variant + at most one synthetic commit.Results
main)fix:(real range)1.0.1-rc.2fix:(real range)1.0.1-rc.2feat:1.1.0-rc.2feat!:2.0.0-rc.2.fix:(real range)1.0.0-rc.31.0.0-rc.3✔fix:(real range)1.0.0-rc.3fix:(real range)1.0.0-rc.3feat:1.0.0-rc.31.0.0-rc.3✔feat!:1.0.0-rc.31.0.0-rc.3✔feat:+BREAKING CHANGE:footer1.0.0-rc.31.0.0-rc.3✔fix:+Release-As: 1.0.01.0.0feat!:+Release-As: 1.0.01.0.0"prerelease": falsefix:(real range)1.0.0"prerelease": falsefeat!:1.0.0bump-minor-pre-majorkeptfix:(real range)1.0.0-rc.3bump-minor-pre-majorkeptfeat!:1.0.0-rc.3bump-minor-pre-majorfix:(real range)1.0.1-rc.2bump-minor-pre-majorfeat!:2.0.0-rc.21.0.0(manifest1.0.0,v1.0.0tag)fix:1.0.1-rc1.0.0(same setup)fix:1.0.1Raw output, key rows (17.6.0):
Cross-check on 17.11.2 — identical:
Rows 5–20 also match what the source says, which is a useful independent check.
PrereleaseVersioningStrategy.determineReleaseTypechecks for aRELEASE ASnoteand short-circuits first (rows 11–12); then, when
version.preReleaseis set andthe relevant triple components are 0, all three updaters call
bumpPrerelease,which increments the trailing digits (
rc.2→rc.3) — rows 5–10.if (!this.prerelease)truncates tomajor.minor.patch— rows 13–14. With noprerelease on the version, the updaters instead bump the triple and attach
prereleaseType, giving1.0.1-rc— row 19.Finally: the literal PR branch
The strongest evidence available. This branch is
upstream/main+ achore:commit carrying the committed config, which is exactly the post-merge state:
Removing
bump-minor-pre-majorBoth occurrences are gone. They were gated on
isPreMajor, whichversion.tsdefines as
major < 1:The workspace has been at major
1since1.0.0-rc.1, so neither copy has doneanything since. They read as protection against an accidental major bump while
providing none.
Rows 15–18 isolate the removal across both configs and both the patch and
breaking paths. Every pairing is identical with and without the keys
(
1.0.0-rc.3/1.0.0-rc.3under the proposed config,1.0.1-rc.2/2.0.0-rc.2under the current one), so removing them alters no computed version. Had any row
differed, the keys would have stayed and this section would say so.
This also discharges a deferred checklist item in
docs/superpowers/plans/1_88_uplift/2026-09-04-rust-188-edition-2024-uplift.md,which asked for exactly this removal at
1.0.0; it's ticked, annotated as doneearly, and rides along here so there's one config change and one dry run.
The prerelease keys must be removed — or
prereleaseflipped tofalse— when1.0.0final ships. Left in place at a non-prerelease version, the nextfix:computes
1.0.1-rc(row 19), and every subsequent stable release becomes anrc until someone edits the config.
This is a real cost, and the docs say so plainly rather than burying it. It is
still a good trade, because the two failure modes are not comparable:
1.0.01.0.1-rc.21.0.1-rcThe old failure was silent and terminal:
v1.0.1-rc.2satisfies the tag regexin both publish workflows and matches the
Cargo.tomlthe release PR itselfwrote, so the tag-vs-manifest guard agrees and nothing in CI objects — and since
1.0.1-rc.2sorts above1.0.0, publishing it would make a later1.0.0adowngrade, permanently forfeiting the rc line. The new failure shows the wrong
version in the release PR title before anything is tagged, and the fix is the
config edit that was missed.
So this trades a silent tripwire for a visible one. It does not remove the
tripwire, which is why the docs still say to read the version in the release PR
title before merging.
Docs
JSON takes no comments, so the caveat lives in the docs — in three places chosen
so it's hard to reach
1.0.0without meeting it:docs/GITHUB_OPERATIONS.md—Pre-releasesrewritten from "every rc needsits own footer" to the automatic bump, with
Release-As:demoted to anoverride (keeping the squash-commit-body caveat, since the override still needs
it). New
Graduating to 1.0.0section carries the removal requirement and thetrade-off table. New
Re-verifying the prerelease behavioursection records thethree harness traps above so the next person doesn't rediscover them. The
now-adopted
Automating the rc linesection is folded in and removed.How commits drive version bumpsis annotated: while therc line is open the table does not apply, because every releasing prefix
collapses to the next rc. The old
bump-minor-pre-majorblockquote is replacedwith a note that the keys were removed and why, so nobody re-adds them.
Cutting a release→ step 3) now says to read theversion in the PR title, and to remove the keys before shipping
1.0.0.AGENTS.mdandCONTRIBUTING.mdget the short version and cross-referencedocs/GITHUB_OPERATIONS.mdas the single source of truth, consistent with howdocs: correct the release-process docs against the real release-please setup #285 left things. The AGENTS.md note is a new paragraph rather than an
extension of the existing long line, to stay clear of MD013.
npx markdownlint-cli2(no arguments): 0 issues in 68 files, matching thebaseline on
main. All internal anchors re-checked, including the twocross-file links that pointed at the removed
#every-rc-needs-its-own-footeranchor.No public API surface changes, so no per-crate
CHANGELOG.mdentry per AGENTS.mdreminder 8.
What release-please does with this PR
The title is
chore:, which on its own produces no release. But the currentcommit range already contains
fix:#280, so a release PR exists either way, andunder the new config any release in this range computes the same thing. The
dry run against this exact branch (above) is the measurement:
1.0.0-rc.3.Expected post-merge effect — this is the real-world confirmation. Once this
lands on
main, therelease-pleaseworkflow re-runs and PR #282 should updateitself in place from
1.0.1-rc.2to1.0.0-rc.3, retitling tochore(main): release 1.0.0-rc.3. release-please force-pushes its own branch onevery run, so this needs no intervention — don't close or hand-edit #282.
#282 must still not be merged until you decide to cut. Nothing here tags,
publishes, or touches #282; its head is still
3704b1cb.Scope
Touches
release-please-config.jsonand Markdown only — no Rust source, noworkflow files, no crate versions,
version.txt,.release-please-manifest.json,or root
CHANGELOG.md. Every release-please invocation was--dry-run. Scratchbranches and the temporary
v1.0.0tag used for row 19 existed only on my forkand are deleted (fork tags are back to just
v1.0.0-rc.2).Refs #283 — please leave that issue open until this merges.
Addendum: re-verified after
mainmovedWhile this PR was open, #286 (
fix(mcp): publish a restarted hyperd endpoint …)merged, moving
mainfrom964ea46tob54103c. That push re-triggered therelease-pleaseworkflow, which recomputed and force-pushed #282's branch — so#282's head SHA changed from
3704b1cbto909b48ff(chore: release mainrebased onto the new
main, plus the workflow'schore: sync Cargo.lock with bumped workspace versionsfollow-up commit). That is the documentedself-correcting behaviour, not an edit to #282: it is still open, still titled
chore: release main, still computing1.0.1-rc.2, still labelledautorelease: pending.Because the commit range grew, the dry runs were re-run against the new base
rather than assumed to still hold:
mainatb54103c1.0.1-rc.2— still matches live #282, harness still faithfulb54103c+ this commit1.0.0-rc.3— unchangedUnchanged as expected: #286 is another
fix:, so patch is still thehighest-precedence bump in the range, and under the prerelease strategy every
bump collapses to the next rc regardless.
This branch is now one commit behind
main(stillMERGEABLE— noconflicts, and it touches no file #286 touched). It was deliberately not
force-pushed, so the green CI run above stays attached to the reviewed commit;
update the branch before merging if your ruleset requires up-to-date branches.