Run shellcheck in the local gate, at CI's exact pin #257

Merged
JMR-dev merged 2 commits from chore/gate-runs-shellcheck into main 2026-09-06 21:28:29 +00:00
JMR-dev commented 2026-09-06 21:16:50 +00:00 (Migrated from github.com)

The local gate checked ktlint, detekt and Android lint but not shellcheck — so a new or edited .sh file was precisely the case where the hook passed and CI's Static analysis leg still went red.

That is not hypothetical. The gate is itself a new .sh file, and the first thing it could not check was itself. It got caught by hand twice on #256 before it got caught here.

The digest is read out of status_check.yml, not copied

shellcheck 0.9.0 and 0.11.0 disagree about how to report a trap handler — SC2317 on seven body lines against SC2329 once on the declaration. Same script, same directive, one red and one green. That disagreement is why CI pins by digest in the first place.

A second copy of that digest in the hook would be worse than none: when it drifts, the symptom is the gate passing while CI fails, which is the exact failure this section exists to prevent. So there is one digest in the repo and the hook reads it:

shellcheck_pin="$(grep -oE 'koalaman/shellcheck@sha256:[0-9a-f]{64}' \
  .github/workflows/status_check.yml | head -1)"

Details

  • Runs over git ls-files '*.sh' — all tracked files, not the diff — because that is what CI does. The job is to predict that leg, not to audit the change.
  • podman preferred over docker for the mount's SELinux relabel (:z), which is what this host needs; docker on CI does without it.
  • Neither runtime present, or the digest unreadable → reported as NOT COVERED, loudly, rather than skipped quietly. A check that silently does not run is worse than one that is absent.

Verified that it bites

A probe script whose only fault was an unquoted ls $foo was staged, and the real pre-commit hook blocked on it:

[local-gate] BLOCKED shellcheck failed. CI runs the same digest over the same files,
  so this is a red Static analysis leg waiting to happen.

It blocked before reaching the JVM gate. Probe removed; all tracked .sh are clean under the pinned digest.

Cost

This commit touches no app/src, so both the pre-commit and pre-push runs skipped the emulator sweep and finished in seconds — the app/src-subtree cache key doing its job. shellcheck itself adds a couple of seconds.

The local gate checked ktlint, detekt and Android lint but **not** shellcheck — so a new or edited `.sh` file was precisely the case where the hook passed and CI's Static analysis leg still went red. That is not hypothetical. The gate is itself a new `.sh` file, and **the first thing it could not check was itself**. It got caught by hand twice on #256 before it got caught here. ## The digest is read out of `status_check.yml`, not copied shellcheck 0.9.0 and 0.11.0 disagree about how to report a trap handler — `SC2317` on seven body lines against `SC2329` once on the declaration. Same script, same directive, one red and one green. That disagreement is why CI pins by digest in the first place. A second copy of that digest in the hook would be worse than none: when it drifts, the symptom is **the gate passing while CI fails**, which is the exact failure this section exists to prevent. So there is one digest in the repo and the hook reads it: ```bash shellcheck_pin="$(grep -oE 'koalaman/shellcheck@sha256:[0-9a-f]{64}' \ .github/workflows/status_check.yml | head -1)" ``` ## Details - Runs over **`git ls-files '*.sh'`** — all tracked files, not the diff — because that is what CI does. The job is to predict that leg, not to audit the change. - **podman preferred over docker** for the mount's SELinux relabel (`:z`), which is what this host needs; docker on CI does without it. - Neither runtime present, or the digest unreadable → reported as **NOT COVERED**, loudly, rather than skipped quietly. A check that silently does not run is worse than one that is absent. ## Verified that it bites A probe script whose only fault was an unquoted `ls $foo` was staged, and the **real pre-commit hook** blocked on it: ``` [local-gate] BLOCKED shellcheck failed. CI runs the same digest over the same files, so this is a red Static analysis leg waiting to happen. ``` It blocked *before* reaching the JVM gate. Probe removed; all tracked `.sh` are clean under the pinned digest. ## Cost This commit touches no `app/src`, so both the pre-commit and pre-push runs skipped the emulator sweep and finished in seconds — the `app/src`-subtree cache key doing its job. shellcheck itself adds a couple of seconds.
Sign in to join this conversation.