# Lint image. Every lint-class check for this repo runs here and # nowhere else: the checks are build steps, so a successful build IS a # clean lint. There is no host lint path and no "am I already in a # container?" bypass -- script/lint and script/fmt-check are reduced to # building the stage below that carries their check. # # Build this only via script/lint or script/fmt-check: both pass the # CHECK_EPOCH build argument that the stages here require, and a bare # `docker build -f Dockerfile.lint .` fails by design. See the guards. # # Why this is a separate file from the main Dockerfile, and why that # one no longer runs `make check`: `make check` runs script/lint, and # script/lint is now a `docker build`. A `RUN make check` in an image # would therefore be docker-in-docker inside a bare alpine with no # docker client and no daemon socket. The checks are split by where # they run instead -- the main Dockerfile runs the production build, # this file runs lint and the format check -- so no image ever shells # back into `make check`. script/cibuild drives all of them, so CI # coverage is unchanged. # alpine 3.21, 2026-02-28 FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 AS base WORKDIR /src # Keep these four instructions byte-identical to the main Dockerfile's, # in the same order: Docker keys layers on the instruction chain, not on # the file they live in, so an identical prefix means this stage is a # cache hit against the main image's layers. script/bootstrap compiles # the pinned Hugo from source, which is by far the most expensive step # here, and it must not be paid twice. COPY script/ script/ RUN script/bootstrap COPY . . # --- lint ------------------------------------------------------------- # # The site's lint is a clean build that surfaces broken internal links # and template path problems: `hugo` fails on build errors and # --printPathWarnings reports render-target collisions. # # The flags are inlined rather than reached through `RUN script/lint`, # and that is forced, not lazy: script/lint is the docker build that # produces this stage, so calling it here is the recursion described # above. Keep these flags in sync with script/lint's documentation. FROM base AS lint # CHECK_EPOCH is a per-invocation nonce supplied by script/lint. Without # it an unchanged tree serves the check layer from cache: the lint never # executes and the build still exits 0, which is precisely the false # green this repo already fixed once in the main Dockerfile. Caching is # explicitly waived for lint, so the value is expanded into the checked # command as well as the guard -- two independent value-keyed # invalidation points, so a cache miss never depends on BuildKit's # treatment of an unreferenced ARG. Declared with no default: a default # is a constant, and a constant is a stable cache key. ARG is # stage-scoped, so the fmt-check stage below declares its own. # # Everything above this line still caches, so script/bootstrap is not # rebuilt. ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN echo "lint epoch: ${CHECK_EPOCH}" && hugo --minify --printPathWarnings # --- fmt-check -------------------------------------------------------- # # The read-only prettier check over this repo's markdown and CSS. Same # version, same scope and same flags as script/fmt, which is the # authoritative copy and stays on the host because it writes to the # working tree; keep the two in sync. The exclusions live in # .prettierignore with the reason for each. # # Built as a sibling of `lint` rather than stacked on top of it so that # a --target build runs exactly one check, and so a lint failure and a # formatting failure are reported independently. FROM base AS fmt-check # Same nonce, same reasoning as the lint stage above. Declared again # because ARG does not cross a FROM. ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN echo "fmt-check epoch: ${CHECK_EPOCH}" && \ npx --yes "prettier@3.4.2" --check \ '**/*.md' '**/*.css' --tab-width 4 --prose-wrap always