Find guards wired onto a helper that can never return non-zero
This commit is contained in:
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user