feat: bake shellcheck and add a client-side pre-push lint gate
v1.8.14 made shell lint a RELEASE gate (scripts/lint-shell.sh, shared by lint.yml
and the new lint-gate job that resolve-versions depends on), and that script
correctly exits 2 when shellcheck is absent -- "a gate that cannot run must not
pass". Measured on v1.8.14 on 2026-09-09 by three routes (command -v, dpkg -l, a
filesystem search): shellcheck was NOT IN THE IMAGE AT ALL. So the gate could not
be run by a developer in any container, only in CI, and the loop stayed
write-shell -> push -> wait for CI -> discover. That is the loop the gate was
added to shorten, after v1.8.14's first attempt burned ~46 min on a tree whose
lint had already been red for 24 hours.
shellcheck 0.10.0-1 added to the Dockerfile.base apt block: ~39 MB installed
(Installed-Size 40112 KB), measured to pull ZERO additional packages under
--no-install-recommends because libc6/libffi8/libgmp10 are already present.
NOTE this forces one full base rebuild -- base-decide hashes Dockerfile.base +
rootfs/, so unlike a scripts/ change it cannot reuse the existing base- layer.
hooks/pre-push is opt-in per clone (git config core.hooksPath hooks), bypassable
with --no-verify, and execs scripts/lint-shell.sh rather than reimplementing it
-- one copy, because a duplicated check that drifts is the failure this repo
keeps paying for. Matches the idiom skillset/ and myconfigs/ already use.
WHY THIS REPO HAD NO HOOKS, since it was reported as drift and is not: a peer
asked tor-ms22 for core.hooksPath per clone on the premise that unset meant the
gates were unverified there. Measured: pi-devbox unset, skillset hooks, myconfigs
common/hooks, pi-toolkit unset -- but `git ls-files | grep -i hook` is EMPTY in
both pi-devbox and pi-toolkit, so there was nothing to point at on any machine
and unset was the only correct value. This closes the real half for pi-devbox;
pi-toolkit still ships none.
Verified, expected result written down before each check:
* refusal paths -- shellcheck absent => rc 2 with the remedy named; linter
missing => rc 2. Never waved through on the assumption CI will catch it.
* the hook is IN the scan set -- "Checking 14 shell file(s)" with it present,
13 with it moved aside, so the extensionless file is found by the shebang
half of the linter's two-signal union. This check exists because the first
attempt was ambiguous: a planted `[ $UNSET_VAR = "x" ]` was not reported,
which could equally have meant "not scanned" or "below -S error". It was the
latter. A count that moves is unambiguous; a clean run is not.
* it catches the REAL v1.8.14 defect -- planting `echo 'the fleet\'s thing'`
in hooks/pre-push yields SC1073/SC1072 at severity error, rc=1.
And the gate earned its keep inside this commit: the first version of the
smoke-test assertion carried a comment beginning "# shellcheck is a GATE
DEPENDENCY", and a comment whose first word is the tool's name is parsed as a
DIRECTIVE, not a comment. The new gate failed it with SC1073/SC1072 before the
push -- same family as the v1.8.14 apostrophe, a line that reads as prose to a
human and as syntax to the parser.
This commit is contained in:
@@ -101,6 +101,27 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# container: `ss` lands at /usr/bin/ss, `ip` at /usr/sbin/ip
|
||||
# (both already on the developer PATH), and `portcheck --all`
|
||||
# then correctly identifies the socat listener on 8765.
|
||||
# shellcheck — shell linter. Added 2026-09-09 to close a CAPABILITY gap, not
|
||||
# a style preference. `scripts/lint-shell.sh` is the release
|
||||
# GATE (the `lint-gate` job that `resolve-versions` depends
|
||||
# on), and it correctly refuses to pass when shellcheck is
|
||||
# missing — "a gate that cannot run must not pass". Measured on
|
||||
# v1.8.14: shellcheck was absent from this image by all three
|
||||
# routes (PATH, dpkg, filesystem), so `bash
|
||||
# scripts/lint-shell.sh` exited 2 in EVERY devbox container and
|
||||
# no developer could run the release gate locally at all. The
|
||||
# loop was therefore write-shell → push → wait for CI → discover,
|
||||
# which is the loop the gate was added to shorten: v1.8.14's
|
||||
# first attempt burned ~46 min on a tree whose lint had already
|
||||
# been red for 24 h. This is also what makes a client-side
|
||||
# pre-push hook possible (see hooks/pre-push); without the
|
||||
# binary that hook would refuse every push. ~39 MB installed
|
||||
# (Installed-Size 40112 KB, shellcheck 0.10.0-1) and measured
|
||||
# to pull ZERO additional packages under
|
||||
# --no-install-recommends: its deps (libc6, libffi8, libgmp10)
|
||||
# are already present. NOTE this file feeds the base-decide
|
||||
# hash (Dockerfile.base + rootfs/), so adding it forces one
|
||||
# full base rebuild.
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y --no-install-recommends && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
@@ -120,6 +141,7 @@ RUN apt-get update && \
|
||||
make \
|
||||
patch \
|
||||
diffutils \
|
||||
shellcheck \
|
||||
git-crypt \
|
||||
age \
|
||||
file \
|
||||
|
||||
Reference in New Issue
Block a user