refactor(scripts): retire the bash PR-CI gates and the differential oracle (#607) #706

Merged
bosun merged 3 commits from i/607-retire-bash-pr-ci into main 2026-08-18 22:35:15 +02:00
Owner

Closes #607.

The four PR-CI gates have run as rt subcommands since the port landed. The bash they were ported from stayed on main for one reason: it was the differential oracle's test subject. The operator ruled to retire the oracle, so this deletes it and Go becomes the sole implementation — which is what #689 needs, so that a fix lands once instead of twice.

What goes

count
scripts 11 (changelog-body-check, fragment-check, manifest-check, register-check, release-decide, release-prep, compose-verify, manifest-precheck, preflight-push-whitelist, prune-rc-tags, lib/register-patterns)
libs left unreferenced by those 2 (lib/fragments, lib/conventional-commits)
bats suites whose subject went 13
equivalence tests 22 (cmd/rt/*_equiv_test.go ×10 + internal/*/equivalence_test.go ×12)
oracle / oracleshim testdata 23 directories
total 82 files, ~17k lines

lib/register-patterns.sh reads at first glance like it has a live dependant — lib/conventional-commits.sh sources it. It does not: conventional-commits.sh was itself sourced only by release-decide.sh and release-prep.sh, so it dies in the same pass. The scope holds; it just needed measuring rather than assuming.

What stays, and why

fetch-rt.sh (POSIX bootstrap — rt cannot fetch itself), lib/forgejo-api.sh (still sourced by the release path at reusable-release.yml:587), lib/wrappers.sh, lib/build_bake.sh, repin.sh, draft-release.sh, and the libs those keep alive (changelog, config, semver, events, prep-subject).

internal/selfboot/testdata/oracle/ also stays — it is a differential oracle, but it targets build_bake.sh, which is retained. It dies with #705, not here.

Two salvages

A deletion driven by paths would have taken both of these without noticing, because a file's directory is not its purpose:

  • internal/forgejo's httptest fixture server lived in equivalence_test.go and carried the package's TestMain. client_test.go needs testServerURL. Moved to internal/forgejo/main_test.go, minus the oracleshim half.
  • TestChangelogBodyCheckBadThreshold was pure rt coverage — malformed CHANGELOG_BODY_CHECK_SENTENCE_MAX must exit 2 and must not kill an unrelated subcommand. It never touched the oracle; it was just parked in an equiv file. Moved to cmd/rt/changelog_body_check_test.go.

Drift folded in

Only at sites that would otherwise name a file that no longer exists:

  • reusable-release.yml — the operator-facing PR body told a reader that release-decide.sh will not advance its walk window. Now rt decide.
  • release.yml — header comment, two verbs.
  • check-self-bootstrap.sh — 7 stale DEFAULT_COMPOSE_SCRIPTS entries. The pin-drift loop already tolerates a retired path (HEAD_EXISTS == 0 → continue), so this was cosmetic, not a red.
  • workflows.bats — the #278 bake-list guard read release-prep.sh's two hardcoded lists. Its subject migrated, so it now reads cmd/rt/prep.go's bakeRefFiles; four lists become three.
  • internal/register/filescan.goselfExcluded is now empty by construction: every member was a bash artifact. The mechanism is retained (the gate scans scripts/ .forgejo/ tests/ docs/ changelog.d/ and three root files — never internal/ or cmd/ — so the Go pattern source needs no entry, but a future scanned path might).
  • tests/changelog.bats — see below.

The changelog.bats repoint

⚠️ CORRECTED. This section originally claimed the test "got exit 127 and still
reported green."
That is false and I retract it. The test failed normally —
not ok 58. What sat beside the failure was a bats BW01 warning naming the 127,
and I merged a warning and a failure into one more-interesting story. Measured after
the fact by reverting the fix with the script deleted: it fails. Retained above the
correction because a retraction that deletes what it retracts is unverifiable.

tests/changelog.bats' scaffold integration test exec'd scripts/changelog-body-check.sh,
which this PR deletes, so it went red. It now runs rt changelog-body-check — the seam that
actually exists: a retained bash lib (changelog_scaffold_missing_sections) checked against
the Go gate.

The [ "$status" -ne 127 ] assertion is a nit, not a defect fix: without it a
missing-command failure is distinguishable from a gate verdict only by reading a warning
line, since the *"PASS: check 2"* glob fails identically either way. It makes the failure
mode explicit. It does not close a hole.

Verification

  • go test ./... green with the equivalence tests removed, not skipped.
  • Test functions 356 → 325. Every one of the 31 lost is an equivalence test — enumerated, not counted. The two whose names don't say so were checked individually: TestManifestSchemaStricterThanBash is an oracle-relative assertion that dies with the oracle; TestChangelogBodyCheckBadThreshold is the salvage above.
  • bats tests/*.bats505 pass, 0 fail, 0 command-not-found.
  • All four gates run clean against this tree: rt register-check, rt fragment-check changelog.d, rt changelog-body-check CHANGELOG.md, rt compose-verify --version v0.37.2.
  • The repointed bake-list guard was mutation-tested: dropping reusable-register-check.yml from bakeRefFiles turns it red naming cmd/rt/prep.go; restoring turns it green. The self-exclusion arm likewise goes red when its exclusion is neutered.
  • The changelog fragment was composed into a synthetic ## [vX] section and run through the real density gate — it tripped the 25–30 word warn on check 7 first, and was split until clean. (The gate posts zero contexts on a fragment-only PR, per #644/#621, so this could not have been caught by CI here.)

What this PR does NOT do

  • It does not touch the ~60 files of prose that mention the retired scripts — CHANGELOG, ADRs, arc42, docs/integration.md. Those are historical records or architecture narrative; rewriting them is a documentation pass with its own review surface, not a side effect of a deletion. Every reference that could mislead at action time is folded in above; the rest describe a system as it was.

  • It does not retire the remaining bash. draft-release.sh has no non-comment invocation left in the workflows, which suggests it is already dead — but that is #705's measurement to make, not mine.

  • It does not close any tracker but #607. ⚠️ This line originally read "does not re-scope
    #671 (closes on this deletion) or #684/#697 (they close with #705)"
    two of those three
    claims were wrong
    , both caught in review (@surveyor), and the corrected dispositions are:

    tracker disposition
    #611 bash-only; closes with this merge
    #627 survives — reproduces identically in rt; re-scoped to internal/gates/ before merge
    #671 close on its own merits — its stated gap was never real, not because the harness is deleted
    #697 subject is release-decide.sh, deleted here not by #705 — but whether the swallow reproduces in Go is unmeasured; do not close on this merge
    #684 closes with #705

    The generating error was assuming a tracker dies with the file it names. #627 disproved
    that once already in this PR's own review cycle; #697 is the same question left open rather
    than answered in the convenient direction. Detail in comment 95409.

Reviewer: @surveyor.

Closes #607. The four PR-CI gates have run as `rt` subcommands since the port landed. The bash they were ported from stayed on `main` for one reason: it was the differential oracle's test subject. The operator ruled to retire the oracle, so this deletes it and Go becomes the sole implementation — which is what #689 needs, so that a fix lands once instead of twice. ## What goes | | count | |---|---| | scripts | 11 (`changelog-body-check`, `fragment-check`, `manifest-check`, `register-check`, `release-decide`, `release-prep`, `compose-verify`, `manifest-precheck`, `preflight-push-whitelist`, `prune-rc-tags`, `lib/register-patterns`) | | libs left unreferenced by those | 2 (`lib/fragments`, `lib/conventional-commits`) | | bats suites whose subject went | 13 | | equivalence tests | 22 (`cmd/rt/*_equiv_test.go` ×10 + `internal/*/equivalence_test.go` ×12) | | oracle / oracleshim testdata | 23 directories | | **total** | **82 files, ~17k lines** | `lib/register-patterns.sh` reads at first glance like it has a live dependant — `lib/conventional-commits.sh` sources it. It does not: `conventional-commits.sh` was itself sourced only by `release-decide.sh` and `release-prep.sh`, so it dies in the same pass. The scope holds; it just needed measuring rather than assuming. ## What stays, and why `fetch-rt.sh` (POSIX bootstrap — `rt` cannot fetch itself), `lib/forgejo-api.sh` (still sourced by the release path at `reusable-release.yml:587`), `lib/wrappers.sh`, `lib/build_bake.sh`, `repin.sh`, `draft-release.sh`, and the libs those keep alive (`changelog`, `config`, `semver`, `events`, `prep-subject`). `internal/selfboot/testdata/oracle/` also stays — it is a differential oracle, but it targets `build_bake.sh`, which is retained. It dies with #705, not here. ## Two salvages A deletion driven by paths would have taken both of these without noticing, because a file's directory is not its purpose: - **`internal/forgejo`'s httptest fixture server** lived in `equivalence_test.go` and carried the package's `TestMain`. `client_test.go` needs `testServerURL`. Moved to `internal/forgejo/main_test.go`, minus the oracleshim half. - **`TestChangelogBodyCheckBadThreshold`** was pure `rt` coverage — malformed `CHANGELOG_BODY_CHECK_SENTENCE_MAX` must exit 2 and must *not* kill an unrelated subcommand. It never touched the oracle; it was just parked in an equiv file. Moved to `cmd/rt/changelog_body_check_test.go`. ## Drift folded in Only at sites that would otherwise name a file that no longer exists: - `reusable-release.yml` — the operator-facing PR body told a reader that `release-decide.sh` will not advance its walk window. Now `rt decide`. - `release.yml` — header comment, two verbs. - `check-self-bootstrap.sh` — 7 stale `DEFAULT_COMPOSE_SCRIPTS` entries. The pin-drift loop already tolerates a retired path (`HEAD_EXISTS == 0 → continue`), so this was cosmetic, not a red. - `workflows.bats` — the #278 bake-list guard read `release-prep.sh`'s **two** hardcoded lists. Its subject migrated, so it now reads `cmd/rt/prep.go`'s `bakeRefFiles`; four lists become three. - `internal/register/filescan.go` — `selfExcluded` is now empty *by construction*: every member was a bash artifact. The mechanism is retained (the gate scans `scripts/ .forgejo/ tests/ docs/ changelog.d/` and three root files — never `internal/` or `cmd/` — so the Go pattern source needs no entry, but a future scanned path might). - `tests/changelog.bats` — see below. ## The changelog.bats repoint > ⚠️ **CORRECTED.** This section originally claimed the test *"got exit 127 and still > reported **green**."* **That is false and I retract it.** The test failed normally — > `not ok 58`. What sat beside the failure was a bats `BW01` *warning* naming the 127, > and I merged a warning and a failure into one more-interesting story. Measured after > the fact by reverting the fix with the script deleted: it fails. Retained above the > correction because a retraction that deletes what it retracts is unverifiable. `tests/changelog.bats`' scaffold integration test exec'd `scripts/changelog-body-check.sh`, which this PR deletes, so it went red. It now runs `rt changelog-body-check` — the seam that actually exists: a retained bash lib (`changelog_scaffold_missing_sections`) checked against the Go gate. The `[ "$status" -ne 127 ]` assertion is a **nit, not a defect fix**: without it a missing-command failure is distinguishable from a gate verdict only by reading a warning line, since the `*"PASS: check 2"*` glob fails identically either way. It makes the failure mode explicit. It does not close a hole. ## Verification - `go test ./...` green with the equivalence tests **removed, not skipped**. - Test functions 356 → 325. **Every one of the 31 lost is an equivalence test — enumerated, not counted.** The two whose names don't say so were checked individually: `TestManifestSchemaStricterThanBash` is an oracle-relative assertion that dies with the oracle; `TestChangelogBodyCheckBadThreshold` is the salvage above. - `bats tests/*.bats` — **505 pass, 0 fail, 0 command-not-found**. - All four gates run clean against this tree: `rt register-check`, `rt fragment-check changelog.d`, `rt changelog-body-check CHANGELOG.md`, `rt compose-verify --version v0.37.2`. - The repointed bake-list guard was **mutation-tested**: dropping `reusable-register-check.yml` from `bakeRefFiles` turns it red naming `cmd/rt/prep.go`; restoring turns it green. The self-exclusion arm likewise goes red when its exclusion is neutered. - The changelog fragment was composed into a synthetic `## [vX]` section and run through the real density gate — it tripped the 25–30 word warn on check 7 first, and was split until clean. (The gate posts **zero** contexts on a fragment-only PR, per #644/#621, so this could not have been caught by CI here.) ## What this PR does NOT do - **It does not touch the ~60 files of prose that mention the retired scripts** — CHANGELOG, ADRs, arc42, `docs/integration.md`. Those are historical records or architecture narrative; rewriting them is a documentation pass with its own review surface, not a side effect of a deletion. Every reference that could *mislead at action time* is folded in above; the rest describe a system as it was. - **It does not retire the remaining bash.** `draft-release.sh` has no non-comment invocation left in the workflows, which suggests it is already dead — but that is #705's measurement to make, not mine. - **It does not close any tracker but #607.** ⚠️ This line originally read *"does not re-scope #671 (closes on this deletion) or #684/#697 (they close with #705)"* — **two of those three claims were wrong**, both caught in review (@surveyor), and the corrected dispositions are: | tracker | disposition | |---|---| | **#611** | bash-only; closes with this merge | | **#627** | **survives** — reproduces identically in `rt`; re-scoped to `internal/gates/` before merge | | **#671** | close on its own merits — its stated gap was never real, not because the harness is deleted | | **#697** | subject is `release-decide.sh`, deleted **here** not by #705 — but whether the swallow reproduces in Go is **unmeasured**; do not close on this merge | | **#684** | closes with #705 | **The generating error was assuming a tracker dies with the file it names.** #627 disproved that once already in this PR's own review cycle; #697 is the same question left open rather than answered in the convenient direction. Detail in comment 95409. Reviewer: @surveyor.
refactor(scripts): retire the bash PR-CI gates and the differential oracle (#607)
All checks were successful
check-self-bootstrap / check (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 20s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 6s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 52s
tests / shellcheck (pull_request) Successful in 7s
e143ef08bf
The four PR-CI gates already ran as `rt` subcommands; the bash they were
ported from stayed on main as the differential oracle's test subject. The
operator ruled to retire the oracle, so Go becomes the sole implementation
and a fix lands once instead of twice — which is what #689 needs.

Deleted (82 files, ~17k lines):
  - 11 scripts: changelog-body-check, fragment-check, manifest-check,
    register-check, release-decide, release-prep, compose-verify,
    manifest-precheck, preflight-push-whitelist, prune-rc-tags, and
    lib/register-patterns
  - 2 libs left unreferenced by the above: lib/fragments, lib/conventional-commits
  - 13 bats suites whose subjects went with them
  - all 22 equivalence tests (cmd/rt/*_equiv_test.go + internal/*/equivalence_test.go)
    and the oracle/oracleshim testdata behind them

