Compare commits

..
Author SHA1 Message Date
JMR-devandClaude Opus 5 7d1d3191a9 Resolve the gate's cache dir with --git-common-dir, and say when it cannot (#258)
CACHE_DIR was the literal ".git/lmc-verify". In a linked worktree `.git` is a
FILE containing `gitdir: ...`, so `mkdir -p .git/lmc-verify` fails with "Not a
directory" -- and because the write is the last thing the script does, it failed
while the gate still printed green and exited 0. Every commit and push from a
worktree then re-swept API 33-36 for nothing, silently. That is the worst shape a
cache can fail in: invisible and expensive, and it was found by an agent paying
for it four times over rather than by the tool saying anything.

Measured both ways: in a worktree the old expression gives
`mkdir: cannot create directory '.git': Not a directory`, exit 1; `git rev-parse
--git-common-dir` gives the real path and exit 0.

--git-common-dir rather than --git-dir so the cache is SHARED between worktrees.
The key is the app/src tree hash, and identical content is identical content
whichever worktree produced it -- a sweep run in one is evidence for all of them.

The write also stops being silent. record_sweep() prints when it cannot record,
because a cache that never fills looks exactly like one that is working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:54:12 -05:00
Jason Ross 68b863fbdb Merge pull request #257 from JMR-dev/chore/gate-runs-shellcheck
Run shellcheck in the local gate, at CI's exact pin
2026-09-06 16:28:29 -05:00
JMR-devandClaude Opus 5 f4174e5b06 Run actionlint in the gate too, at CI's exact pin
The other half of the hole the previous commit closed. `git ls-files '*.sh'` does
not match workflow `run:` blocks, and a good deal of this repo's bash lives
there -- so a workflow edit was still the case where the gate passed and CI's
Static analysis leg went red.

Pinned by digest, read out of status_check.yml rather than copied, for the reason
the shellcheck section gives and for actionlint's own: its documented install is
`curl | bash` off a moving branch, which does not belong in a repo that pins every
action by SHA.

The container runtime detection and the SELinux `:z` mount option are hoisted out
of the shellcheck branch so both checks share one answer rather than deciding it
twice and drifting.

