From 7b71bb6ccff5d3458a202f29e97f1ad8659913a0 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 05:09:31 +0000 Subject: [PATCH 1/3] feat: add publish-dvm-image CI workflow (issue #3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `.github/workflows/publish-dvm-image.yml` — the image-publish workflow carved from `toon-protocol/town`'s `publish-townhouse-images.yml` (dvm slice only, linux/amd64 single-platform). Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/publish-dvm-image.yml | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/publish-dvm-image.yml diff --git a/.github/workflows/publish-dvm-image.yml b/.github/workflows/publish-dvm-image.yml new file mode 100644 index 0000000..19c99da --- /dev/null +++ b/.github/workflows/publish-dvm-image.yml @@ -0,0 +1,113 @@ +# Publish DVM Image to GHCR +# +# Trigger scenarios: +# - Push of tag v* → build + push versioned tag + :latest, sign digest. +# - workflow_dispatch → build + push versioned tag only (NO :latest move), for rehearsals. +# +# Single-platform: linux/amd64 only. arm64 deferred — QEMU crashes on the +# Solana/Rust dependency chain ("Illegal instruction"). Native arm64 runners +# can be added later via per-arch matrix + imagetools create manifest-merge. +# +# Prerequisite: Dockerfile.dvm must build standalone from this repo's context +# (issue #2 — trim to dvm-only). Until #2 is merged the build step will fail +# because the Dockerfile still references monorepo paths (packages/*, docker/). +# +# Cosign verify (post-publish runbook): +# digest=$(docker buildx imagetools inspect ghcr.io/toon-protocol/store:latest \ +# --format '{{.Manifest.Digest}}') +# cosign verify ghcr.io/toon-protocol/store@$digest \ +# --certificate-identity-regexp '^https://github.com/toon-protocol/store/' \ +# --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' + +name: Publish DVM Image + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version tag for image (e.g. 0.1.0-rc1). Required for workflow_dispatch.' + required: true + type: string + +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: false + +permissions: + contents: read + packages: write + id-token: write # required for cosign keyless OIDC token + +env: + IMAGE: ghcr.io/toon-protocol/store + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 90 + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata (tags) + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') }} + type=match,pattern=v(.*),group=1 + type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }} + type=sha,prefix=sha- + + - name: Build and push + id: build + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + file: Dockerfile.dvm + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=store-linux-amd64 + cache-to: type=gha,mode=max,scope=store-linux-amd64 + + - name: Install cosign + uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1 + + - name: Sign image digest + env: + COSIGN_YES: 'true' + run: | + cosign sign ${{ env.IMAGE }}@${{ steps.build.outputs.digest }} + + - name: Smoke-test DVM image (entrypoint + node binary) + run: | + set -euo pipefail + IMG="${{ env.IMAGE }}@${{ steps.build.outputs.digest }}" + echo "Verifying DVM image: ${IMG}" + docker run --rm --entrypoint sh "${IMG}" -c \ + "test -s '/app/entrypoint-dvm.js'" \ + || { echo "FAIL: /app/entrypoint-dvm.js missing or empty"; exit 1; } + NODE_VERSION=$(docker run --rm --entrypoint node "${IMG}" --version) + echo "node --version: ${NODE_VERSION}" + case "${NODE_VERSION}" in + v20.*) ;; + *) echo "FAIL: expected node v20.x, got ${NODE_VERSION}"; exit 1 ;; + esac + echo "✅ /app/entrypoint-dvm.js present + ${NODE_VERSION}" From b3bee6541f3eeeae46860eb74bc11eec0eb7b7df Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 05:14:27 +0000 Subject: [PATCH 2/3] fix: smoke-test DVM image before push, not after MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the single build-and-push step into build-only → smoke-test → push so a failing smoke test prevents the broken image from ever reaching GHCR. Pass 1 loads into the local Docker daemon (no push); Pass 2 is push-only and hits the GHA BuildKit cache for a near-instant re-tag. Addresses review comment on PR #10 (ALLiDoizCode, 2026-06-20). Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/publish-dvm-image.yml | 47 +++++++++++++++---------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/.github/workflows/publish-dvm-image.yml b/.github/workflows/publish-dvm-image.yml index 19c99da..0ab8319 100644 --- a/.github/workflows/publish-dvm-image.yml +++ b/.github/workflows/publish-dvm-image.yml @@ -74,7 +74,35 @@ jobs: type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }} type=sha,prefix=sha- - - name: Build and push + # Pass 1: build only — load into local daemon for smoke test (no push yet) + - name: Build (no push) + id: build-local + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + file: Dockerfile.dvm + platforms: linux/amd64 + load: true + tags: smoke-test-local:latest + cache-from: type=gha,scope=store-linux-amd64 + cache-to: type=gha,mode=max,scope=store-linux-amd64 + + - name: Smoke-test DVM image (pre-push) + run: | + set -euo pipefail + IMG="smoke-test-local:latest" + docker run --rm --entrypoint sh "${IMG}" -c \ + "test -s '/app/entrypoint-dvm.js'" \ + || { echo "FAIL: /app/entrypoint-dvm.js missing or empty"; exit 1; } + NODE_VERSION=$(docker run --rm --entrypoint node "${IMG}" --version) + case "${NODE_VERSION}" in + v20.*) ;; + *) echo "FAIL: expected node v20.x, got ${NODE_VERSION}"; exit 1 ;; + esac + echo "/app/entrypoint-dvm.js present + ${NODE_VERSION}" + + # Pass 2: push only — BuildKit GHA cache makes this near-instant (no rebuild) + - name: Push id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: @@ -85,7 +113,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=store-linux-amd64 - cache-to: type=gha,mode=max,scope=store-linux-amd64 - name: Install cosign uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1 @@ -95,19 +122,3 @@ jobs: COSIGN_YES: 'true' run: | cosign sign ${{ env.IMAGE }}@${{ steps.build.outputs.digest }} - - - name: Smoke-test DVM image (entrypoint + node binary) - run: | - set -euo pipefail - IMG="${{ env.IMAGE }}@${{ steps.build.outputs.digest }}" - echo "Verifying DVM image: ${IMG}" - docker run --rm --entrypoint sh "${IMG}" -c \ - "test -s '/app/entrypoint-dvm.js'" \ - || { echo "FAIL: /app/entrypoint-dvm.js missing or empty"; exit 1; } - NODE_VERSION=$(docker run --rm --entrypoint node "${IMG}" --version) - echo "node --version: ${NODE_VERSION}" - case "${NODE_VERSION}" in - v20.*) ;; - *) echo "FAIL: expected node v20.x, got ${NODE_VERSION}"; exit 1 ;; - esac - echo "✅ /app/entrypoint-dvm.js present + ${NODE_VERSION}" From af747f2b0a5b0b0ea30ef8294f0c644c1c4e3a3b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 05:21:42 +0000 Subject: [PATCH 3/3] fix: move cosign digest out of shell interpolation into env var Addresses script-injection anti-pattern flagged in review round 2. `steps.build.outputs.digest` is now bound to a step-level DIGEST env var and referenced as ${DIGEST} in the shell command instead of being interpolated via ${{ }} directly into the run block. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/publish-dvm-image.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-dvm-image.yml b/.github/workflows/publish-dvm-image.yml index 0ab8319..e9c17a0 100644 --- a/.github/workflows/publish-dvm-image.yml +++ b/.github/workflows/publish-dvm-image.yml @@ -120,5 +120,6 @@ jobs: - name: Sign image digest env: COSIGN_YES: 'true' + DIGEST: ${{ steps.build.outputs.digest }} run: | - cosign sign ${{ env.IMAGE }}@${{ steps.build.outputs.digest }} + cosign sign "${IMAGE}@${DIGEST}"