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
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:
32
README.md
32
README.md
@@ -616,25 +616,31 @@ them. We provide:
|
||||
the pinned linter: a local `make check` and CI cannot disagree about
|
||||
lint findings.
|
||||
* `script/docker` — build the Docker image tagged via
|
||||
`script/projectname`
|
||||
`script/projectname`. Passes a fresh `--build-arg CHECK_EPOCH` for the
|
||||
same reason `script/cibuild` does, so a local image build cannot be
|
||||
green on checks it replayed from cache.
|
||||
* `script/cibuild` — CI entrypoint: `docker build` (the `Dockerfile`
|
||||
runs `make fmt-check` and `make lint` in its lint stage and `make
|
||||
test` in its builder stage). This is the full CI-equivalent gate — it
|
||||
runs the checks in the same containers CI does, from a clean copy of
|
||||
the tree, so it also catches anything that depends on host state. It
|
||||
passes a fresh `--build-arg CHECK_EPOCH`, which the `Dockerfile`
|
||||
declares immediately above the check `RUN`s in both stages. Those
|
||||
layers are keyed on that value, so a new value re-runs them even on a
|
||||
byte-identical tree, and a green from this script means the checks
|
||||
executed. Dependency and module layers sit above the `ARG` and still
|
||||
cache, so a build is not cold.
|
||||
passes a fresh `--build-arg CHECK_EPOCH`, unique per invocation, which
|
||||
the `Dockerfile` declares immediately above the check `RUN`s in both
|
||||
stages and expands into each check command. Those layers are keyed on
|
||||
that value, so a new value re-runs them even on a byte-identical tree,
|
||||
and a green from this script means the checks executed. Dependency and
|
||||
module layers sit above the `ARG` and still cache, so a build is not
|
||||
cold.
|
||||
|
||||
That guarantee is conditional on the fresh value, so **run the gate
|
||||
through `script/cibuild`, not by invoking `docker build` yourself**. A
|
||||
bare `docker build .` supplies no `CHECK_EPOCH`; the empty default is
|
||||
a constant, so the second and every later build on an unchanged tree
|
||||
serves all three check layers from cache, executes nothing, and still
|
||||
exits 0. Issue #91 tracks making that case fail loudly instead.
|
||||
A build that supplies no `CHECK_EPOCH` — a bare `docker build .` —
|
||||
fails rather than lying. An unset `ARG` is an empty string and an
|
||||
empty string is a stable cache key, so without a guard such a build
|
||||
would serve all three check layers from cache, execute nothing, and
|
||||
still exit 0. Each check stage therefore asserts the value is
|
||||
non-empty before running anything, and because failed steps are never
|
||||
cached that assertion fires on every invocation rather than once. Use
|
||||
`script/cibuild` (or `script/docker`, which passes the same arg); a
|
||||
bare `docker build .` is now a loud error.
|
||||
* `script/precommit` — pre-commit gate: `go mod tidy` + `go fmt` (must
|
||||
not change files), then `script/check`
|
||||
* `script/install-precommit` — install the git pre-commit hook that
|
||||
|
||||
Reference in New Issue
Block a user