# Lint phase. The linter is invoked directly rather than through `make # lint` or `script/lint`, which are themselves a docker build and would # recurse into a daemon that does not exist in a build step. # # node 22-alpine, 2026-02-22 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint WORKDIR /app COPY script/ script/ COPY package.json yarn.lock ./ RUN script/bootstrap COPY . . RUN yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always # Test phase, same shape and for the same reason. # # node 22-alpine, 2026-02-22 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS test WORKDIR /app COPY script/ script/ COPY package.json yarn.lock ./ RUN script/bootstrap COPY . . RUN echo "No tests defined." # Development environment, and the last stage: a plain `docker build .` # names no target and so builds this one. Nothing is wanted from the two # phases above; the copies are what make BuildKit build them first, so # this image cannot be produced unless lint and test passed. A stage # appended after this one would drop all three out of a plain build. # # node 22-alpine, 2026-02-22 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 WORKDIR /app COPY --from=lint /app/package.json /dev/null COPY --from=test /app/package.json /dev/null # script/bootstrap installs all prerequisites. Manifests are copied # first so that layer stays cached until dependencies change. COPY script/ script/ COPY package.json yarn.lock ./ RUN script/bootstrap COPY . . # The version is computed on the host and passed in, because # .dockerignore excludes .git. ARG VERSION=dev LABEL org.opencontainers.image.version="${VERSION}"