ci: make a failed Clang download fail the step that downloads it, at all ten sites (fixes #681, refs #674) - #684
Conversation
…all ten sites (fixes #681) Ten steps across `ci.yml` and `mutation.yml` installed the pinned Clang with wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} `wget -q` writes no error document, so on an HTTP 4xx/5xx it exits 8 having written zero bytes. `bash` then reads an empty script, does nothing, and exits 0. GitHub runs a bare `run:` as `bash -e {0}` -- errexit, and *not* pipefail -- so the pipeline's status is bash's. The step reported success having installed no compiler, and the leg went red several steps later at Configure, naming a missing `clang++` rather than the download that never happened. All ten now use the same three lines the `Install sccache` steps got in #683 (morph#672), so the repository has one download idiom rather than two: curl -sSL --fail -o /tmp/llvm.sh https://apt.llvm.org/llvm.sh test -s /tmp/llvm.sh sudo bash /tmp/llvm.sh ${{ env.CLANG_VERSION }} `--fail` makes curl report the HTTP status as its own exit code; writing to a file means nothing downstream runs when it does not arrive; `test -s` covers the one case `--fail` cannot see, a 200 with an empty body. This is not a retry policy and it does not make an apt.llvm.org outage less likely -- it makes the step that failed be the step the log names. The sites: ci.yml's linux-compilers, linux-sanitizers, linux-coverage, kanban-tsan, bank-sanitizers, ladder-sanitizers, linux-all-features (the `else` branch of its gcc/clang split), clang-format and clang-tidy, plus mutation.yml's install step. The full argument lives once, on linux-compilers; the other nine carry a three-line pointer to it. Proof, not assertion: each shape written to a file and run as `bash -e <file>`, which is what GitHub does, against a local origin that serves `/llvm.sh` and 404s everything else. =========== OLD SHAPE, 404 (the defect) =========== step exit=0 =========== OLD SHAPE, 200 (the happy path) =========== llvm.sh ran, version argument = 22 step exit=0 =========== NEW SHAPE, 404 (must be non-zero) =========== curl: (22) The requested URL returned error: 404 step exit=22 =========== NEW SHAPE, 200 EMPTY BODY (must be non-zero) =========== step exit=1 =========== NEW SHAPE, 200 (must be zero and must run the script) =========== llvm.sh ran, version argument = 22 step exit=0 The old shape's 404 and its 200 are indistinguishable, which is the ticket. And against apt.llvm.org itself rather than a local stand-in: $ curl -sSL -o llvm.sh https://apt.llvm.org/does-not-exist.sh exit=0 564 bytes of error page $ curl -sSL --fail -o llvm.sh https://apt.llvm.org/does-not-exist.sh curl: (22) The requested URL returned error: 404 exit=22 Measured on 7d4ca45, GNU wget 1.25.0, curl 8.22.0, bash 5.3.15, Arch Linux. Not verified: the behaviour of `ubuntu-24.04`'s own curl and wget builds, and no CI run has yet exercised a deliberate 404 on a runner. The claim this rests on is that `curl --fail` exits non-zero on an HTTP error status, which is curl's documented contract and is the same property #683 already depends on at ten sccache sites on those same runners. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Runner verificationAll ten converted, zero executable occurrences left: I briefly thought one had been missed — The before/after proof is the right shape and the mutation makes it evidence: Running each shape as On the count: nine versus ten — the tree moved, neither of us was wrongMy triage said nine sites; this PR converted ten and says my number was wrong. It was correct when measured and went stale within the hour: #683's own new #674 — the handback is right, and the blocker is mine to resolveHanding it back was correct, and for a better reason than cost: the measured build-time delta (~1.7 min on the critical path, ~3–7% of a run) is acceptable, and the lane says so rather than hiding behind it. The real blocker is a lane boundary I drew: finishing #674 means rewriting nine Three findings there are worth more than the fix, and all are measured on
Leaving Not verified: no runner has pointed a step at a 404 — all #681 evidence is local (curl 8.22.0 on Arch), and 🤖 Generated with Claude Code |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
One commit, one ticket. #674 is handed back rather than implemented — the reasoning, the measurements and a working design are in a comment on the issue, summarised at the bottom.
#681 — a failed Clang install reported success, at ten sites
Ten steps across
ci.ymlandmutation.ymlinstalled the pinned Clang withwget -qwrites no error document, so on an HTTP 4xx/5xx it exits 8 having written zero bytes;bashreads an empty script, does nothing, exits 0. GitHub runs a barerun:asbash -e {0}— errexit, not pipefail — so the pipeline's status is bash's. The step reported success having installed no compiler, and the leg went red several steps later at Configure, naming a missingclang++rather than the download that never happened.All ten now run the same three lines #683 gave the
Install sccachesteps, so the repository has one download idiom rather than two:--failmakes curl report the HTTP status as its own exit code; a file means nothing downstream runs when the download did not arrive;test -scovers the one case--failcannot see, a 200 with an empty body.Sites:
linux-compilers,linux-sanitizers,linux-coverage,kanban-tsan,bank-sanitizers,ladder-sanitizers,linux-all-features(theelsebranch of its gcc/clang split),clang-format,clang-tidy, andmutation.yml's install step. The full argument lives once onlinux-compilers; the other nine carry a three-line pointer to it. The ticket's triage comment says nine sites and the issue body says ten — ten is right, and all ten are done.Proof that a 404 now fails the step
Not "verified against a working URL", which is the defect the ticket is. Each shape was written to a file and run as
bash -e <file>— what GitHub actually does — against a local origin serving/llvm.shand 404ing everything else:The old shape's 404 and its 200 are indistinguishable. And against apt.llvm.org itself rather than a stand-in:
Review reasoning, inline (no
/code-review, no/simplify)--fail) restores exit 0 on the 404, which is the OLD-SHAPE row above. The two rows are the same command differing only in the flag.test -searn its line? Yes, and it is the row--failalone fails: a 200 with an empty body exits 1 only because of it.sudo bash <file>vssudo bash -s --. Argument passing is unchanged:sudo bash /tmp/llvm.sh 22passes22as$1exactly as-s -- 22did. The 200 rows above assert it (version argument = 22).curlexist on every runner that runs these steps? All ten are Linux (ubuntu-24.04or self-hosted), and ci: three robustness gaps — a check that refused the narrow case, a download that blamed tar, and a sanitizer matrix the bank example was never in (fixes #675, fixes #679, refs #672) #683 already putcurl --failon tenInstall sccachesteps on the same runners, several of them in the same jobs. Reusing the idiom rather than inventing awget -Oone keeps that to a single assumption./tmp/llvm.shis written unprivileged and then run undersudo. Unchanged from the piped form, which also fedsudo bashbytes fetched unprivileged over the same TLS connection.docs/superpowers/plans/2026-08-18-kanban-rung4-completion.mdstill contains the old line and is deliberately untouched: it is a dated plan document, not a live workflow.What I did not verify
No CI run has yet pointed a step at a 404 on a runner. Everything above is local: GNU wget 1.25.0, curl 8.22.0, bash 5.3.15, Arch Linux, measured on
7d4ca453. The behaviour ofubuntu-24.04's own curl build is unverified.The one claim this branch rests on:
curl --failexits non-zero on an HTTP error status. That is curl's documented contract, it is what the apt.llvm.org measurement above shows, and it is the same property tenInstall sccachesteps already depend on on these runners.Gates run before pushing
check_workflow_job_banners.py,check_workflow_option_coverage.py,check_catch2_pin.sh,check_ci_clang_pin.sh,check_bidi_controls.py— all exit 0 — and every workflow parses as YAML.#674 — handed back, with a working design and the numbers
Not on build-time cost: that is acceptable. Cold, no compiler cache,
-j4, the shallow clone is 1.7 s, the configure delta 0.5 s, and Catch2's 108 objects build in 98.5 s; 17 Linux legs installcatch2from apt today, so the critical path grows ~1.7 min (3-7% of a 26-74 min run). Warm cost should be near zero via sccache — inferred, not measured.Three things came out of the measurements that the ticket does not have:
find_packageresolves to/usr/include, which CMake omits as an implicit directory. The divergence is real; it is the default system include path, one level below where the ticket puts it. Read literally, the ticket's own "close as invalid" condition is satisfied by this measurement, and it should not close as invalid.CMAKE_DISABLE_FIND_PACKAGE_Catch2+ a fetched Catch2, exactly as the ticket names it, turns the clang-tidy job permanently red. A fetched include dir arrives as-I, so clang-tidy treats Catch2 macro expansions as user code: on one test TU, 1 finding becomes 45 (21cppcoreguidelines-avoid-do-while, 11misc-use-anonymous-namespaceout ofREQUIRE/TEST_CASE).FetchContent_Declare(... SYSTEM)restores exact parity — measured at 1 finding, the same one — and the suite passes:100% tests passed out of 1564.NOLINT(bugprone-chained-comparison)the nine*/tests/.clang-tidyfiles say arrived in 3.15.3. Pinning it makes all nine suppressions inert and their prose (CI pins catch2 3.4.0 -- ubuntu-24.04's package) false.Which is why it is handed back rather than half-landed: finishing it requires rewriting those nine files and retiring
scripts/check_catch2_pin.shin the same change, and one of the nine isexamples/bank/tests/.clang-tidy, held by a concurrent lane. Deleting the gate leaves a false claim unchecked; reducing the gate makes it fail on that file; editing the file crosses a lane boundary. Shipping the CMake half alone is precisely the "two mechanisms claiming the same thing" the ticket warns against.A side finding recorded on the issue: because the current code already fetches v3.8.1 when no system Catch2 is present, those nine suppressions are already inert for any contributor without the distro package, on
7d4ca453, before anything changes.🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW