build: pin the runner's Node and PostgreSQL client packages

nodejs-22 and postgresql-18-client are installed at exact package
versions, so a rebuild of the same commit runs the same Node and the
same pg_dump; apk upgrade still moves the libraries under them. No
updater reads an apk pin. The image scan does the job instead: Wolfi
records the fixed version for each advisory, so a pinned package with a
published fix turns the HIGH/CRITICAL gate red and names the version.
A guard holds the pin format and keeps the client's minor on the bundled
database's.
This commit is contained in:
Marc-André Bombeck committed 2026-11-03 23:27:06 +01:00
1 parent a11ea8444d
commit 42cae738f1
2 files changed
+69 -1

No files matched your search

+13 -1
View File
@@ -106,7 +106,19 @@ FROM cgr.dev/chainguard/wolfi-base:latest@sha256:629d9acaceda7746cd9293365eb0d8e
#
# busybox, part of the base, is the shell the entrypoint runs under and
# carries `tar`/`gzip` for the GeoLite2 extraction and `addgroup`/`adduser`.
RUN apk add --no-cache nodejs-22 tzdata postgresql-18-client && \
#
# The two packages the app runs on are pinned to an exact package version,
# so a rebuild of the same commit runs the same Node and the same pg_dump;
# `apk upgrade` cannot move a pinned package, and moves only the libraries
# under them. No updater reads these pins. What moves them is the image
# scan: Wolfi records the fixed package version for every advisory, so a
# pinned package with a published fix turns the HIGH/CRITICAL gate red and
# names the version to pin. To bump, take the newest from
# `apk policy nodejs-22` / `apk policy postgresql-18-client` in the pinned
# base. `src/__tests__/runtime-package-pins.test.ts` holds the pin format,
# and the client's minor to the bundled database's in docker-compose.yml.
# tzdata stays unpinned on purpose: a zone rule change is a fix to take.
RUN apk add --no-cache nodejs-22=22.23.2-r1 tzdata postgresql-18-client=18.6-r7 && \
apk upgrade --no-cache && \
mkdir -p /usr/local/bin
COPY --from=builder /app/.native-abi /tmp/.native-abi
@@ -0,0 +1,56 @@
/**
* The runtime packages the image installs from apk are pinned, and the
* PostgreSQL client moves with the bundled database.
*
* The runner installs Node and the PostgreSQL client from the Wolfi
* repository. Unpinned, a rebuild of the same commit could ship another
* Node or another pg_dump than the one the release was checked with. No
* updater reads an apk pin; the image scan is what flags a pinned package
* once Wolfi publishes a fix (the Dockerfile comment explains the bump), so
* this file only holds the pins' shape and their agreement with the rest of
* the repository.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const ROOT = process.cwd();
const DOCKERFILE = readFileSync(join(ROOT, "Dockerfile"), "utf8");
const COMPOSE = readFileSync(join(ROOT, "docker-compose.yml"), "utf8");
function runner(): string {
const start = DOCKERFILE.search(/^FROM .* AS runner$/m);
expect(start, "the Dockerfile has a runner stage").toBeGreaterThan(0);
return DOCKERFILE.slice(start);
}
function apkAdds(): string[] {
return [...runner().matchAll(/apk add --no-cache ([^&\n\\]+)/g)].flatMap(
(m) => m[1]!.trim().split(/\s+/),
);
}
describe("runtime package pins", () => {
it("pins nodejs-22 to one exact Node 22 package version", () => {
const pins = apkAdds().filter((p) => p.startsWith("nodejs"));
expect(pins).toHaveLength(1);
expect(pins[0]).toMatch(/^nodejs-22=22\.\d+\.\d+-r\d+$/);
});
it("pins postgresql-18-client to the bundled database's minor", () => {
const pins = apkAdds().filter((p) => p.startsWith("postgresql"));
expect(pins).toHaveLength(1);
const client = /^postgresql-18-client=(18\.\d+)-r\d+$/.exec(pins[0]!);
expect(client, `an exact pin, not ${pins[0]}`).not.toBeNull();
const db = /^\s*image:\s*postgres:(\d+\.\d+)-alpine$/m.exec(COMPOSE);
expect(db, "the compose file pins the database").not.toBeNull();
// One decision: a database minor bump moves the client with it.
expect(client![1]).toBe(db![1]);
});
it("leaves no other package floating except tzdata and the build-only npm", () => {
const unpinned = apkAdds().filter((p) => !p.includes("="));
expect(new Set(unpinned)).toEqual(new Set(["tzdata", "npm"]));
});
});