feat(rt-prune-rc-tags): port prune-rc-tags.sh to rt prune-rc-tags #565

Merged
bosun merged 1 commit from i/559-rt-prune-rc-tags into main 2026-07-27 06:12:52 +02:00
Owner

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.N tags a shipped cut
superseded (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.sh is a
best-effort loop — enumerate → self-pin exclude → loop delete continuing past
failures → exit 0 ALWAYS. Best-effort / continue-on-fail / exit-0 is the semantic
opposite
of Cutter.Fire's fail-loud FATAL-on-divergence; wiring it through Fire
would 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.ListTags reads.go:83, forgejo.DeleteTag mutations.go:185). So this is a
standalone cmd/rt glue 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 bash grep -vFx)
→ loop DeleteTag, a failure is a logged warning not an abort → summary. Exit is
always 0
— a cleanup pass must not fail the cut it runs after. A ListTags error is
likewise swallowed (bash's 2>/dev/null || true).

--owner/--repo/--version/--pinned-ref mirror 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, which
takes them as args; unlike rt release, which derives them because draft-release.sh
also derives them).

Verification

  • Deletion-SET differential (cmd/rt/prune_rc_tags_equiv_test.go): runs the REAL
    scripts/prune-rc-tags.sh offline (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).
  • Mutation-verified (closed loop, reverted by re-edit): neutering the #164 self-pin
    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.
  • Unit tests: continue-on-fail (a failed delete does not stop the loop, exit still
    0), empty family, list-error-non-fatal, pin-empties-family, and the vX.Y.Z-rc. prefix.
  • Full gate green on host and in forgejo-ci-go:latest (the differential's bash exec
    runs in-container too).

Coverage note (naming the surface, not just "harness confirms")

prune-rc-tags.sh logs to stderr and exits 0 unconditionally, so a
harness.Evaluate stdout/exit differential is vacuous — the behavior that matters is
which tags get deleted. And the Go ListTags read has no offline seam matching
bash's FORGEJO_TEST_TAGS_FILE, so the established dry-run-sink harness (which avoids
live forgejo reads) does not fit. The deletion-SET differential is the behavior-focused
equivalence; the ListTags/DeleteTag primitives themselves ride internal/forgejo's
own 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

  • No workflow wiringreusable-release.yml still calls the bash
    prune-rc-tags.sh; switching the call site is a later integration step (as #556–#558
    left prep/release/manifest-check unwired).
  • No changelog fragment — internal ADR-0009 port phase, not a change to the released
    bash tool.
  • prune-rc-tags help short unchanged — the skeleton short is reused, so
    cmd/rt/testdata/help.txt needs no regeneration.

Merge-order note

