All checks were successful
check / check (push) Successful in 1m9s
Add a root Dockerfile.lint that carries the checks as build steps -- a `lint` stage running `hugo --minify --printPathWarnings` and a `fmt-check` stage running the prettier check -- and reduce script/lint and script/fmt-check to building their stage. A successful build is a clean check. There is no host path and deliberately no "am I already inside a container?" branch, which would be a host lint path in disguise. The two stages share a `base` whose first four instructions are byte-identical to the main Dockerfile's, so the expensive `RUN script/bootstrap` layer that compiles the pinned Hugo from source is a cache hit against the main image instead of a second build of the same thing. Resolve the resulting recursion by splitting the checks by where they run, not with an escape hatch. `make check` runs script/lint, so the main Dockerfile can no longer `RUN make check`: that would be docker-in-docker inside a bare Alpine with no docker client and no daemon socket, and script/cibuild is what CI runs on every push. The main Dockerfile therefore runs `make test`, the production build, and script/cibuild builds it and then calls script/lint and script/fmt-check. CI still covers the production build, lint and the format check, and it runs exactly what a developer runs. script/fmt stays on the host because it rewrites the working tree, which a container build cannot do. That makes it the authoritative copy of the prettier version, scope and flags that the fmt-check stage duplicates; both sides carry a keep-in-sync note. The duplication is forced: any `RUN script/fmt-check` inside the image is the recursion again. Caching is waived for the checks in the shape this repo already settled: `ARG CHECK_EPOCH` with no default, declared and guarded separately in each stage because ARG does not cross a FROM, with the value expanded into the checked command as well as the guard so invalidation does not rest on BuildKit's treatment of an unreferenced ARG. All four image-building entrypoints now generate and pass it -- script/cibuild, script/docker, script/lint, script/fmt-check. Verified: two consecutive script/lint runs on an unchanged tree both executed hugo for real, with script/bootstrap CACHED; a constant-epoch counterfactual restored the false green (exit 0, lint layer CACHED, no hugo output); an empty epoch failed closed on the guard; a broken template failed the lint stage and an unformatted README failed the fmt-check stage, both reverted and re-run clean; script/cibuild and `make check` are green with all three checks demonstrably executing.
86 lines
4.0 KiB
Docker
86 lines
4.0 KiB
Docker
# 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
|