Verified that it bites rather than assumed: status_check.yml was given a
`needs: [a-job-that-does-not-exist]`, and the real pre-commit hook blocked with
actionlint's own message -- `job "static-analysis" needs job
"a-job-that-does-not-exist" which does not exist in this workflow [job-needs]`.
Workflow restored; nothing but the hook is in this diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 16:19:49 -05:00
JMR-devandClaude Opus 5 dd01f9f27c Run shellcheck in the local gate, at CI's exact pin
The 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. The first file it could not check was itself, and it
was caught by hand twice before it was caught here.

THE DIGEST IS READ OUT OF status_check.yml RATHER THAN 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 is why CI pins by digest, and it is also why a second
copy of the digest in this file would be worse than none: when it drifts, the
symptom is the gate passing and CI failing, which is the exact failure this
section prevents.

Runs over `git ls-files '*.sh'` -- all tracked files, not the diff -- because
that is what CI does, and the job here is to predict that leg rather than audit
the change. podman is preferred over docker for the mount's SELinux relabel;
neither present, or the digest unreadable, reports the check as NOT COVERED
rather than skipping it quietly.

Verified that it bites rather than assumed: a probe script whose only fault was
an unquoted `ls $foo` was staged, and the real pre-commit hook blocked on SC2086
before it reached the JVM gate. Probe removed; all tracked .sh are clean under
the pinned digest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 16:16:23 -05:00
Jason Ross dd76229e90 Merge pull request #256 from JMR-dev/test/publish-delete-arm-real-provider
Delete the document a failed save could not write (#250)
2026-09-06 16:11:13 -05:00
2 changed files with 97 additions and 3 deletions
+10
View File
@@ -413,6 +413,16 @@ install for code that can never run — and on API 37 the full APK does not fit
the level is uncovered when it is not — CI's gating leg being what answers for it then. It never
claims five levels having run four.
**It runs shellcheck and actionlint too, at CI's exact pins** — shellcheck over
`git ls-files '*.sh'`, actionlint over the workflows, the same digests and the same file sets
that leg uses. actionlint is not an afterthought to shellcheck but the other half of the same
hole: much of this repo's bash lives in workflow `run:` blocks, which `'*.sh'` does not match at
all. That gap was found the hard way: the gate checked ktlint,
detekt and Android lint, so a new `.sh` file was precisely the case where it passed and CI still
went red, and the first file it could not check was itself. **The digest is read out of
`status_check.yml` rather than copied** — two copies drift, and the symptom of that drift is the
gate passing while CI fails, which is the one thing this check exists to prevent.
The sweep is cached under the hash of the **`app/src` subtree**, not the whole repo tree. Keying
it on the whole tree was the first cut and it was wrong: editing a comment in `CLAUDE.md` threw
away a sweep of byte-identical application code and re-ran forty minutes of emulators to prove
+87 -3
View File
@@ -44,10 +44,28 @@ cd "$REPO_ROOT" || exit 1
MODE="$(basename "$0")"
ZERO="0000000000000000000000000000000000000000"
CACHE_DIR=".git/lmc-verify"
# `git rev-parse --git-common-dir`, not a literal ".git" (#258). In a linked worktree `.git` is a
# FILE containing `gitdir: ...`, so `mkdir -p .git/lmc-verify` fails with "Not a directory" -- and
# because the write is the last thing this script does, it failed while the gate still printed
# green and exited 0. Every push from a worktree then re-swept 33-36 for nothing, silently, which
# is the worst shape a cache can fail in: invisible and expensive.
#
# --git-common-dir rather than --git-dir so the cache is SHARED across worktrees. The key is the
# app/src tree hash, and identical content is identical content whichever worktree produced it.
CACHE_DIR="$(git rev-parse --git-common-dir)/lmc-verify"
GRADLE_GATE=(:app:assembleDebug :app:testDebugUnitTest :app:compileDebugAndroidTestKotlin
:app:ktlintCheck :app:detekt :app:lintDebug)
# Says so when it cannot record, rather than leaving a cache that silently never fills (#258).
record_sweep() {
[ -n "$tree" ] || return 0
if mkdir -p "$CACHE_DIR" 2>/dev/null && : > "$CACHE_DIR/$tree" 2>/dev/null; then
return 0
fi
printf '\n\033[1m[local-gate]\033[0m could not record the sweep under %s -- it will re-run next
time. Not fatal, but it means every commit and push pays for it again.\n' "$CACHE_DIR"
}
say() { printf '\n\033[1m[local-gate]\033[0m %s\n' "$*"; }
die() {
printf '\n\033[1;31m[local-gate] BLOCKED\033[0m %s\n' "$*"
@@ -105,6 +123,72 @@ done <<< "$changed_files"
# --- the cheap gate always runs -----------------------------------------------------------
# --- shellcheck, at CI's exact pin ---------------------------------------------------------
# WHY THIS IS HERE. The gate ran ktlint, detekt and Android lint but not shellcheck, so a new or
# edited `.sh` file was precisely the case where this hook passed and CI's Static analysis leg
# still went red. That is not hypothetical: this script is itself a new `.sh` file, and the first
# thing it could not check was itself. It was caught by hand twice before it was caught here.
#
# THE DIGEST IS READ OUT OF status_check.yml, NOT COPIED INTO THIS FILE. shellcheck 0.9.0 and
# 0.11.0 disagree about how to report a trap handler -- SC2317 on seven body lines versus SC2329
# once on the declaration, same script, same directive, one red and one green. That disagreement
# is why CI pins by digest, and a second copy of the digest here would drift from it silently.
# When it drifts, the symptom is this gate passing and CI failing: the exact thing this section
# exists to prevent. So there is one digest in the repo and this reads it.
#
# ALL TRACKED FILES, not just changed ones, because that is what CI does -- `git ls-files '*.sh'`.
# The point is to predict that leg, not to audit the diff.
shellcheck_pin="$(grep -oE 'koalaman/shellcheck@sha256:[0-9a-f]{64}' \
.github/workflows/status_check.yml | head -1)"
# :z is podman's SELinux relabel and is what this host needs; docker on CI does without it.
runtime=""
mount=":z"
for candidate in podman docker; do
if command -v "$candidate" >/dev/null 2>&1; then
runtime="$candidate"
[ "$candidate" = "docker" ] && mount=""
break
fi
done
if [ -z "$shellcheck_pin" ]; then
say "NOT COVERED: shellcheck. Could not read the pinned digest out of
.github/workflows/status_check.yml -- if that pin moved or was reformatted, fix this grep
rather than leaving the check silently absent."
elif [ -z "$runtime" ]; then
say "NOT COVERED: shellcheck. Neither podman nor docker is on PATH, and there is no shellcheck
system package on this host. CI's Static analysis leg is what answers for .sh files then."
else
say "shellcheck ($runtime, $shellcheck_pin)"
if ! git ls-files -z '*.sh' |
xargs -0 -r "$runtime" run --rm -v "$PWD:/mnt$mount" "docker.io/$shellcheck_pin"; then
die "shellcheck failed. CI runs the same digest over the same files, so this is a red
Static analysis leg waiting to happen."
fi
fi
# --- actionlint, the half shellcheck cannot see ---------------------------------------------
# A good deal of this repo's bash lives in workflow `run:` blocks, which `git ls-files '*.sh'`
# does not match at all -- so without this a workflow edit is the same hole the section above
# just closed: green here, red on Static analysis. Pinned by digest for the reason in that
# section, and for actionlint's own: its documented install is `curl | bash` off a moving branch,
# which does not belong in a repo that pins every action by SHA.
actionlint_pin="$(grep -oE 'rhysd/actionlint@sha256:[0-9a-f]{64}' \
.github/workflows/status_check.yml | head -1)"
if [ -z "$actionlint_pin" ]; then
say "NOT COVERED: actionlint. Could not read the pinned digest out of
.github/workflows/status_check.yml -- fix this grep rather than leaving the check absent."
elif [ -z "$runtime" ]; then
say "NOT COVERED: actionlint. Neither podman nor docker is on PATH; CI's Static analysis leg
is what answers for the workflows then."
else
say "actionlint ($runtime, $actionlint_pin)"
if ! "$runtime" run --rm -v "$PWD:/repo$mount" -w /repo "docker.io/$actionlint_pin" -color; then
die "actionlint failed. CI runs the same digest over the same workflows."
fi
fi
say "$MODE: running the JVM gate"
if ! ./gradlew "${GRADLE_GATE[@]}" --continue; then
die "the JVM gate failed (assemble, unit tests, androidTest compile, ktlint, detekt, lint)."
@@ -114,7 +198,7 @@ fi
if [ "$touches_source" -eq 0 ] && [ "$touches_tests" -eq 0 ]; then
say "no app/src changes; the instrumented sweep is not required for this one"
mkdir -p "$CACHE_DIR" && [ -n "$tree" ] && : > "$CACHE_DIR/$tree"
record_sweep
exit 0
fi
@@ -157,7 +241,7 @@ else
attach the Pixel 10 Pro XL to have this hook cover it too."
fi
mkdir -p "$CACHE_DIR" && [ -n "$tree" ] && : > "$CACHE_DIR/$tree"
record_sweep
# Name the levels rather than claiming "every supported level". The first cut said the latter on
# both paths, including the one that had just printed NOT COVERED two lines above -- a false claim
# printed by the tool whose whole job is to stop false claims reaching CI.