# Lint phase. The linters are 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.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint WORKDIR /app COPY script/ script/ COPY package.json yarn.lock ./ RUN script/bootstrap COPY . . RUN yarn run eslint . RUN yarn run prettier --check . # Test phase, same shape and for the same reason. The suite runs without # verbose output first and is rerun verbosely only if it fails; the timeout # catches a hung test. # # node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS test WORKDIR /app COPY script/ script/ COPY package.json yarn.lock ./ RUN script/bootstrap COPY . . # Unlike the template, the suite runs as the image's non-root `node` user: # root ignores directory permissions, so the tests of a destination that is # not writable would otherwise be skipped. vitest writes into /app. RUN chown -R node:node /app USER node RUN timeout 90 yarn run vitest run --reporter=dot || \ { echo "--- Rerunning with verbose for details ---"; \ timeout 90 yarn run vitest run --reporter=verbose; exit 1; } # Build stage, 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.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09 FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 WORKDIR /app COPY --from=lint /app/package.json /dev/null COPY --from=test /app/package.json /dev/null 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}" RUN make build