Skip to content

feat: add publish-dvm-image CI workflow#10

Closed
toon-backlog-bot[bot] wants to merge 3 commits into
mainfrom
agent/3-add-image-publish-workflow
Closed

feat: add publish-dvm-image CI workflow#10
toon-backlog-bot[bot] wants to merge 3 commits into
mainfrom
agent/3-add-image-publish-workflow

Conversation

@toon-backlog-bot

Copy link
Copy Markdown
Contributor

Summary

  • Adds .github/workflows/publish-dvm-image.yml — the image-publish workflow carved from toon-protocol/town's publish-townhouse-images.yml, dvm slice only.
  • Single-platform linux/amd64. arm64 deferred (QEMU crashes on Solana/Rust dep chain).
  • Includes cosign keyless OIDC signing and a dvm smoke-test post-push.

Closes #3

What this does

Trigger Effect
push: tags: v* Build + push ghcr.io/toon-protocol/store:latest, :0.x.y, :sha-<hash>, sign digest
workflow_dispatch Build + push versioned tag + :sha-<hash> only — no :latest move

Action SHAs match the pinned versions in toon-protocol/town's source workflow:

  • actions/checkout v4: 34e114876b0b11c390a56381ad16ebd13914f8d5
  • docker/setup-buildx-action v3: 8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
  • docker/login-action v3: c94ce9fb468520275223c153574b00df6fe4bcc9
  • docker/build-push-action v6: 10e90e3645eae34f1e60eeb005ba3a3d33f178e8
  • docker/metadata-action v5: c299e40c65443455700f0fdfc63efafe5b349051
  • sigstore/cosign-installer v3.9.1: 398d4b0eeef1380460a10c8013a76f728fb906ac