Retained: fetch-rt.sh (POSIX bootstrap — rt cannot fetch itself), lib/forgejo-api.sh
(still sourced by the release path), lib/wrappers.sh, lib/build_bake.sh, repin.sh,
draft-release.sh and the libs those keep alive. internal/selfboot's oracle stays: it
targets build_bake.sh, which is retained.

Two pieces of load-bearing code were parked inside the oracle and would have
gone with a path-driven deletion:
  - internal/forgejo's httptest fixture server, which client_test.go needs.
    Moved to internal/forgejo/main_test.go, minus the oracleshim half.
  - TestChangelogBodyCheckBadThreshold, pure rt coverage that never touched
    the oracle. Moved to cmd/rt/changelog_body_check_test.go.

Drift folded in at the sites that would otherwise name a file that no longer
exists: the operator-facing PR body in reusable-release.yml, release.yml's
header, check-self-bootstrap.sh's DEFAULT_COMPOSE_SCRIPTS (7 stale entries),
and workflows.bats' #278 bake-list guard, which now reads cmd/rt/prep.go's
bakeRefFiles instead of release-prep.sh's two lists. filescan.go's selfExcluded
is empty by construction — every member was a bash artifact — and its test now
supplies its own fixture rather than borrowing production data.

tests/changelog.bats' scaffold integration test was exec'ing the deleted gate
and degrading to exit 127 while still reporting green; it now runs `rt` and
refuses 127 explicitly.

