diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index d689e07306..711d0aff2a 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -432,6 +432,7 @@ jobs: IMAGE_TAG: ghcr.io/paperclipai/paperclip-daytona-runner:e2e-content-${{ needs.catalog.outputs.daytona_image_content_id }} IMAGE_CACHE: ghcr.io/paperclipai/paperclip-daytona-runner:e2e-buildcache-amd64 TARGET_SHA: ${{ needs.authorize.outputs.target_sha }} + TARGET_LOCK_SHA256: ${{ needs.target_lock.outputs.lock_sha256 }} TARGET_REF: ${{ needs.authorize.outputs.target_ref }} DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | @@ -445,6 +446,7 @@ jobs: exit 0 fi [[ "$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@" 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 '"')" @@ -464,6 +466,7 @@ jobs: --platform linux/amd64 \ --build-arg "PAPERCLIP_RUNNER_CONTENT_ID=${IMAGE_CONTENT_ID}" \ --build-arg "PAPERCLIP_RUNNER_SOURCE_REVISION=${TARGET_SHA}" \ + --build-arg "PAPERCLIP_RUNNER_LOCK_SHA256=${TARGET_LOCK_SHA256}" \ --file docker/daytona-runner/Dockerfile \ --tag "$IMAGE_TAG" \ "${cache_args[@]}" \ diff --git a/docker/daytona-runner/README.md b/docker/daytona-runner/README.md index dd3686a9c1..bee316460a 100644 --- a/docker/daytona-runner/README.md +++ b/docker/daytona-runner/README.md @@ -100,6 +100,11 @@ its frozen install, matching CI when a source commit precedes the lockfile bot. The complete resolved lockfile must match `PAPERCLIP_RUNNER_LOCK_SHA256` before package installation or lifecycle execution. Review and refresh that digest with source dependency changes; registry-time resolution drift fails closed. +The Product E2E workflow resolves one lockfile before the image build. It +verifies the downloaded artifact, then passes that artifact's SHA-256 as the +`PAPERCLIP_RUNNER_LOCK_SHA256` build argument. The Dockerfile checks the resolved +lock against this value before installation. The fixed Dockerfile default is +for standalone builds; it must not replace a campaign's verified lock digest. Keep one latest stable CLI installation per provider; refresh exact runtime versions and qualification digests together, never install a private older copy or download dependencies when a task starts. diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index 1588319845..acacba9603 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -29,6 +29,8 @@ describe("runner E2E Daytona image contract", () => { ), ]); const normalizedDockerfile = dockerfile.replace(/\\\r?\n\s*/g, " "); + const daytonaImageJob = workflow.match(/^ daytona_image:\n[\s\S]*?(?=^ \w+:)/m)?.[0]; + expect(daytonaImageJob).toBeDefined(); expect(dockerfile).toContain("--bin paperclip-runnerd"); expect(dockerfile).toContain("build-provider-pack.mjs /provider-pack"); expect(normalizedDockerfile).not.toContain( @@ -93,6 +95,22 @@ describe("runner E2E Daytona image contract", () => { expect(workflow).toContain( '--build-arg "PAPERCLIP_RUNNER_CONTENT_ID=${IMAGE_CONTENT_ID}"', ); + expect(daytonaImageJob).toContain( + "TARGET_LOCK_SHA256: ${{ needs.target_lock.outputs.lock_sha256 }}", + ); + expect(daytonaImageJob).toContain( + '[[ "$TARGET_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]]', + ); + expect(daytonaImageJob).toContain( + '--build-arg "PAPERCLIP_RUNNER_LOCK_SHA256=${TARGET_LOCK_SHA256}"', + ); + expect( + daytonaImageJob!.indexOf('[[ "$TARGET_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]]'), + ).toBeLessThan( + daytonaImageJob!.indexOf( + '--build-arg "PAPERCLIP_RUNNER_LOCK_SHA256=${TARGET_LOCK_SHA256}"', + ), + ); expect(workflow).toContain( "IMAGE_CACHE: ghcr.io/paperclipai/paperclip-daytona-runner:e2e-buildcache-amd64", ); @@ -111,8 +129,6 @@ describe("runner E2E Daytona image contract", () => { expect(workflow).toContain(`docker buildx imagetools inspect "$immutable"`); expect(workflow).toContain(`--format '{{json .Image}}'`); expect(workflow).not.toContain(`docker --config "$anonymous_config" pull`); - const daytonaImageJob = workflow.match(/^ daytona_image:\n[\s\S]*?(?=^ \w+:)/m)?.[0]; - expect(daytonaImageJob).toBeDefined(); // Remote Daytona manifests must use registry inspection. Local oracle // images in the test job can still use the Docker daemon. expect(daytonaImageJob).not.toContain("docker image inspect");