Make a missing CHECK_EPOCH fail the build (closes #91)
All checks were successful
check / check (push) Successful in 3m2s
All checks were successful
check / check (push) Successful in 3m2s
PR #89 stopped script/cibuild replaying cached check layers, but left a gap: a bare `docker build .` with no --build-arg still faked. An unset ARG is an empty string, an empty string is a stable cache key, and the check layers replay from it. That gap mattered because REPO_POLICIES.md names `docker build .` verbatim as a command that must be green, so the documented command was the one that lied. Both check stages now carry `RUN [ -n "$CHECK_EPOCH" ] || exit 1` immediately under their own ARG. Failed steps are never cached, so this fails on every invocation rather than once - a bare build now stops with a named error instead of reporting a green it did not earn. Each stage needs its own guard because ARG scope is per-stage; a gate-carrying stage without one is a silent hole if ordering ever changes. The check RUNs now reference the value (`echo "check epoch: ${CHECK_EPOCH}" && make <target>`), so the cache miss is contractual rather than resting on BuildKit's current treatment of unreferenced ARGs, and the epoch is visible in the build log. The epoch becomes "$(date +%s%N)$$" so concurrent invocations in the same second cannot collide. busybox silently drops %N and exits 0, so $$ is what makes it correct there. The bare-assignment form is retained deliberately: inlining the substitution into --build-arg would, under set -eu, yield an empty and therefore constant epoch without aborting. script/docker gets the same treatment - it is not the gate, but two entrypoints disagreeing about whether the tree is green is its own hazard, and local builds are almost always warm. Verified by negative control rather than inspection: a bare build fails twice consecutively here and succeeds twice on the parent commit, so the change is demonstrably not a no-op. The builder-stage guard was fired directly with a targeted probe build, since the lint stage otherwise fails first and would leave it unexercised.
This commit was merged in pull request #92.
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