Uh oh!
There was an error while loading. Please reload this page.
fix(release): retry the cosign call, bounded and transient-only (backend#2379) - #563
Conversation
…end#2379) On 2026-08-23 tag v0.10.10-rc.2 was cut by the 07:04 staging hop. One of release.yml's eight legs -- darwin/arm64 -- died on a sigstore TUF CDN reset (`read: connection reset by peer` fetching 10.root.json). The tag is the workflow's TRIGGER, so it already existed; the GitHub Release is created by `publish`, which is gated on all eight legs. So one flaky leg left a git tag with no Release and no assets -- a broken publish channel -- for 6h04m, until `gh run rerun --failed` at 13:07. fr-assist's cli smoke battery caught it, not the build. The gate is not the bug and is untouched: a release missing darwin/arm64 is worse than no release. The unretried network call is the bug, and it is retried here -- on the one cosign call, not by re-running the job, which would redo seven good legs and widen the very window this closes. scripts/cosign-retry.sh wraps `cosign`, forwarding its arguments verbatim: BOUNDED at most 3 attempts, backoff 5s then 15s, per-attempt cap 120s enforced in bash (not coreutils `timeout`, absent on macOS, which would leave the cap unverifiable locally). Worst case 6m20s, inside the job's timeout-minutes: 20. CLASSIFIED only output matching a known transient sigstore/network signature is retried. A rejected OIDC token or a missing blob fails on the FIRST attempt -- an indiscriminate retry turns a real signing refusal into a slow failure, which is worse than the bug. FAIL CLOSED cosign exiting 0 is a claim; the artifact is the evidence. Every --output-* file named on the command line (derived from the actual argv, not a second naming convention) must exist and be non-empty, or the leg fails -- unretried, because a zero exit with no signature is not a network symptom. scripts/tests/cosign-retry-verify.sh pins all three by driving the real script with cosign replaced by a PATH shim, asserting exit status AND the number of cosign invocations per case, so a test cannot pass by failing for a different reason than its name. Its failure texts are written independently of the script's pattern list -- including the verbatim 2026-08-23 message -- since a list checked against itself is blind. It runs in build.yml's Installer (shell) job: hermetic, no network, no tag needed. Mutation-proved, 7 mutations, each anchor asserted to apply exactly once and each mutant asserted to parse (an inert mutation and real coverage look identical in a log): classifier removed -> the two "genuine refusal" cases redden; fail-closed check disabled -> both artifact cases redden; default budget 3->1 -> the shipped-defaults case reddens; budget comparison off by one -> both attempt-count cases redden; cap disarmed -> the hung-attempt case reddens; backoff constant -> both delay cases redden; short-schedule guard disabled -> the config-refusal case reddens. Baseline and restored both 15/15. RELEASE_CHECKLIST.md gains the recovery for "tag exists, no Release at all", preserving release.yml's reasoning that a re-run or a dispatch AT the tag is the only valid rebuild: cosign embeds the run's ref in the keyless identity and the installers trust only @refs/tags/v.*, so a branch dispatch publishes signatures every customer install rejects. Verified: bash -n + shellcheck -S warning -x on both scripts, yaml.safe_load + actionlint on both workflows, harness 15/15, file-budget.sh green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saqlainsyed007
left a comment
There was a problem hiding this comment.
Approving. Reviewed the cosign-retry wrapper at high effort last pass; CI is now green with no open threads.
scripts/cosign-retry.sh is correct on all three properties it claims: BOUNDED (≤3 attempts, 5s/15s backoff, per-attempt 120s cap enforced in bash via background+poll+TERM/KILL returning 124 — portable to macOS where coreutils timeout is absent; worst case 6m20s < the job's 20m); CLASSIFIED (rc=124 and matched sigstore/network signatures retry, everything else is a genuine refusal and exits on the first attempt); FAIL-CLOSED (every --output-* file, derived from argv, must exist and be non-empty or exit 3 — not retried, since a zero exit with no signature isn't a network symptom). set -uo pipefail without -e is right for a loop that inspects failures, and the backoff-schedule length is guarded. The release-workflow wiring is a minimal cosign → wrapper swap; the all-eight-legs publish gate is untouched. The hermetic 15-case harness asserts both exit status and cosign invocation count with independently-written failure texts — mutation-proof.
Green, mergeable, no threads.
Uh oh!
There was an error while loading. Please reload this page.
…-closed (cli#568) (#570) scripts/cosign-retry.sh tracked --output-* paths in a way that failed both directions (two Bugbot findings on PR #563): - FAIL OPEN on stale artifacts: retry attempts never cleared prior --output-* files, so a leftover non-empty .cert/.sig from an EARLIER failed attempt satisfied the `[ -s ]` fail-closed check after a later `exit 0` that wrote nothing -- reporting a signature that does not exist, the one property the harness exists to prevent. Fix: remove every parsed --output-* path before each attempt, so `[ -s ]` is a claim about the current attempt alone. - FAIL CLOSED on equals-form flags: outputs_from_args set prev unconditionally, so after --output-x=val the token still matched --output-* and the next arg was misparsed as a phantom output path, failing a genuine equals-form sign with exit 3. Fix: reset prev after handling the equals form. Tests (scripts/tests/cosign-retry-verify.sh): add a stale-leftover case (exits 0 before the fix, now fails closed), equals-form parse cases, and a wipe-ordering guard. The two bug-catching cases were confirmed to FAIL against the pre-fix script; all 19 pass on the fix. shellcheck --severity=error and bash -n clean, matching the build.yml gates. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Declares `0.10.13` as the next release version so the release train can cut an rc for it. `v0.10.12` is already tagged -- both `v0.10.12` and `v0.10.12-rc.1` exist -- and the delta waiting on `develop` touches `internal/doctor/doctor.go`, under the `internal/*` publish_path in release-train's `repos.yml`: - #563 fix(release): retry the cosign call, bounded and transient-only - #564 feat(doctor): measure disk, the dimension it never looked at With `VERSION` still reading `0.10.12`, the staging hop's version preflight refuses: rc NOT tagged - v0.10.12 already exists and the delta DOES touch published files (bump VERSION on develop before the next release) Measured on the 2026-08-25 staging hop: the `cli` leg failed this preflight, so `cli` sat the hop out. Same shape as the 0.10.12 bump (#562) one day earlier -- each hop that ships published files needs its own bump, and the previous one is consumed by the hop that shipped it. Patch-level, matching the whole 0.10.x series. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Summary
v0.10.10-rc.2existed as a git tag with no GitHub Release and no assets for 6h04m because one ofrelease.yml's eight legs hit a sigstore TUF CDN reset. This retries that one network call.The ordering, which is the whole defect: the tag is the workflow's trigger, so it exists before any signing happens. The Release object is created by
publish, which isneeds: release— gated on all eight legs. So the tag necessarily precedes the assets, and any single-leg failure leaves a tag with nothing attached. That ordering cannot be reordered away; the only lever is making the flaky call not fail.Measured on run 32624602531:
v0.10.10-rc.2pushed (staging hop, PR #554)darwin/arm64FAILURE —tuf: failed to download 10.root.json … read: connection reset by peerAggregate + create GitHub ReleaseSKIPPEDgh run rerun --failed→ leg green6h04m with a tag and no artifacts. Caught by fr-assist's
cli smoke battery, not by the build.What this does not do
The aggregate gate is untouched.
publishstill requires all eight legs. A release missingdarwin/arm64is worse than no release, and weakening the gate would ship a partially-signed release — strictly worse than the symptom. And the retry is on the cosign call, not the job: a job-level rerun redoes seven good legs and widens the very window this closes.The retry design
scripts/cosign-retry.shwrapscosign, forwarding its arguments verbatim.BOUNDED — at most 3 attempts, backoff 5s then 15s, per-attempt wall-clock cap 120s. Worst case
3×120 + 5 + 15 = 6m20s, inside the job'stimeout-minutes: 20, so the retry can never itself be why a leg is killed. The cap is enforced in bash rather than with coreutilstimeout, which is absent on macOS — an untestable branch is how a cap stops being real.CLASSIFIED — only output matching a known transient sigstore/network signature is retried. A rejected OIDC token (
401 Unauthorizedfrom Fulcio) or a missing blob fails on the first attempt, withNOT retryingin the log. This is the point the ticket is sharpest about: an unbounded or indiscriminate retry converts a genuine signing refusal into a slow, noisy failure, which is worse than the bug being fixed.FAIL CLOSED —
cosignexiting 0 is a claim; the artifact is the evidence. Every--output-*file named on the command line must exist and be non-empty afterwards, or the leg fails. The file list is derived from the actual argv, not from a second copy of the naming convention. This case is not retried: a zero exit with no signature is not a network symptom.Verification
scripts/tests/cosign-retry-verify.sh(15 cases, wired intobuild.yml'sInstaller (shell)job — hermetic, no network, no tag required) drives the real script withcosignreplaced by a PATH shim. Every case asserts the exit status and the number of cosign invocations, so a case cannot pass by failing for a different reason than its name claims. Its failure texts are written independently of the script's pattern list — including the verbatim 2026-08-23 message — because a list checked against itself is blind.Mutation-proved, 7 mutations. For each: the anchor was asserted to occur exactly once and the mutant asserted to parse before trusting the red — an inert mutation and real coverage are indistinguishable in a log.
-ge→-gtBaseline 15/15, restored 15/15, zero mutation markers left behind.
Also run:
bash -n+shellcheck -S warning -xon both scripts (both now inbuild.yml's shellcheck list),yaml.safe_load+actionlinton both workflows,scripts/file-budget.shgreen.Docs
RELEASE_CHECKLIST.mdgains the recovery for "tag exists, no Release at all", preservingrelease.yml's reasoning that a re-run — or a dispatch at the tag ref — is the only valid rebuild: cosign embeds the run's ref in the keyless identity and the installers trust only@refs/tags/v.*, so a branch dispatch publishes signatures every customer install rejects (Bugbot on promotion #428).Audit: is this the first instance, or the first noticed?
Ran
git tagvs the actual published artifact across the five version-cutting repos named in release-train'sPUBLISH-PATHS.md.cli,clientanddata-ingestorsare 1:1 and clean.design-systemandtracebloc-py-packagecreate no GitHub Releases by design, so they were checked against their real registries instead.One other instance found, a different repo and mechanism — reported separately, not fixed here:
tracebloc-py-packagev0.18.0(tagged 2026-08-06) never published.Publish to TestPyPIsucceeded, thenVerify install from TestPyPI— asleep 60followed by a single un-retriedpip install— lost the index-propagation race, sopublish-pypiwas skipped and0.18.0exists on neither TestPyPI nor PyPI 18 days later. Same shape as this bug: one un-retried third-party call between the tag and the artifact.Closes tracebloc/backend#2379
Note
Medium Risk
Touches keyless signing in the release path, but keeps the all-legs publish gate and only retries classified transients; a classifier miss could still flake or delay a genuine refusal.
Overview
Stops a single flaky Sigstore
sign-blobfrom leaving a git tag with no GitHub Release.publishstill requires all eight matrix legs; the retry is on that one network call, not a job re-run.scripts/cosign-retry.shwrapscosignwith at most 3 attempts (5s/15s backoff, 120s per-attempt cap), retries only known TUF/CDN/transport failures, and fails closed if a zero exit produces empty--output-*artifacts. Genuine refusals (OIDC 401, missing blob) fail on the first attempt.A hermetic PATH-shim harness (
cosign-retry-verify.sh) is wired into the Installer job, andRELEASE_CHECKLIST.mddocuments tag-with-no-assets recovery (rerun failed jobs or dispatch at the tag ref).Reviewed by Cursor Bugbot for commit 31f018b. Bugbot is set up for automated code reviews on this repo. Configure here.