# The check runner as a single container. Build from the repository root: # # docker build -f packages/git-ci/Dockerfile -t pdsjs-git-ci . # # Built from workspace source rather than the published packages, so the # image matches the commit it was built from. # # The steps a workflow names run in this container, as this container's # user, with this container's environment. That is the isolation boundary: # watch only repositories whose contributors you trust. FROM node:22-trixie-slim AS builder # corepack resolves the pnpm version from package.json's packageManager # field, and prompts before downloading it, which would hang this build. ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 RUN corepack enable WORKDIR /app # @pdsjs/git-ci reaches only @pdsjs/core, @pdsjs/git and pako, none of which # compile anything. The workspace root carries better-sqlite3, whose install # script compiles; --ignore-scripts skips it, and the daemon never loads it, # so the build needs no toolchain. COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY packages packages RUN pnpm install --frozen-lockfile --prod --ignore-scripts \ --filter @pdsjs/git-ci... # trixie (glibc 2.41), not bookworm (2.36): a workflow that installs the # repository under test loads its native addons here, and better-sqlite3's # prebuilt binary requires glibc >= 2.38. FROM node:22-trixie-slim AS runtime WORKDIR /app # git clones the repository under test from the PDS over https. The # toolchain is for the repository's own dependencies: a package with a # native addon and no prebuild for this platform compiles on install, and a # workflow that cannot install cannot run. # docker-cli and docker-buildx are the client alone. A workflow that builds an # image talks to the daemon on the host through the socket the deployment # mounts; nothing runs a daemon in here. buildx is what serves a Dockerfile # using BuildKit syntax, which the classic builder rejects. # # Three installs rather than one. Each RUN is one image layer, a registry # takes one request per layer, and a Cloudflare Worker refuses a request # over 100 MB on the lowest plan. The whole toolchain in one layer is 155 MB # compressed; split this way the largest layer stays near 80 MB. g++ is the # bulk, so it stands alone. RUN apt-get update \ && apt-get install -y --no-install-recommends \ git ca-certificates gosu python3 make \ && rm -rf /var/lib/apt/lists/* RUN apt-get update \ && apt-get install -y --no-install-recommends \ g++ \ && rm -rf /var/lib/apt/lists/* RUN apt-get update \ && apt-get install -y --no-install-recommends \ docker-cli docker-buildx \ && rm -rf /var/lib/apt/lists/* # Workflows that install dependencies reach for the package manager their # lockfile names, so corepack resolves it from the repository under test. ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 RUN corepack enable # The remote helper, for a repository smart HTTP cannot serve. `git clone # atproto://...` finds it here by name. RUN printf '#!/bin/sh\nexec node /app/packages/git/src/cli.js "$@"\n' \ > /usr/local/bin/git-remote-atproto \ && chmod 755 /usr/local/bin/git-remote-atproto # Runs unprivileged. The node image ships a "node" user for exactly this. # The entrypoint starts as root only to take the state mount, then drops. RUN mkdir -p /data && chown node:node /data COPY --from=builder --chown=node:node /app /app COPY --chmod=755 packages/git-ci/docker-entrypoint.sh /usr/local/bin/ ENV NODE_ENV=production # The cursor and the ref map, which decide what replays after a restart. # Mount a volume at /data to keep them. There is no VOLUME instruction: some # hosts reject one, and `docker run -v` needs no declaration here. Without a # mount, or with one the daemon cannot write, it still runs; a restart then # replays from the live head. ENV PDSJS_CI_STATE=/data/state.json # PDSJS_CI_IDENTIFIER, PDSJS_CI_PASSWORD and PDSJS_CI_REPOS are deliberately # unset. The CLI prints its usage and exits rather than inventing them. ENTRYPOINT ["docker-entrypoint.sh"] CMD ["node", "packages/git-ci/src/cli.js", "watch"]