igneum/tools/ci/scratch-spare-check.sh
igneum-labs 210084b0e5 build server: the remote checkout's clean spares a lane's scratch (AP-H1, the lost-scratch class)
remote-run.sh checkout_tree ran `git clean -fd` on the box mirror before every build from any agent, so the attack rows
lost attack-f3/, attack-f1-venv/ and tools/attack/*/target to each other's builds (7 October 2026, 09:2x UK). The clean now
also spares the fixed prefixes attack-*, scratch-*, target-attack-*, .build-remote.log and every glob in the mirror-local
.igneum-scratch-spare (one per line, # comments, the file itself spared), keeps the target and stamp excludes and still runs
without -x. The clean-tree test asks `git clean -nd` with the same excludes instead of filtering the status list, so a spared
dir is not "not clean". --self-test: a fixed-prefix dir at the root and nested, a declared dir and the spare file survive, an
undeclared dir is removed. tools/ci/scratch-spare-check.sh in the pre-push gate fails when the clean line loses the spare
arguments, spare_args stops reading the file, a fixed prefix goes, or -x appears. docs/plans/build-server.md R4a says how a
lane declares its prefix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 08:24:07 +00:00

50 lines
4.4 KiB
Bash
Executable file

#!/usr/bin/env bash
# The lost-scratch class (7 October 2026, 09:2x UK, the attack-pass lane's hazard AP-H1): infra/build-server/remote-run.sh
# checkout_tree() runs `git clean -fd` on the box mirror of a worktree before every build from any agent, so the attack rows'
# untracked scratch (attack-f3/, attack-f1-venv/, tools/attack/*/target) was deleted by the next build from another row.
# Rule: the clean spares a lane's scratch. It keeps the target and stamp excludes, carries the spare arguments (SPARE_ARGS,
# filled by spare_args from the fixed prefixes attack-*, scratch-*, target-attack-*, .build-remote.log and every glob in the
# mirror-local `.igneum-scratch-spare`), and never carries -x or -X (which would drop .git/info/exclude and the ignored files
# with it). This check reads checkout_tree() and spare_args() and fails when any of that is missing.
#
# tools/ci/scratch-spare-check.sh # exit 1 with the reason
# tools/ci/scratch-spare-check.sh --self-test # the real script passes; a clean line without SPARE_ARGS, one with -x, a script
# # that no longer reads .igneum-scratch-spare, and one missing a fixed prefix fail
set -euo pipefail
cd "$(dirname "$0")/../.."
FIXED='attack-* scratch-* target-attack-* .build-remote.log'
check() { # <file>: exit 0 when checkout_tree's clean keeps its excludes, spares declared scratch and carries no -x
local f="$1" body line fixed p
body=$(sed -n '/^checkout_tree()/,/^}/p' "$f")
[ -n "$body" ] || { echo "scratch-spare: $f has no checkout_tree() function"; return 1; }
line=$(printf '%s\n' "$body" | grep -E '^[[:space:]]*git clean ' | head -1)
[ -n "$line" ] || { echo "scratch-spare: $f: no git clean line in checkout_tree"; return 1; }
if printf '%s\n' "$line" | grep -qE '(^|[[:space:]])-[A-Za-z]*[xX]'; then echo "scratch-spare: $f: the clean carries -x/-X (ignored files and .git/info/exclude would go): $line"; return 1; fi
for p in '-e target ' "-e 'target-*'" "-e '.build-remote-sha-*'" "-e '.cross-remote-sha-*'" '-e sccache'; do
printf '%s \n' "$line" | grep -qF -- "$p" || { echo "scratch-spare: $f: the clean lost the exclude $p: $line"; return 1; }
done
printf '%s\n' "$line" | grep -qF -- '"${SPARE_ARGS[@]}"' || { echo "scratch-spare: $f: the clean does not carry the spare arguments (\"\${SPARE_ARGS[@]}\"): $line"; return 1; }
printf '%s\n' "$body" | grep -qE '^[[:space:]]*spare_args ' || { echo "scratch-spare: $f: checkout_tree never calls spare_args before the clean"; return 1; }
sed -n '/^spare_args()/,/^}/p' "$f" | grep -qF '.igneum-scratch-spare' || { echo "scratch-spare: $f: spare_args() does not read the mirror-local .igneum-scratch-spare file"; return 1; }
fixed=$(grep -E '^SPARE_FIXED=' "$f" | head -1)
[ -n "$fixed" ] || { echo "scratch-spare: $f: no SPARE_FIXED list"; return 1; }
for p in $FIXED; do
printf '%s\n' "$fixed" | grep -qF -- "'$p'" || { echo "scratch-spare: $f: the fixed spare list lacks '$p': $fixed"; return 1; }
done
echo "scratch-spare: $f: the clean keeps its excludes, spares the fixed prefixes and .igneum-scratch-spare, and carries no -x"
}
if [ "${1:-}" = --self-test ]; then
t=$(mktemp -d); trap 'rm -rf "$t"' EXIT
real=infra/build-server/remote-run.sh
check "$real" >/dev/null || { echo "scratch-spare self-test: the real script FAILED the check"; exit 1; }
sed -E '/^[[:space:]]*git clean /s/ "\$\{SPARE_ARGS\[@\]\}"//' "$real" > "$t/no-spare.sh"
if check "$t/no-spare.sh" >/dev/null 2>&1; then echo "scratch-spare self-test: a clean without the spare arguments PASSED (the check is blind)"; exit 1; fi
sed -E '/^[[:space:]]*git clean /s/-qfd /-qfdx /' "$real" > "$t/with-x.sh"
if check "$t/with-x.sh" >/dev/null 2>&1; then echo "scratch-spare self-test: a clean with -x PASSED (the check is blind)"; exit 1; fi
sed '/^spare_args()/,/^}/s/\.igneum-scratch-spare/.somewhere-else/' "$real" > "$t/no-file.sh"
if check "$t/no-file.sh" >/dev/null 2>&1; then echo "scratch-spare self-test: a spare_args that ignores .igneum-scratch-spare PASSED (the check is blind)"; exit 1; fi
sed -E "/^SPARE_FIXED=/s/'scratch-\*' //" "$real" > "$t/no-fixed.sh"
if check "$t/no-fixed.sh" >/dev/null 2>&1; then echo "scratch-spare self-test: a fixed list without scratch-* PASSED (the check is blind)"; exit 1; fi
echo "scratch-spare self-test: the real script passes; no spare arguments, -x, no spare file, and a missing fixed prefix each fail"; exit 0
fi
check infra/build-server/remote-run.sh