#!/bin/bash # ══════════════════════════════════════════════════════════════════════════════════════════════ # PURPOSE # Find guards that cannot fire. A script that writes # # do_the_work || STEP_OK=false # # is only telling the truth if do_the_work can actually return non-zero. When it cannot, the # flag stays true no matter what happened, and the run reports a step it never completed. # # This is the most expensive bug shape in this repo — a summary that says ✅ is the thing the # operator trusts instead of reading the log. adapter.sh already carries the scar: "every # caller that wrote `platform_push_setup_state || X=false` was testing a constant." # # Written 2026-08-24. Its first run found partnership_offboard.sh Step 3 reporting a stack # cleanup that could not fail and, on a second path, one that had not run at all. # # OPERATIONAL MODEL # For every `cmd || VAR=false` in the tree, the command left of the || is resolved and # classified: # # CANFAIL a shell function with an explicit non-zero exit path — the guard is real # EXTERNAL chown, docker, ssh, rsync and friends — can fail, nothing to check # IDIOM [[ … ]] && X=true || X=false — a conditional, not a guard # NEVERFAILS a function with no non-zero path — the guard is decorative # UNKNOWN could not be resolved; reported rather than assumed either way # # A function "can fail" if it contains `return` or `exit` with a non-zero literal or any # variable. The variable case matters: cleanup_partner_containers ends `return "$_rc"`, and a # pattern that only looked for digits reported a working guard as broken. # # UNKNOWN is a first-class verdict, not a failure. Deciding what a bare `done` returns means # evaluating the last command of the last loop, and a checker that guesses at that would # produce confident wrong answers in both directions. # # DESIGN PRINCIPLES # Resolve the command, never word-split it. # An early build stripped a leading "do" to handle `for x in …; do cmd || F=false; done` # and turned do_final_sync into a phantom _final_sync that resolved to nothing. The # inline-loop case is matched on "; do " with required trailing space; nothing else is # trimmed from a command name. # # Comments are excluded before anything else. # Two of the matches in this repo are prose describing the bug, in adapter.sh and # rsync_stop.sh. A checker that reports the documentation of a fixed bug as the bug # teaches the operator to skim its output. # # Every verdict names where it looked. # A NEVERFAILS line carries the file and line of the function it judged, because the # first question is always "which definition did it find" — a name can exist twice. # # OPERATIONAL SAFEGUARDS # Read-only. Greps and reads; writes nothing, executes nothing it finds. # Exits 1 when any NEVERFAILS is reported, 0 otherwise — safe to gate a commit on. # UNKNOWN never fails the run. It is a prompt to look, not a defect claim. # # RUNTIME MODES # ══════════════════════════════════════════════════════════════════════════════════════════════ # # audit_guards.sh # Classify every guard in the repo. Exits 1 if any guard cannot fire. # # audit_guards.sh --all # Show every verdict, including the guards that are sound. # # audit_guards.sh --unknown # Show only the guards that could not be resolved. # # ══════════════════════════════════════════════════════════════════════════════════════════════ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SHOW_ALL=false ONLY_UNKNOWN=false for a in "$@"; do case "$a" in --all) SHOW_ALL=true ;; --unknown) ONLY_UNKNOWN=true ;; *) echo "unknown argument: $a" >&2; exit 2 ;; esac done cd "$REPO_ROOT" || { echo "cannot reach repo root: $REPO_ROOT" >&2; exit 2; } # Locate a function definition and print "file:line" followed by its body. _fn_body() { local fn="$1" loc f ln loc=$(grep -rnE "^[[:space:]]*(function[[:space:]]+)?${fn}[[:space:]]*\(\)" \ --include='*.sh' . 2>/dev/null | grep -v '^./.git' | head -1) [[ -z "$loc" ]] && return 1 f=${loc%%:*} ln=$(echo "$loc" | cut -d: -f2) echo "${f#./}:$ln" awk -v s="$ln" 'NR>=s { print; if (NR > s && /^\}/) exit }' "$f" } dead=0 sound=0 unknown=0 other=0 while IFS= read -r hit; do file=${hit%%:*} lno=$(echo "$hit" | cut -d: -f2) code=$(echo "$hit" | cut -d: -f3-) # Everything left of the ||, with an inline loop header removed. The trailing space after # "do" is required — without it this eats the prefix of do_final_sync. cmd=$(echo "$code" | sed 's/[[:space:]]*||.*//' \ | sed 's/^[[:space:]]*//' \ | sed 's/^.*;[[:space:]]*do[[:space:]]\{1,\}//') head=$(echo "$cmd" | awk '{print $1}') verdict=""; detail="" case "$head" in '[['|'[' | test ) verdict=IDIOM; detail="conditional, not a guard" ;; chown|chmod|find|rm|mv|cp|docker|ssh|rsync|systemctl|timeout|curl|git|mkdir|ln|tar ) verdict=EXTERNAL; detail="external command, can fail" ;; -*|2\>*|1\>*|\>* ) # A guard written across several lines: the match landed on a continuation of an # external command (find … -exec …, a redirect) rather than on its head. Reading # back to the head would mean parsing line continuations; the classification is # the same either way, so it is recorded as what it is. verdict=EXTERNAL; detail="continuation of a multi-line external command" ;; esac if [[ -z "$verdict" ]]; then if body=$(_fn_body "$head"); then where=$(echo "$body" | head -1) body=$(echo "$body" | tail -n +2) # Non-zero literal, or any variable — quoted or bare. if echo "$body" | grep -qE '^[[:space:]]*(return|exit)[[:space:]]+("?\$|[1-9])'; then verdict=CANFAIL; detail="$where" else verdict=NEVERFAILS detail="$where — no non-zero exit path" fi else verdict=UNKNOWN; detail="could not resolve '$head'" fi fi case "$verdict" in NEVERFAILS) dead=$((dead+1)) ;; CANFAIL) sound=$((sound+1)) ;; UNKNOWN) unknown=$((unknown+1)) ;; *) other=$((other+1)) ;; esac if $ONLY_UNKNOWN; then show=false [[ "$verdict" == UNKNOWN ]] && show=true elif $SHOW_ALL || [[ "$verdict" == NEVERFAILS ]]; then show=true else show=false fi if $show; then printf '%-11s %s:%s\n %s\n %s\n' \ "$verdict" "$file" "$lno" "$(echo "$cmd" | cut -c1-76)" "$detail" fi done < <(grep -rnE '\|\|[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=false' --include='*.sh' . 2>/dev/null \ | grep -v '^./.git' \ | grep -vE ':[0-9]+:[[:space:]]*#' \ | sed 's|^\./||') echo echo "──────────────────────────────────────────────────────────────" printf 'sound %s external/idiom %s unresolved %s CANNOT FIRE %s\n' \ "$sound" "$other" "$unknown" "$dead" [[ $dead -gt 0 ]] && exit 1 exit 0