diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index 55a08b6e689..6d216fc600b 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -647,9 +647,26 @@ jobs: [[ "$IMAGE_CONTENT_ID" =~ ^[0-9a-f]{64}$ ]] [[ "$TARGET_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]] identity="^https://github.com/${GITHUB_REPOSITORY}/.github/workflows/runner-full-stack-e2e.yml@" + verify_signed() { + cosign verify \ + --certificate-identity-regexp "$identity" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + "$IMAGE_TAG@$1" >/dev/null + } + # Reuse an existing content tag only when its digest carries this + # workflow's signature. A run interrupted between push and sign + # leaves an unsigned tag; rebuilding, pushing and signing replaces it + # instead of failing every later campaign with "no signatures found". + digest="" if docker buildx imagetools inspect "$IMAGE_TAG" >/dev/null 2>&1; then - digest="$(docker buildx imagetools inspect "$IMAGE_TAG" --format '{{json .Manifest.Digest}}' | tr -d '"')" - else + existing="$(docker buildx imagetools inspect "$IMAGE_TAG" --format '{{json .Manifest.Digest}}' | tr -d '"')" + if verify_signed "$existing" 2>/dev/null; then + digest="$existing" + else + echo "::warning title=Daytona image::Existing $IMAGE_TAG ($existing) has no verifiable workflow signature; rebuilding and signing it" + fi + fi + if [ -z "$digest" ]; then cache_args=( --cache-from "type=registry,ref=${IMAGE_CACHE}" ) diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index cd6a7019b88..cd0eb22f67a 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -282,6 +282,14 @@ describe("runner E2E Daytona image contract", () => { ); expect(workflow).not.toContain("e2e-git-${{ github.sha }}"); expect(workflow).toContain("cosign sign --yes"); + // An existing content tag is reused only when its signature verifies; an + // unsigned tag left by an interrupted run is rebuilt, pushed and signed. + const imageJob = daytonaImageJob ?? ""; + const reuse = imageJob.indexOf('if verify_signed "$existing"'); + expect(reuse).toBeGreaterThan(-1); + expect(imageJob.indexOf('if [ -z "$digest" ]; then')).toBeGreaterThan(reuse); + expect(imageJob.indexOf("cosign sign --yes")).toBeGreaterThan(reuse); + expect(imageJob).not.toMatch(/imagetools inspect "\$IMAGE_TAG" >\/dev\/null 2>&1; then\n\s*digest=/); expect(workflow).toContain("docker logout ghcr.io"); expect(workflow).toContain(`docker buildx imagetools inspect "$immutable"`); expect(workflow).toContain(`--format '{{json .Image}}'`);