From 3e547b8568653876a2cfbef28bcec9ab516af00a Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Mon, 1 Jun 2026 14:02:38 +0000 Subject: [PATCH] fix(docker): bake pnpm via npm to remove Corepack runtime downloads (GRO-1981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GRO-1983 fast restoration swapped Corepack's pnpm shim for a real `npm install -g pnpm@9.15.4` binary, which is the right move. But the GRO-1997 evidence gate still showed the first `reset-demo-data` pod (...-nh7vg) hitting `getaddrinfo EAI_AGAIN registry.npmjs.org` before a retry succeeded — the cache was writable, the cold-cache registry download wasn't eliminated. This is the durable fix: 1. `ENV COREPACK_ENABLE_DOWNLOAD_FALLBACK=0` in `base` and `runner`: defence in depth so a Corepack shim can never silently re-download pnpm, even if it is somehow re-introduced. 2. `ENV HOME=/tmp` in the `migrate`, `seed`, and `reset` stages: under `readOnlyRootFilesystem: true` + `runAsUser: 1000`, the default HOME path is read-only, and pnpm fails the first time it tries to write a config or state file. The job pods already mount a writable emptyDir at `/tmp`; point HOME there. 3. CI smoke tests for `seed` and `reset` images (matching the existing `migrate` smoke): point `registry.npmjs.org` at 127.0.0.1 in a throwaway container, assert `which pnpm` resolves to `/usr/local/bin/pnpm` (real binary, not shim), and that `pnpm --version` succeeds without network egress. If Corepack ever sneaks back in, CI catches it on every PR. The vestigial `RUN mkdir -p /home/node/.cache/node/corepack` in the `builder` stage (mentioned in the spec) was already removed in GRO-1909 (commit 0a3eb8a), so nothing to do there. Follow-on cleanup of the per-job `COREPACK_HOME` env vars and `node-cache` emptyDir mounts in `groombook/infra` is intentionally deferred to a coordinated infra PR once the new image is deployed — keeping the existing infra in place during the transition avoids a flag-day. GRO-1985, hardening follow-up to GRO-1984 / GRO-1983. Closes parent: GRO-1981. Co-Authored-By: Paperclip --- .gitea/workflows/ci.yml | 29 +++++++++++++++++++++++++++++ Dockerfile | 14 +++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9103390..d848d3b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -156,3 +156,32 @@ jobs: ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/reset:latest' || '' }} cache-from: type=registry,ref=git.farh.net/groombook/cache:reset cache-to: type=registry,ref=git.farh.net/groombook/cache:reset,mode=max + + - name: Smoke test seed image (blackhole npmjs.org) + run: | + set -euo pipefail + IMAGE="git.farh.net/groombook/seed:${{ steps.version.outputs.tag }}" + docker pull "$IMAGE" + # GRO-1985: pnpm must be a real binary, not a Corepack shim, and must + # not try to reach registry.npmjs.org on invocation. + docker run --rm \ + --add-host registry.npmjs.org:127.0.0.1 \ + --entrypoint="" \ + "$IMAGE" \ + sh -c 'set -e; test "$(which pnpm)" = "/usr/local/bin/pnpm"; pnpm --version' + echo "seed image: pnpm resolves to /usr/local/bin/pnpm and runs offline ✓" + + - name: Smoke test reset image (blackhole npmjs.org) + run: | + set -euo pipefail + IMAGE="git.farh.net/groombook/reset:${{ steps.version.outputs.tag }}" + docker pull "$IMAGE" + # GRO-1985: pnpm must be a real binary, not a Corepack shim, and must + # not try to reach registry.npmjs.org on invocation. Validates the + # hard requirement from the issue: reset runs offline. + docker run --rm \ + --add-host registry.npmjs.org:127.0.0.1 \ + --entrypoint="" \ + "$IMAGE" \ + sh -c 'set -e; test "$(which pnpm)" = "/usr/local/bin/pnpm"; echo "HOME=$HOME"; pnpm --version' + echo "reset image: pnpm resolves to /usr/local/bin/pnpm, HOME=/tmp, runs offline ✓" diff --git a/Dockerfile b/Dockerfile index 5fea669..a77a7df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,12 @@ FROM node:22-alpine AS base # invocations of `pnpm` work without DNS access to registry.npmjs.org. # The corepack shim delegates to corepack, which re-validates against # npmjs.org on first use — that fails in air-gapped UAT seed/migrate/reset -# Jobs. GRO-1983 / GRO-1889 / GRO-1909. +# Jobs. GRO-1983 / GRO-1889 / GRO-1909 / GRO-1981 / GRO-1985. RUN npm install -g pnpm@9.15.4 +# Belt-and-braces: disable Corepack's download fallback so that even if a +# Corepack shim is somehow invoked at runtime, it will not try to fetch +# pnpm from registry.npmjs.org. Belt for the real-binary trousers. GRO-1985. +ENV COREPACK_ENABLE_DOWNLOAD_FALLBACK=0 WORKDIR /app # Install deps @@ -26,6 +30,8 @@ RUN pnpm --filter @groombook/types build && \ # Runtime FROM node:22-alpine AS runner RUN npm install -g pnpm@9.15.4 +# Same defence-in-depth as base: no Corepack fallback. GRO-1985. +ENV COREPACK_ENABLE_DOWNLOAD_FALLBACK=0 WORKDIR /app ENV NODE_ENV=production @@ -46,12 +52,18 @@ CMD ["node", "dist/index.js"] # Migrate stage — runs drizzle-kit migrate against the database FROM builder AS migrate +# pnpm needs a writable HOME for any config/state it writes. With +# readOnlyRootFilesystem: true and runAsUser: 1000, /home/node is read-only. +# The job pods mount a writable emptyDir at /tmp; point HOME there. GRO-1985. +ENV HOME=/tmp CMD ["pnpm", "--filter", "@groombook/db", "migrate"] # Seed stage — populates the database with test data FROM builder AS seed +ENV HOME=/tmp CMD ["pnpm", "--filter", "@groombook/db", "seed"] # Reset stage — drops all tables, re-runs migrations, and re-seeds FROM builder AS reset +ENV HOME=/tmp CMD ["pnpm", "--filter", "@groombook/db", "reset"]