# Lint image: every lint run happens here, and nowhere else. The repo is # COPYed into a digest-pinned image and the linters run as build steps, so a # successful build IS a clean lint. `script/lint` does nothing but build this # file, which also works where the docker daemon is remote and bind mounts are # impossible. Nothing that runs inside a container may call `script/lint`: # that is why Dockerfile no longer runs `make check`. # node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint WORKDIR /app # Manifests before sources, so the dependency install layer stays cached # until package.json or yarn.lock changes. script/bootstrap ends in # `yarn install --frozen-lockfile`; the lint steps below are deliberately # not cached. COPY script/ script/ COPY package.json yarn.lock ./ RUN script/bootstrap COPY . . # LINT_EPOCH is a cache buster, with the same fail-closed contract as # CHECK_EPOCH in Dockerfile. No lint cache is wanted: on an unchanged tree # Docker serves the linter layers in well under a second, having linted # nothing, and the build still exits 0. The guard makes an absent argument a # hard failure — an unset ARG is the empty string, which is a perfectly # stable cache key, so a plain `docker build -f Dockerfile.lint .` would # otherwise get exactly that false green. Every layer below this one is a # child of the guard, so a fresh epoch forces all of them to execute. ARG LINT_EPOCH RUN [ -n "$LINT_EPOCH" ] || exit 1 # The linters are invoked directly rather than through `make lint`, because # `make lint` is the build of this file. RUN yarn run eslint . RUN yarn run prettier --check .