Skip to content

chore: automate the release-candidate version bump - #287

Merged
StefanSteiner merged 1 commit into
tableau:mainfrom
StefanSteiner:chore/automate-rc-versioning
Sep 6, 2026
Merged

chore: automate the release-candidate version bump#287
StefanSteiner merged 1 commit into
tableau:mainfrom
StefanSteiner:chore/automate-rc-versioning

Conversation

@StefanSteiner

@StefanSteiner StefanSteiner commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Adds prerelease handling to release-please-config.json so the rc line increments
automatically, 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-major keys, and updates the release
docs that #285 just landed — they document the footer-driven process as the only
mechanism, which this change makes stale.

The config diff

   "include-component-in-tag": false,
   "separate-pull-requests": false,
-  "bump-minor-pre-major": true,
   "skip-github-release": true,
   "packages": {
     ".": {
       "release-type": "simple",
       "component": "hyper-api-rust",
       "changelog-path": "CHANGELOG.md",
-      "bump-minor-pre-major": true,
       "skip-github-release": true,
+      "prerelease": true,
+      "prerelease-type": "rc",
+      "versioning": "prerelease",
       "extra-files": [

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:

  • The config schema's root is allOf: [ReleaserConfigOptions, {packages, …}], so
    the root accepts exactly the same keys a package entry does. All three are
    schema-valid in either position.
  • parseConfig builds defaultConfig = extractReleaserConfig(config) from the
    top level, then for each package does
    mergeReleaserConfig(defaultConfig, extractReleaserConfig(config.packages[path])).
    mergeReleaserConfig resolves every field as
    pathConfig.X ?? defaultConfig.X ?? <built-in default> — including
    prerelease, prereleaseType, and versioning.
  • Dry runs confirmed it rather than assuming: package-only, top-level-only, and
    both-places all compute 1.0.0-rc.3.

So the choice is about maintainability, and there are two reasons to pick the
package:

  1. It is the winning side of the merge. A future top-level default cannot
    silently override it.
  2. One location, one edit at 1.0.0. The keys have to come out when 1.0.0
    ships (see the caveat below). Mirroring them into both places — the pattern
    skip-github-release currently follows — would double the number of places
    someone 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-release is left duplicated as-is. It is redundant but not inert, and
churning it isn't in scope for a change to the file that drives releases.

On prerelease being overloaded: the schema documents it as "create the
GitHub release as prerelease", but PrereleaseVersioningStrategy also reads it to
decide whether to increment the prerelease suffix or truncate it. Here the
release-creation meaning has no effect at allskip-github-release: true
means release-please never creates a GitHub Release; a maintainer does that by
hand and passes --prerelease to gh release create. So in this repo the key is
purely a versioning switch.

Verification

npx release-please release-pr --dry-run, against real config files on pushed
branches
. Two constraints forced that shape, both verified rather than assumed:

  • release-pr has no --prerelease flag — only github-release does — so
    prerelease: true cannot be tested via CLI flags.
  • --local / --local-path does not read the config from the local clone.
    Proved it by putting "release-as": "9.9.9" and a custom
    pull-request-title-pattern in a local clone's committed config: both were
    ignored, 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@v5 pins". It isn't. The action's package.json
declares the range ^17.6.0, and its package-lock.json and bundled
dist/index.js at the v5 tag both resolve to 17.6.0 (dist/index.js line
48369: exports.VERSION = '17.6.0';, inside release-please's own
x-release-please-start-version markers). So the faithful version is 17.6.0. I
ran 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:

$ npx release-please@17.6.0 release-pr \
    --repo-url=tableau/hyper-api-rust --target-branch=main \
    --config-file=release-please-config.json \
    --manifest-file=.release-please-manifest.json \
    --token="$(gh auth token)" --dry-run

Would open 1 pull requests
title: chore: release main
branch: release-please--branches--main
<details><summary>1.0.1-rc.2</summary>
## [1.0.1-rc.2](https://github.com/tableau/hyper-api-rust/compare/v1.0.0-rc.2...v1.0.1-rc.2) (2026-09-06)
### Bug Fixes
* recover from panic/cancellation-induced mutex poisoning and leaked transactions (#280) …
updates: 9

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 the v1.0.0-rc.2 tag. The fork has no
GitHub Releases, so it got its own control before use — a scratch branch
byte-identical to upstream/main with the current config also computed
1.0.1-rc.2. Substrate validated.

Every case: branch off upstream/main (so the real fix: #280 is in range) + one
chore: commit installing the config variant + at most one synthetic commit.

Results

# Config Highest-precedence commit Computed Expected
1 current (real repo, main) fix: (real range) 1.0.1-rc.2 matches live #282
2 current (fork control) fix: (real range) 1.0.1-rc.2 substrate ✔
3 current feat: 1.1.0-rc.2
4 current feat!: 2.0.0-rc.2
5 proposed, keys on . fix: (real range) 1.0.0-rc.3 1.0.0-rc.3
6 proposed, keys top-level fix: (real range) 1.0.0-rc.3 placement ✔
7 proposed, keys in both fix: (real range) 1.0.0-rc.3 placement ✔
8 proposed feat: 1.0.0-rc.3 1.0.0-rc.3
9 proposed feat!: 1.0.0-rc.3 1.0.0-rc.3
10 proposed feat: + BREAKING CHANGE: footer 1.0.0-rc.3 1.0.0-rc.3
11 proposed fix: + Release-As: 1.0.0 1.0.0 footer still overrides ✔
12 proposed feat!: + Release-As: 1.0.0 1.0.0 footer still overrides ✔
13 proposed, "prerelease": false fix: (real range) 1.0.0 graduation ✔
14 proposed, "prerelease": false feat!: 1.0.0 graduation ✔
15 proposed + bump-minor-pre-major kept fix: (real range) 1.0.0-rc.3 same as #5
16 proposed + bump-minor-pre-major kept feat!: 1.0.0-rc.3 same as #9
17 current − bump-minor-pre-major fix: (real range) 1.0.1-rc.2 same as #2
18 current − bump-minor-pre-major feat!: 2.0.0-rc.2 same as #4
19 proposed, at 1.0.0 (manifest 1.0.0, v1.0.0 tag) fix: 1.0.1-rc the caveat, confirmed ✔
20 current, at 1.0.0 (same setup) fix: 1.0.1 caveat control ✔

Raw output, key rows (17.6.0):

CASE=control-current            rp=17.6.0   prs=1  version=1.0.1-rc.2
CASE=placement-pkg              rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=placement-top              rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=placement-both             rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=cur-feat                   rp=17.6.0   prs=1  version=1.1.0-rc.2
CASE=cur-breaking               rp=17.6.0   prs=1  version=2.0.0-rc.2
CASE=prop-feat                  rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=prop-breaking              rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=prop-bc-footer             rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=prop-release-as            rp=17.6.0   prs=1  version=1.0.0
CASE=prop-release-as-breaking   rp=17.6.0   prs=1  version=1.0.0
CASE=prerelease-false           rp=17.6.0   prs=1  version=1.0.0
CASE=prerelease-false-breaking  rp=17.6.0   prs=1  version=1.0.0
CASE=bmpm-kept-fix              rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=bmpm-kept-breaking         rp=17.6.0   prs=1  version=1.0.0-rc.3
CASE=cur-nobmpm-breaking        rp=17.6.0   prs=1  version=2.0.0-rc.2
CASE=cur-nobmpm-fix             rp=17.6.0   prs=1  version=1.0.1-rc.2
CASE=post100-prop-fix           rp=17.6.0   prs=1  version=1.0.1-rc
CASE=post100-current-fix        rp=17.6.0   prs=1  version=1.0.1

Cross-check on 17.11.2 — identical:

CASE=x-control                  rp=17.11.2  prs=1  version=1.0.1-rc.2
CASE=x-prop-fix                 rp=17.11.2  prs=1  version=1.0.0-rc.3
CASE=x-prop-breaking            rp=17.11.2  prs=1  version=1.0.0-rc.3
CASE=x-prerelease-false         rp=17.11.2  prs=1  version=1.0.0
CASE=x-post100-prop             rp=17.11.2  prs=1  version=1.0.1-rc

Rows 5–20 also match what the source says, which is a useful independent check.
PrereleaseVersioningStrategy.determineReleaseType checks for a RELEASE AS note
and short-circuits first (rows 11–12); then, when version.preRelease is set and
the relevant triple components are 0, all three updaters call bumpPrerelease,
which increments the trailing digits (rc.2rc.3) — rows 5–10. if (!this.prerelease) truncates to major.minor.patch — rows 13–14. With no
prerelease on the version, the updaters instead bump the triple and attach
prereleaseType, giving 1.0.1-rc — row 19.

Finally: the literal PR branch

The strongest evidence available. This branch is upstream/main + a chore:
commit carrying the committed config, which is exactly the post-merge state:

$ npx release-please@17.6.0 release-pr \
    --repo-url=StefanSteiner/hyper-api-rust \
    --target-branch=chore/automate-rc-versioning … --dry-run

Would open 1 pull requests
<details><summary>1.0.0-rc.3</summary>
## [1.0.0-rc.3](…/compare/v1.0.0-rc.2...v1.0.0-rc.3) (2026-09-06)

Removing bump-minor-pre-major

Both occurrences are gone. They were gated on isPreMajor, which version.ts
defines as major < 1:

get isPreMajor(): boolean {
  return this.major < 1;
}

The workspace has been at major 1 since 1.0.0-rc.1, so neither copy has done
anything 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.3 under the proposed config, 1.0.1-rc.2/2.0.0-rc.2
under 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 done
early, and rides along here so there's one config change and one dry run.

⚠️ The caveat this introduces

The prerelease keys must be removed — or prerelease flipped to false — when
1.0.0 final ships.
Left in place at a non-prerelease version, the next fix:
computes 1.0.1-rc (row 19), and every subsequent stable release becomes an
rc 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:

Before (footer on every rc) After (automatic rc bump)
Correct human action needed on every rc release once, at 1.0.0
Version if forgotten 1.0.1-rc.2 1.0.1-rc
How you find out nothing objects wrong version in the PR title
Recoverable? no, once published yes, before merging

The old failure was silent and terminal: v1.0.1-rc.2 satisfies the tag regex
in both publish workflows and matches the Cargo.toml the release PR itself
wrote, so the tag-vs-manifest guard agrees and nothing in CI objects — and since
1.0.1-rc.2 sorts above 1.0.0, publishing it would make a later 1.0.0 a
downgrade, 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.0 without meeting it:

  • docs/GITHUB_OPERATIONS.mdPre-releases rewritten from "every rc needs
    its own footer" to the automatic bump, with Release-As: demoted to an
    override (keeping the squash-commit-body caveat, since the override still needs
    it). New Graduating to 1.0.0 section carries the removal requirement and the
    trade-off table. New Re-verifying the prerelease behaviour section records the
    three harness traps above so the next person doesn't rediscover them. The
    now-adopted Automating the rc line section is folded in and removed.
  • The bump table in How commits drive version bumps is annotated: while the
    rc line is open the table does not apply, because every releasing prefix
    collapses to the next rc. The old bump-minor-pre-major blockquote is replaced
    with a note that the keys were removed and why, so nobody re-adds them.
  • The release checklist (Cutting a release → step 3) now says to read the
    version in the PR title, and to remove the keys before shipping 1.0.0.
  • AGENTS.md and CONTRIBUTING.md get the short version and cross-reference
    docs/GITHUB_OPERATIONS.md as the single source of truth, consistent with how
    docs: 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 the
baseline on main. All internal anchors re-checked, including the two
cross-file links that pointed at the removed
#every-rc-needs-its-own-footer anchor.

No public API surface changes, so no per-crate CHANGELOG.md entry per AGENTS.md
reminder 8.

What release-please does with this PR

The title is chore:, which on its own produces no release. But the current
commit range already contains fix: #280, so a release PR exists either way, and
under 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, the release-please workflow re-runs and PR #282 should update
itself in place from 1.0.1-rc.2 to 1.0.0-rc.3
, retitling to
chore(main): release 1.0.0-rc.3. release-please force-pushes its own branch on
every 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.json and Markdown only — no Rust source, no
workflow files, no crate versions, version.txt, .release-please-manifest.json,
or root CHANGELOG.md. Every release-please invocation was --dry-run. Scratch
branches and the temporary v1.0.0 tag used for row 19 existed only on my fork
and 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 main moved

While this PR was open, #286 (fix(mcp): publish a restarted hyperd endpoint …)
merged, moving main from 964ea46 to b54103c. That push re-triggered the
release-please workflow, which recomputed and force-pushed #282's branch — so
#282's head SHA changed from 3704b1cb to 909b48ff (chore: release main
rebased onto the new main, plus the workflow's chore: sync Cargo.lock with bumped workspace versions follow-up commit). That is the documented
self-correcting behaviour, not an edit to #282: it is still open, still titled
chore: release main, still computing 1.0.1-rc.2, still labelled
autorelease: pending.

Because the commit range grew, the dry runs were re-run against the new base
rather than assumed to still hold:

Config Target Computed
current real main at b54103c 1.0.1-rc.2 — still matches live #282, harness still faithful
proposed b54103c + this commit 1.0.0-rc.3 — unchanged

Unchanged as expected: #286 is another fix:, so patch is still the
highest-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 (still MERGEABLE — no
conflicts, 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.

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
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