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:
49
Dockerfile
49
Dockerfile
@@ -23,32 +23,32 @@ COPY . .
|
||||
#
|
||||
# CHECK_EPOCH must stay immediately above these RUNs. These layers are
|
||||
# keyed on its value, so they are cache-eligible only for a value
|
||||
# already built against this same tree. script/cibuild passes a fresh
|
||||
# value on every invocation, which is what makes its green mean the
|
||||
# checks really executed.
|
||||
# already built against this same tree. script/cibuild and script/docker
|
||||
# each pass a fresh value on every invocation, which is what makes their
|
||||
# green mean the checks really executed.
|
||||
#
|
||||
# The guarantee is conditional on that fresh value, not absolute. A
|
||||
# build that omits --build-arg -- a bare `docker build .` -- gets an
|
||||
# empty CHECK_EPOCH, and an empty string is a constant: the first such
|
||||
# build runs the checks, and every one after it on an unchanged tree
|
||||
# replays these layers from cache, never executing a check and still
|
||||
# exiting 0, a green nothing earned. Gate through script/cibuild.
|
||||
# Making the missing-arg case fail loudly instead is tracked in #91.
|
||||
# The value is expanded into each check command rather than left to a
|
||||
# bare declaration, so the cache miss does not depend on BuildKit's
|
||||
# unreferenced-ARG handling staying as it is. It also puts the epoch in
|
||||
# the build log, where a reader can see the layer was keyed fresh.
|
||||
#
|
||||
# CHECK_EPOCH is deliberately not referenced by the commands below: a
|
||||
# declared-but-unreferenced ARG does enter BuildKit's cache key, which
|
||||
# is measured on this host rather than assumed (PR #89). Upstream
|
||||
# sneak/prompts #26 prefers expanding the value into the command so
|
||||
# that the miss is contractual rather than dependent on that behavior
|
||||
# staying as it is; adopting that here is tracked in #91. Do not delete
|
||||
# this ARG as dead code -- the gate depends on it.
|
||||
# The guard is what makes a build that omits --build-arg fail instead of
|
||||
# lie. An unset ARG is an empty string, and an empty string is a
|
||||
# perfectly stable cache key: without the guard the first such build
|
||||
# runs the checks and every one after it on an unchanged tree replays
|
||||
# these layers from cache, executes nothing, and still exits 0. Failed
|
||||
# steps are never cached, so the guard fails on EVERY invocation rather
|
||||
# than once -- a bare `docker build .` is now a loud error, not a quiet
|
||||
# green. Do not give CHECK_EPOCH a default value; a default would
|
||||
# satisfy the guard with a constant and restore the hole.
|
||||
#
|
||||
# ARG scope is per-stage, so the builder stage declares its own.
|
||||
# Everything above this line (apk, go.mod, `go mod download`) is
|
||||
# deliberately outside the busted range and keeps caching.
|
||||
ARG CHECK_EPOCH
|
||||
RUN make fmt-check
|
||||
RUN make lint
|
||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make fmt-check
|
||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make lint
|
||||
|
||||
# Build stage
|
||||
# golang:1.26.1-alpine, 2026-03-17
|
||||
@@ -71,12 +71,13 @@ RUN go mod download
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Run tests. See the CHECK_EPOCH comment in the lint stage, including
|
||||
# the conditions the guarantee depends on; ARG scope is per-stage, so
|
||||
# this stage needs its own declaration, and it must stay immediately
|
||||
# above the check RUN.
|
||||
# Run tests. See the CHECK_EPOCH comment in the lint stage for the
|
||||
# mechanism; ARG scope is per-stage, so this stage needs its own
|
||||
# declaration, its own guard, and its own expansion, and they must stay
|
||||
# immediately above the check RUN.
|
||||
ARG CHECK_EPOCH
|
||||
RUN make test
|
||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
|
||||
|
||||
# Build (pure Go, no CGO required since we use modernc.org/sqlite)
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-X 'sneak.berlin/go/vaultik/internal/globals.Version=${VERSION}' -X 'sneak.berlin/go/vaultik/internal/globals.Commit=$(git rev-parse HEAD 2>/dev/null || echo unknown)' -X 'sneak.berlin/go/vaultik/internal/globals.CommitDate=$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)'" -o /vaultik ./cmd/vaultik
|
||||
|
||||
Reference in New Issue
Block a user