#!/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 ''`, the race-5090.ps1 shape; or a literal # `jobs\\...`, 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 ... # 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