igneum/tools/ci/windows-paths-check.sh
igneum-labs 60eb06ec24 CI hardening: one gate script for the hook and CI, the research exclusion list, the Windows paths check, the red watcher, the box runner switch, the failure classification
168 non-green runs since the first workflow run were classified (docs/analysis/ci-failures-2026-10-06.md): 102 were tree checks that finish in under 25 s on the pushing machine, 40 were GitHub-side refusals nobody saw.

tools/ci/pre-push.sh is the one list of fast checks; ci.yml's site job calls it with --ci and the pre-push hook with --hook (full gate for master and release-*, structural checks for other refs; never writes into the worktree). tools/ci/export-exclude.txt lists research documents outside the public export list, pruned by identity-check.sh and by the mirror's sync.sh (self-test: an excluded path may quote the patterns, an exported one may not); polish.md and this record are its first entries, which makes master green. tools/ci/windows-paths-check.sh (colon, trailing dot or space, reserved names, over 240 characters) runs as the pre-commit hook on staged paths and in the gate. tools/ci/red-watch.mjs plus the red job on the box's runner record one line per failed master or release-* run to /srv/ci-red/red.jsonl; igneum-ci-red.timer posts each once to the updates channel. pow and sims read IGNEUM_CI_RUNNER for the box. no-foreign-tree-writes.sh no longer exits silently on its warning pipeline under pipefail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 20:58:54 +00:00

56 lines
3.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# Every tracked path must be one Windows can hold. 6 October 2026: a screenshot named after an address carried a colon
# (docs/plans/site-ui-3-shots/after/address_igneumdev:qz9h....jpg), actions/checkout on windows-latest failed with
# "invalid path" (git exit 128), and every Windows build of the tree died at the checkout for forty minutes.
#
# Rules (NTFS and the Win32 namespace):
# no : * ? " < > | in a name, and no control character;
# no name ending in a dot or a space;
# no reserved device name as a name or as the stem of one (CON, PRN, AUX, NUL, COM1-9, LPT1-9, any case);
# no path longer than 240 characters (MAX_PATH is 260 and a checkout prefix takes the rest).
#
# tools/ci/windows-paths-check.sh every tracked path (CI, the pre-push gate)
# tools/ci/windows-paths-check.sh --staged the paths being committed (the pre-commit hook)
# tools/ci/windows-paths-check.sh --self-test the rules fire on each bad shape and pass a good one
# Exit 1 with the offending paths listed.
set -euo pipefail
check_paths() {
# stdin: one path per line. Prints one line per offence. Returns 1 if any. One awk pass (a subprocess per path took
# 23 s over 2,500 files on the Mac; this takes well under a second).
awk '
function reserved(name, stem) { stem = name; sub(/\..*$/, "", stem); return toupper(stem) ~ /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/ }
{
p = $0; if (p == "") next
if (p ~ /[:*?"<>|]/ || p ~ /[\001-\037\177]/) { print " " p " (a character Windows forbids: one of : * ? \" < > | or a control character)"; bad = 1; next }
if (p ~ /[. ]$/ || p ~ /(^|\/)[^\/]*[. ]\//) { print " " p " (a name ending in a dot or a space)"; bad = 1; next }
if (length(p) > 240) { print " " p " (" length(p) " characters; over 240)"; bad = 1; next }
n = split(p, parts, "/")
for (i = 1; i <= n; i++) if (reserved(parts[i])) { print " " p " (reserved device name " parts[i] ")"; bad = 1; break }
}
END { exit bad ? 1 : 0 }'
}
if [ "${1:-}" = "--self-test" ]; then
fails=0
for bad in 'docs/shots/address_igneumdev:qz9h.jpg' 'a/b/what?.md' 'a/trailing./x' 'a/trailing ' 'docs/nul.txt' 'x/COM1' 'x/LpT3.log' "$(printf 'd/%0.s' $(seq 1 125))f.txt"; do
if printf '%s\n' "$bad" | check_paths >/dev/null; then echo "self-test failed: accepted '$bad'"; fails=1; fi
done
for good in 'docs/plans/site-ui-3-shots/after/address_igneumdev-qz9h.jpg' 'tools/ci/windows-paths-check.sh' 'a/console.log' 'a/null.rs' 'a/com10.txt' 'a/.gitignore' 'a/b.c.d'; do
if ! printf '%s\n' "$good" | check_paths >/dev/null; then echo "self-test failed: rejected '$good'"; fails=1; fi
done
[ "$fails" = 0 ] && echo "self-test passed: colon, question mark, trailing dot, trailing space, NUL, COM1, LpT3 and a 250-character path fail; seven ordinary paths pass"
exit $fails
fi
cd "$(git rev-parse --show-toplevel)"
if [ "${1:-}" = "--staged" ]; then
list="$(git diff --cached --name-only --diff-filter=ACR -z | tr '\0' '\n')"; what="staged paths"
else
list="$(git ls-files -z | tr '\0' '\n')"; what="tracked paths"
fi
if out="$(printf '%s\n' "$list" | check_paths)"; then
echo "windows-paths: every one of $(printf '%s\n' "$list" | grep -c . || true) $what is valid on Windows"
else
echo "windows-paths: these $what cannot exist on Windows (rename them before committing):"; printf '%s\n' "$out"; exit 1
fi