Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): retire the remaining early-exit pipe consumers (client#686) - #688
Conversation
…686) The sites #680 and #683 did not reach. Same transform: capture the producer, match the captured value, so the producer always runs to completion. `case` where the needle is a fixed substring — it also drops the `A && B` set -e subtlety two of these carried. Fixed — misbranch (inside `if`/`&&`, so pipefail's 141 reads as "no match"): - detect-gpu.sh:28,34 -- `lspci | grep -qi` -> GPU_VENDOR left "none" on a GPU host, i.e. a CPU-mode cluster. lspci is the one producer here that is routinely large enough to lose the race on its own (a dense server enumerates well past a stdio buffer), and one capture now serves both probes plus the AMD label. - install-client-helm.sh:449 -- repo believed absent -> re-runs `helm repo add`, which is unguarded on the next line and fails when the name exists with a different URL, escalating the misbranch into an aborted install. - install-client-helm.sh:594 -- loses --reset-then-reuse-values, so a reconcile silently stops picking up new chart defaults. `helm upgrade --help` is several KB in chunks and the flag sorts early. - install-client-helm.sh:835 -- sticky 8.4 lost -> resolves 5.7 against an 8.4 datadir, which MySQL 5.7 will not open. - setup-linux.sh:281,348 -- docker-group membership misread; 348 is nested now so the two mode guards still short-circuit ahead of `id`, which the old `&&` also did. - setup-linux.sh:898 -- nvidia runtime not detected -> CPU-only cluster on a Tier-0 GPU host that already has the toolkit. - setup-linux.sh:1151 -- the capture was already there (Asad #458); this drops the leftover `printf | grep -q` re-pipe of it. Fixed — abort: - preflight.sh:100 -- `findmnt | head -1` in an ASSIGNMENT, so 141 aborts the installer inside preflight with no message. The sibling mount pipeline two lines down was fixed in #680; this one was missed. Note errexit only propagates out of a command substitution on bash >= 4.4, so this bites on Linux (where findmnt exists at all) and not on the macOS system bash. Hardening, not live bugs — the shape is retired but the abort cannot happen today, and the commit says so rather than implying a field fix: - install.sh:538 -- the cosign checksum slice. Its only caller is `if ! ensure_cosign`, and a condition context suppresses errexit for the whole function, so the 141 is swallowed and `want` is already correct. Retired anyway: a function in the signature-verification path should not depend on how its caller happens to be written. - common.sh:262 -- argument position, where a 141 never trips errexit. Deliberately NOT changed, with the reason, so the next sweep does not re-open them: - diagnose.sh:61,96 -- `run_diagnose` runs `set +e` as its first statement, so no site in that function can abort. The support bundle was never at risk. - gpu-plugins.sh:112 -- the `|| echo ""` already guards it, and `head -5` has emitted its lines before the SIGPIPE propagates, so RAW keeps the correct value (verified: the pre-fix pipeline returns 141 but RAW is intact). - detect-gpu.sh:22,23,36 -- argument position inside `success`/`log`. - preflight.sh:727, common.sh:393, and the `awk`-without-`exit` sites -- a builtin printf under the buffer, an existing `|| true`, or a consumer that reads to EOF. 14 tests across 4 files, every one checked against the pre-fix code. Two things make them non-vacuous and both were got wrong first: the match must LEAD (a trailing match makes grep read the whole stream), and the filler must come from an EXTERNAL command — a producer built from bash builtins, or a mock ending in `return 0`, masks the SIGPIPE and the test passes unfixed. The preflight test additionally calls the function BARE, because the production command- substitution shape cannot abort on the bash 3.2 the suite runs on locally. setup-linux.bats' `id -nG` shape assertion is updated: it pinned the old `| grep -qw docker` text. It still pins what it was written to pin — that both probes key off $_grant_user and never bare $USER. Refs tracebloc/backend#1778 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…libs The bootstrap verifies every sub-script it fetches against this manifest before running the privileged steps, so editing common/detect-gpu/install-client-helm/ preflight/setup-linux without regenerating it makes the installer refuse its own scripts. Produced by scripts/gen-manifest.sh. install.sh itself is the bootstrap and is not listed in its own manifest, so the cosign change there needs no hash. Refs tracebloc/backend#1778 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…early-exit-pipe-sites # Conflicts: # scripts/manifest.sha256
LukasWodka
commented
Aug 12, 2026
CI triage — |
saadqbal
left a comment
There was a problem hiding this comment.
Clean, careful sweep. Traced all 10 converted sites: each preserves exact semantics (first-line / match-boolean / first-field via case), every new capture var is local, and answer-correctness holds — a genuine producer failure still lands on empty-then-return 1 or the safe default, same as before, never a silent wrong answer. The re-exec guard staying nested so the two mode checks short-circuit ahead of id is the right call. Manifest matches the five changed libs (recomputed). New bats are non-vacuous — match leads, filler from an external seq, explicit no-inversion cases — and green locally.
One non-blocking follow-up: detect-gpu.sh:22-23 still has nvidia-smi … | head -1 in argument position — exactly the shape you retired in assert_tool_runs here, two branches above the lspci block you did convert. A multi-GPU host emits one line per card so head SIGPIPEs nvidia-smi; it's abort-safe (arg position) and answer-correct, so not a bug, just the one spot in a touched file the fleet-wide sweep skipped. Fine to leave for a follow-up.
Uh oh!
There was an error while loading. Please reload this page.
Closes#686.
The sites #680 and #683 did not reach. Same transform every time: capture the producer, match the captured value, so the producer always runs to completion —
casewhere the needle is a fixed substring, which additionally drops theA && Bset -e subtlety two of these carried.I re-derived reachability per site rather than taking the issue's list at face value, and three of its entries do not hold. Those are left alone, with evidence, so the next sweep doesn't re-open them.
The mechanism, measured
Four contexts, measured on this branch rather than reasoned about — they decide which sites are real:
set -euo pipefailv="$(producer | head -1)"cmd "$(producer | head -1)"if producer | grep -q Xset +eif ! fTwo more that shaped the fixes:
lspci,helm upgrade --helpandkubectl get nodesare the genuinely exposed producers andid -nGis not.preflight.sh:100aborts on Linux (the only placefindmntexists) and not locally — which also made my first test for it vacuous.Sites changed
Misbranch — inside
if/&&, so pipefail's 141 is read as "no match"lib/detect-gpu.sh:28,34lspci | grep -qi→GPU_VENDORstaysnoneon a GPU host → CPU-mode cluster. One capture now serves both probes and the AMD label.lib/install-client-helm.sh:449helm repo add, which is unguarded on the next line and fails when the name exists with a different URL — the misbranch escalates into an aborted installlib/install-client-helm.sh:594--reset-then-reuse-values→ reconcile silently stops picking up new chart defaultslib/install-client-helm.sh:835lib/setup-linux.sh:281usermod/warnlib/setup-linux.sh:348id, exactly as the old&&didlib/setup-linux.sh:898lib/setup-linux.sh:1151printf | grep -qre-pipe of itAbort
lib/preflight.sh:100findmnt | head -1in an assignment → 141 aborts the installer inside preflight with no message. The siblingmountpipeline two lines down was fixed in #680; this one was missed. Linux-only, per the bash-version note above.Hardening — shape retired, but it cannot abort today
Called out as such rather than dressed up as field fixes:
install.sh:538if ! ensure_cosign, and a condition context suppresses errexit for the whole function, so the 141 is swallowed andwantis already correct. Retired anyway: a function sitting in the signature-verification path should not depend on how its caller happens to be written, and one bare call would make it an abort of the bootstrap.lib/common.sh:262Sites deliberately left alone
The issue lists these as bugs; they are not.
lib/diagnose.sh:61,96run_diagnoseexecutesset +eas its first statement (line 42), before every site in the function, and it is only ever entered viainstall-k8s.sh:117. No site in that function can abort. The support bundle was never at risk — the issue's "would break diagnostics exactly when they are needed" does not hold.lib/gpu-plugins.sh:112|| echo ""already guards the abort, and the value survives:head -5has emitted its five lines into the capture before the SIGPIPE propagates. Measured — the pre-fix pipeline returns 141 whileRAWstill holds the correct five keys. Changing it would have been churn on a signed-manifest file for no behaviour change.lib/detect-gpu.sh:22,23,36success/log; proven not to trip errexit, andhead -1captures the right value regardlesslib/preflight.sh:727printfbuiltin emitting a sub-1 KB issuer string — one write, far under the pipe buffer, so SIGPIPE is impossiblelib/common.sh:393|| truecommon.sh:591,preflight.sh:84,106,install.sh:320,322,install-client-helm.sh:78,245,576,diagnose.sh:135awkwithoutexit, or atailconsumer — all read the stream to completioncommon.sh:847(Darwin ARCH),cluster.sh,assess.sh,setup-macos.shMutation evidence
Every test was run against the pre-fix code. Where a function has fallback probes the whole function was reverted, per the #683 lesson.
detect-gpu.shlspci block (both probes + label)preflight.sh:100[ "$status" -eq 0 ], i.e. the abort reproducedsetup-linux.sh:898install-client-helm.sh:449+:835(both reverted together)Two of my tests were vacuous on the first cut and are worth flagging, because they are the traps this class sets:
preflighttest used the productiont="$(_pf_fstype …)"shape and passed against unfixed code — errexit does not propagate out of a command substitution on bash 3.2, which is what the suite runs on locally. It now calls the function bare, which asserts the same contract on every bash._resolve_chart_refmock ended inreturn 0, which maskedseq's SIGPIPE; and the_resolve_mysql_enginetest ran on arm64, where the fallthrough also yields 8.4 so the misbranch was invisible. Fixed by dropping the mock'sreturn 0and pinningARCH=x86_64.Both are now noted in the test comments so the next person doesn't repeat them. Filler comes from an external command (
seq) throughout — a producer built only from bash builtins does not reproduce a real command's SIGPIPE death.Test plan
bash scripts/gen-manifest.sh— regenerated; on my own commit exactly 5 hashes changed, matching the 5 edited libs (gpu-plugins.shunchanged, confirming that revert was clean).install.shis the bootstrap and is not listed in its own manifest.make check— green (style guard, shellcheck, drift, helm lint on 4 value sets + ingestor), before and after the merge below.make bats— 937 tests, 0 failures on my commits alone, up from 923 on thedevelopI branched from (+14: 6detect-gpu.batsnew file, 4install-client-helm.bats, 3setup-linux.bats, 1preflight.bats). 960, 0 failures after merging currentdevelop.bash -nclean on all 6 edited scripts; shellcheck reports nothing new (the remainingcommon.shwarnings are pre-existing and in untouched lines).#683 merged mid-flight
#683 landed on
developwhile this was in progress, touchingcommon.sh,setup-linux.shandmanifest.sha256— all files this PR also edits.developis merged in here: the libs auto-merged (its_record_err/install_cleanupwork and my probe rewrites are disjoint), and the only conflict wasmanifest.sha256, resolved by regenerating it from the merged file contents rather than hand-picking either side.make checkand the full suite were re-run on the merged tree.scripts/tests/detect-gpu.batsis new —detect-gpu.shhad no suite at all.One existing test updated
setup-linux.bats'id -nGassertion grepped the source for the literalid -nG "$_grant_user" … | grep -qw docker, which this change removes. It still pins what it was written to pin — that both probes key off$_grant_userand never bare$USER— plus that the captured value is word-matched rather than substring-matched.🤖 Generated with Claude Code