e45a9c4206
find_modern_bash accepted any 5.x, so a bash 5.0 was pinned and pr-reviews-test.sh then failed on an invalid option. The probe now reads major and minor; a 5.0 case and a 5.1 control guard it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
158 lines
5.5 KiB
Bash
Executable File
158 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# The one entry point for every check_the_checker in scripts/. CI calls THIS,
|
|
# and so does the local gate, so a suite cannot be green under one shell and
|
|
# red under the other. Reasoning: context/code/tooling.md section 3.
|
|
#
|
|
# It does three things a bare `./scripts/X-test.sh` does not:
|
|
#
|
|
# 1. pins a bash >= 5.1, taken from nix when the ambient one is older, so the
|
|
# macOS system bash 3.2 can never be the shell a suite passes under;
|
|
# 2. scrubs the environment down to an allowlist, because `nix develop`
|
|
# exports `name`, `out`, `system`, `shell`, `builder` and two dozen more
|
|
# generically-named variables that CI does not have -- a `set -u` fault
|
|
# reading one of them is SILENT inside the dev shell and fatal in CI;
|
|
# 3. refuses a shell script in scripts/ whose shebang is not
|
|
# `#!/usr/bin/env bash`, so the sh/bash split cannot come back.
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
usage: checkers.sh [--list] [--self-test] [name ...]
|
|
|
|
name a suite to run, spelled without the -test.sh suffix
|
|
(e.g. `pr-reviews` for scripts/pr-reviews-test.sh).
|
|
With no name, every suite in scripts/ runs.
|
|
--list print the suite names and exit
|
|
--self-test run scripts/checkers-test.sh, which injects the faults this
|
|
runner exists to catch
|
|
|
|
exit 0 every suite passed
|
|
exit 1 a suite failed, or a shebang is not `#!/usr/bin/env bash`
|
|
exit 3 no bash >= 5.1 could be found and nix could not supply one
|
|
exit 4 usage error
|
|
|
|
THIS SCRIPT'S `$?` MUST BE THE ONE YOU READ. Nothing may follow it in the same
|
|
pipeline or `;` list -- a trailing `echo "exit=$?"` takes the status.
|
|
EOF
|
|
}
|
|
|
|
# The allowlist. A variable not named here does not reach a suite, which is the
|
|
# whole point: the dev shell's `name` masked improvise-f9qf's fault for a day.
|
|
# CHECKERS_KEEP adds names, space-separated, for a caller that needs one.
|
|
ALLOW="HOME TMPDIR TMP TEMP TERM LANG LC_ALL USER LOGNAME
|
|
CI GITHUB_ACTIONS GITHUB_TOKEN GH_TOKEN
|
|
NIX_PATH NIX_REMOTE NIX_SSL_CERT_FILE SSL_CERT_FILE
|
|
XDG_CACHE_HOME XDG_CONFIG_HOME XDG_DATA_HOME"
|
|
|
|
# Prints the absolute path of a bash >= 5.1, or nothing. The ambient one is tried
|
|
# first so a machine that already has a modern bash pays no nix startup at all.
|
|
find_modern_bash() {
|
|
local candidate major minor
|
|
candidate="$(command -v bash || true)"
|
|
if [ -n "$candidate" ]; then
|
|
read -r major minor < <("$candidate" -c 'echo "${BASH_VERSINFO[0]} ${BASH_VERSINFO[1]}"' \
|
|
2>/dev/null || echo 0 0)
|
|
if [ "$(( ${major:-0} * 100 + ${minor:-0} ))" -ge 501 ] 2>/dev/null; then
|
|
echo "$candidate"; return 0
|
|
fi
|
|
fi
|
|
command -v nix >/dev/null 2>&1 || return 0
|
|
nix shell nixpkgs#bash --command bash -c 'command -v bash' 2>/dev/null || true
|
|
}
|
|
|
|
# Every shell script in scripts/ says `#!/usr/bin/env bash` and nothing else.
|
|
# `#!/bin/sh` is dash in CI and bash 3.2 in sh-mode on macOS: a second platform
|
|
# split, undetected for the same reason the first one was.
|
|
check_shebangs() {
|
|
local bad=0 f first
|
|
for f in "$HERE"/*; do
|
|
[ -f "$f" ] || continue
|
|
first="$(head -1 "$f")"
|
|
case "$first" in
|
|
'#!'*sh|'#!'*sh\ *)
|
|
case "$first" in
|
|
'#!/usr/bin/env bash') ;;
|
|
*) printf 'checkers: %s has shebang %s, wanted #!/usr/bin/env bash\n' \
|
|
"${f##*/}" "$first" >&2; bad=1 ;;
|
|
esac ;;
|
|
esac
|
|
done
|
|
return $bad
|
|
}
|
|
|
|
# A check_the_checker is named after the thing it checks, so `X-test.sh` is a
|
|
# suite only when `X` or `X.sh` is here too. That excludes `node-test.sh`,
|
|
# which is a test RUNNER whose own checker is `node-test-test.sh`.
|
|
suites() {
|
|
local f n
|
|
for f in "$HERE"/*-test.sh; do
|
|
[ -f "$f" ] || continue
|
|
n="${f##*/}"; n="${n%-test.sh}"
|
|
if [ -f "$HERE/$n" ] || [ -f "$HERE/$n.sh" ]; then printf '%s\n' "$n"; fi
|
|
done
|
|
}
|
|
|
|
# $1 the pinned bash, $2 the suite file. The suite runs with the pinned bash
|
|
# FIRST on PATH, so a script it invokes through its own `#!/usr/bin/env bash`
|
|
# -- or through `sh` -- gets the pinned one too, not the system 3.2.
|
|
run_suite() {
|
|
local pinned="$1" file="$2"
|
|
local bindir; bindir="$(dirname "$pinned")"
|
|
local -a env_args=(-i)
|
|
local v
|
|
for v in $ALLOW ${CHECKERS_KEEP:-}; do
|
|
if [ -n "${!v+set}" ]; then env_args+=("$v=${!v}"); fi
|
|
done
|
|
env_args+=("PATH=$bindir:${PATH:-/usr/bin:/bin}")
|
|
env "${env_args[@]}" "$pinned" "$file"
|
|
}
|
|
|
|
main() {
|
|
local self_test=0
|
|
local -a names=()
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--list) suites; return 0 ;;
|
|
--self-test) self_test=1 ;;
|
|
-h|--help) usage; return 4 ;;
|
|
-*) printf 'checkers: unknown option %s\n' "$1" >&2; usage; return 4 ;;
|
|
*) names+=("$1") ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
local pinned; pinned="$(find_modern_bash)"
|
|
if [ -z "$pinned" ]; then
|
|
echo 'checkers: no bash >= 5.1 on PATH and nix cannot supply one.' >&2
|
|
echo ' Enter the dev shell (direnv allow / nix develop) and retry.' >&2
|
|
return 3
|
|
fi
|
|
printf 'checkers: bash %s at %s\n' \
|
|
"$("$pinned" -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"')" "$pinned"
|
|
|
|
local rc=0
|
|
check_shebangs || rc=1
|
|
|
|
if [ "$self_test" = 1 ]; then
|
|
names=(checkers)
|
|
elif [ ${#names[@]} -eq 0 ]; then
|
|
while IFS= read -r line; do names+=("$line"); done < <(suites)
|
|
fi
|
|
|
|
local n file
|
|
for n in ${names[@]+"${names[@]}"}; do
|
|
file="$HERE/$n-test.sh"
|
|
if [ ! -f "$file" ]; then
|
|
printf 'checkers: no such suite: %s\n' "$n" >&2
|
|
rc=4; continue
|
|
fi
|
|
printf '\n=== %s ===\n' "$n"
|
|
run_suite "$pinned" "$file" || rc=1
|
|
done
|
|
return $rc
|
|
}
|
|
|
|
main "$@"
|