feat(rt-prune-rc-tags): port prune-rc-tags.sh to rt prune-rc-tags #565
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!565
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/559-rt-prune-rc-tags"
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?
rt prune-rc-tags — the Go port of
scripts/prune-rc-tags.sh(#559)Ports the post-cut cleanup that deletes the
vX.Y.Z-rc.Ntags a shipped cutsuperseded (AGENTS.md §2.4). The last orchestrator-adjacent script of the Phase-6 arc.
Framing (framing-verify → ratify) — NOT a Cutter/Fire path
The dispatch floated that #559 might "exercise Fire replay naturally." Framing-verify
against the oracle refuted that (Bosun-ratified the re-frame):
prune-rc-tags.shis abest-effort loop — enumerate → self-pin exclude → loop delete continuing past
failures →
exit 0ALWAYS. Best-effort / continue-on-fail / exit-0 is the semanticopposite of
Cutter.Fire's fail-loud FATAL-on-divergence; wiring it through Firewould either force Fire to grow a best-effort mode (interface strain) or make every
prune failure abort (wrong vs the oracle). And both primitives already exist
(
forgejo.ListTagsreads.go:83,forgejo.DeleteTagmutations.go:185). So this is astandalone
cmd/rtglue loop — zero new internal substrate, zero Cutter accretion(the refinement count stays 3/5).
Flow
ListTags(vX.Y.Z-rc.*)→ exclude the live pin if present (the #164 self-orphan guard:the pin is NOT superseded by the cut running at it; deleting it would orphan the
consumer wrapper's
uses:@<ref>; an exact whole-line match, like the bashgrep -vFx)→ loop
DeleteTag, a failure is a logged warning not an abort → summary. Exit isalways 0 — a cleanup pass must not fail the cut it runs after. A
ListTagserror islikewise swallowed (bash's
2>/dev/null || true).--owner/--repo/--version/--pinned-refmirror the bash positional args(
OWNER REPO CUT_VERSION [PINNED_REF]). Owner/repo are explicit like the bash tool— NOT derived from the git remote (that would diverge from
prune-rc-tags.sh, whichtakes them as args; unlike
rt release, which derives them becausedraft-release.shalso derives them).
Verification
cmd/rt/prune_rc_tags_equiv_test.go): runs the REALscripts/prune-rc-tags.shoffline (FORGEJO_TEST_TAGS_FILE+FORGEJO_API_DRY_RUN)and compares the set of tags it deletes against the Go orchestrator's (a fake forge
seeded with the identical tag list) — on a with-pin case (the #164 exclusion) and a
no-pin case. Both sides must agree; the expected set is non-empty (a vacuous-empty
case is rejected as proving nothing).
exclusion reds BOTH the unit test (
DELETED THE LIVE PIN … self-orphan guard failed)AND the differential (
DELETION-SET DIVERGENCE: go includes the pin, bash doesn't).The load-bearing guard is genuinely covered.
0), empty family, list-error-non-fatal, pin-empties-family, and the vX.Y.Z-rc. prefix.
forgejo-ci-go:latest(the differential's bash execruns in-container too).
Coverage note (naming the surface, not just "harness confirms")
prune-rc-tags.shlogs to stderr and exits 0 unconditionally, so aharness.Evaluatestdout/exit differential is vacuous — the behavior that matters iswhich tags get deleted. And the Go
ListTagsread has no offline seam matchingbash's
FORGEJO_TEST_TAGS_FILE, so the established dry-run-sink harness (which avoidslive forgejo reads) does not fit. The deletion-SET differential is the behavior-focused
equivalence; the
ListTags/DeleteTagprimitives themselves rideinternal/forgejo'sown equivalence (#541). The bash side is necessarily log-parsed (its dry-run decisions
surface only in the "deleted " log); the parse keys on the stable marker and takes
the tag name structurally.
What this PR does NOT do
reusable-release.ymlstill calls the bashprune-rc-tags.sh; switching the call site is a later integration step (as #556–#558left prep/release/manifest-check unwired).
bash tool.
prune-rc-tagshelp short unchanged — the skeleton short is reused, socmd/rt/testdata/help.txtneeds no regeneration.Merge-order note
Branched off
main@b215acf(pre-#558). The only overlap with PR#564 (#558) is thecmd/rt/main.goimplementedCmdsmap — each PR adds a distinct key(
manifest-checkvsprune-rc-tags), so a rebase onto #558-merged main is a trivialresolution. Independent otherwise.
Refs #559
Review — PR#565, rt prune-rc-tags (Go port of prune-rc-tags.sh, #559)
Independent deep-verify at head
6072fa3(basemain@b215acf). +423/-2, 4 files, zero new internal substrate. Ran the full gate, verified the port is line-faithful toprune-rc-tags.sh, and drove my own mutation on the #164 self-pin exclusion through both the unit test and the deletion-set differential.No must-fix, no should-consider. APPROVED below.
What I verified (reproduced, not read)
go build/go vet/golangci-lint(0 issues) /go test ./... -count=1green (go1.26.2). CI combined-success 8/8 on6072fa3.${CUT_VERSION#v}→v…-rc.), best-effort continue-on-fail, exit-0-always (return nilunconditional), summary line format,pinned=nonedefault — all matchprune-rc-tags.sh.grep -vFxt == o.pinnedRefis an exact whole-string compare, faithful to fixed-string (-F) whole-line (-x) inverted match. The "excluding live pin" log fires only when a member was actually excluded (bash's[[ "$FILTERED" != "$TAGS" ]]).harness.Evaluate. The differential runs the realprune-rc-tags.shoffline (FORGEJO_TEST_TAGS_FILE+FORGEJO_API_DRY_RUN), parses its deleted-set on the stabledeletedmarker, and compares against the Go orchestrator's set. Confirmed it genuinely spawns bash (both cases exercised).len(tc.want)==0 → t.Fatalf), and uses a three-way anchor —bashDeleted == wantANDgoDeleted == bashDeleted— so a both-wrong-but-agreeing pass can't slip through (the independentwantis the third leg). Both cases have non-empty expected sets.if false) reds bothTestPruneRCTags_deletesFamilyExcludesPin(DELETED THE LIVE PIN v1.2.0-rc.2) ANDTestPruneRCTagsEquivalence/…live_pin(DELETION-SET DIVERGENCE— go deletes the pin, bash doesn't). That the differential reds proves it detects a real Go↔bash divergence, not a vacuous match. Reverted byte-identical (cmpvs pristine6072fa3clean).continue-on-fail(a delete failure warns + increments failed, loop continues —TestPruneRCTags_continueOnFail), list-error swallowed (TestPruneRCTags_listErrorIsNonFatal), pin-only family → nothing attempted (TestPruneRCTags_pinEmptiesFamily). All exit 0.Benign note (not a concern)
Go logs a distinct message for a list-error ("could not list tags … nothing pruned") vs an empty list ("no rc tags to prune"), where bash collapses both into the single "no rc tags" branch. This is stderr-only (uncompared, and irrelevant to the deletion set) and arguably more informative — the deletion-set behavior is identical (both prune nothing). Noting for completeness, not as a divergence to fix.
Verdict
APPROVED, head-pinned at
6072fa3. The port is faithful, best-effort semantics preserved, and the deletion-set differential is the correct instrument for a vacuous-stdout/exit-0 command — well-guarded against a false green and mutation-confirmed to catch a real divergence. Yours to land (a trivialcmd/rt/main.goimplementedCmds rebase against #558 at merge, distinct keys). With #558's fix landing next, this closes the Phase-6 orchestrator/utility arc for milestone #78.— Surveyor