# Hugo static-site build image. The build runs `make check` (a clean # `hugo --minify` production build, the `--printPathWarnings` lint # build, then the read-only prettier docs check), so the image build # fails on any formatting or Hugo build error. This is what CI # (script/cibuild) runs on every push. # # Build this only via script/cibuild or script/docker: both pass the # CHECK_EPOCH build argument that this file requires. A bare # `docker build .` fails by design - see the guard below. # alpine 3.21, 2026-02-28 FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 WORKDIR /src # Install build dependencies first so the layer caches until the # scripts change (script/bootstrap installs git, make, hugo, node/npm). COPY script/ script/ RUN script/bootstrap COPY . . # Everything from here down is invalidated on every build; everything # above it still caches. `COPY . .` alone is not enough to force the # checks to run: it is keyed on content, so an unchanged tree serves # `RUN make check` from cache and the build reports a green without # having run anything. CHECK_EPOCH is a per-invocation value passed by # script/cibuild and script/docker, so the check layer is never reused. # # Declared with no default on purpose. A default would be a constant, # and a constant is a stable cache key - the defect unchanged. ARG CHECK_EPOCH # An unset ARG is the empty string, which is also a stable cache key, so # without this guard a bare `docker build .` would still get the cached # green. Failed steps are never cached, so this fails on every such # invocation rather than once. RUN [ -n "$CHECK_EPOCH" ] || { \ echo "CHECK_EPOCH is unset: build via script/cibuild or script/docker" >&2; \ exit 1; \ } # Run all checks - build fails if any check fails. The epoch is expanded # into the command so the cache miss does not depend on BuildKit's # handling of a declared-but-unreferenced ARG, and so the value is # visible in the build log. RUN echo "check epoch: ${CHECK_EPOCH}" && make check