diff --git a/test/e2e/support/sandbox-images-workflow-boundary.test.ts b/test/e2e/support/sandbox-images-workflow-boundary.test.ts index 765e1f665e3..defcf647deb 100644 --- a/test/e2e/support/sandbox-images-workflow-boundary.test.ts +++ b/test/e2e/support/sandbox-images-workflow-boundary.test.ts @@ -215,6 +215,21 @@ describe("sandbox image workflow boundary", () => { ); }); + it("rejects a continued Buildx rebuild in the Hermes image consumer", () => { + const { imageWorkflow, mainWorkflow } = readWorkflows(); + imageWorkflow.jobs["test-hermes-sandbox-image"].steps!.push({ + name: "Rebuild Hermes production image", + run: [ + "docker buildx \\", + " build --load -f agents/hermes/Dockerfile -t nemoclaw-hermes-production .", + ].join("\n"), + }); + + expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toContain( + "Hermes image test consumer must not rebuild the prebuilt image", + ); + }); + it("rejects a duplicate Hermes production-image build", () => { const { imageWorkflow, mainWorkflow } = readWorkflows(); const producer = imageWorkflow.jobs["build-hermes-sandbox-image"]; @@ -256,6 +271,54 @@ describe("sandbox image workflow boundary", () => { ); }); + it("rejects an assigned Buildx push flag in an image consumer", () => { + const { imageWorkflow, mainWorkflow } = readWorkflows(); + imageWorkflow.jobs["state-dir-guard-metadata"].steps!.push({ + name: "Publish from metadata consumer", + run: "docker buildx build --push=true -t registry.example.invalid/nemoclaw .", + }); + + expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toContain( + "state-dir-guard-metadata step \x27Publish from metadata consumer\x27 must not write images to a registry", + ); + }); + + it("rejects a mixed-case assigned Buildx push flag in an image consumer", () => { + const { imageWorkflow, mainWorkflow } = readWorkflows(); + imageWorkflow.jobs["state-dir-guard-metadata"].steps!.push({ + name: "Publish from metadata consumer", + run: "docker buildx build --push=True -t registry.example.invalid/nemoclaw .", + }); + + expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toContain( + "state-dir-guard-metadata step \x27Publish from metadata consumer\x27 must not write images to a registry", + ); + }); + + it("rejects a shell-expanded Buildx push flag in an image consumer", () => { + const { imageWorkflow, mainWorkflow } = readWorkflows(); + imageWorkflow.jobs["state-dir-guard-metadata"].steps!.push({ + name: "Publish from expanded metadata consumer", + run: 'docker buildx build --push="${PUSH_IMAGES}" -t registry.example.invalid/nemoclaw .', + }); + + expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toContain( + "state-dir-guard-metadata step \x27Publish from expanded metadata consumer\x27 must not write images to a registry", + ); + }); + + it("treats an explicit false Buildx push assignment as non-writing", () => { + const { imageWorkflow, mainWorkflow } = readWorkflows(); + imageWorkflow.jobs["state-dir-guard-metadata"].steps!.push({ + name: "Disable publishing from metadata consumer", + run: "docker buildx build --push=false -t nemoclaw-metadata-consumer .", + }); + + expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).not.toContain( + "state-dir-guard-metadata step \x27Disable publishing from metadata consumer\x27 must not write images to a registry", + ); + }); + it("requires the Hermes artifact download before its load", () => { const { imageWorkflow, mainWorkflow } = readWorkflows(); const consumer = imageWorkflow.jobs["test-hermes-sandbox-image"]; diff --git a/tools/e2e/sandbox-images-workflow-boundary.mts b/tools/e2e/sandbox-images-workflow-boundary.mts index 08cc8993e7b..83830173735 100644 --- a/tools/e2e/sandbox-images-workflow-boundary.mts +++ b/tools/e2e/sandbox-images-workflow-boundary.mts @@ -63,8 +63,9 @@ const EXPECTED_AUTH_ENV = { DOCKERHUB_TOKEN: `\${{ ${TRUSTED_PREDICATE} && secrets.DOCKERHUB_TOKEN || '' }}`, }; const FULL_SHA_ACTION = /^[^\s@]+@[0-9a-f]{40}$/u; +// Shell-expanded values are unknown; only literal `--push=false` is statically non-writing. const REGISTRY_WRITE = - /(?:\bdocker\s+(?:image\s+)?push\b|\bdocker\s+buildx\s+build\b[^\n]*\s--push(?:\s|$)|\b(?:oras|crane)\s+push\b|\bskopeo\s+copy\b)/u; + /(?:\bdocker\s+(?:image\s+)?push\b|\bdocker\s+buildx\s+build\b[^\n]*\s--push(?:=(?!false(?=$|[\s;&|<>()]))[^\s;&|<>()]+)?(?=$|[\s;&|<>()])|\b(?:oras|crane)\s+push\b|\bskopeo\s+copy\b)/u; function normalizeShellContinuations(run: string): string { return run.replace(/\\\r?\n[ \t]*/gu, " "); @@ -883,7 +884,7 @@ function validateHermesImageReuse(errors: string[], workflow: SandboxImagesWorkf const consumerBuilds = steps(testJob).filter( (step) => String(step.uses ?? "").startsWith("docker/build-push-action@") || - /\bdocker\s+(?:build|buildx\s+build)\b/u.test(step.run ?? ""), + /\bdocker\s+(?:build|buildx\s+build)\b/u.test(normalizeShellContinuations(step.run ?? "")), ); if (consumerBuilds.length !== 0) { errors.push("Hermes image test consumer must not rebuild the prebuilt image");