fix(ci): pin multi-arch docker base digests

This commit is contained in:
Peter Steinberger
2026-03-08 02:55:09 +00:00
parent 722c5e5d33
commit 5759b93dda
4 changed files with 75 additions and 15 deletions

View File

@@ -33,16 +33,53 @@ type DependabotConfig = {
updates?: DependabotUpdate[];
};
function resolveFirstFromReference(dockerfile: string): string | undefined {
const argDefaults = new Map<string, string>();
for (const line of dockerfile.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
if (trimmed.startsWith("FROM ")) {
break;
}
const argMatch = trimmed.match(/^ARG\s+([A-Z0-9_]+)=(.+)$/);
if (!argMatch) {
continue;
}
const [, name, rawValue] = argMatch;
const value = rawValue.replace(/^["']|["']$/g, "");
argDefaults.set(name, value);
}
const fromLine = dockerfile.split(/\r?\n/).find((line) => line.trimStart().startsWith("FROM "));
if (!fromLine) {
return undefined;
}
const fromMatch = fromLine.trim().match(/^FROM\s+(\S+?)(?:\s+AS\s+\S+)?$/);
if (!fromMatch) {
return undefined;
}
const imageRef = fromMatch[1];
const argName =
imageRef.match(/^\$\{([A-Z0-9_]+)\}$/)?.[1] ?? imageRef.match(/^\$([A-Z0-9_]+)$/)?.[1];
if (!argName) {
return imageRef;
}
return argDefaults.get(argName);
}
describe("docker base image pinning", () => {
it("pins selected Dockerfile FROM lines to immutable sha256 digests", async () => {
for (const dockerfilePath of DIGEST_PINNED_DOCKERFILES) {
const dockerfile = await readFile(resolve(repoRoot, dockerfilePath), "utf8");
const fromLine = dockerfile
.split(/\r?\n/)
.find((line) => line.trimStart().startsWith("FROM "));
expect(fromLine, `${dockerfilePath} should define a FROM line`).toBeDefined();
expect(fromLine, `${dockerfilePath} FROM must be digest-pinned`).toMatch(
/^FROM\s+\S+@sha256:[a-f0-9]{64}(?:\s+AS\s+\S+)?$/,
const imageRef = resolveFirstFromReference(dockerfile);
expect(imageRef, `${dockerfilePath} should define a FROM line`).toBeDefined();
expect(imageRef, `${dockerfilePath} FROM must be digest-pinned`).toMatch(
/^\S+@sha256:[a-f0-9]{64}$/,
);
}
});

View File

@@ -7,6 +7,22 @@ const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
const dockerfilePath = join(repoRoot, "Dockerfile");
describe("Dockerfile", () => {
it("uses shared multi-arch base image refs for all root Node stages", async () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
expect(dockerfile).toContain(
'ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:22-bookworm@sha256:b501c082306a4f528bc4038cbf2fbb58095d583d0419a259b2114b5ac53d12e9"',
);
expect(dockerfile).toContain(
'ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:22-bookworm-slim@sha256:9c2c405e3ff9b9afb2873232d24bb06367d649aa3e6259cbe314da59578e81e9"',
);
expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps");
expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build");
expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS base-default");
expect(dockerfile).toContain("FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-slim");
expect(dockerfile).toContain("current multi-arch manifest list entry");
expect(dockerfile).not.toContain("current amd64 entry");
});
it("installs optional browser dependencies after pnpm install", async () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile");