build: run the image on a Wolfi runner

The dependency and build stages stay on node:22-trixie-slim, where pnpm
picks the glibc native packages. The runner moves to wolfi-base, pinned
by its index digest: glibc, about fifty packages, and every one of them
known to the image scan, Node and the PostgreSQL client included.

nodejs-22, tzdata and postgresql-18-client come from apk, so the pgclient
stage and its pin guard go. The builder writes its Node module ABI and
glibc version out and the runner refuses to build on another ABI or an
older glibc. Wolfi's npm is major 12 and runs no install scripts unless
package.json allows them, so the Prisma CLI install names the three
packages that have one, and the build checks that the schema engine file
is there before asking the CLI about it. npm is installed for that step
and removed in the same layer; the check that nothing is left asks a
fresh shell, whose command cache cannot remember the deleted binary.
This commit is contained in:
Marc-André Bombeck committed 2026-11-03 21:55:30 +01:00
1 parent 377e0d0d3a
commit 5d36afb487
9 files changed
+105 -124

No files matched your search

+17 -11
View File
@@ -61,20 +61,26 @@ describe("canvas binary hoist", () => {
expect(runner).not.toMatch(/@napi-rs\+canvas[^"'\s]*@\*/);
});
it("builds every Node stage on the glibc Debian base", () => {
const froms = DOCKERFILE.match(/^FROM \S+/gm) ?? [];
const node = froms.filter((from) => from.startsWith("FROM node:"));
expect(node).toHaveLength(3);
for (const from of node) {
expect(from).toMatch(/^FROM node:22-trixie-slim@sha256:[0-9a-f]{64}$/);
}
// The only other stage supplies the PostgreSQL client tools, built on
// the same Debian release so its binaries find their libraries.
expect(froms.filter((from) => !from.startsWith("FROM node:"))).toEqual([
it("installs on Debian and runs on Wolfi, both glibc", () => {
const froms = DOCKERFILE.match(/^FROM \S+ AS \S+$/gm) ?? [];
expect(froms).toEqual([
expect.stringMatching(
/^FROM postgres:18\.\d+-trixie@sha256:[0-9a-f]{64}$/,
/^FROM node:22-trixie-slim@sha256:[0-9a-f]{64} AS deps$/,
),
expect.stringMatching(
/^FROM node:22-trixie-slim@sha256:[0-9a-f]{64} AS builder$/,
),
expect.stringMatching(
/^FROM cgr\.dev\/chainguard\/wolfi-base:latest@sha256:[0-9a-f]{64} AS runner$/,
),
]);
// The native packages pnpm picked in the builder must load in the
// runner: same module ABI, and a glibc no older than the builder's.
expect(stage("builder")).toContain("> /app/.native-abi");
expect(stage("runner")).toContain(
'[ "$RUN_ABI" = "$BUILD_ABI" ] || { echo "ERROR: runner Node module ABI',
);
expect(stage("runner")).toContain("is older than the builder's");
});
it("fails the build when the copied binary is another version", () => {
@@ -40,32 +40,6 @@ describe("the bundled database pin", () => {
}
});
it("ships pg_dump of the same minor the bundled database runs", () => {
const db = /^\s*image:\s*postgres:(\d+\.\d+)-alpine$/m.exec(
read("docker-compose.yml"),
);
const client =
/^FROM postgres:(\d+\.\d+)-trixie@sha256:[0-9a-f]{64} AS pgclient$/m.exec(
read("Dockerfile"),
);
expect(db, "the compose file pins the database").not.toBeNull();
expect(client, "the Dockerfile has a pinned pgclient stage").not.toBeNull();
// The two pins are one decision; a Dependabot PR moves them separately.
expect(client![1]).toBe(db![1]);
});
it("keeps a major bump of the client tools out of the Dockerfile watch too", () => {
const dependabot = read(".github/dependabot.yml");
const docker =
/package-ecosystem:\s*"docker"\n([\s\S]*?)(?=\n\s*- package-ecosystem:)/.exec(
dependabot,
);
expect(docker, "a docker ecosystem entry").not.toBeNull();
expect(docker![1]).toMatch(
/dependency-name:\s*"postgres"[\s\S]{0,200}version-update:semver-major/,
);
});
it("lets a major version through only as a decision, never as an update PR", () => {
const dependabot = read(".github/dependabot.yml");
// The first run of the compose ecosystem proposed 16.15 -> 18.6. A
+2 -2
View File
@@ -9,7 +9,7 @@
* restart loop (live incident: a Synology DS1520+ self-host, exit code 132
* ~18 s after every boot once the thumbnail backfill touched Skia).
*
* The published image is Debian (glibc) and runs the glibc build. Under an
* The published image runs on glibc (Wolfi) and loads the glibc build. Under an
* emulated AVX2-less Atom core that build drew a canvas and rendered
* a PDF page to JPEG where the musl build died, but it still carries AVX2 code
* and not every path it can take was exercised, so the x64 gate stays closed
@@ -26,7 +26,7 @@
* fallback, and dies with SIGILL as soon as the module loads on an ARMv8.0
* core such as the Raspberry Pi 4's Cortex-A72 (#1311). The glibc arm64 build
* uses neither and runs there, which is why the published image moved off
* Alpine. The musl branch stays for a host that runs the app on musl itself,
* musl. The musl branch stays for a host that runs the app on musl itself,
* from source on Alpine or from an image built on it.
*
* Detection reads `/proc/cpuinfo` once on Linux: x64 requires the `avx2`