ci: preserve weekly Docker tool cache across commits (#13190)

Keep stable Docker tool installation layers independent of application build version and commit metadata. Preserve the existing weekly tool refresh and runtime build stamp.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley 2026-09-10 20:10:31 -07:00 committed by GitHub
parent 399daa1f25
commit 3fd556b8f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 34 additions and 8 deletions

View File

@ -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 \

View File

@ -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 \

View File

@ -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");