diff --git a/Dockerfile b/Dockerfile index b51a2cfa97..901289778e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -103,14 +103,6 @@ RUN rm -rf packages/paperclip-runner/runner/target FROM base AS production ARG USER_UID=1000 ARG USER_GID=1000 -# Real version for this build, computed from `git describe` on the CI runner -# (the image has no .git, so the server cannot derive it at runtime). Empty for -# local `docker build`, which just leaves the server on its normal fallbacks. -ARG PAPERCLIP_BUILD_VERSION="" -# The exact commit this image was built from, for the same reason: server-info -# falls back to PAPERCLIP_BUILD_COMMIT when git is unavailable, which feeds the -# /api/health `commit` field that deploy tooling verifies. Empty locally. -ARG PAPERCLIP_BUILD_COMMIT="" # Refreshes the tool layer below when it changes (CI stamps an ISO week, so # the @latest CLI tools advance weekly). Without it the cached layer would # freeze the tools until an unrelated cache bust. @@ -133,6 +125,13 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh COPY --chown=node:node --from=build /app /app +# Declare per-build metadata after the stable RUN layers. Docker includes +# in-scope ARG values in a RUN's environment even when its command does not +# mention them; declaring these earlier invalidates the weekly tool cache. +# The build stage still receives the commit before writing dist/build-info.json. +# Empty for local builds, preserving the server's normal version fallbacks. +ARG PAPERCLIP_BUILD_VERSION="" +ARG PAPERCLIP_BUILD_COMMIT="" ENV NODE_ENV=production \ HOME=/paperclip \ HOST=0.0.0.0 \ diff --git a/doc/DOCKER.md b/doc/DOCKER.md index 93ffe55554..c89664fd0a 100644 --- a/doc/DOCKER.md +++ b/doc/DOCKER.md @@ -18,6 +18,14 @@ Build arguments: |-----|---------|---------| | `USER_UID` | `1000` | UID for the container `node` user (match your host UID to avoid permission issues on bind mounts) | | `USER_GID` | `1000` | GID for the container `node` group | +| `CLI_TOOLS_CACHE_EPOCH` | empty | Refresh the CLI-install layer; CI supplies the current ISO week | +| `PAPERCLIP_BUILD_VERSION` | empty | Runtime version when Git metadata is unavailable | +| `PAPERCLIP_BUILD_COMMIT` | empty | Source commit written into the server build stamp and runtime environment | + +Changing the build version or commit preserves the CLI-install cache. The +tool layer refreshes when its weekly epoch, base image, installation command, +or earlier build inputs change. Local builds can set a new epoch explicitly +to refresh tools without clearing the entire build cache. ```sh docker build -t paperclip-local \ diff --git a/server/src/__tests__/docker-build-stamp.test.ts b/server/src/__tests__/docker-build-stamp.test.ts index f013d30ec6..454b8b912e 100644 --- a/server/src/__tests__/docker-build-stamp.test.ts +++ b/server/src/__tests__/docker-build-stamp.test.ts @@ -35,6 +35,25 @@ function stageBody(source: string, stageName: string): string { return source.slice(start, end); } +it("keeps per-build runtime metadata out of the weekly CLI-install cache", () => { + const production = stageBody(dockerfile, "production"); + const tools = production.search(/^RUN echo "cli-tools-epoch:/m); + const entrypoint = production.search(/^RUN chmod \+x \/usr\/local\/bin\/docker-entrypoint\.sh/m); + const runtime = production.search(/^ENV NODE_ENV=production/m); + const epoch = production.search(/^ARG CLI_TOOLS_CACHE_EPOCH\b/m); + expect(tools).toBeGreaterThanOrEqual(0); + expect(entrypoint).toBeGreaterThan(tools); + expect(epoch).toBeGreaterThanOrEqual(0); + expect(epoch).toBeLessThan(tools); + for (const name of ["PAPERCLIP_BUILD_VERSION", "PAPERCLIP_BUILD_COMMIT"]) { + const declarations = [...production.matchAll(new RegExp(`^ARG ${name}\\b`, "gm"))]; + expect(declarations).toHaveLength(1); + expect(declarations[0].index).toBeGreaterThan(entrypoint); + expect(declarations[0].index).toBeLessThan(runtime); + expect(production.slice(runtime)).toContain(`${name}=\${${name}}`); + } +}); + describe("docker build-stamp wiring", () => { it("declares PAPERCLIP_BUILD_COMMIT in the build stage before the server build", () => { const build = stageBody(dockerfile, "build");