feat: add publish-dvm-image CI workflow#10
Conversation
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
left a comment
There was a problem hiding this comment.
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:
- Build + push →
:latest,:0.x.y,:sha-*all land in GHCR - Cosign signs the digest
- Smoke test — checks
/app/entrypoint-dvm.jspresent + 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 aboveThis 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-actionv6 generates provenance by default. No action needed, but worth confirming the GHCR package settings accept attestation manifests. workflow_dispatchalways pushes (push: trueis unconditional): The PR description is clear that dispatch is intended to push a non-:latestversioned 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>
|
Addressed the blocking smoke-test ordering issue in b3bee65. What changed: Split the single
|
ALLiDoizCode
left a comment
There was a problem hiding this comment.
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>
|
Addressed the script-injection anti-pattern in af747f2. What changed: The - name: Sign image digest
env:
COSIGN_YES: 'true'
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign "${IMAGE}@${DIGEST}"
Non-blocking TOCTOU note (acknowledged): Pass 1 ( |
ALLiDoizCode
left a comment
There was a problem hiding this comment.
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.digestwired to the Push step (id: build,push: true), where build-push-action v6 emits it. ✓:latestguard expression correctly evaluates to false onworkflow_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.
|
Superseded by #18, which added the image-publish workflows on |
Summary
.github/workflows/publish-dvm-image.yml— the image-publish workflow carved fromtoon-protocol/town'spublish-townhouse-images.yml, dvm slice only.linux/amd64. arm64 deferred (QEMU crashes on Solana/Rust dep chain).Closes #3
What this does
push: tags: v*ghcr.io/toon-protocol/store:latest,:0.x.y,:sha-<hash>, sign digestworkflow_dispatch:sha-<hash>only — no:latestmoveAction SHAs match the pinned versions in
toon-protocol/town's source workflow:actions/checkoutv4:34e114876b0b11c390a56381ad16ebd13914f8d5docker/setup-buildx-actionv3:8d2750c68a42422c14e847fe6c8ac0403b4cbd6fdocker/login-actionv3:c94ce9fb468520275223c153574b00df6fe4bcc9docker/build-push-actionv6:10e90e3645eae34f1e60eeb005ba3a3d33f178e8docker/metadata-actionv5:c299e40c65443455700f0fdfc63efafe5b349051sigstore/cosign-installerv3.9.1:398d4b0eeef1380460a10c8013a76f728fb906acDropped from source workflow (out of scope per issue)
preflightjob (connector version alignment) — townhouse concernmerge-multi-archjob /imagetools createmanifest-merge — only needed for multi-archbuild-image-manifestjob — noimage-manifest.jsonin this reposmoke-imagesas a separate job — merged inline as a step inbuild-and-pushpublish-npmjob — this package isprivateubuntu-24.04-armrunnerlinux/arm64leg (deferred per resolved decision Trim store repo to dvm-only: remove leftover monorepo build contexts #2 in the issue)Prerequisite dependency
Dockerfile.dvmis 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
Deviations from Agent Assessment
risk:medium(notrisk:low) — proceeded per explicit user instruction; the change is purely additive (one YAML file, no code or config mutations).## Agent Assessmentsection — the## Scope/## Implementation notes/## Resolved decisionssections serve the same purpose and are sufficiently detailed.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