1111 required : true
1212 type : choice
1313 options :
14+ - finalize # drop the bN suffix: release the current beta's base as stable (0.3.0b2 -> 0.3.0)
1415 - minor
1516 - bugfix
1617 - major
1718 - pre-release # increments the bN counter on the current base version
1819 beta :
19- description : " Mark as beta pre-release (adds bN suffix; always true for pre-release) "
20+ description : " Mark as beta pre-release (adds bN suffix). Ignored for ' pre-release' (always beta) and 'finalize' (always stable). "
2021 required : false
2122 type : boolean
2223 default : false
@@ -67,17 +68,42 @@ jobs:
6768 minor = int(m.group(2))
6869 patch = int(m.group(3))
6970 beta_n = int(m.group(4)) if m.group(4) else None
70-
71- if bump == "major":
71+ on_beta = beta_n is not None
72+
73+ if bump == "finalize":
74+ # Release the current beta's base as stable: just drop the bN
75+ # suffix, keep major.minor.patch. e.g. 0.3.0b2 -> 0.3.0.
76+ if not on_beta:
77+ raise SystemExit(
78+ f"'finalize' requires a beta base version, but current "
79+ f"version {current!r} has no bN suffix. Use minor / bugfix "
80+ f"/ major to start a new release instead."
81+ )
82+ is_beta = False
83+ elif bump == "pre-release":
84+ # Keep the same base; just walk the beta counter forward.
85+ is_beta = True
86+ beta_n = (beta_n or 0) + 1
87+ elif on_beta:
88+ # We are on a beta of the NEXT release (e.g. 0.3.0b2). The base
89+ # major.minor.patch is that upcoming version, so minor/bugfix/major
90+ # must bump relative to the LAST STABLE (base - the in-progress
91+ # component), not skip a whole version. The common intent from a
92+ # beta is 'finalize', so steer the user there rather than guess.
93+ raise SystemExit(
94+ f"Current version {current!r} is a beta of the upcoming "
95+ f"{major}.{minor}.{patch} release. To ship it, use bump="
96+ f"'finalize' (-> {major}.{minor}.{patch}). A '{bump}' bump from "
97+ f"a beta would skip {major}.{minor}.{patch} entirely "
98+ f"(e.g. -> {'%d.%d.0' % (major, minor + 1) if bump == 'minor' else '...'}); "
99+ f"that is almost never intended."
100+ )
101+ elif bump == "major":
72102 major, minor, patch = major + 1, 0, 0
73103 elif bump == "minor":
74104 minor, patch = minor + 1, 0
75105 elif bump == "bugfix":
76106 patch += 1
77- elif bump == "pre-release":
78- # Keep the same base; just walk the beta counter forward.
79- is_beta = True
80- beta_n = (beta_n or 0) + 1
81107
82108 if is_beta:
83109 if bump != "pre-release":
@@ -88,10 +114,15 @@ jobs:
88114 PYEOF
89115 )
90116
117+ # Derive is_beta from the COMPUTED version (ends in bN?), not the raw
118+ # input — so 'finalize' is always treated as stable and a mismatched
119+ # beta checkbox can't mislabel the switcher / skip the root redirect.
120+ if [[ "$NEW_VERSION" =~ b[0-9]+$ ]]; then IS_BETA_OUT=true; else IS_BETA_OUT=false; fi
121+
91122 echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
92123 echo "tag=v$NEW_VERSION" >> "$GITHUB_OUTPUT"
93124 echo "branch=release/v$NEW_VERSION" >> "$GITHUB_OUTPUT"
94- echo "is_beta=${{ inputs.beta }}" >> "$GITHUB_OUTPUT"
125+ echo "is_beta=$IS_BETA_OUT" >> "$GITHUB_OUTPUT"
95126 echo "Bumping (${{ inputs.bump }}): $CURRENT → $NEW_VERSION"
96127
97128 # ── Bump version strings ─────────────────────────────────────────────
@@ -118,7 +149,7 @@ jobs:
118149 - name : Update docs/switcher.json
119150 env :
120151 VERSION_TAG : ${{ steps.version.outputs.tag }}
121- IS_BETA : ${{ inputs.beta }}
152+ IS_BETA : ${{ steps.version.outputs.is_beta }}
122153 shell : python
123154 run : |
124155 import json, re, pathlib, os
@@ -144,7 +175,7 @@ jobs:
144175
145176 # ── Update root redirect for stable releases ─────────────────────────
146177 - name : Update root redirect (stable releases only)
147- if : ${{ inputs.beta == false && inputs.bump != 'pre-release' }}
178+ if : ${{ steps.version.outputs.is_beta == ' false' && inputs.bump != 'pre-release' }}
148179 env :
149180 VERSION_TAG : ${{ steps.version.outputs.tag }}
150181 shell : python
@@ -200,7 +231,7 @@ jobs:
200231 - Version bumped to \`${TAG}\` in \`pyproject.toml\` and \`docs/conf.py\`
201232 - \`CHANGELOG.rst\` updated from towncrier fragments
202233 - \`docs/_root/switcher.json\` updated with the new version entry
203- $([ '${{ inputs.beta }}' = 'false' ] && echo '- Root redirect updated to point to this release' || echo '')
234+ $([ '${{ steps.version.outputs.is_beta }}' = 'false' ] && echo '- Root redirect updated to point to this release' || echo '')
204235
205236 ### Review checklist
206237 - [ ] \`CHANGELOG.rst\` reads well — edit the fragment text directly if needed
0 commit comments