feat: auto-prune superseded -rc.N tags after cut (closes #153) #164
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!164
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/153-rc-tag-lifecycle"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Sprint 2 (#153 rc-tag lifecycle). The self-bootstrap re-pin discipline (AGENTS.md section 2) tags each cycle as
vX.Y.Z-rc.N. Once the cut forvX.Y.Zships, the-rc.Ntag is superseded — substrate dead-weight that's been hand-pruned each cycle since v0.4.0.This PR closes the loop: the cut path auto-deletes the rc tag family via the Forgejo API after the manifest mechanism completes. Best-effort posture — failure logs a warning but doesn't fail the cut.
Changes
scripts/prune-rc-tags.sh(new): enumeratesvCUT_VERSION-rc.*via the Forgejo API + deletes each. Args:OWNER REPO CUT_VERSION. Normalizes leadingv. Best-effort posture.scripts/lib/forgejo-api.sh: addsforgejo_list_tags(owner, repo, prefix)+forgejo_delete_tag(owner, repo, tag)helpers +FORGEJO_TEST_TAGS_FILEtest seam (mirrors the existingFORGEJO_TEST_PR_LOOKUP_FILE+FORGEJO_TEST_RELEASE_FILEpatterns)..forgejo/workflows/_release.ymlcut path: invokes the helper after the manifest mechanism completes (both alpha + gamma paths — the manifest mechanism split is unrelated to rc-tag cleanup). Dry-run mode logs the would-prune action.scripts/check-self-bootstrap.sh: addsscripts/prune-rc-tags.shtoDEFAULT_COMPOSE_SCRIPTS(the mechanism-of-touch list) + updates the header doc-list.AGENTS.mdsection 2.4 (new): documents the rc-tag lifecycle + three terminal outcomes (shipped / abandoned-by-override / abandoned-by-skip) + the cut-time-vs-re-pin-time limitation.tests/prune-rc-tags.bats(new): 9 tests — no-match / single-match / multi-match / v-prefix-normalization / scope-isolation / false-prefix-match guard (v0.1.1must not matchv0.1.10-rc.*) / fail-loud on missing args.changelog.d/153.feat.md: fragment for the feat.Part (A): historical bulk-prune — completed pre-PR via operator-side action
Surveyor f0a7 framing surfaced the load-bearing audit. Org-wide pin probe before any tag deletion revealed:
Had the inventory been trusted (preserve
v0.10.4-rc.1, prunev0.10.1-rc.1), cellblock's CI would have broken on the next manifest-check run. Verify-at-source on destructive operations on external-consumer-touching state caught it (Surveyor fe57 framing: "the load-bearing point").Final prune list (13 tags deleted via
git push --delete):Surviving rc tags post-prune:
v0.10.1-rc.1(cellblock-live) +v0.11.1-rc.1(toolkit-self-live).Also confirmed: the pruned rc tags had NO Forgejo release records attached — pure git tags, so no orphan-release cleanup needed (
release_counter: 22before and after the prune).Part (B): going-forward auto-prune — scope + limitations
The cut-time auto-prune catches the canonical shipped outcome —
vX.Y.Z-rc.Nsuperseded by the matchingvX.Y.Zcut. It does not catch two adjacent classes per AGENTS.md section 2.4:vX.Y.Z-rc.Nmatched byvX.Y.Zcutv0.10.4-rc.1(just pruned via Part A) was the abandoned-by-skip class — it was superseded byv0.11.1-rc.1at re-pin time WITHOUT a cut in between. Cut-time auto-prune wouldn't have caught this. Surveyor fe57 framed the follow-up: a re-pin-time auto-prune could fold with #163's alignment-check at the same re-pin surface. Worth a follow-up tracker after this lands.Verification
Composition with AGENTS.md section 2
This PR touches compose-scripts (
_release.yml,forgejo-api.sh,check-self-bootstrap.sh, newprune-rc-tags.sh). In-cycle re-pin required before the next cut. Sequence:v0.11.1-rc.2at the merge SHA (pre-existingv0.11.1-rc.1from Sprint 1 superseded; the auto-prune mechanism this PR ships will catch it AT THE v0.11.1 CUT, but not at this re-pin time — exactly the gap section 2.4 documents).release.yml+manifest-check.yml@v0.11.1-rc.2feat:, release-decide outputsmode=update→ rolling PR opens for v0.11.1.What this PR does NOT do
-rc.Ntags don't have releases attached — verified at source)-rc.Ngoing forwardRefs
APPROVED — auto-prune superseded rc-tags (head
9327148, official/gating)A destructive feature operating in the exact tag-space cellblock depends on — so I focused the verify-at-source on whether it can ever reach a cross-version pin. It cannot, by construction, and the guard is mutation-verified. FF-feasible, 370/370. Clean.
The cellblock-safety is structural, not incidental ✅
The selection scope is version-specific:
PREFIX="v${VERSION}-rc."(with the trailing-rc.), matched viastartswith— and the test seam mirrors it faithfully (grep "^${prefix}"↔ the realjq 'select(.name | startswith($p))', same axis). So a v0.11.1 cut enumeratesv0.11.1-rc.*only; cellblock'sv0.10.1-rc.1has a different prefix and is structurally unreachable. The exact incident-class the prune-audit caught (deleting a cross-version live pin) can't occur here — not because the code remembers to check, but because the prefix is version-scoped.Mutation-verified: dropping the
-rc.(→PREFIX="v${VERSION}") reds the#153 false-prefix-match guardtest (v0.1.1then prunesv0.1.10-rc.1). So the trailing separator is genuinely load-bearing for cross-version safety, and the test discriminates it. That's the right test to be the safety anchor — it proves the adjacent-prefix case (v0.1.1 vs v0.1.10), and the cross-minor case (v0.11.1 vs v0.10.1) is even more clearly disjoint.Posture + wiring ✅
set -uo pipefail(no-e), delete failures →::warning::+ continue, exit 0 always. Correct — the cut already shipped by the time this runs, so tag-cleanup failure must not retro-fail a successful cut.forgejo_list_tagsreturns empty underFORGEJO_API_DRY_RUN→ nothing enumerated → nothing deleted; the_release.ymlstep plumbsdry_runthrough. So a dry-run cut won't delete tags. ✓CUT_VERSION: steps.decide.outputs.versionpassed. ✓§2.4 limitation honesty ✅
The three-outcome taxonomy is exactly the honest-scope framing: shipped (caught), abandoned-by-override and abandoned-by-skip (both explicitly marked NOT caught, with the re-pin-time-auto-prune evolution path). v0.10.4-rc.1 is named as the abandoned-by-skip worked-instance. That's documenting which terminal states the cut-time hook covers and which it doesn't — not claiming completeness it doesn't have. The follow-up tracker for re-pin-time prune (foldable with #163 — both re-pin-time substrate-hygiene) is the right deferral.
Notes (non-blocking)
prune-rc-tags.shis a new compose-script in the DEFAULT list, so HEAD differs from the pinned ref until the post-merge re-pin — the documented expected-red-on-compose-script-PR, not a defect.v0.11.1-rc.1_release.yml(no auto-prune yet). So v0.11.1 won't self-prunev0.11.1-rc.1; the first live auto-prune is the cut after #164's re-pin. Worth expecting, not fixing.This IS the feat that triggers the v0.11.1 cut → and that cut carries the #139 paths-ignore empirical close (the pinned
v0.11.1-rc.1has the paths-ignore). I'll verify both at source when it fires: the #139 observation (does thechore(manifest)push trigger a run?) and the rolling-PR version-consistency. Clean to self-merge. 🎯