Dropped from source workflow (out of scope per issue)

  • preflight job (connector version alignment) — townhouse concern
  • merge-multi-arch job / imagetools create manifest-merge — only needed for multi-arch
  • build-image-manifest job — no image-manifest.json in this repo
  • smoke-images as a separate job — merged inline as a step in build-and-push
  • publish-npm job — this package is private
  • Per-arch matrix, QEMU, ubuntu-24.04-arm runner
  • linux/arm64 leg (deferred per resolved decision Trim store repo to dvm-only: remove leftover monorepo build contexts #2 in the issue)

Prerequisite dependency

Dockerfile.dvm is not yet buildable standalone — it still references monorepo paths (packages/*, docker/, etc.). Issue #2 / PR #9 must land first for this workflow to go green. The workflow file itself is correct; the build step will fail until #2 is merged.

Verification

python3 -c "import yaml; yaml.safe_load(open('.github/workflows/publish-dvm-image.yml'))" && echo "YAML valid"
# → YAML valid

# actionlint not available on the runner; YAML syntax confirmed valid via Python

Deviations from Agent Assessment

  • Issue carries risk:medium (not risk:low) — proceeded per explicit user instruction; the change is purely additive (one YAML file, no code or config mutations).
  • Issue body has no ## Agent Assessment section — the ## Scope / ## Implementation notes / ## Resolved decisions sections serve the same purpose and are sufficiently detailed.
  • Smoke-test is included inline (as a step in build-and-push) rather than as a separate job, since the single-job topology eliminates the artifact-passing ceremony the source workflow needed.

🤖 Generated with Claude Code

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 <noreply@anthropic.com>

@ALLiDoizCode ALLiDoizCode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: publish-dvm-image.yml

Overall this is a solid first cut of the publish workflow. Action SHAs are pinned, concurrency is sane, cosign keyless signing is wired up correctly, and the tag logic correctly withholds :latest on workflow_dispatch runs. One blocking correctness issue needs fixing before merge.


Blocking: smoke test runs after :latest is already pushed and signed

The current step order in build-and-push is:

  1. Build + push → :latest, :0.x.y, :sha-* all land in GHCR
  2. Cosign signs the digest
  3. Smoke test — checks /app/entrypoint-dvm.js present + node v20

If the smoke test fails at step 3, the broken image is already tagged :latest and signed. There is no cleanup/rollback step. The smoke test cannot act as a safety gate when the gate runs after the publish it is meant to protect.

Recommended fix — split build and publish into two passes:

      # Pass 1: build only (no push) and load into Docker daemon for local smoke test
      - name: Build (no push)
        id: build-local
        uses: docker/build-push-action@...
        with:
          context: .
          file: Dockerfile.dvm
          platforms: linux/amd64
          load: true          # <-- load into local daemon, do NOT push
          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 cache makes this near-instant (no rebuild)
      - name: Push
        id: build
        uses: docker/build-push-action@...
        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
          # omit cache-to on push pass — already written above

This way :latest is never published if the smoke test fails. The GHA cache absorbs almost all of the second build's cost.


Non-blocking notes (no changes required)

  • Prerequisite dependency on #9: Acknowledged in the PR body. The workflow YAML itself is correct; this is a known blocked-until-#9-merges situation.
  • No SBOM / provenance attestation: docker/build-push-action v6 generates provenance by default. No action needed, but worth confirming the GHCR package settings accept attestation manifests.
  • workflow_dispatch always pushes (push: true is unconditional): The PR description is clear that dispatch is intended to push a non-:latest versioned tag. The header comment calling it a "rehearsal" is slightly misleading, but not a bug.

Request: address the smoke-test ordering before merge so the gate actually gates.

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 <noreply@anthropic.com>
@toon-backlog-bot

Copy link
Copy Markdown
Contributor Author

Addressed the blocking smoke-test ordering issue in b3bee65.

What changed: Split the single Build and push step into three sequential steps:

  1. Build (no push)load: true, tag smoke-test-local:latest, writes GHA cache.
  2. Smoke-test DVM image (pre-push) — runs against smoke-test-local:latest (local daemon only). If this fails, the job aborts before any image lands in GHCR.
  3. Pushpush: true with the metadata tags, reads from GHA cache (no rebuild). steps.build.outputs.digest unchanged so the cosign sign step required no edits.

:latest and the signed digest are now only published if the smoke test passes.

@toon-backlog-bot toon-backlog-bot Bot added the review-round:1 Fix loop round 1 label Jun 20, 2026

@ALLiDoizCode ALLiDoizCode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: publish-dvm-image.yml. The prior blocking concern (smoke test after push) is correctly fixed -- ordering is now build-local then smoke-test then push then sign. One new blocking issue remains. BLOCKING: The cosign sign step injects steps.build.outputs.digest directly into the shell run block via a dollar-brace expression. GitHub Security Hardening for Actions explicitly identifies this as a script-injection anti-pattern. Fix: move the digest to an env var (DIGEST: dollar-brace steps.build.outputs.digest) and reference it as the DIGEST shell variable in the cosign command. Risk here is low since docker/build-push-action is SHA-pinned and digest format is constrained, but the pattern should not be normalised. NON-BLOCKING: TOCTOU gap -- Pass 1 (load:true) and Pass 2 (push:true) are separate build invocations; cosign signs the Pass 2 digest, not the Pass 1 tested image. GHA cache makes them effectively identical so no change required, but worth documenting as a conscious decision. SUMMARY: fix the env-var pattern in the cosign sign step; everything else looks good.

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 <noreply@anthropic.com>
@toon-backlog-bot

Copy link
Copy Markdown
Contributor Author

Addressed the script-injection anti-pattern in af747f2.

What changed: The Sign image digest step now binds steps.build.outputs.digest to a step-level DIGEST env var and references it as ${DIGEST} in the shell command — no ${{ }} interpolation inside the run block.

      - name: Sign image digest
        env:
          COSIGN_YES: 'true'
          DIGEST: ${{ steps.build.outputs.digest }}
        run: |
          cosign sign "${IMAGE}@${DIGEST}"

IMAGE is already available as a shell env var from the workflow-level env: block (ghcr.io/toon-protocol/store), so both sides of the argument are now pure shell variables.

Non-blocking TOCTOU note (acknowledged): Pass 1 (load:true) and Pass 2 (push:true) are separate BuildKit invocations; cosign signs the Pass 2 digest. GHA cache makes the two layers effectively identical. This is a conscious decision — no change made, consistent with the reviewer's assessment.

@toon-backlog-bot toon-backlog-bot Bot added review-round:2 Fix loop round 2 and removed review-round:1 Fix loop round 1 labels Jun 20, 2026

@ALLiDoizCode ALLiDoizCode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both prior blocking issues are correctly resolved in this revision.

Smoke-test ordering (Round 1): Step sequence is Build (no push, load: true) → Smoke test → Push → Sign. :latest cannot land in GHCR if the smoke test fails. ✓

Script-injection fix (Round 2): ${{ steps.build.outputs.digest }} is bound in the env: block as DIGEST; the run: block references only ${DIGEST} and ${IMAGE} (shell variables). No ${{ }} interpolation inside run:. ✓

Additional adversarial checks:

  • Smoke-test path (/app/entrypoint-dvm.js) matches the Dockerfile's runtime layout (WORKDIR /app + COPY --from=builder /runtime ./). ✓
  • steps.build.outputs.digest wired to the Push step (id: build, push: true), where build-push-action v6 emits it. ✓
  • :latest guard expression correctly evaluates to false on workflow_dispatch (github.ref_type == 'branch'). ✓
  • Permissions minimally scoped: contents: read, packages: write, id-token: write. ✓
  • All 6 action SHAs pinned. ✓

Prerequisite dependency on PR #9 (Dockerfile standalone build) is properly documented in the header comment. TOCTOU gap between Pass 1 and Pass 2 remains acknowledged and acceptable. No new blocking issues.

@ALLiDoizCode

Copy link
Copy Markdown
Contributor

Superseded by #18, which added the image-publish workflows on main (publish-store-image.yml + publish-store-connector-image.yml). The publish-dvm-image.yml carved here predates the dvm→store rename. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-round:2 Fix loop round 2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add image-publish CI workflow: multi-arch DVM image build and push to GHCR

1 participant