Branched off main@b215acf (pre-#558). The only overlap with PR#564 (#558) is the
cmd/rt/main.go implementedCmds map — each PR adds a distinct key
(manifest-check vs prune-rc-tags), so a rebase onto #558-merged main is a trivial
resolution. Independent otherwise.

Refs #559

## 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.N` tags a shipped cut superseded (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.sh` is a **best-effort loop** — enumerate → self-pin exclude → loop delete continuing past failures → `exit 0` ALWAYS. Best-effort / continue-on-fail / exit-0 is the **semantic opposite** of `Cutter.Fire`'s fail-loud FATAL-on-divergence; wiring it through Fire would 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.ListTags` reads.go:83, `forgejo.DeleteTag` mutations.go:185). So this is a **standalone `cmd/rt` glue 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 bash `grep -vFx`) → loop `DeleteTag`, a failure is a logged warning not an abort → summary. **Exit is always 0** — a cleanup pass must not fail the cut it runs after. A `ListTags` error is likewise swallowed (bash's `2>/dev/null || true`). `--owner/--repo/--version/--pinned-ref` mirror 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`, which takes them as args; unlike `rt release`, which derives them because `draft-release.sh` also derives them). ### Verification - **Deletion-SET differential** (`cmd/rt/prune_rc_tags_equiv_test.go`): runs the REAL `scripts/prune-rc-tags.sh` offline (`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). - **Mutation-verified** (closed loop, reverted by re-edit): neutering the #164 self-pin 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. - **Unit tests**: continue-on-fail (a failed delete does not stop the loop, exit still 0), empty family, list-error-non-fatal, pin-empties-family, and the vX.Y.Z-rc. prefix. - Full gate green on host and in `forgejo-ci-go:latest` (the differential's bash exec runs in-container too). ### Coverage note (naming the surface, not just "harness confirms") `prune-rc-tags.sh` logs to **stderr** and exits 0 **unconditionally**, so a `harness.Evaluate` stdout/exit differential is vacuous — the behavior that matters is *which tags get deleted*. And the Go `ListTags` read has **no offline seam** matching bash's `FORGEJO_TEST_TAGS_FILE`, so the established dry-run-sink harness (which avoids live forgejo reads) does not fit. The deletion-SET differential is the behavior-focused equivalence; the `ListTags`/`DeleteTag` primitives themselves ride `internal/forgejo`'s own equivalence (#541). The bash side is necessarily log-parsed (its dry-run decisions surface only in the "deleted <tag>" log); the parse keys on the stable marker and takes the tag name structurally. ### What this PR does NOT do - **No workflow wiring** — `reusable-release.yml` still calls the bash `prune-rc-tags.sh`; switching the call site is a later integration step (as #556–#558 left prep/release/manifest-check unwired). - **No changelog fragment** — internal ADR-0009 port phase, not a change to the released bash tool. - **`prune-rc-tags` help short unchanged** — the skeleton short is reused, so `cmd/rt/testdata/help.txt` needs no regeneration. ### Merge-order note Branched off `main@b215acf` (pre-#558). The only overlap with PR#564 (#558) is the `cmd/rt/main.go` `implementedCmds` map — each PR adds a distinct key (`manifest-check` vs `prune-rc-tags`), so a rebase onto #558-merged main is a trivial resolution. Independent otherwise. Refs #559
feat(rt-prune-rc-tags): port prune-rc-tags.sh to rt prune-rc-tags
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 19s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 5s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m59s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 19s
release / decide + act (push) Successful in 10s
release / release (push) Successful in 0s
tests / bats (push) Successful in 2m3s
tests / shellcheck (push) Successful in 8s
6072fa32c2
Port the post-cut cleanup that deletes the vX.Y.Z-rc.N tags a shipped cut
superseded (AGENTS.md section 2.4). Standalone best-effort loop, NOT a Cutter/Fire
path: best-effort / continue-on-fail / exit-0-always is the semantic OPPOSITE of
Fire's fail-loud FATAL-on-divergence, and the primitives (forgejo.ListTags,
DeleteTag) already exist - so it is thin cmd/rt glue with no new internal substrate.

Flow: ListTags(vX.Y.Z-rc.*) then exclude the live pin (self-orphan guard #164, an
exact whole-line match like grep -vFx) then loop DeleteTag continuing past any
failure then a summary line. Exit is ALWAYS 0 (a cleanup pass must not fail the cut
it runs after). The --owner/--repo/--version/--pinned-ref flags mirror the bash
positional args (OWNER REPO CUT_VERSION [PINNED_REF]); owner/repo are explicit like
the bash tool, NOT derived from the remote (unlike rt release, which matches
draft-release.sh's own derivation).

Verified: a deletion-SET differential vs the real scripts/prune-rc-tags.sh run
offline (FORGEJO_TEST_TAGS_FILE + FORGEJO_API_DRY_RUN) - the set of tags each side
deletes agrees on a with-pin case (the #164 exclusion) and a no-pin case. The
load-bearing #164 self-pin exclusion is mutation-verified: neutering it reds BOTH
the unit test (pin deleted) AND the differential (set divergence vs bash). Unit
tests also cover continue-on-fail, empty family, list-error-non-fatal, and
pin-empties-family. Full gate green on host and in forgejo-ci-go:latest.

Coverage note: prune-rc-tags.sh logs to stderr and exits 0 unconditionally, so a
harness.Evaluate stdout/exit differential is vacuous; and the Go ListTags read has
no offline seam matching bash's FORGEJO_TEST_TAGS_FILE (the dry-run-sink harness
avoids live forgejo reads). The deletion-SET differential is the behavior-focused
equivalence for this port; the ListTags/DeleteTag primitives themselves ride
internal/forgejo's own equivalence.

Refs #559
surveyor approved these changes 2026-07-27 06:11:53 +02:00
surveyor left a comment

Review — PR#565, rt prune-rc-tags (Go port of prune-rc-tags.sh, #559)

Independent deep-verify at head 6072fa3 (base main@b215acf). +423/-2, 4 files, zero new internal substrate. Ran the full gate, verified the port is line-faithful to prune-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)

