Make a missing CHECK_EPOCH fail the build instead of faking it (closes #91)
All checks were successful
check / check (pull_request) Successful in 2m13s

Adopt the four remaining upstream CHECK_EPOCH hardening items from
sneak/prompts #26, closing the gap #85 left open deliberately.

Fail closed on a missing value. Each check stage now asserts
`[ -n "$CHECK_EPOCH" ] || exit 1` before running anything. An unset ARG
is an empty string and an empty string is a stable cache key, so the
second and every later bare `docker build .` on an unchanged tree
replayed all three check layers, executed nothing, and still exited 0 --
and `docker build .` is the command REPO_POLICIES.md names verbatim as a
thing that must be green, so the documented command was precisely the
one that lied. Failed steps are never cached, which is what makes the
guard fire on every invocation rather than once.

Expand the epoch into each check command rather than leaving it a bare
declaration, so the cache miss does not depend on BuildKit's
unreferenced-ARG handling staying as it is, and so the value appears in
the build log where a reader can see the layer was keyed fresh.

Make the epoch unique per invocation rather than per second:
`epoch="$(date +%s%N)$$"`. `%N` alone is not enough, since busybox drops
it silently and exits 0, handing back second granularity with no
warning; `$$` differs between concurrent invocations regardless. The
bare-assignment form is kept on purpose -- inlined into an argument, a
failing substitution does not abort under `set -eu` and would yield an
empty constant epoch, restoring the exact false green this prevents.

Pass the same fresh value from script/docker. It is not the CI gate, but
local builds are almost always warm, so it was the likelier fooling in
practice, and two entrypoints disagreeing about whether the tree is
green is worse than either being wrong alone.

The ARG placement from #85 is unchanged, below apk add, COPY go.mod
go.sum and go mod download, so dependency layers still cache and the
build is not cold. Verified by negative control rather than inspection;
measurements are recorded once, in the PR verification comment.

.golangci.yml, the lint-stage FROM line and its digest, script/lint,
REPO_POLICIES.md and .gitea/workflows/check.yml are untouched.
This commit is contained in:
clawbot
2026-08-09 07:52:42 +00:00
parent c3bb3b5580
commit b101b4e21c
5 changed files with 102 additions and 40 deletions

View File

@@ -15,7 +15,17 @@ main() {
# fresh value here is what forces them to re-run: without it an
# unchanged tree replays them from cache, the checks never execute,
# and the build still exits 0. The ARG sits immediately above the
# check RUNs, so dependency and module layers still cache.
# check RUNs, so dependency and module layers still cache. The
# Dockerfile also refuses to build at all when CHECK_EPOCH is empty,
# so a missing value fails loudly here rather than passing quietly.
#
# The value must be unique per invocation, not per second. `date +%s`
# is second-granular, so two concurrent invocations in the same
# second get identical epochs and the later one can be served from
# cache -- the original defect in miniature. `%N` alone does not fix
# it: busybox silently drops %N, exits 0, and hands back second
# granularity with no warning. `$$` is what makes this correct
# regardless, since concurrent invocations have different pids.
#
# Assign the epoch on its own line rather than inline in the
# argument. Under `set -eu` a command substitution that fails
@@ -25,7 +35,7 @@ main() {
# script exists to prevent -- so the guard would disarm itself and
# still exit 0. As a bare assignment, `set -e` catches a failing
# `date` and no build starts.
epoch="$(date +%s)"
epoch="$(date +%s%N)$$"
docker build --build-arg CHECK_EPOCH="$epoch" .
}