Merge pull request #122 from dan-claude-bot/test/shellcheck-sweep-floor

fix: floor the shellcheck sweep on bin/cast, and stop skipping newline-less files
This commit is contained in:
Daniel Marin 2026-07-21 14:29:57 +01:00 committed by GitHub
commit 7e8f54b47b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 1 deletions

View file

@ -38,7 +38,13 @@ mapfile -t files < <(
git ls-files | while IFS= read -r f; do
case "$f" in *.sh) continue ;; esac
[ -f "$f" ] || continue
IFS= read -r line <"$f" || continue
# `read` returns 1 at EOF even when it populated `line` — which is what
# happens on a file whose FIRST line has no trailing newline (a
# shebang-only file with no final newline). A bare `|| continue` would
# skip exactly that file, silently. Fall through whenever `line` is
# non-empty; the empty case is a genuinely empty file, which has no
# shebang and is meant to be skipped.
IFS= read -r line <"$f" || [ -n "$line" ] || continue
case "$line" in '#!'*) ;; *) continue ;; esac
# reduce the shebang to a bare interpreter name: drop the '#!', drop any
# flags, then keep the last path/word component — so both '#!/bin/sh -e'
@ -46,6 +52,15 @@ mapfile -t files < <(
interp="${line#\#!}"
interp="${interp%% -*}"
interp="${interp##*[ /]}"
# Two known limits of this reduction, both theoretical in this repo:
# - `zsh` is on the allowlist, but shellcheck has no zsh support and
# emits SC1071 for it. So a tracked zsh script makes the sweep fail
# HARD rather than get linted. That is the right end state — a
# script nobody can lint should be loud, not skipped — but the
# outcome is "blocked", not "clean". Drop zsh here only if the repo
# ever gains one and the answer is to exempt it on purpose.
# - `#!/usr/bin/env -S bash` reduces to `env` and is not matched.
# Nothing in the repo uses `-S`; see #121 for why that is left.
case "$interp" in sh | bash | dash | ksh | zsh) printf '%s\n' "$f" ;; esac
done
} | sort -u
@ -71,6 +86,34 @@ if [ -n "$unlinted" ]; then
exit 1
fi
# --- the floor: extensionless scripts --------------------------------------
#
# The check above is derived from `git ls-files '*.sh'`, so it says nothing
# about scripts that have no `.sh` extension — those enter the set only via
# the shebang scan. `bin/cast` is one, and it is the shipped entrypoint. So
# it is covered by the DERIVATION and not by the ASSERTION: break or delete
# the shebang branch and bin/cast drops out of the sweep while this script
# still exits 0. That is #118's failure mode — a lint quietly narrowing while
# CI stays green — one level in from where the *.sh check closed it (#121).
#
# There is no non-circular way to re-derive "every extensionless shell
# script" here; any second derivation would be the same shebang scan, and
# would break with it. So the floor is named rather than computed: the known
# extensionless scripts are listed, and the sweep must contain them. A rename
# turns this red, which is correct — the floor is the thing that has to be
# updated deliberately.
required=(bin/cast)
for req in "${required[@]}"; do
if ! printf '%s\n' "${files[@]}" | grep -qxF "$req"; then
echo "shellcheck-all: '$req' is not in the swept set" >&2
echo "it has no .sh extension, so it enters only via the shebang scan above —" >&2
echo "that scan is broken, or the file moved. See #121." >&2
exit 1
fi
done
# --- lint ------------------------------------------------------------------
printf 'shellcheck: linting %d tracked scripts\n' "${#files[@]}"

View file

@ -372,6 +372,18 @@ actually cutting it, and this file starts there.
with a class check in box#112's shape: the sweep asserts its own list
covers `git ls-files '*.sh'` and fails naming the strays otherwise, so a
future sweep that quietly narrows is red rather than green over nothing.
- **The shellcheck sweep's own blind spot: extensionless scripts** (#121) —
the class check above asserts the swept set covers `git ls-files '*.sh'`,
which says nothing about scripts with no `.sh` extension. `bin/cast` is
one, and it enters the set only through the shebang scan — covered by the
derivation, not by the assertion. Break that scan and the shipped
entrypoint drops out of the lint while the check still exits 0: #118's
failure mode one level in. The sweep now also asserts a named floor of
known extensionless scripts, verified to go red when the shebang branch is
broken. Fixed alongside it: `IFS= read -r line <"$f" || continue` skipped
any file whose first line had no trailing newline, because `read` returns
1 at EOF even having populated `line` — a shebang-only file with no final
newline was silently unswept.
- **`cast` no longer leaves a full repo clone in the temp dir on every run**
(#117) — `resolveCheckout()` mkdtemps an `infra-checkout-` directory and