From 8974e9329fb4f03085eedc394762897fbf995a82 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 30 Jul 2026 19:38:32 -0400 Subject: [PATCH 1/4] fix(images): clear platform pull before validation --- .github/workflows/managed-images.yaml | 1 + test/managed-image-publication-workflow.test.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index bb64cbb452b..c92a2ac96f6 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -811,6 +811,7 @@ jobs: DOCKER_CONFIG="$anonymous_config" docker pull \ --platform "$platform" \ "$cohort_reference" + docker image rm "$cohort_reference" >/dev/null done done < <(jq -c '.[]' "$cohort_manifests") diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 7a4d487e40e..2e45b751c36 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -464,6 +464,7 @@ describe("complete managed-image publication workflow", () => { const steps = promoter.steps ?? []; const barrier = step(promoter, "Validate complete managed image candidate set"); const promotion = step(promoter, "Promote validated multi-platform managed image cohort"); + const promotionRun = promotion.run ?? ""; expect(promoter.needs).toBe("build-and-validate"); expect(step(promoter, "Download all validated managed image candidates").with).toEqual({ @@ -484,7 +485,11 @@ describe("complete managed-image publication workflow", () => { 'docker buildx imagetools create --tag "$cohort_alias" "${sources[@]}"', ); expect(promotion.run).toContain(') == ["linux/amd64", "linux/arm64"]'); - expect(promotion.run).toContain('DOCKER_CONFIG="$anonymous_config" docker pull'); + expect(promotionRun).toContain('DOCKER_CONFIG="$anonymous_config" docker pull'); + expect(promotionRun).toContain('docker image rm "$cohort_reference"'); + expect(promotionRun.indexOf('docker image rm "$cohort_reference"')).toBeGreaterThan( + promotionRun.indexOf('DOCKER_CONFIG="$anonymous_config" docker pull'), + ); expect(promotion.run).toContain( 'consumer_aliases=("$(jq -r \'.image\' <<<"$openclaw_manifest"):${GITHUB_SHA}")', ); From 3287abe3a464efe7924e91b58b62bd2fe8a10528 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 30 Jul 2026 19:44:54 -0400 Subject: [PATCH 2/4] test(images): verify platform pull cleanup --- ...managed-image-publication-workflow.test.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 2e45b751c36..fa443a9b2c7 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -464,7 +464,6 @@ describe("complete managed-image publication workflow", () => { const steps = promoter.steps ?? []; const barrier = step(promoter, "Validate complete managed image candidate set"); const promotion = step(promoter, "Promote validated multi-platform managed image cohort"); - const promotionRun = promotion.run ?? ""; expect(promoter.needs).toBe("build-and-validate"); expect(step(promoter, "Download all validated managed image candidates").with).toEqual({ @@ -485,11 +484,6 @@ describe("complete managed-image publication workflow", () => { 'docker buildx imagetools create --tag "$cohort_alias" "${sources[@]}"', ); expect(promotion.run).toContain(') == ["linux/amd64", "linux/arm64"]'); - expect(promotionRun).toContain('DOCKER_CONFIG="$anonymous_config" docker pull'); - expect(promotionRun).toContain('docker image rm "$cohort_reference"'); - expect(promotionRun.indexOf('docker image rm "$cohort_reference"')).toBeGreaterThan( - promotionRun.indexOf('DOCKER_CONFIG="$anonymous_config" docker pull'), - ); expect(promotion.run).toContain( 'consumer_aliases=("$(jq -r \'.image\' <<<"$openclaw_manifest"):${GITHUB_SHA}")', ); @@ -598,6 +592,14 @@ describe("complete managed-image publication workflow", () => { const accepted = runManagedImagePromotion(promotion); const acceptedCalls = accepted.calls.join("\n"); + const cohortDigest = `sha256:${"f".repeat(64)}`; + const pullCleanupCalls = accepted.calls.filter( + (call) => call.startsWith("pull ") || call.startsWith("image rm "), + ); + const expectedPullCalls = publicationAgents.flatMap((agent) => { + const reference = `ghcr.io/nvidia/nemoclaw/${agent}-sandbox@${cohortDigest}`; + return publicationPlatforms.map((platform) => `pull --platform ${platform} ${reference}`); + }); const lastCohortStage = Math.max( acceptedCalls.indexOf(`hermes-sandbox:cohort-${cohort}`), acceptedCalls.indexOf(`langchain-deepagents-code-sandbox:cohort-${cohort}`), @@ -606,6 +608,17 @@ describe("complete managed-image publication workflow", () => { const rootPointer = acceptedCalls.indexOf(`openclaw-sandbox:${revision}`); expect(accepted.status, accepted.stderr).toBe(0); + expect(pullCleanupCalls).toHaveLength(expectedPullCalls.length * 2); + expect(pullCleanupCalls.filter((call) => call.startsWith("pull ")).toSorted()).toEqual( + expectedPullCalls.toSorted(), + ); + for (let index = 0; index < pullCleanupCalls.length; index += 2) { + const pull = pullCleanupCalls[index]; + const cleanup = pullCleanupCalls[index + 1]; + const reference = pull?.match(/^pull --platform linux\/(?:amd64|arm64) (.+)$/u)?.[1]; + expect(reference).toBeDefined(); + expect(cleanup).toBe(`image rm ${reference}`); + } expect(lastCohortStage).toBeGreaterThanOrEqual(0); expect(rootPointer).toBeGreaterThan(lastCohortStage); expect(acceptedCalls).not.toContain(`hermes-sandbox:${revision}`); From 40619830279d928b80b4e18b6cfae5b0b993b1a7 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 30 Jul 2026 19:46:39 -0400 Subject: [PATCH 3/4] test(images): support repository TypeScript target --- test/managed-image-publication-workflow.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index fa443a9b2c7..1f3f7cd043a 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -609,8 +609,8 @@ describe("complete managed-image publication workflow", () => { expect(accepted.status, accepted.stderr).toBe(0); expect(pullCleanupCalls).toHaveLength(expectedPullCalls.length * 2); - expect(pullCleanupCalls.filter((call) => call.startsWith("pull ")).toSorted()).toEqual( - expectedPullCalls.toSorted(), + expect(pullCleanupCalls.filter((call) => call.startsWith("pull ")).sort()).toEqual( + expectedPullCalls.sort(), ); for (let index = 0; index < pullCleanupCalls.length; index += 2) { const pull = pullCleanupCalls[index]; From f4ab97473ba2ac0f78368bb53959f5734a7e8c04 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 30 Jul 2026 19:52:59 -0400 Subject: [PATCH 4/4] test(images): require immediate pull cleanup --- test/managed-image-publication-workflow.test.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 1f3f7cd043a..052aa837e1e 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -593,9 +593,6 @@ describe("complete managed-image publication workflow", () => { const accepted = runManagedImagePromotion(promotion); const acceptedCalls = accepted.calls.join("\n"); const cohortDigest = `sha256:${"f".repeat(64)}`; - const pullCleanupCalls = accepted.calls.filter( - (call) => call.startsWith("pull ") || call.startsWith("image rm "), - ); const expectedPullCalls = publicationAgents.flatMap((agent) => { const reference = `ghcr.io/nvidia/nemoclaw/${agent}-sandbox@${cohortDigest}`; return publicationPlatforms.map((platform) => `pull --platform ${platform} ${reference}`); @@ -608,16 +605,15 @@ describe("complete managed-image publication workflow", () => { const rootPointer = acceptedCalls.indexOf(`openclaw-sandbox:${revision}`); expect(accepted.status, accepted.stderr).toBe(0); - expect(pullCleanupCalls).toHaveLength(expectedPullCalls.length * 2); - expect(pullCleanupCalls.filter((call) => call.startsWith("pull ")).sort()).toEqual( + expect(accepted.calls.filter((call) => call.startsWith("pull ")).sort()).toEqual( expectedPullCalls.sort(), ); - for (let index = 0; index < pullCleanupCalls.length; index += 2) { - const pull = pullCleanupCalls[index]; - const cleanup = pullCleanupCalls[index + 1]; - const reference = pull?.match(/^pull --platform linux\/(?:amd64|arm64) (.+)$/u)?.[1]; + for (const pull of expectedPullCalls) { + const index = accepted.calls.indexOf(pull); + const reference = pull.match(/^pull --platform linux\/(?:amd64|arm64) (.+)$/u)?.[1]; expect(reference).toBeDefined(); - expect(cleanup).toBe(`image rm ${reference}`); + expect(index).toBeGreaterThanOrEqual(0); + expect(accepted.calls[index + 1]).toBe(`image rm ${reference}`); } expect(lastCohortStage).toBeGreaterThanOrEqual(0); expect(rootPointer).toBeGreaterThan(lastCohortStage);