# node 22-alpine, 2026-02-22 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 WORKDIR /app # script/bootstrap installs all prerequisites (make via apk here; node # and yarn are already in the base image, so those steps are skipped). # Dependency manifests are copied first so the bootstrap layer is # cached until they change. COPY script/ script/ COPY package.json yarn.lock ./ RUN script/bootstrap COPY . . # CHECK_EPOCH is a per-invocation nonce supplied by script/cibuild and # script/docker. Without it an unchanged tree serves this layer from # cache and the build reports a green it never ran. ARG is stage-scoped, # so it must be redeclared in every stage that runs checks. The guard # makes a bare `docker build .` fail loudly instead of silently reusing # the empty (and therefore stable) cache key. Expand the value into the # command so the cache miss does not depend on BuildKit's handling of an # unreferenced ARG. Both the guard and the check RUN reference the value, # so both are value-keyed: there are two independent invalidation points # here, not one. Keep both. ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 # The individual non-lint checks, NOT `make check`. Lint is deliberately # absent here: `script/lint` is itself a `docker build` (of # Dockerfile.lint), so running `make check` in this image would attempt # a docker build inside a build step, where there is no daemon. Putting # `make check` back reintroduces exactly that recursion. Lint is not # skipped — script/cibuild runs script/lint first, in its own container, # before this build starts. RUN echo "check epoch: ${CHECK_EPOCH}" && script/test RUN script/fmt-check