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>
56 lines
3.3 KiB
Bash
Executable file
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
|