fix(ci): mark the new lane scripts executable, and let the contract see them

scripts/ci/lint-mem.sh and scripts/ci/msan-lane.sh were committed at mode
100644, so the workflow step that runs them directly died with
"Permission denied" (exit 126). scripts/lint-mem-gate.py gets the same
treatment: it is invoked through python3 today, but it carries a shebang
and should not depend on that.

The exec-bit contract already exists to catch precisely this, and it did
not, because it derives its candidate set from `git ls-files -s '*.sh'`
-- tracked files only. A brand-new script is invisible there until it is
committed, so the check passes on the run where the defect is introduced
and only starts failing on the run that ships it. The window where the
contract is most useful was the one window it could not see.

It now also considers not-yet-tracked scripts by their filesystem mode.
Verified against the real defect rather than in the abstract: with
lint-mem.sh untracked and non-executable the contract reports
"_lint.yml:76 executes scripts/ci/lint-mem.sh directly, but its committed
mode is 100644", and passes once the bit is set.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-08-03 15:42:32 +02:00
1 parent 9a41d83158
commit 64bd272cb2
4 files changed
+11

No files matched your search

Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
Regular → Executable
View File
File mode changed.
+11
View File
@@ -26,6 +26,7 @@ cd "$ROOT"
python3 - "$ROOT" <<'PY'
import pathlib
import os
import re
import subprocess
import sys
@@ -43,6 +44,16 @@ for line in git("ls-files", "-s", "*.sh").splitlines():
if mode == "100644":
non_exec.add(path)
# ...and NOT-YET-TRACKED ones, by their filesystem mode. A brand-new script is
# invisible to `ls-files` until it is committed, so without this the contract
# passes on the very run where the defect is introduced and only starts failing
# after the commit that ships it -- which is how scripts/ci/lint-mem.sh reached
# CI at mode 100644 and died with "Permission denied" (exit 126). The window
# where the check is most useful is exactly the window it could not see.
for path in git("ls-files", "--others", "--exclude-standard", "*.sh").splitlines():
if path and not os.access(root / path, os.X_OK):
non_exec.add(path)
# Places that actually execute things.
search_roots = [root / ".github" / "workflows", root / "scripts",
root / "test-infrastructure"]