igneum/tools/ci/kit-path-check.sh
igneum-labs b2e6d6159e CI: run jobs test their fetched kit before use (the wiped-jobs-folder class)
The app's install clears the jobs folder on a PC, so a run job whose kit was fetched by an earlier fetch job finds
nothing after an update and fails in seconds (5 October 2026, 21:49Z, the AMD kit; bench-log 4df339f). Rule: a run
playbook that reaches a path under the jobs folder other than its own tests the kit is there before its first use,
and the fetch is republished under a new id after any app update.

tools/ci/kit-path-check.sh reads every *.ps1 under relay/playbooks/ and tools/. A kit root is a path derived from
the jobs folder (`$jobs = Split-Path $env:IGNEUM_JOB_DIR` then `Join-Path $jobs '<fetch id>'`, the race-5090.ps1
shape) or one carrying a literal `jobs\` (the amd-card-test.ps1 shape); every path built from it belongs to that kit.
A presence check (Test-Path, [IO.File]::Exists, [IO.Directory]::Exists, Get-Item or Get-ChildItem with -ErrorAction)
on the root or anything under it covers the whole kit. A use before that line fails with "kit path used before a
presence check: republish the fetch after any app update", as does a literal jobs\ path in a command with no check.
The job's own folder ($env:IGNEUM_JOB_DIR) is not a kit path.

Fixtures: kit-path-ok.ps1 (both shapes, checked; a sibling pack file covered by the worker's check) and
kit-path-unchecked.ps1 (the worker run before its check, a literal never checked); --self-test asserts the lines.
Wired into ci.yml after the bash-body step, and into publish-jobs.sh add --kind run beside the other two checks;
test-publish-jobs.sh gains the refusal (34 passed, 0 failed). The current tree: race-5090.ps1 is the one playbook
with a kit, checked before use. README-ship.md: the rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 22:37:29 +00:00

110 lines
6.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# The wiped-jobs-folder class (5 October 2026, 21:49Z, bench-log 39f02ff): the app's install clears the jobs folder on
# a PC, so a run job whose kit was fetched by an earlier fetch job finds nothing after an update and fails in seconds.
# Rule: a run playbook that reaches a path under the app's jobs folder other than its own
# (`$jobs = Split-Path $env:IGNEUM_JOB_DIR` then `Join-Path $jobs '<fetch id>'`, the race-5090.ps1 shape; or a literal
# `jobs\<id>\...`, the amd-card-test.ps1 shape) tests that the kit is there (Test-Path, [IO.File]::Exists,
# [IO.Directory]::Exists, Get-Item or Get-ChildItem with -ErrorAction) before its first use, and republishes the fetch
# after any app update. A check on the kit's root or on anything under it covers the whole kit. This check reads every
# *.ps1 under relay/playbooks/ and tools/ and fails on a kit path used before a presence check. The job's own folder
# ($env:IGNEUM_JOB_DIR, made by the app for the run) is not a kit path.
#
# Usage: tools/ci/kit-path-check.sh # the tree (tools/ci/fixtures/ left out)
# tools/ci/kit-path-check.sh <file.ps1>... # named files (the jobs publisher runs it on the script it publishes)
# tools/ci/kit-path-check.sh --self-test # must fire on the unchecked fixture and stay quiet on the correct one
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
check_files() {
python3 - "$@" <<'PY'
import re, sys
SEED_LIT = re.compile(r'jobs\\') # a literal path under the jobs folder
SEED_FOLDER = re.compile(r'Split-Path\s+\$env:IGNEUM_JOB_DIR') # the jobs folder itself: the parent of this job's folder
CHECK = re.compile(r'Test-Path|\[IO\.(File|Directory)\]::Exists|Get-(Item|ChildItem)\b[^|]*-ErrorAction')
ASSIGN = re.compile(r'^\s*\$([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.*)$')
# a right-hand side that only builds a path (the kit var it names is then a path, not a use of one)
DERIV = re.compile(r'^(Join-Path\b|Split-Path\b|\(|"|\'|\[IO\.Path\]::Combine|\$env:|\$[A-Za-z_][A-Za-z0-9_]*\s*(\+|\.(TrimEnd|Replace)|$))')
TEXT_ONLY = re.compile(r'^\s*(Write-Output|Write-Host|Write-Verbose|Say|Log)\b|^\s*"')
VAR = re.compile(r'\$([A-Za-z_][A-Za-z0-9_]*)\b(?!:)')
MSG = 'kit path used before a presence check: republish the fetch after any app update'
rc = 0
files = sys.argv[1:]
with_kits = 0
for path in files:
try:
lines = open(path, encoding='utf-8', errors='replace').read().split('\n')
except OSError as e:
print('FAIL %s: %s' % (path, e)); rc = 1; continue
folder = set() # vars that are the jobs folder (always there)
root_of = {} # kit var -> its root var
root_line = {} # root var -> the line it is defined on
checked = set() # roots with a presence check so far
reported = set()
inline_checked = False
fails = []
for ln, raw in enumerate(lines, 1):
s = raw.strip()
if not s or s.startswith('#'): continue
refs = set(VAR.findall(raw))
kit_refs = refs & set(root_of)
m = ASSIGN.match(raw)
if m:
name, rhs = m.group(1), m.group(2).strip()
if SEED_FOLDER.search(rhs) and not SEED_LIT.search(rhs):
folder.add(name); continue
deriv = bool(DERIV.match(rhs))
if deriv and (SEED_LIT.search(rhs) or (refs & folder)):
root_of[name] = name; root_line[name] = ln; continue
if deriv and kit_refs:
root_of[name] = root_of[sorted(kit_refs)[0]]; continue
if CHECK.search(raw):
for v in kit_refs: checked.add(root_of[v])
if SEED_LIT.search(raw): inline_checked = True
continue
for v in sorted(kit_refs):
r = root_of[v]
if r in checked or r in reported: continue
reported.add(r)
fails.append('FAIL %s:%d %s ($%s, kit path $%s defined at line %d)' % (path, ln, MSG, v, r, root_line[r]))
if SEED_LIT.search(raw) and not inline_checked and not TEXT_ONLY.match(raw):
inline_checked = True
fails.append('FAIL %s:%d %s (a literal jobs\\ path in a command, never checked)' % (path, ln, MSG))
if fails:
rc = 1
for f in fails: print(f)
elif root_of or inline_checked:
with_kits += 1
print('ok %s (%d kit path%s, checked before use)' % (path, len(root_line), '' if len(root_line) == 1 else 's'))
print('kit-path-check: %d files, %d with a fetched kit, %s' % (len(files), with_kits, 'all checked before use' if rc == 0 else 'FAILURES above'))
sys.exit(rc)
PY
}
if [ "${1:-}" = "--self-test" ]; then
F="$HERE/fixtures"
ok_out="$(check_files "$F/kit-path-ok.ps1" 2>&1)"; ok_rc=$?
bad_out="$(check_files "$F/kit-path-unchecked.ps1" 2>&1)"; bad_rc=$?
echo "self-test correct fixture: rc $ok_rc"; printf '%s\n' "$ok_out" | sed 's/^/ /'
echo "self-test unchecked fixture: rc $bad_rc"; printf '%s\n' "$bad_out" | sed 's/^/ /'
[ $ok_rc -eq 0 ] || { echo "SELF-TEST FAILED: the correct fixture was flagged"; exit 1; }
printf '%s\n' "$ok_out" | grep -q '^ok .*2 kit paths' || { echo "SELF-TEST FAILED: the correct fixture holds 2 kit paths, a different number was found"; exit 1; }
[ $bad_rc -ne 0 ] || { echo "SELF-TEST FAILED: the unchecked fixture passed"; exit 1; }
for want in 'kit-path-unchecked.ps1:9 ' 'kit-path-unchecked.ps1:13 '; do
printf '%s\n' "$bad_out" | grep -q "^FAIL .*$want.*republish the fetch after any app update" || { echo "SELF-TEST FAILED: the unchecked use at $want was not reported"; exit 1; }
done
[ "$(printf '%s\n' "$bad_out" | grep -c '^FAIL ')" -eq 2 ] || { echo "SELF-TEST FAILED: 2 failures expected"; exit 1; }
echo "self-test passed: the unchecked kit paths fail, the checked ones pass"
exit 0
fi
if [ $# -gt 0 ]; then
check_files "$@"; exit $?
fi
cd "$REPO"
files=$(git ls-files 'relay/playbooks/**' 'tools/**' | grep -E '\.ps1$' | grep -v '^tools/ci/fixtures/' || true)
if [ -z "$files" ]; then echo "kit-path-check: no .ps1 files under relay/playbooks/ or tools/"; exit 0; fi
# shellcheck disable=SC2086
check_files $files