Quality-gate finding 7a: bump_branch_in_flight matched ANY chore/auto-bump-*
branch, so one stale historical branch (branches are never cleaned up) stood
the safety net down permanently. The guard now matches exactly
chore/auto-bump-{NEW} — the in-flight version — with new_version=None as the
documented legacy escape hatch; wait_out_closed_path passes NEW_VERSION
through.
Quality-gate finding 7b (docs): release-flow.md claimed the closed path was
'genuinely absent, not merely slow' and that the paths 'can never
double-bump'. Rewritten: attribution is best-effort; a >90s delayed closed
path can race into a double-bump, bounded by tag idempotency (duplicate
release creation is a no-op); stale-branch cleanup is a known limitation.
Red evidence: anchored-match tests failed against the prefix-wide guard.
Green: 12/12.
3.7 KiB
3.7 KiB
Rune release flow: two paths to a release (#192)
Master __version__ can never be allowed to silently diverge from the latest
release tag. Two workflows cooperate to guarantee that; both share the same
machinery in scripts/.
Path 1 — pull_request:closed (authoritative)
.gitea/workflows/auto-version.yml, job release:
- Semver is decided from the merged PR's title (
feat:→ minor,feat!:/BREAKING CHANGE→ major, but the 0.x convention bumps the minor; everything else → patch). - If the PR did not already carry a version bump (
__version__still equals the latest tag),scripts/auto_bump_pr.pyruns the protected-branch-safe bump-PR machinery: branch → contents-API edits of the five version surfaces → PR → wait for the protected check → API merge. scripts/create_release.pycuts the release (idempotent: skips if the tag already exists).
This path is authoritative when the event fires — no changes to its decision logic were made in #192.
Path 2 — push (safety net)
The pull_request:closed event is intermittently dropped (PR #190 merged with
no run while #189 fired minutes earlier). The same workflow therefore also
triggers on every push to master, job safety-net:
scripts/version_convergence.pycompares master's__version__with the latest tag and classifies commit subjects since the tag (feat/feat!→ minor,fix/perf→ patch, docs/chore/test/refactor → nothing). It prints the decision as JSON:{"converged": bool, "warranted_bump": "minor"|"patch"|null, "commits_since_tag": N, "sample": [...]}.- If drift is detected (bump never rode the merge) and no
chore/auto-bump-{NEW}branch appears within the attribution grace window (best-effort evidence that the closed path is genuinely absent), the safety net runs the samescripts/auto_bump_pr.pymachinery with the subject-warranted kind, then cuts the release via the samescripts/create_release.py. - If master is ahead of the tag but no subject warrants a bump, the job only logs an advisory — the maintenance reconciliation owns that case.
Path 3 — scheduled drift reconciliation
.gitea/workflows/maintenance.yml, job release-drift-reconciliation
(weekly + workflow_dispatch):
- Runs
scripts/version_convergence.py; on drift it files or updates a deduped forge issue titledRelease drift: master vX.Y.Z vs tag vA.B.C(scripts/file_drift_issue.py). - Strictly advisory: it never auto-releases without the PR flow. Humans/agent land the bump PR; the next merge or the safety net cuts the release.
Idempotency & conservation
scripts/create_release.pyskips when a release for the tag exists (retry/replay safety).- Attribution between the two paths is best-effort, not airtight. The
safety net (
scripts/auto_bump_pr.py --safety-net) stands down when the bump branch for the exact in-flight version (chore/auto-bump-{NEW}) exists, and the guard is anchored to that version so one stale historical branch cannot stand the net down forever. Two misattributions remain possible: a closed path delayed beyond the 90s grace window can produce a double-bump race, and a stale branch of the same version (never cleaned up — stale-branch cleanup is a known limitation) can stand the net down incorrectly. - Double-bump is bounded by tag idempotency, not by the guard. If the
race happens, both paths compute the same next version; whichever release
creation lands first wins and the other is a no-op (
release_existsskip). Version surfaces can only converge to the same value, so the worst case is a redundant PR, never a wrong version. - Docs/chore-only merges still produce no release, same as the pre-#192 no-bump PR path.