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>
50 lines
4.4 KiB
Bash
Executable file
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
|