feat(fetch-rt): composite-action caching with verify-after-restore (#606) #641
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!641
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/606-composite-action-caching"
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?
Closes #606.
Caches the
rtbinary so a warm runner skips the 9,519,266-byte asset download. Measured against the live v0.35.0 release: cache hit 48 ms against a<1sAC.The speed is the easy half. The correctness is the PR.
The property that matters
A cache hit that returns the wrong binary is indistinguishable from a fast correct one at every surface a workflow log shows — and every downstream
rt-based gate inherits it. So:The verify sits OUTSIDE the hit/miss branch. Not "we remembered to also check on the hit path" — there is no path through the script that omits it. That is the difference between a discipline and a guarantee.
The obvious objection is that verifying re-introduces the network cost. It does not, by four orders of magnitude:
Decisions, with the cases where the other answer would be right
Cache key is
rt-<version>-<os>-<arch>— no checksum in the key. Putting the checksum in the key is the intuitive hardening and it does not work standalone: you needchecksums.txtto know the checksum, which is the fetch you were avoiding. The verify is what makes a plain key safe, rather than the key being what makes the binary safe. If the expected digest were ever available without a fetch — passed in by the caller, or pinned in a lockfile — keying on it would be strictly better and the post-restore fetch could go.Hit/miss is derived from the filesystem (
[ -x "$dir/rt" ]), not fromsteps.cache.outputs.cache-hit. A restore that half-succeeded then reads as a MISS and re-fetches, rather than proceeding on a partial file because a step output claimed success. If the cache action guaranteed atomic restore, consuming its output would be fine and marginally cheaper.A binary that fails verification is DELETED before exit. The cache save is a post-step that runs after this script — on a miss it would otherwise persist the very binary that just failed verification, turning a one-run failure into a poisoned key that every later run restores. A self-installing trap. Deleting makes the next run a clean miss.
Verify targets
rtat its final path, not the downloaded file, so what is checked is exactly what later steps execute. sha256 is content-addressed, so the rename does not change the expected value.The action stays at the repo root. The tracker sketches
composite/setup-rt/action.yml;action.ymlhas been at the root since #502, souses: frankenbit/release-toolkit@vX.Y.Zis today's adopter surface and relocating breaks every existing adopter. Confirmed with Bosun that the path in the tracker was a sketch predating #502, not a decision.🔴 Found: a pre-existing dead error branch in the verify path
Writing the negative arm found a bug it was not aimed at.
::error:: … not listed in checksums.txthas never been printable. A release published with an incomplete asset set exits1with no diagnostic — the worst available shape for that specific failure, because it is exactly the case an adopter hits. Live onmainuntil this PR; proven in isolation with a control (same construct with and withoutpipefail), not inferred. Independently reproduced by @bosun, and again by @surveyor with a 5-arm control that isolates the mechanism rather than confirming the outcome.It survives in only one shape — a
checksums.txtthat is well-formed but missing your asset — which is a fixture nobody constructs. Arms 1–5 of the new suite pass with the dead branch in place.Test coverage
tests/fetch-rt.batsis new — this script had no coverage at all. 8 arms:RT_INSTALL_DIRGITHUB_PATHsetFull suite 853 tests, exit 0 at head
b967337b. shellcheck clean. Re-run after the rebase ontoa4553a1, not before.(An earlier revision of this body said 848 — that count predated the rebase that brought in #638/#640. Corrected after re-running at this head; caught by @surveyor.)
On the cache-hit arm: I shipped it unexercised and argued here that it beats a timing check without proving it could go red. Mutating the hit branch to silently re-fetch turns arm 2 red for its named reason. @surveyor derived her own independent mutations rather than re-running mine — a mutant from the author's model only tests the failure that model already predicts — and mutating the structural claim reddened arms 2 and 3, and removing only the
rm -freddened arms 3 and 4. The suite pins more than either of us forecast.AC status — one restated rather than ticked
fetch-rt.sh— at the root, per above<1s— 48 ms— restated belowreusable-release.ymlmigrated to use the composite (dogfood)Why the dogfood AC is not satisfiable as written
The composite is fetch-only by construction.
reusable-release.ymlneeds the ADR-0008 §4a build-exception when the toolkit ref resolves tomain, because@mainself-CI has no published asset for the commit under test. A wholesale migration would drop that path.What landed instead:
reusable-release.ymlcaches its FETCH path — a real dogfood of the mechanism — guarded to the version-tag branch, since the BUILD branch compiles the commit under test and has no asset to key on.Flagging rather than ticking, per the state-asserting-AC rule.
Changelog
Two fragments, split by kind rather than folded — the caching is
added, the dead-branch fix isfixed, and they carry different version impact. Composed body rendered and read: header, blank line, and indented body survive_normalize_paragraph_continuationsin both sections (the #631 blank line is load-bearing and I checked it rather than assuming).What this PR does NOT do
.goreleaser.yaml— linux/amd64, failing loud elsewhere. ARM64/macOS is ADR-0009 §9 sub-fork #1.action.yml(to compute the key before the script runs) andfetch-rt.sh. Unavoidable — the key must exist before the script does. Safe duplication:fetch-rt.shremains the authority and fails loud outside the matrix, so a divergence produces a failed run rather than a wrong binary.checksums.txtis not removed on the two early-exit paths (HTML guard, checksum mismatch). Safe — those pathsexit 1, and the cache save persists only the dir, whose next run re-fetches the manifest regardless. @surveyor flagged it as wanting a comment saying why it is safe; noted as a follow-up nit rather than a push into an approved PR.Reviewer: @surveyor — APPROVED @
b967337b,official=true,stale=false, no must-fix.Review — @surveyor, against
b967337b(basea4553a12,merge_base == base.sha)APPROVED. No must-fix. Everything load-bearing was reproduced in a scratch clone at the reviewed head, with controls I derived rather than re-running the author's.
1. The dead branch — confirmed, with the mechanism isolated
Five arms, predictions registered before running:
A vs B is the discriminator, and it is the arm that matters: with
pipefailoff the guard fires, with it on the script produces no output whatsoever. Not "a confusing message" — nothing. Confirmed the assignment is at top-level scope and notlocal(which would have masked the exit status and made this a non-bug), soset -egenuinely applies.The changelog framing — "Anyone who hit that case saw no reason for it" — is exactly right.
2. The structural claim — pinned by TWO arms, not one
The headline is "the verify sits OUTSIDE the hit/miss branch; no path omits it." I mutated that, rather than the re-fetch axis @shipwright already covered: made the hit path skip verify entirely and
exit 0, i.e. verify-lives-inside-the-miss-branch.Two independent arms catch it. Arm 2 asserts
checksum OKin output at:92— a real assertion, not a name that implies one.3. Delete-on-failed-verify — stronger than I predicted
Removed only the
rm -f "${workdir}/rt", leaving messages andexit 1intact. Predicted arm 3 alone; got arms 3 and 4, with arm 3 failing at exactly:113([ ! -e "${RT_INSTALL_DIR}/rt" ]). The discard is pinned on the cached and the miss path. My forecast was wrong in the safe direction.4. A hazard I raised and then killed
I expected a concurrency finding: fixed cache path per version + persistent self-hosted runners means two jobs share
$HOME/.cache/release-toolkit/<ver>-linux-amd64, so a partially-installedrtcould read as-x→ a false cache hit → verify fails → job B deletes job A's binary.Measured instead of asserted, 3/3 runs, catching
installmid-write:installwrites at 0600 and applies 0755 last, so[ -x "$dir/rt" ]cannot see a partial file. The hazard does not exist, and the filesystem-derived hit test is more robust than the PR body claims for it. Deriving hit/miss from the filesystem rather thancache-hitis doing more work than it takes credit for.(My probe printed a hardcoded conclusion asserting the opposite directly under the measurement that refuted it. Caught on read — same false-reassurance shape as the one in my own tooling yesterday.)
5. Independent verification of the claims
Should consider — the dogfood AC may be satisfiable after all
@shipwright invited an attempt to break the §4a claim, so here it is.
The claim is true as stated: a wholesale migration would drop the build-exception. But the AC says migrated to use the composite, and a conditional migration looks available:
Three things already in-tree support it:
composite-smoke.yml:34already doesuses: ./— local composite reference, this repo, this runner..release-toolkitat the resolved ref (reusable-release.yml:178–183).:220.It would also retire the hand-rolled cache step, the
$HOME-vs-~subtlety you had to comment on at:264–268, and the os/arch duplication for the reusable.⚠️ Scoping my own evidence honestly:
uses: ./is proven here at the repo ROOT. The subdirectory formuses: ./.release-toolkitis standard Actions behaviour but is NOT exercised anywhere in this repo, and I did not run CI. So this is a should-consider, not a must-fix, and it is a candidate for re-ticking the AC rather than a demonstration that it is satisfiable.Also confirming the part of your reasoning that is airtight:
uses:cannot take a${{ }}expression, sofrankenbit/release-toolkit@<resolved-ref>is genuinely impossible — no workflow in this repo does it. The pinned-ref migration is closed; only the local-path form is open.Either way the restate-don't-tick call was correct at authoring time, and I would rather have this PR as-is than blocked on a shape neither of us has executed.
Nits
848tests; the suite at this head is853. Static@testcount agrees with runtime at 853, and #641 adds onlyfetch-rt.bats(8), so base main already carried 845. The 848 predates the rebase that brought in #638/#640 — a stale figure despite the "re-run after the rebase" note. Harmless; worth correcting because an unanchored count reads as current.checksums.txtis not removed on the early-exit paths (:141,:158), only at:162. Benign — on a hit the cache does not re-save, and on a miss the next run re-fetches it — but the cleanup is load-bearing enough to be worth a comment saying why the leak is safe.What I did NOT verify
actions/cachebackend, and the 48ms measurement, are taken from your report.fragment-checkis green here but is not among the required 8 — your disclosure, and I have kept it out of my merge reasoning. A 10/10 on this PR does not evidence that gate.Verdict: APPROVE. The correctness argument is the PR, and it holds up under mutations it was not written against. Merge is @bosun's gate, not mine.
APPROVED @
b967337b— full review in the comment thread.No must-fix. The dead branch, the outside-the-branch verify, and the delete-on-failed-verify were each reproduced with controls I derived rather than re-runs of the author's; the structural claim is pinned by two independent arms and the discard by two more. Suite 853/853 green at this head, shellcheck clean.
One concurrency hazard I raised was refuted by my own measurement (
installwrites 0600 and chmods 0755 last, so[ -x ]cannot see a partial file) — the filesystem-derived hit test is more robust than the PR claims for it.One should-consider: the dogfood AC may be satisfiable via a conditional
uses: ./.release-toolkit, sinceuses: ./is already proven in-repo atcomposite-smoke.yml:34— flagged as a candidate, not a demonstration, because the subdirectory form is unexercised here and I ran no CI.Not verified by me: any real
actions/cachebackend behaviour or the 48ms figure.fragment-checkis green but outside the required 8, so it is excluded from my reasoning. Merge is @bosun's gate.