feat(reusable-release): migrate 7 callsites bash→rt + 2c-hybrid rt bootstrap #597
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!597
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/582-reusable-rt-callsites"
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?
What
Phase 7 (milestone #80) workflow integration — the load-bearing gate that makes the Phase 6/6b
rtports take effect in production.reusable-release.ymlnow:rtonto PATH itself (it previously ran only checked-out bash), andrt <subcommand>s instead of.release-toolkit/scripts/*.sh.Single PR (S1) because the bootstrap and the callsite swap mutually require each other — you can't
rt decidewithoutrton PATH, and bootstrappingrtis pointless with no callsite using it. Build order is test-in-place: bootstrap →rt --versionsmoke → flip callsites.Measured: 7 files, +248/−96 (3 commits: the migration + a review-fold Go preflight + a stale bats-guard fix).
The bootstrap fork — why 2c hybrid (ratified)
Framing-verify surfaced that
rtis not on PATH in the reusable: it checks out toolkit source into.release-toolkit/and runs bash (install depsprovides yq/jq/curl, not Go). Gettingrtthere is a real design fork:release.ymlfloating-pins the reusable@main(#456) and goreleaser publishes nomainassetgo buildfor all refsvX.Y.Z, build formainDiscriminator (verified against real ref formats):
case "$ref" in v[0-9]*) fetch ;; *) build ;; esac→ everyvN.N.N[-pre.N]fetches;main/empty/non-vbuilds.§4a "build-per-run = never" — the build path is a documented toolkit-self exception
ADR-0008 §4a rejects build-per-run (Option C) as "never — the exact runner-assumption the port exists to remove." That rejection targets "a Go toolchain dep on every adopter runner." The 2c build path is a narrow exception that does not create that harm. It is convention-guarded, not structural: the build path is reached whenever the ref resolves to
main— adopters following the@vX.Y.Zpin-a-release-tag discipline (#336) fetch and never build, but nothing structurally stops an adopter pinning@mainoff-contract, which would reach it and require Go. Acommand -v gopreflight fails that case loud + actionable (naming the@vX.Y.Zremedy) rather than with a crypticgo: command not found— guarding the actual requirement, not a fragile repo-identity proxy (which would wrongly reject a legitimate toolkit fork building@main). Toolkit-self (runs_on: go) always has Go and never trips it; building@mainlets the toolkit dogfood its currentrt. Stated at the point of use (bootstrap comment) and in the ADR-0008 §4a note.fetch+verify extracted to
scripts/fetch-rt.sh(F-iii)The composite
action.ymland the reusable bootstrap both need the fetch+checksum-verify logic. Options considered:uses:. Rejected: zero precedent on this Forgejo for aworkflow_callreusable invoking a local composite action; resolution is unproven and the blast radius is the whole adopter population (can't hermetic-test).scripts/fetch-rt.sh;action.ymland the reusable both call it. DRY + robust + matches the repo's own extract-to-script-for-coverage pattern (cf.manifest-precheck.sh), and makes checksum-verify a single grep-able/testable surface.action.yml's external behavior is byte-preserved (composite-smoke.ymlre-verifies it post-merge via its manual dispatch).Callsites migrated
Per-command arg-surface parity was verified against each
rtsubcommand's cobra flags:release-decide.shrt decide| tee -a $GITHUB_OUTPUTpreservedcompose-verify.shrt compose-verify--version/--changelogpreflight-push-whitelist.shrt preflight-push-whitelistdraft-release.shrt release--version/--config/--dry-run;PUBLISH_MODE=/CUT_PATHenv preservedmanifest-precheck.shrt manifest-precheckPRECHECK_DECISION=$(…)capture preservedprune-rc-tags.shrt prune-rc-tags--owner/--repo/--version/--pinned-refflags;||continue-on-fail preservedrelease-prep.shrt prep--rolling-mode/--target-version/--configStructural-preservation reqs (GITHUB_OUTPUT tee,
PRECHECK_DECISIONcapture,||continue-on-fail, env prefixes) are shell wrappers around the callsite and are unchanged. Env-passthrough parity (e.g.rt releasereadingPUBLISH_MODE/CUT_PATH) is covered by the green per-command equivalence tests.Scope disclosure — what this PR does and does NOT verify
composition_equiv_test.go) is an rt-correctness oracle (it drives the bash/rt stages directly), not a YAML-wiring oracle. It de-risks thatrt ≡ bashper stage; it does not exercise the reusable YAML swap.bats tests/workflows.bats(structural callsite/ordering guards) + the per-command equivalence tests (green) + the AC's scratch-consumer end-to-end smoke (live-runner surface, #595-adjacent). The live PREVENT-block empirical stays deferred to #595 (beyond Phase-7 hermetic scope).invoking …logs, theunknown modeerror). Deeper explanatory comments and operator-remediation strings that reference the bash scripts' internal behavior are left as-is — the bash implementations still exist and remain the source of the ported logic.Mutation-closed-loop (nit-2)
Dropping the bootstrap-defeated
git_artifactsfrom the harnessRequireNonEmptyis provably behavior-neutral (git_artifacts is always non-empty from the seed commit, so it never bit; stdout was and remains the binding control). Reconfirmed empirically — mutating the oracle to break the wire (skip prep → empty stdout, git_artifacts still non-empty):→ CannotGrade, not a vacuous green. Reverted by re-edit;
grep -rn MUTATIONclean; suite green post-revert.Gate
go build/go vetclean ·go test -count=1 ./...green (19 pkgs, incl. the composition test which rebuildsrt+ drives the real bash oracle) ·golangci-lint0 issues ·shellcheck scripts/fetch-rt.shclean · YAML parse OK (action.yml,reusable-release.yml) ·bats tests/*.batsgreen (782 tests, incl. #774)forgejo-ci-go:latest, go1.26.2):go build/go vet/go test -count=1 ./...all greenbash -n+shellcheckon the bootstrap block clean · full Go re-run proportionately skipped (no Go change)rtinvocations (mutation-verified — with thert releaseinvocation removed but its echo kept, the guard reddens; a bare substring match would have passed vacuously).Folded
RequireNonEmptychange above.command -v gofail-loud preflight on the BUILD path + "convention-guarded" wording downgrade.rtinvocations, line-anchored to stay non-vacuous.Refs: #582, #502
Review — PR#597, migrate 7 reusable-release.yml callsites bash→rt + 2c-hybrid bootstrap (#582, Phase-7 FINALE)
Independent deep-verify across three heads (
6547d9c→1ebee7b→a529c50; basemain@1a97eb4, in sync throughout). The last Phase-7 tracker: 7 cut/prepare callsites migrated tort, a 2c-hybrid FETCH/BUILD bootstrap, thefetch-rt.shextraction, and the two #596 nits folded. Two substantive findings surfaced and closed in-cycle; the final head is fully verified.APPROVED at
a529c50. Full bats 782 green, CI 8/8. Details below, by head.Finding 1 (crux) — the §4a "airtight" claim was convention-guarded, not structural → Go preflight folded
The 2c-hybrid BUILD path is a documented exception to ADR-0008 §4a ("build-per-run = never"). I traced the ref resolution (
reusable-release.yml:132-176):REF = BUILD_BAKED_TOOLKIT_REF, overridden tomainwhen the consumer wrapper pins@main(:170). So an adopter who pins@mainstructurally reaches BUILD — the airtightness is conventional (adopters pin@vX.Y.Z), not structural. Engineer independently traced the same logic and self-corrected in the same window (a clean cross-actor cycle). The fix (1ebee7b): acommand -v gopreflight at the top of the BUILD branch, failing loud + actionable (names the@vX.Y.Zremedy) instead of a crypticgo: command not found; guarded on the actual requirement (Go present), not a repo-identity proxy — which correctly handles toolkit-self (has Go → never trips), forks-with-Go (build), fork/adopter@main-without-Go (loud). The ADR + comment "airtight/never reach" language downgraded to "convention-guarded, not structural." This is the codified scope-at-point-of-use + #242 discipline (fix the substrate, not a doc warning).Finding 2 (red CI) — the migration broke a grep-keyed test-guard → stale #774 fixed
1ebee7bwas CI-red:tests/bats #774(the #260 "preflight runs BEFORE the cut" ordering guard) failed. Root cause: the migration changedpreflight-push-whitelist.sh→rt preflight-push-whitelist(:438) anddraft-release.sh→rt release(:461), butworkflows.bats:#774grepped the old bash strings and wasn't in the PR. I verified the #260 invariant was preserved (rt preflight-push-whitelist@438 <rt release@461 — a stale test, not a real reorder), swept all reusable-referencing bats (only #774 stale; others grep bake-markers/script-files), and routed the must-fix.The fix (
a529c50) is better than what I proposed, and I verified the improvement: my suggested barefind('rt release')would have matched Engineer's new echo"cut: invoking rt release…"(:452, before the real invocation), so a workflow keeping the echo but dropping the invocation would pass vacuously — reintroducing the comment-false-positive #774 exists to prevent. Engineer anchored at line-start (^[ \t]*rt release\b— the rt subcommand as first token), and I reproduced the closed loop: comment-out the invocation @461 keeping the echo → anchored match-1(RED, correct); bare-substring → found at the echo (VACUOUS PASS). His extension is not just better, it's necessary given his migration added that echo.The rest — verified and unchanged across the fold heads
fetch-rt.shextraction: byte-faithful to the removedaction.ymlinline (os/arch resolve, auth header, the HTML-200-masks-substitution trap, checksum grep+sha256sum+compare,install -m0755— all identical) + one additive enhancement (aGITHUB_PATH-unset → stdout fallback so the fetch+verify stays exercisable off-runner; runner path unchanged).action.ymlis now a thinbash …/fetch-rt.shwrapper. Single source of truth for the security-critical checksum-verify. ✓v[0-9]*→FETCH (incl. pre-releasesv1.2.3-rc.1),main/empty/non-v→BUILD — matches the claimed table. FETCH failure fails loud (no build fallback: case branches don't fall through,set -e). ✓RequireNonEmpty→stdout-only (git_artifacts kept inCompare, dropped as a toothless positive control) + the L120 "SOLE content" comment. Composition test still green; the stdout positive control still bites. ✓1ebee7b) changed only reusable-release.yml + ADR; the test-sync fix (a529c50) changed only workflows.bats. Every other file byte-unchanged, so each prior verification carries forward.Verdict
APPROVED, head-pinned at
a529c50. The migration is complete and correct:fetch-rt.shbyte-faithfully extracts the checksum-verify, the discriminator routes correctly with no fetch→build fallback, the §4a exception is honestly re-framed as convention-guarded with a loud+actionable Go preflight closing the adopter@main foot-gun, the #260 ordering guard is repaired with a line-start anchor that resists the echo-vacuity (mutation-confirmed), and the two #596 nits landed. Full bats 782 green, CI 8/8. Two real findings (convention-vs-structural + the stale grep-guard), both closed in-cycle. This closes Phase 7. Yours to land.— Surveyor