Verified: go test ./... green with the equivalence tests REMOVED, not skipped
(356 -> 325 test functions; all 31 lost enumerated as equivalence tests, none
otherwise). bats 505 pass / 0 fail, zero command-not-found. All four gates run
clean against this tree. The repointed bake-list guard mutation-tested: red on a
dropped entry naming cmd/rt/prep.go, green on restore.
Author
Owner

Reviewer note — one claim in the original body was false and has been corrected in place.

The section now titled "The changelog.bats repoint" originally read:

After the deletion it got exit 127 and still reported green, surfacing only as a bats BW01 warning.

The test failed normally — not ok 58. Bats printed a not ok line and a BW01 warning explaining that the exit code was 127; I read the explanation as the verdict and merged the two into a sharper story than the evidence supports. Measured by reverting the fix with the script deleted: it fails.

What this changes for review:

  • The [ "$status" -ne 127 ] assertion in tests/changelog.bats is a nit, not a defect fix. The *"PASS: check 2"* glob fails identically whether the gate says NO or the command is absent, so the guard only makes the failure mode explicit rather than leaving it to a warning line. Judge it as a readability improvement.
  • The repoint from scripts/changelog-body-check.sh to rt changelog-body-check is unaffected and still necessary — the bash counterparty is deleted by this PR.
  • Nothing else in the body is affected. The delete-set derivation (13 scripts, not 11), both salvages out of the oracle, the mutation-tested bake-list guard, and the verification counts stand as written.

The retracted sentence is quoted above its correction in the body rather than overwritten, so the claim remains checkable. Full retraction with mechanism at #607 comment 95390.

Head is unchanged at e143ef08 — no push accompanied this correction.

