All checks were successful
check / check (pull_request) Successful in 2m57s
script/cibuild was a bare `docker build .` with no cache control. The
Dockerfile does `COPY . .` and then `RUN make fmt-check` / `RUN make
lint` in the lint stage and `COPY . .` / `RUN make test` in the builder
stage. On an unchanged tree Docker served those RUN layers from cache,
so the checks never executed, and the build still exited 0 -- the exit
code, which is the one signal automation trusts, was wrong, and wrong
in the direction that matters: the longer a branch sits unchanged, the
more likely its "verification" is a replay, which is exactly its state
just before a merge.
The fix is an `ARG CHECK_EPOCH` declared immediately above the check
RUNs, with script/cibuild passing a fresh value on every invocation.
ARG scope is per-stage in Docker, so the lint stage and the builder
stage each declare their own; covering only one would leave half the
gate fake.
Placement is the substance of the change. The ARG sits below the
`apk add`, `COPY go.mod go.sum`, and `go mod download` layers in both
stages, so only the check layers are invalidated: earlier and every
build would be cold, later and the checks would stay cached.
The epoch is assigned to its own variable rather than substituted
inline into the --build-arg:
epoch="$(date +%s)"
docker build --build-arg CHECK_EPOCH="$epoch" .
Under `set -eu` a command substitution that fails inside an argument
does not abort the script. Inline, a failing `date` would leave
CHECK_EPOCH an empty string; an empty string is a constant; and a
constant CHECK_EPOCH is precisely the cached-check false green this
commit exists to eliminate -- so the guard would have carried a silent
path to the defect it guards against. As a bare assignment, `set -e`
aborts before any build starts.
The guarantee is conditional, and README.md and the Dockerfile now say
so instead of claiming the check layers can never be cached. They are
keyed on CHECK_EPOCH, so they re-run for any value not yet built
against this tree -- but a build that omits --build-arg gets the empty
default, and on an unchanged tree every build after the first then
replays them, executes nothing, and exits 0. That state was produced
by measurement rather than reasoned about. Issue #91 tracks the
upstream hardening that would make the missing-arg case fail loudly,
along with the expanded ARG form, a per-invocation epoch, and
script/docker.
The bare unreferenced ARG form is kept deliberately, not because it
matches upstream -- upstream has since settled on expanding the value
into the check command. A declared-but-unreferenced ARG does enter
BuildKit's cache key, which is measured on this host rather than
assumed, and upstream records that repos on the bare form need no
rework. Moving to the expanded form is hardening, tracked in #91.
The script/cibuild header comment claimed the Dockerfile runs
script/check via make check. It does not: it runs make fmt-check and
make lint in the lint stage and make test in the builder stage.
Corrected.
Measurements are recorded once, in the verification comment on PR #89:
a back-to-back script/cibuild pair on an unchanged tree, and the
counterfactual that withholds --build-arg and reproduces the original
false green on its second run. They are deliberately not restated here
or in TODO.md, so there is a single record that cannot disagree with
itself.
.golangci.yml is unchanged (sha256 021cc83f4e6f...643346bcb), as is the
lint-stage FROM line that is the single source of truth for the linter
version, script/lint's pinned-image logic, and
.gitea/workflows/check.yml, whose only step is script/cibuild.
33 lines
1.4 KiB
Bash
Executable File
33 lines
1.4 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/cibuild: run the CI build. The Dockerfile does not run
|
|
# script/check; it runs `make fmt-check` and `make lint` in its lint
|
|
# stage and `make test` in its builder stage. A successful build
|
|
# implies those three passed, provided they actually ran -- which is
|
|
# what the CHECK_EPOCH below is for.
|
|
# Generic: needs no adaptation. The Gitea workflow runs this on push.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
# The Dockerfile's check layers are keyed on CHECK_EPOCH, so a
|
|
# 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.
|
|
#
|
|
# Assign the epoch on its own line rather than inline in the
|
|
# argument. Under `set -eu` a command substitution that fails
|
|
# inside an argument does NOT abort the script: CHECK_EPOCH would
|
|
# become an empty string, an empty string is a constant, and a
|
|
# constant CHECK_EPOCH is exactly the cached-check false green this
|
|
# 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)"
|
|
docker build --build-arg CHECK_EPOCH="$epoch" .
|
|
}
|
|
|
|
main "$@"
|