From b7cb8cd9d0e37921ab847e55bfecb84b9125c8b1 Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 3 Sep 2026 20:39:30 +0000 Subject: [PATCH] Force check layers to execute on every cibuild run (closes #89) script/cibuild ran a bare "docker build .". The Dockerfile does "COPY . ." and then runs the checks, so on an unchanged tree every check layer was a cache hit: the suite never executed and the build still exited 0. A green from script/cibuild did not mean the checks had passed, only that they had passed at some point in the past. Declare "ARG CHECK_EPOCH" in each stage that runs a check, positioned below the dependency layers and immediately above the first check, and have script/cibuild pass a fresh "$(date +%s)" on every invocation. A changed build arg invalidates every layer below its declaration, so the checks always execute while the base images, "go mod download" and the "yarn install" in mdfmt stay cached. All three check-running stages are covered: lint (fmt-check-go, lint), mdfmt (prettier --check) and builder (test). ARG is scoped per stage, so a stage without its own declaration would keep serving a cached pass and be indistinguishable from a working fix at the exit code. "--no-cache" was not used: it would also discard "go mod download" and the yarn install, for no additional guarantee. The README's build-status claim is accurate again and now says why. --- Dockerfile | 14 ++++++++++++++ README.md | 8 ++++++-- script/cibuild | 7 ++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 82c49a1..7bf54d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,14 @@ COPY . . # Touch .pb.go so make does not try to regenerate via protoc (file is committed) RUN touch mfer/mf.pb.go +# Cache buster. script/cibuild passes a fresh CHECK_EPOCH on every invocation, +# which invalidates every layer below this line so the checks always really +# execute. Layers ABOVE it (base image, go mod download) keep their cache, so +# this costs nothing but the checks themselves. Every stage that runs a check +# needs its own copy: ARG is scoped per stage, and a stage without one silently +# serves a cached pass. See https://git.eeqj.de/sneak/mfer/issues/89. +ARG CHECK_EPOCH + # Go half of fmt-check only: this image has no node, so no prettier. The # markdown half runs in the mdfmt stage below. RUN make fmt-check-go @@ -27,6 +35,9 @@ RUN yarn install --frozen-lockfile COPY . . +# Cache buster, so the check below always executes; see the lint stage. +ARG CHECK_EPOCH + # No make in this image; call the script entrypoint directly. RUN script/prettier --check @@ -47,6 +58,9 @@ COPY . . # Touch .pb.go so make does not try to regenerate via protoc (file is committed) RUN touch mfer/mf.pb.go +# Cache buster, so the check below always executes; see the lint stage. +ARG CHECK_EPOCH + RUN make test RUN cd cmd/mfer && go build -tags urfave_cli_no_docs -o /mfer . diff --git a/README.md b/README.md index ec05d47..17d1846 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,12 @@ javascript library is planned. # Build Status -CI runs via `script/cibuild` (`docker build .`), which executes `make check` -(formatting, linting, tests). The `main` branch must always be green. +CI runs via `script/cibuild`, which builds the Dockerfile. Every stage that runs +a check declares an `ARG CHECK_EPOCH` above it, and `script/cibuild` passes a +fresh value on every invocation, so the check layers cannot be served from the +Docker layer cache. A successful build therefore implies that formatting, +linting, and tests actually ran and passed on the current tree. The `main` +branch must always be green. # Entrypoints diff --git a/script/cibuild b/script/cibuild index 3da5857..d5ae6a6 100755 --- a/script/cibuild +++ b/script/cibuild @@ -6,9 +6,14 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" +# A bare "docker build ." serves the check layers straight from the Docker +# layer cache when the tree has not changed, so the build exits 0 without ever +# running the checks. CHECK_EPOCH changes on every invocation and the +# Dockerfile declares it above the checks in every stage that runs one, which +# forces them to execute while leaving the dependency layers cached. main() { cd "$ROOT" - docker build . + docker build --build-arg CHECK_EPOCH="$(date +%s)" . } main "$@"