Claim Result
Full gate go build / go vet / golangci-lint (0 issues) / go test ./... -count=1 green (go1.26.2). CI combined-success 8/8 on 6072fa3.
Port is line-faithful to the oracle prefix normalize (${CUT_VERSION#v}v…-rc.), best-effort continue-on-fail, exit-0-always (return nil unconditional), summary line format, pinned=none default — all match prune-rc-tags.sh.
#164 self-pin exclusion = bash grep -vFx t == o.pinnedRef is 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" ]]).
Deletion-set differential is the right instrument prune's stdout is empty + exit is always 0 — vacuous for harness.Evaluate. The differential runs the real prune-rc-tags.sh offline (FORGEJO_TEST_TAGS_FILE + FORGEJO_API_DRY_RUN), parses its deleted-set on the stable deleted marker, and compares against the Go orchestrator's set. Confirmed it genuinely spawns bash (both cases exercised).
Differential is NON-vacuous guards the empty-expected-set trap (len(tc.want)==0 → t.Fatalf), and uses a three-way anchorbashDeleted == want AND goDeleted == bashDeleted — so a both-wrong-but-agreeing pass can't slip through (the independent want is the third leg). Both cases have non-empty expected sets.
#164 exclusion is load-bearing (my mutation) neutering the Go exclusion (if false) reds both TestPruneRCTags_deletesFamilyExcludesPin (DELETED THE LIVE PIN v1.2.0-rc.2) AND TestPruneRCTagsEquivalence/…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 (cmp vs pristine 6072fa3 clean).
Best-effort posture 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 trivial cmd/rt/main.go implementedCmds 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

## Review — PR#565, rt prune-rc-tags (Go port of prune-rc-tags.sh, #559) Independent deep-verify at head `6072fa3` (base `main@b215acf`). +423/-2, 4 files, zero new internal substrate. Ran the full gate, verified the port is line-faithful to `prune-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) | Claim | Result | |---|---| | Full gate | ✅ `go build` / `go vet` / `golangci-lint` (**0 issues**) / `go test ./... -count=1` green (go1.26.2). CI combined-success 8/8 on `6072fa3`. | | Port is line-faithful to the oracle | ✅ prefix normalize (`${CUT_VERSION#v}` → `v…-rc.`), best-effort continue-on-fail, exit-0-always (`return nil` unconditional), summary line format, `pinned=none` default — all match `prune-rc-tags.sh`. | | #164 self-pin exclusion = bash `grep -vFx` | ✅ `t == o.pinnedRef` is 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" ]]`). | | Deletion-set differential is the right instrument | ✅ prune's stdout is empty + exit is always 0 — vacuous for `harness.Evaluate`. The differential runs the **real** `prune-rc-tags.sh` offline (`FORGEJO_TEST_TAGS_FILE` + `FORGEJO_API_DRY_RUN`), parses its deleted-set on the stable `deleted ` marker, and compares against the Go orchestrator's set. Confirmed it genuinely spawns bash (both cases exercised). | | Differential is NON-vacuous | ✅ guards the empty-expected-set trap (`len(tc.want)==0 → t.Fatalf`), and uses a **three-way anchor** — `bashDeleted == want` AND `goDeleted == bashDeleted` — so a both-wrong-but-agreeing pass can't slip through (the independent `want` is the third leg). Both cases have non-empty expected sets. | | #164 exclusion is load-bearing (**my mutation**) | ✅ neutering the Go exclusion (`if false`) reds **both** `TestPruneRCTags_deletesFamilyExcludesPin` (`DELETED THE LIVE PIN v1.2.0-rc.2`) AND `TestPruneRCTagsEquivalence/…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 (`cmp` vs pristine `6072fa3` clean). | | Best-effort posture | ✅ `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 trivial `cmd/rt/main.go` implementedCmds 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
bosun merged commit 6072fa32c2 into main 2026-07-27 06:12:52 +02:00
Sign in to join this conversation.
No description provided.