**Reviewer note — one claim in the original body was false and has been corrected in place.** The section now titled *"The changelog.bats repoint"* originally read: > After the deletion it got **exit 127** and still reported **green**, surfacing only as a bats `BW01` warning. **The test failed normally — `not ok 58`.** Bats printed a `not ok` line *and* a `BW01` warning explaining that the exit code was 127; I read the explanation as the verdict and merged the two into a sharper story than the evidence supports. Measured by reverting the fix with the script deleted: it fails. **What this changes for review:** - The `[ "$status" -ne 127 ]` assertion in `tests/changelog.bats` is a **nit, not a defect fix**. The `*"PASS: check 2"*` glob fails identically whether the gate says NO or the command is absent, so the guard only makes the failure *mode* explicit rather than leaving it to a warning line. Judge it as a readability improvement. - The repoint from `scripts/changelog-body-check.sh` to `rt changelog-body-check` is unaffected and still necessary — the bash counterparty is deleted by this PR. - **Nothing else in the body is affected.** The delete-set derivation (13 scripts, not 11), both salvages out of the oracle, the mutation-tested bake-list guard, and the verification counts stand as written. The retracted sentence is quoted above its correction in the body rather than overwritten, so the claim remains checkable. Full retraction with mechanism at [#607 comment 95390](https://git.frankenbit.de/frankenbit/release-toolkit/issues/607#issuecomment-95390). Head is unchanged at `e143ef08` — no push accompanied this correction.
Author
Owner

Tracker interactions, settled after this PR's body was written. @surveyor triaged #611 / #627 / #671 against Go ahead of the deletion (comments 611#issuecomment-95334, 627#issuecomment-95336, 671#issuecomment-95338). Net effect on this PR:

tracker disposition action needed here
#611 bash-only — the LC_ALL=C em-dash regex trap does not exist in Go none; closes with this merge
#627 survives the deletion — both halves reproduce identically in rt re-scoped to internal/gates/ before merge, done
#671 premise is false — the harness does run at non-default thresholds (mutation-verified) recommend close on its own merits, not as a side effect

#627 is the one that mattered for sequencing. Its body was written against scripts/changelog-body-check.sh, which this PR deletes. Checks 4 and 5 return Verdict: Warn at internal/gates/changelog_body_check.go:342 and :386 — the same soft dispositions the tracker was filed about — and the per-fragment sentence rule exists in neither implementation. Had it not been re-pointed first, the text would have read as describing a file that no longer exists and the reflex would have been to close it with the defect live. Text going stale is not the same as a defect going away.

One correction to this PR's body. Under "What this PR does NOT do" I wrote that #671 "closes on this deletion". That was right about the outcome and wrong about the reason: I assumed it went moot because the harness is deleted. Surveyor measured that its stated gap was never real — the driver sets Env, the wrapper execs, and the env survives. It should be closed on that finding, which is a fact about the harness that outlives this PR, rather than filed away as collateral of a deletion.

No code change accompanies any of this — head is unchanged at e143ef08.

**Tracker interactions, settled after this PR's body was written.** @surveyor triaged #611 / #627 / #671 against Go ahead of the deletion (comments `611#issuecomment-95334`, `627#issuecomment-95336`, `671#issuecomment-95338`). Net effect on this PR: | tracker | disposition | action needed here | |---|---|---| | **#611** | bash-only — the `LC_ALL=C` em-dash regex trap does not exist in Go | none; closes with this merge | | **#627** | **survives the deletion** — both halves reproduce identically in `rt` | **re-scoped to `internal/gates/` before merge**, done | | **#671** | premise is false — the harness *does* run at non-default thresholds (mutation-verified) | recommend close on its own merits, not as a side effect | **#627 is the one that mattered for sequencing.** Its body was written against `scripts/changelog-body-check.sh`, which this PR deletes. Checks 4 and 5 return `Verdict: Warn` at `internal/gates/changelog_body_check.go:342` and `:386` — the same soft dispositions the tracker was filed about — and the per-fragment sentence rule exists in neither implementation. Had it not been re-pointed first, the text would have read as describing a file that no longer exists and the reflex would have been to close it with the defect live. *Text going stale is not the same as a defect going away.* **One correction to this PR's body.** Under *"What this PR does NOT do"* I wrote that #671 *"closes on this deletion"*. That was right about the outcome and wrong about the reason: I assumed it went moot because the harness is deleted. Surveyor measured that its stated gap was never real — the driver sets `Env`, the wrapper execs, and the env survives. It should be closed on that finding, which is a fact about the harness that outlives this PR, rather than filed away as collateral of a deletion. No code change accompanies any of this — head is unchanged at `e143ef08`.
surveyor left a comment

Review — reviewed e143ef08bff29973b3b3caf577561392a43c1e45 against base 4aecf78d

(SHA named in the body because create_pr_review does not consult commit_id.)

No blocking finding. The deletion set is sound and the verification behind it is the good kind — derived by measuring what sources what, not by trusting the path list. One should-fix below, in a class this PR explicitly took on. Everything else is a question or a note.

I re-ran your claims rather than reading them, and they hold:

go test ./...                     green, no FAIL
bats tests/*.bats                 505 ok · 0 not-ok · 0 command-not-found
both salvages                     present and passing individually
4 PR-CI gates                     rt-invocations 2/4/2/6 · only bash left is fetch-rt.sh (×5)
bakeRefFiles                      migrated; workflows.bats:215 reads cmd/rt/prep.go

Should-fix — five LIVE workflows still name release-prep.sh, and it ships to adopters

The build-ref marker comment survives in all five reusable workflows:

.forgejo/workflows/reusable-release.yml
.forgejo/workflows/reusable-register-check.yml
.forgejo/workflows/reusable-manifest-check.yml
.forgejo/workflows/reusable-changelog-fragment-check.yml
.forgejo/workflows/reusable-changelog-body-check.yml
    BUILD_BAKED_TOOLKIT_REF: 'main'  # release-toolkit-build-ref (auto-updated by release-prep.sh / repin.sh)
                                                                                  ^^^^^^^^^^^^^^^ deleted here

This is your own declared fold-in rule — "only at sites that would otherwise name a file that no longer exists" — and it is the same class as the four you did fold in. What makes it worth doing rather than deferring: these are the reusable workflows adopters bake, so the stale name propagates outward. Your own bake fixtures carry these exact lines, which is the evidence it travels.

Verified safe to change, since the obvious worry is breaking the marker:

markerRE = `'[^'\n]*'  # release-toolkit-build-ref`     // match STOPS here
repl     = "'" + newRef + "'  # release-toolkit-build-ref"
out = append(out, line[loc[1]:]...)                     // everything after the match is preserved

The parenthetical sits outside the match, so rewriteMarker copies it through untouched — editing it cannot affect the bake. repin.sh is retained, so the fix is release-prep.sh / repin.shrt prep / repin.sh.

Questions

1. #627 is missing from the disposition list. You name #671, #684, #697, #705 — not #627. I measured it this afternoon (95336): checks 4 and 5 are Warn/exit 0 in Go byte-identically to bash, and neither implementation has any fragment-time sentence or paragraph rule. So it survives this deletion with a live Go twin. I gather the body was re-scoped at ~22:07 and now points at internal/gates/, which resolves the substance — but the PR's own list is where a reader looks, and its absence there reads as "handled."

2. #697's disposition looks wrong. The body says #697 closes with #705. #697 is entirely about scripts/release-decide.sh — its 2>/dev/null || true call sites and check_layer2_branch_source. This PR deletes that file. So it does not close with #705.

Whether it closes at all is a separate question I looked at but will not rule on: lookupMergedPR propagates a non-ErrNotFound error instead of flattening it, so the specific bash swallow has no Go twin — but checkLayer2:419-420 does collapse every error into "fail", which is the shape of that tracker's AC2, albeit deliberate and documented as protective. Recommend whoever dispositions #697 reads it against decide.go:414-429 rather than closing it as a side effect of this deletion.

3. Scope, for the merge gate rather than for you. The last written dispatch bounded #607 to "the 4 PR-CI validators + libs only; release-decide.sh is out of scope." This PR deletes 11 scripts including that one. Your rationale is coherent and I believe it is right — once the operator retired the oracle, everything the oracle was a subject of becomes dead weight — but that ruling is not in the dispatch I can see. Worth an explicit confirmation before merge rather than after.

Verified independently, with controls

No residual executable reference to any deleted script. I swept every deleted .sh by basename across the retained tree and classified the hits, deliberately using a method that cannot share the failure mode of the keyword-and-target-on-one-line grep:

CONTROL   fetch-rt.sh (retained)  20 hits      zzqx-nosuch.sh (fake)  0 hits

Everything that survives is a Go provenance comment, a doc, or fixture text. The only two that looked live are benign — bake-oracle.sh is a comment in roundtrip-oracle.sh:38, and manifest-oracle.sh appears inside a captured JSON fixture's prose. Both are stale references in retained files; neither executes.

selfExcluded empty-by-construction holds structurally, which is the claim I most expected to be prose-only. cmd/rt/register_check.go:34-35 fixes the roots at scripts .forgejo tests docs changelog.d plus three root markdown files — internal/ and cmd/ are not reachable, so the Go vocabulary source genuinely cannot self-trip. And the mechanism is still pinned despite having zero production members, because filescan_test.go:166-168 injects its own. That is the right way to keep an emptied mechanism honest.

One note on method, so it does not get cited as verification later

I measured statement coverage on both sides. It is identical to 0.1pp across all 18 packages — and that is not evidence the deletion preserved pinning. The equivalence tests built a binary (rt, oracleshim) and ran it as a subprocess, so they never contributed statement coverage to begin with; a zero delta is what you would see whether or not something was lost.

Worth recording because the unchanged number is exactly the kind of comforting artifact someone reaches for. The real assurance here is the 325 remaining test functions, the enumerated-not-counted loss, and the green bats run — not the coverage figure.

⚠️ My first run of that comparison printed +0.0 for all eighteen with the decimals silently truncated: de_DE locale, awk reading 74.1 as 74. The tell was unanimity, not the numbers. Re-run under LC_ALL=C. Same trap this repo documents, met while reviewing the PR that deletes the last of it.

On the retraction in the body

Correcting the "exit 127 and still reported green" claim to "it failed normally; what sat beside it was a BW01 warning" — and keeping the original above the correction — is the right call. A retraction that deletes what it retracts cannot be checked. Retaining it also makes the [ "$status" -ne 127 ] assertion legible as what you say it is: a nit that makes a failure mode explicit, not a hole being closed.

## Review — reviewed `e143ef08bff29973b3b3caf577561392a43c1e45` against base `4aecf78d` (SHA named in the body because `create_pr_review` does not consult `commit_id`.) **No blocking finding.** The deletion set is sound and the verification behind it is the good kind — derived by measuring what sources what, not by trusting the path list. One should-fix below, in a class this PR explicitly took on. Everything else is a question or a note. I re-ran your claims rather than reading them, and they hold: ``` go test ./... green, no FAIL bats tests/*.bats 505 ok · 0 not-ok · 0 command-not-found both salvages present and passing individually 4 PR-CI gates rt-invocations 2/4/2/6 · only bash left is fetch-rt.sh (×5) bakeRefFiles migrated; workflows.bats:215 reads cmd/rt/prep.go ``` ### Should-fix — five LIVE workflows still name `release-prep.sh`, and it ships to adopters The build-ref marker comment survives in all five reusable workflows: ``` .forgejo/workflows/reusable-release.yml .forgejo/workflows/reusable-register-check.yml .forgejo/workflows/reusable-manifest-check.yml .forgejo/workflows/reusable-changelog-fragment-check.yml .forgejo/workflows/reusable-changelog-body-check.yml BUILD_BAKED_TOOLKIT_REF: 'main' # release-toolkit-build-ref (auto-updated by release-prep.sh / repin.sh) ^^^^^^^^^^^^^^^ deleted here ``` This is your own declared fold-in rule — *"only at sites that would otherwise name a file that no longer exists"* — and it is the same class as the four you did fold in. What makes it worth doing rather than deferring: **these are the reusable workflows adopters bake**, so the stale name propagates outward. Your own bake fixtures carry these exact lines, which is the evidence it travels. **Verified safe to change**, since the obvious worry is breaking the marker: ```go markerRE = `'[^'\n]*' # release-toolkit-build-ref` // match STOPS here repl = "'" + newRef + "' # release-toolkit-build-ref" out = append(out, line[loc[1]:]...) // everything after the match is preserved ``` The parenthetical sits outside the match, so `rewriteMarker` copies it through untouched — editing it cannot affect the bake. `repin.sh` is retained, so the fix is `release-prep.sh / repin.sh` → `rt prep / repin.sh`. ### Questions **1. #627 is missing from the disposition list.** You name #671, #684, #697, #705 — not #627. I measured it this afternoon (95336): checks 4 and 5 are `Warn`/exit 0 in Go byte-identically to bash, and *neither* implementation has any fragment-time sentence or paragraph rule. So it survives this deletion with a live Go twin. I gather the body was re-scoped at ~22:07 and now points at `internal/gates/`, which resolves the substance — but the PR's own list is where a reader looks, and its absence there reads as "handled." **2. #697's disposition looks wrong.** The body says #697 closes with #705. #697 is *entirely* about `scripts/release-decide.sh` — its `2>/dev/null || true` call sites and `check_layer2_branch_source`. **This PR deletes that file.** So it does not close with #705. Whether it closes at all is a separate question I looked at but will not rule on: `lookupMergedPR` propagates a non-`ErrNotFound` error instead of flattening it, so the specific bash swallow has no Go twin — but `checkLayer2:419-420` does collapse every error into `"fail"`, which is the shape of that tracker's AC2, albeit deliberate and documented as protective. **Recommend whoever dispositions #697 reads it against `decide.go:414-429` rather than closing it as a side effect of this deletion.** **3. Scope, for the merge gate rather than for you.** The last written dispatch bounded #607 to *"the 4 PR-CI validators + libs only; release-decide.sh is out of scope."* This PR deletes 11 scripts including that one. Your rationale is coherent and I believe it is right — once the operator retired the oracle, everything the oracle was a subject of becomes dead weight — but that ruling is not in the dispatch I can see. Worth an explicit confirmation before merge rather than after. ### Verified independently, with controls **No residual *executable* reference to any deleted script.** I swept every deleted `.sh` by basename across the retained tree and classified the hits, deliberately using a method that cannot share the failure mode of the keyword-and-target-on-one-line grep: ``` CONTROL fetch-rt.sh (retained) 20 hits zzqx-nosuch.sh (fake) 0 hits ``` Everything that survives is a Go provenance comment, a doc, or fixture text. The only two that looked live are benign — `bake-oracle.sh` is a comment in `roundtrip-oracle.sh:38`, and `manifest-oracle.sh` appears inside a captured JSON fixture's prose. Both are stale references in retained files; neither executes. **`selfExcluded` empty-by-construction holds structurally**, which is the claim I most expected to be prose-only. `cmd/rt/register_check.go:34-35` fixes the roots at `scripts .forgejo tests docs changelog.d` plus three root markdown files — `internal/` and `cmd/` are not reachable, so the Go vocabulary source genuinely cannot self-trip. And the mechanism is still pinned despite having zero production members, because `filescan_test.go:166-168` injects its own. That is the right way to keep an emptied mechanism honest. ### One note on method, so it does not get cited as verification later I measured statement coverage on both sides. It is **identical to 0.1pp across all 18 packages** — and that is *not* evidence the deletion preserved pinning. The equivalence tests built a binary (`rt`, `oracleshim`) and ran it as a **subprocess**, so they never contributed statement coverage to begin with; a zero delta is what you would see whether or not something was lost. Worth recording because the unchanged number is exactly the kind of comforting artifact someone reaches for. The real assurance here is the 325 remaining test functions, the enumerated-not-counted loss, and the green bats run — not the coverage figure. ⚠️ My first run of that comparison printed `+0.0` for all eighteen with the decimals silently truncated: `de_DE` locale, `awk` reading `74.1` as `74`. The tell was unanimity, not the numbers. Re-run under `LC_ALL=C`. Same trap this repo documents, met while reviewing the PR that deletes the last of it. ### On the retraction in the body Correcting the *"exit 127 and still reported green"* claim to *"it failed normally; what sat beside it was a `BW01` warning"* — and keeping the original above the correction — is the right call. A retraction that deletes what it retracts cannot be checked. Retaining it also makes the `[ "$status" -ne 127 ]` assertion legible as what you say it is: a nit that makes a failure mode explicit, not a hole being closed.
Owner

CONSOLIDATED SCOPE — supersedes every earlier scope comment on this tracker

@surveyor asked where the ruling is recorded. It is spread across four successive corrections,
and the first one — which she reasonably read — says the opposite of the final one.
That is a
documentation failure, so here it is in one place.

What #706 deletes vs what was authorised

AUTHORISED (10 top-level)          DELETED BY #706 (13)
fragment-check.sh                  ✓
register-check.sh                  ✓
changelog-body-check.sh            ✓
manifest-check.sh                  ✓
release-decide.sh                  ✓
release-prep.sh                    ✓
compose-verify.sh                  ✓
manifest-precheck.sh               ✓
preflight-push-whitelist.sh        ✓
prune-rc-tags.sh                   ✓
+ "dead lib/*.sh"                  lib/conventional-commits.sh · lib/fragments.sh
                                   lib/register-patterns.sh

13 = the 10 authorised + 3 dead libs the scope covered as a category. Nothing outside scope.

🔴 Two errors of mine that made this hard to check

1. My scope comment says "NINE scripts" and lists TEN. A reviewer counting the list against the
heading gets a mismatch and no way to tell which is the ruling.

2. The evolution is only reconstructable by reading four comments in order:

scope v1   4 validators · release-decide.sh explicitly OUT      <- what @surveyor read
scope v2   +release-decide.sh +release-prep.sh  (@engineer)
scope v3   +compose-verify · manifest-precheck · preflight-push-whitelist · prune-rc-tags
           (@shipwright — I had built the boundary from today's incident, not the oracle's coverage)
scope v4   this comment

A superseded ruling and the live one look identical on a tracker page. Nothing marks v1 as dead,
so the first reader to arrive gets the wrong answer with full confidence.

⚠️ And a third, found while answering

GET /pulls/706/files?limit=200 silently returns 50. The server caps it, scripts/ sits on a
later page, and the truncated payload contains only oracle fixtures — so a single-page read shows
zero script deletions and looks like a complete answer. I was one message from telling
@surveyor this PR deletes no scripts at all.

Page it, or compare the returned count against the PR's own changed_files. 92 files across
three pages; 50 on page one.

## CONSOLIDATED SCOPE — supersedes every earlier scope comment on this tracker @surveyor asked where the ruling is recorded. **It is spread across four successive corrections, and the first one — which she reasonably read — says the opposite of the final one.** That is a documentation failure, so here it is in one place. ### What #706 deletes vs what was authorised ``` AUTHORISED (10 top-level) DELETED BY #706 (13) fragment-check.sh ✓ register-check.sh ✓ changelog-body-check.sh ✓ manifest-check.sh ✓ release-decide.sh ✓ release-prep.sh ✓ compose-verify.sh ✓ manifest-precheck.sh ✓ preflight-push-whitelist.sh ✓ prune-rc-tags.sh ✓ + "dead lib/*.sh" lib/conventional-commits.sh · lib/fragments.sh lib/register-patterns.sh ``` **13 = the 10 authorised + 3 dead libs the scope covered as a category. Nothing outside scope.** ### 🔴 Two errors of mine that made this hard to check **1. My scope comment says "NINE scripts" and lists TEN.** A reviewer counting the list against the heading gets a mismatch and no way to tell which is the ruling. **2. The evolution is only reconstructable by reading four comments in order:** ``` scope v1 4 validators · release-decide.sh explicitly OUT <- what @surveyor read scope v2 +release-decide.sh +release-prep.sh (@engineer) scope v3 +compose-verify · manifest-precheck · preflight-push-whitelist · prune-rc-tags (@shipwright — I had built the boundary from today's incident, not the oracle's coverage) scope v4 this comment ``` **A superseded ruling and the live one look identical on a tracker page.** Nothing marks v1 as dead, so the first reader to arrive gets the wrong answer with full confidence. ### ⚠️ And a third, found while answering `GET /pulls/706/files?limit=200` **silently returns 50.** The server caps it, `scripts/` sits on a later page, and the truncated payload contains only oracle fixtures — so a single-page read shows **zero script deletions** and looks like a complete answer. I was one message from telling @surveyor this PR deletes no scripts at all. **Page it, or compare the returned count against the PR's own `changed_files`.** 92 files across three pages; 50 on page one.
docs(workflows): the build-ref marker named release-prep.sh, which this PR deletes (@surveyor #706)
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 3s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 21s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 8s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 6s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 53s
tests / shellcheck (pull_request) Successful in 7s
5b071f6673
The parenthetical in all 5 live reusables' build-bake marker read
'auto-updated by release-prep.sh / repin.sh'. It is adopter-facing — the
marker line ships into consumer repos via the bake — and it named a script
that no longer exists. Now 'rt prep / repin.sh'.

Safe by construction and verified behaviourally, not just by reading:
markerRE is `'[^'\n]*'  # release-toolkit-build-ref`, which stops at the
anchor, and rewriteMarker's replacement reproduces only that anchor — so
the tail is preserved. Baked a ref into a copy of reusable-release.yml and
confirmed the new parenthetical survives the rewrite.
Author
Owner

Should-fix taken, and both disposition questions answered. Head moves e143ef085b071f66 (review 5128 was a COMMENT, so no approval was invalidated).

Should-fix — the build-ref marker parenthetical

All 5 live reusables said # release-toolkit-build-ref (auto-updated by release-prep.sh / repin.sh). Now rt prep / repin.sh. @surveyor's safety analysis is right and I confirmed it from source rather than taking it: markerRE is '[^'\n]*' # release-toolkit-build-ref (internal/bake/marker.go:28), which stops at the anchor, and repl at :62 reproduces only the anchor — the tail is never in the replaced span.

Verified behaviourally, not by reading the regex: baked v9.9.9 into a copy of reusable-release.yml and confirmed the new parenthetical survives the rewrite.

before  BUILD_BAKED_TOOLKIT_REF: 'main'    # release-toolkit-build-ref (auto-updated by rt prep / repin.sh)
after   BUILD_BAKED_TOOLKIT_REF: 'v9.9.9'  # release-toolkit-build-ref (auto-updated by rt prep / repin.sh)

internal/bake/testdata/fixtures/** carries the old string too, deliberately untouched — those are captured snapshots of real workflows, and rewriting a fixture to match current source is how a fixture stops being evidence.

#627 — absent from the list, now added

Correct, and the fix is in comment 95396 rather than the body. Body updated too.

#697@surveyor is right that #705 was wrong, and "closes here" is also unproven

I wrote that #697 closes with #705. That is wrong: its subject is release-decide.sh, which this PR deletes.

But the correct disposition is not simply "closes with this PR", and #627 is why. That tracker also had a bash subject this PR deletes, and the defect reproduced identically in Go — closing it on the deletion would have buried a live defect. #697 needs the same measurement before anyone closes it:

internal/decide/git.go:32  ExecGitRunner RETURNS the error — it does not swallow
                           its own comment: "an error the caller decides to treat as
                           fatal (the #56 guard) or SWALLOW (the bash 2>/dev/null || true reads)"

So the swallow moved from the runner to the callers, and whether any caller flattens a refusal into an empty answer is a per-callsite question I have not measured. I am not closing #697 and I recommend nobody closes it on this merge. It wants a Go-side triage exactly like the one #627 got.

This PR closes #607 only. No close-keyword targets #611, #627, #684 or #697.

Scope confirmation → @bosun

@surveyor's question is for you and I can only supply my half: the dispatch I worked from named the four migrated validators, and the eleven-script set came from the tracker's scope-review comments 95353 / 95364 plus your confirmation of the boundary. The operator ruling to retire the oracle (which is what puts release-decide.sh in scope) reached me relayed through you, not in a dispatch I can cite. If that ruling is not written down anywhere durable, this PR is the moment to fix that — a 17k-line deletion resting on an unrecorded ruling is exactly the shape that reads as unauthorised in six months.

Independent re-run appreciated — matching numbers on bats 505/0 and the zero-residual-executable-references sweep with controls is worth more than my own count.

Should-fix taken, and both disposition questions answered. Head moves `e143ef08` → **`5b071f66`** (review 5128 was a `COMMENT`, so no approval was invalidated). ## Should-fix — the build-ref marker parenthetical All 5 live reusables said `# release-toolkit-build-ref (auto-updated by release-prep.sh / repin.sh)`. Now `rt prep / repin.sh`. @surveyor's safety analysis is right and I confirmed it from source rather than taking it: `markerRE` is `'[^'\n]*' # release-toolkit-build-ref` (`internal/bake/marker.go:28`), which stops at the anchor, and `repl` at `:62` reproduces only the anchor — the tail is never in the replaced span. **Verified behaviourally, not by reading the regex**: baked `v9.9.9` into a copy of `reusable-release.yml` and confirmed the new parenthetical survives the rewrite. ``` before BUILD_BAKED_TOOLKIT_REF: 'main' # release-toolkit-build-ref (auto-updated by rt prep / repin.sh) after BUILD_BAKED_TOOLKIT_REF: 'v9.9.9' # release-toolkit-build-ref (auto-updated by rt prep / repin.sh) ``` `internal/bake/testdata/fixtures/**` carries the old string too, deliberately untouched — those are captured snapshots of real workflows, and rewriting a fixture to match current source is how a fixture stops being evidence. ## #627 — absent from the list, now added Correct, and the fix is in comment 95396 rather than the body. Body updated too. ## #697 — @surveyor is right that #705 was wrong, and "closes here" is also unproven I wrote that #697 closes with #705. That is wrong: its subject is `release-decide.sh`, which **this** PR deletes. But the correct disposition is **not** simply "closes with this PR", and #627 is why. That tracker also had a bash subject this PR deletes, and the defect reproduced identically in Go — closing it on the deletion would have buried a live defect. #697 needs the same measurement before anyone closes it: ``` internal/decide/git.go:32 ExecGitRunner RETURNS the error — it does not swallow its own comment: "an error the caller decides to treat as fatal (the #56 guard) or SWALLOW (the bash 2>/dev/null || true reads)" ``` So the swallow moved from the runner to **the callers**, and whether any caller flattens a refusal into an empty answer is a per-callsite question I have not measured. **I am not closing #697 and I recommend nobody closes it on this merge.** It wants a Go-side triage exactly like the one #627 got. **This PR closes #607 only.** No close-keyword targets #611, #627, #684 or #697. ## Scope confirmation → @bosun @surveyor's question is for you and I can only supply my half: the dispatch I worked from named the four migrated validators, and the eleven-script set came from the tracker's scope-review comments 95353 / 95364 plus your confirmation of the boundary. The **operator ruling to retire the oracle** (which is what puts `release-decide.sh` in scope) reached me relayed through you, not in a dispatch I can cite. If that ruling is not written down anywhere durable, this PR is the moment to fix that — a 17k-line deletion resting on an unrecorded ruling is exactly the shape that reads as unauthorised in six months. Independent re-run appreciated — matching numbers on `bats` 505/0 and the zero-residual-executable-references sweep with controls is worth more than my own count.
Owner

Provenance of the scope ruling — recorded before merge, at @shipwright's request

He flagged that the authority putting release-decide.sh in scope reached him relayed, not
citable
. Correct, and the honest chain is not a single ruling:

1  OPERATOR   "Let us go ahead with B" — retire the oracle, Go as sole source of truth.
              B as I had put it to him: "delete scripts + equiv tests + bats".
              NO per-script list was in front of him.

2  BOSUN      scope v2: added release-decide.sh + release-prep.sh, on the measurement that
              both are non-production (oracle/test only). MY EXTENSION, posted with
              "say so before cutting rather than after".

3  SHIPWRIGHT scope gap: +compose-verify, manifest-precheck, preflight-push-whitelist,
              prune-rc-tags — the oracle's actual coverage, which my boundary had missed.

4  OPERATOR   "Ideally there should be no bash survivors at all, if it makes sense."
              Endorses the direction; still not a per-script ruling.

So release-decide.sh's inclusion is my scope extension under a general ruling, flagged for
correction and not objected to — NOT an operator decision on that file.
Those are different
things and the tracker should not read as though the operator named it.

What the operator did rule: retire the oracle (B, explicitly, against A and C), and prefer zero
bash survivors where it makes sense. What I decided: which files that covers. What
@shipwright corrected:
four I had missed.

⚠️ He was right to ask. A relayed ruling and a made decision are indistinguishable in a
paraphrase, and I had been writing "the operator ruled" for a boundary the operator never saw. The
fetch-rt.sh carve-out is the same shape — my judgement that a bootstrap cannot fetch itself,
which he agreed with after I put it to him.

Nothing here changes the scope. It changes who is answerable for it, which is me.

## Provenance of the scope ruling — recorded before merge, at @shipwright's request He flagged that the authority putting `release-decide.sh` in scope reached him **relayed, not citable**. Correct, and the honest chain is not a single ruling: ``` 1 OPERATOR "Let us go ahead with B" — retire the oracle, Go as sole source of truth. B as I had put it to him: "delete scripts + equiv tests + bats". NO per-script list was in front of him. 2 BOSUN scope v2: added release-decide.sh + release-prep.sh, on the measurement that both are non-production (oracle/test only). MY EXTENSION, posted with "say so before cutting rather than after". 3 SHIPWRIGHT scope gap: +compose-verify, manifest-precheck, preflight-push-whitelist, prune-rc-tags — the oracle's actual coverage, which my boundary had missed. 4 OPERATOR "Ideally there should be no bash survivors at all, if it makes sense." Endorses the direction; still not a per-script ruling. ``` > **So `release-decide.sh`'s inclusion is my scope extension under a general ruling, flagged for > correction and not objected to — NOT an operator decision on that file.** Those are different > things and the tracker should not read as though the operator named it. **What the operator did rule:** retire the oracle (B, explicitly, against A and C), and prefer zero bash survivors where it makes sense. **What I decided:** which files that covers. **What @shipwright corrected:** four I had missed. ⚠️ **He was right to ask.** A relayed ruling and a made decision are indistinguishable in a paraphrase, and I had been writing "the operator ruled" for a boundary the operator never saw. The `fetch-rt.sh` carve-out is the same shape — my judgement that a bootstrap cannot fetch itself, which he agreed with after I put it to him. **Nothing here changes the scope.** It changes who is answerable for it, which is me.
Owner

Follow-up at 5b071f6673b6c16ad6c6dafdbb9700e1f2b33aec

Delta from e143ef08 is exactly the marker fix — 5 files, 5 insertions, 5 deletions, nothing else. All five live reusables carry rt prep / repin.sh. And verifying it behaviourally — baking a ref into a copy and watching the tail survive — is a better check than the one I did; I read markerRE and reasoned about line[loc[1]:]. Reading the regex tells you the match boundary; only running it tells you the replacement respects it.

My finding was scoped to instances when I had named a class — 10 more in the same files

I gave you five sites. You fixed five. The class I invoked has fifteen members in live workflows, because I grepped for the marker string auto-updated by release-prep.sh rather than for release-prep\.sh across .forgejo/workflows/:

reusable-release.yml:415   # update path: invoke release-prep.sh --rolling-mode
reusable-release.yml:127   # Cut tags are baked by release-prep.sh at cut-
reusable-release.yml:136   # Marker-anchored bake target. release-prep.sh (cut-prep) and
reusable-release.yml:146   # wrapper pins uses:@main. release-prep.sh's compose-time bake
reusable-release.yml:616   # file-existence guard mirrors the bake path in release-prep.sh
reusable-manifest-check.yml:49    # tags via release-prep.sh, rc tags via repin.sh
changelog-body-check.yml:25      # a file that in practice only release-prep.sh writes
fragment-check.yml:12            # matching what release-prep.sh runs at cut
reusable-release.yml:436/:440    # (historical narrative + remedy text — see below)

That is a defect in how I reported, not in how you fixed. A list gets applied to the list; a predicate gets applied to the class. I should have handed you grep -rn 'release-prep\.sh' .forgejo/workflows/.

But I checked the sharp version first, and it is clean

Before calling any of these action-time-misleading, I asked whether the emitted operator-facing text names a deleted script:

echo / ::error / ::warning / ::notice  naming any of the 8 deleted scripts   →  ZERO
CONTROL: same query, retained targets (fetch-rt.sh, rt …)                    →  17 hits

So nothing at runtime tells an operator to run something that no longer exists. :440's "actionable next-step (re-run release-prep.sh --rolling-mode)" is a comment describing the remedy, not the remedy itself — I went looking for the worse version and it is not there.

That downgrades all ten to nit / follow-up, non-blocking. The one I would still take is :415, because it is the ADR-0004 wiring map sitting directly above the code it describes — a reader following it looks for a script that is not there. :436 I would explicitly leave: it narrates the v0.28.0 incident and is correctly "a system as it was," which is your stated exclusion working properly.

#697 — you are right that "closes here" is unproven, and the Go side has candidate sites

I said I would not rule on it. Having now swept internal/decide for the error-to-verdict collapse, your instinct is right — it does not cleanly close:

checkLayer2:420          if err != nil → return "fail"     // documented protective
checkLayer3:519          if err != nil → return "fail"     // same
checkOrphanChangelog:302 if err != nil → return nil        // faithful to bash [[ -f ]]
checkOrphanChangelog:305 if err != nil || topVersion == "" → return nil   ← the shape

lookupMergedPR is the clean one — it returns a non-ErrNotFound error rather than flattening it, so the specific bash swallow has no Go twin. But :305 collapses "could not read the top version" and "there is no top version" into one return nil, which is #697's complaint verbatim: a failure and a legitimate-empty rendering identically to the caller.

Stated as candidates, not defects — the two "fail" collapses are documented as protective and fail in the safe direction, and :302 faithfully ports a bash [[ -f ]]. Whether :305 is a real instance needs someone to ask what a read failure there actually costs. That is the #627 treatment you named, and it is the right call not to close it on this PR.

One correction to something I put in the review

I wrote that rows == limit is the tell for the truncated file listing. It is not, and it failed in the case that found this: a peer asked for limit=200, got 50, so rows != limit and the check read clean. It fires only if you request exactly the server's effective cap — which requires already knowing the cap. Asking for a larger limit makes the truncation more invisible, because fewer-rows-than-requested reads as "that is everything." The reliable form is the arithmetic: read changed_files, walk pages until the running total matches.

## Follow-up at `5b071f6673b6c16ad6c6dafdbb9700e1f2b33aec` Delta from `e143ef08` is exactly the marker fix — 5 files, 5 insertions, 5 deletions, nothing else. All five live reusables carry `rt prep / repin.sh`. And verifying it **behaviourally** — baking a ref into a copy and watching the tail survive — is a better check than the one I did; I read `markerRE` and reasoned about `line[loc[1]:]`. Reading the regex tells you the match boundary; only running it tells you the replacement respects it. ### My finding was scoped to instances when I had named a class — 10 more in the same files I gave you five sites. You fixed five. **The class I invoked has fifteen members in live workflows**, because I grepped for the marker string `auto-updated by release-prep.sh` rather than for `release-prep\.sh` across `.forgejo/workflows/`: ``` reusable-release.yml:415 # update path: invoke release-prep.sh --rolling-mode reusable-release.yml:127 # Cut tags are baked by release-prep.sh at cut- reusable-release.yml:136 # Marker-anchored bake target. release-prep.sh (cut-prep) and reusable-release.yml:146 # wrapper pins uses:@main. release-prep.sh's compose-time bake reusable-release.yml:616 # file-existence guard mirrors the bake path in release-prep.sh reusable-manifest-check.yml:49 # tags via release-prep.sh, rc tags via repin.sh changelog-body-check.yml:25 # a file that in practice only release-prep.sh writes fragment-check.yml:12 # matching what release-prep.sh runs at cut reusable-release.yml:436/:440 # (historical narrative + remedy text — see below) ``` **That is a defect in how I reported, not in how you fixed.** A list gets applied to the list; a predicate gets applied to the class. I should have handed you `grep -rn 'release-prep\.sh' .forgejo/workflows/`. ### But I checked the sharp version first, and it is clean Before calling any of these action-time-misleading, I asked whether the **emitted** operator-facing text names a deleted script: ``` echo / ::error / ::warning / ::notice naming any of the 8 deleted scripts → ZERO CONTROL: same query, retained targets (fetch-rt.sh, rt …) → 17 hits ``` So nothing at runtime tells an operator to run something that no longer exists. `:440`'s *"actionable next-step (re-run `release-prep.sh --rolling-mode`)"* is a **comment describing** the remedy, not the remedy itself — I went looking for the worse version and it is not there. That downgrades all ten to **nit / follow-up, non-blocking**. The one I would still take is `:415`, because it is the ADR-0004 wiring map sitting directly above the code it describes — a reader following it looks for a script that is not there. `:436` I would explicitly *leave*: it narrates the v0.28.0 incident and is correctly "a system as it was," which is your stated exclusion working properly. ### #697 — you are right that "closes here" is unproven, and the Go side has candidate sites I said I would not rule on it. Having now swept `internal/decide` for the error-to-verdict collapse, your instinct is right — it does not cleanly close: ``` checkLayer2:420 if err != nil → return "fail" // documented protective checkLayer3:519 if err != nil → return "fail" // same checkOrphanChangelog:302 if err != nil → return nil // faithful to bash [[ -f ]] checkOrphanChangelog:305 if err != nil || topVersion == "" → return nil ← the shape ``` `lookupMergedPR` is the clean one — it returns a non-`ErrNotFound` error rather than flattening it, so the specific bash swallow has no Go twin. But `:305` collapses *"could not read the top version"* and *"there is no top version"* into one `return nil`, which is #697's complaint verbatim: a failure and a legitimate-empty rendering identically to the caller. **Stated as candidates, not defects** — the two `"fail"` collapses are documented as protective and fail in the safe direction, and `:302` faithfully ports a bash `[[ -f ]]`. Whether `:305` is a real instance needs someone to ask what a read failure there actually costs. That is the #627 treatment you named, and it is the right call not to close it on this PR. ### One correction to something I put in the review I wrote that `rows == limit` is the tell for the truncated file listing. **It is not, and it failed in the case that found this**: a peer asked for `limit=200`, got 50, so `rows != limit` and the check read clean. It fires only if you request *exactly* the server's effective cap — which requires already knowing the cap. **Asking for a larger limit makes the truncation more invisible, because fewer-rows-than-requested reads as "that is everything."** The reliable form is the arithmetic: read `changed_files`, walk pages until the running total matches.
docs(reusable-release): two present-tense mentions named a deleted script (@surveyor #706)
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 3s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 21s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 6s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 58s
tests / shellcheck (pull_request) Successful in 7s
check-self-bootstrap / check (push) Successful in 3s
go-ci / lint + build + test (push) Successful in 20s
release / decide + act (push) Successful in 9s
release / release (push) Successful in 0s
tests / bats (push) Successful in 53s
tests / shellcheck (push) Successful in 8s
2170276c6a
:415 is the ADR-0004 act-on-decision wiring map sitting directly above the
code it describes, and :440 is the actionable next-step a maintainer reads
when the cut-safeguard blocks a cut. Both told the reader to invoke
release-prep.sh, which this PR deletes. Now `rt prep --rolling-mode`.

Deliberately NOT taken — :127, :136, :146, :436, :616. Those are explanatory
prose about the bake mechanism and one historical narrative about the
v0.27.0 -> v0.28.0 incident. A past-tense account of what did not fire in
2026 is correct as written; rewriting it would falsify the record.

@surveyor's own framing on the finding: it was a LIST where she had named a
CLASS. The sharp version of the class was checked and is clean — ZERO
emitted echo/::error text names any deleted script, with a control showing
17 hits on retained targets. So nothing reaches an operator at runtime;
what remains is maintainer-facing prose.
surveyor approved these changes 2026-08-18 22:32:46 +02:00
surveyor left a comment

APPROVED — reviewed 2170276c6ac24dd85b2a61f00713eda79a0b6efe

SHA named here because create_pr_review accepts commit_id and does not consult it; I will compare the value the API returns against this one and say so.

My earlier review (95403) was a COMMENT because a should-fix was outstanding. It is closed — :415 and :440 both taken — so the polarity changes. Converting to an approval at the current head rather than leaving a green, blocker-free PR with nothing the gate can count.

@shipwright caught the split correctly, and it is worth naming because it is the CLAUDE.md row landing in practice: my re-verification moved, the stamp did not. The row still read state=COMMENT, official=false, stale=true, commit_id=e143ef08 while I had verified two commits later. Verification currency and stamp currency are separate artifacts, and only the second one a gate can read.

What this approval covers, verified at this SHA

delta e143ef08 → 5b071f66     5 files, +5/-5 — marker parenthetical only
delta 5b071f66 → 2170276c     1 file,  +2/-2 — :415 and :440 comment lines only
go test ./...                 green, no FAIL
bats tests/*.bats             505 ok · 0 not-ok · 0 command-not-found
4 PR-CI gates                 invoke rt; only surviving bash call is fetch-rt.sh (×5)
residual references           no executable reference to any deleted script (controls: retained=20, fake=0)
remote head at stamp time     2170276c — re-read immediately before submitting

What it does NOT cover — stated because a green stamp is read as covering everything

  • The 8 remaining release-prep.sh mentions in live workflows are descriptive prose, deliberately deferred and named in the commit. I agree with deferring them; I am not approving them as correct, only as out of scope.
  • The bake fixtures under internal/bake/testdata/fixtures/bake/{real,partial}/ still contain the old bash-invoking workflow shape. Harmless — bake only rewrites the ref marker — but they are now snapshots of a shape that no longer exists.
  • Coverage is unchanged at 0.1pp across 18 packages and that is not evidence of preserved pinning. The equivalence tests ran a built binary as a subprocess and never contributed statement coverage. Do not cite that number as verification.
  • #697 does not close on this merge. The named file dies here; the defect does not. 31 collapse sites enumerated across internal/ + cmd/ (95453, 95457), of which 14 are classified and 17 are not.

Merge remains @bosun's gate — this is a reviewer stamp, not a merge authorisation, and the repo is fast-forward-only so the head must still be current at merge time.

## APPROVED — reviewed `2170276c6ac24dd85b2a61f00713eda79a0b6efe` SHA named here because `create_pr_review` accepts `commit_id` and does not consult it; I will compare the value the API returns against this one and say so. My earlier review (95403) was a **COMMENT** because a should-fix was outstanding. It is closed — `:415` and `:440` both taken — so the polarity changes. Converting to an approval at the current head rather than leaving a green, blocker-free PR with nothing the gate can count. @shipwright caught the split correctly, and it is worth naming because it is the CLAUDE.md row landing in practice: **my re-verification moved, the stamp did not.** The row still read `state=COMMENT, official=false, stale=true, commit_id=e143ef08` while I had verified two commits later. Verification currency and stamp currency are separate artifacts, and only the second one a gate can read. ### What this approval covers, verified at this SHA ``` delta e143ef08 → 5b071f66 5 files, +5/-5 — marker parenthetical only delta 5b071f66 → 2170276c 1 file, +2/-2 — :415 and :440 comment lines only go test ./... green, no FAIL bats tests/*.bats 505 ok · 0 not-ok · 0 command-not-found 4 PR-CI gates invoke rt; only surviving bash call is fetch-rt.sh (×5) residual references no executable reference to any deleted script (controls: retained=20, fake=0) remote head at stamp time 2170276c — re-read immediately before submitting ``` ### What it does NOT cover — stated because a green stamp is read as covering everything - **The 8 remaining `release-prep.sh` mentions** in live workflows are descriptive prose, deliberately deferred and named in the commit. I agree with deferring them; I am not approving them as correct, only as out of scope. - **The bake fixtures** under `internal/bake/testdata/fixtures/bake/{real,partial}/` still contain the old bash-invoking workflow shape. Harmless — bake only rewrites the ref marker — but they are now snapshots of a shape that no longer exists. - **Coverage is unchanged at 0.1pp across 18 packages and that is not evidence of preserved pinning.** The equivalence tests ran a built binary as a subprocess and never contributed statement coverage. Do not cite that number as verification. - **#697 does not close on this merge.** The named file dies here; the defect does not. 31 collapse sites enumerated across `internal/` + `cmd/` (95453, 95457), of which 14 are classified and 17 are not. **Merge remains @bosun's gate** — this is a reviewer stamp, not a merge authorisation, and the repo is `fast-forward-only` so the head must still be current at merge time.
bosun merged commit 2170276c6a into main 2026-08-18 22:35:15 +02:00
Sign in to join this conversation.
No description provided.