Skip to content

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

Description

@toon-backlog-bot

Context

Both README.md and CLAUDE.md flag this as a required follow-up:

"Add the image-publish workflow (carve from the monorepo publish-townhouse-images.yml's dvm job): build → push to GHCR → signed digest."

This repo (toon-protocol/store) currently has no local CI workflow that builds or publishes the DVM image. The four workflows that exist on the default branch (backlog-manager.yml, issue-decomposer.yml, issue-executor.yml, pr-reviewer.yml) are backlog/agent-loop automation only — they do not build Docker images. There is no .github/workflows/ directory in the working tree yet.

The image is built from Dockerfile.dvm (repo root) over src/entrypoint-dvm.ts — an esbuild-bundled multi-stage build that wraps @toon-protocol/sdk's createArweaveDvmHandler (NIP-90 kind:5094 Arweave blob storage). It is a container, not an npm package (@toon-protocol/store is private), so this workflow does image publish only — no npm publish leg.

The canonical source is the monorepo workflow publish-townhouse-images.yml, available locally at ../town/.github/workflows/publish-townhouse-images.yml (repo toon-protocol/town). That workflow builds four images (townhouse-api, town, mill, dvm); this task carves out only the dvm job and adapts it to this single-image repo. Two of its concerns do not apply here and must be dropped: the npm-publish job and the image-manifest.json / compose-template machinery (those are townhouse-package concerns, not store concerns).

Critical prerequisite (the workflow cannot succeed without it)

Dockerfile.dvm as carved still assumes the monorepo layout. It COPYs paths that do not exist in this repo: packages/* (bls/core/sdk/...), docker/, pnpm-lock.yaml, pnpm-workspace.yaml, patches/, and the header documents a --build-context memvid=../memvid flag. A docker build -f Dockerfile.dvm . in this repo will fail today. The companion "trim to dvm-only" follow-up (also in CLAUDE.md/README) — tracked as the separate issue #2 — must land first (or as part of a coordinated change) so that Dockerfile.dvm builds standalone from this repo's context. This issue depends on a buildable Dockerfile.dvm. Do not merge a publish workflow that is guaranteed red. (This is a sequencing dependency, not an open question — the spec below is ready to implement once #2 lands.)

Scope

Create a single workflow file:

  • Path: .github/workflows/publish-dvm-image.yml
  • Name: Publish DVM Image
  • Triggers:
    • push: tags: ['v*'] — publishes versioned tag(s) + :latest.
    • workflow_dispatch — manual build/push of a versioned tag only (no :latest move), for rehearsals.
  • GHCR image name: ghcr.io/toon-protocol/store (hardcoded via a single env.IMAGE so the name is set in exactly one place). See Resolved decisions.
  • Build file: Dockerfile.dvm at the repo root, context: ..
  • Platform: linux/amd64 only (single-platform) for this first iteration. See Resolved decisions.
  • Build topology: single ubuntu-latest build-and-push for linux/amd64. No QEMU, no per-arch matrix, no imagetools create manifest-merge step (those were only needed for the dropped arm64 leg).
  • Registry login: docker/login-action against ghcr.io using ${{ github.actor }} + ${{ secrets.GITHUB_TOKEN }} (no PAT needed).
  • Permissions: contents: read, packages: write (and id-token: write only if cosign signing is kept — see below).
  • Tags: via docker/metadata-action:latest (tag pushes only), the v* semver tag, and a sha-<shortsha> tag.
  • Tooling versions: pin actions by commit SHA, matching the versions already used in the source workflow (actions/checkout v4, docker/setup-buildx-action v3, docker/login-action v3, docker/build-push-action v6, docker/metadata-action v5).

Out of scope

  • Any change to the agent-loop workflows (backlog-manager, issue-decomposer, issue-executor, pr-reviewer).
  • npm publishing (this package is private — there is no publishable npm artifact).
  • image-manifest.json, compose-template rendering, and the CONNECTOR_VERSION_DEFAULT / constants.ts alignment preflight — all townhouse-package concerns absent from this repo.
  • Building any other Dockerfile in the repo (Dockerfile.mill, Dockerfile.town, Dockerfile.townhouse-api, the akash-*/oyster/nix/sdk-e2e/toon-client ones). DVM only.
  • linux/arm64 support and the multi-arch manifest-merge machinery — deferred (see Resolved decisions Trim store repo to dvm-only: remove leftover monorepo build contexts #2).
  • The "trim Dockerfile.dvm to dvm-only" work itself — that is the separate prerequisite issue Trim store repo to dvm-only: remove leftover monorepo build contexts #2 (see Critical prerequisite).

Acceptance criteria

  • .github/workflows/publish-dvm-image.yml exists and is valid YAML (parses; actionlint clean if available).
  • Triggers are exactly: push on v* tags and workflow_dispatch.
  • Builds Dockerfile.dvm with context: . for linux/amd64.
  • Logs in to ghcr.io via docker/login-action with ${{ secrets.GITHUB_TOKEN }}; job permissions include packages: write.
  • Image is tagged :latest (tag-push only), the v* semver tag, and a sha-<shortsha> tag, via docker/metadata-action.
  • The GHCR image name is ghcr.io/toon-protocol/store, set in exactly one place (env.IMAGE).
  • Actions are pinned by commit SHA (matching the source workflow's pins) rather than floating tags.
  • No npm-publish, no image-manifest.json, no compose-rendering steps are present.
  • A workflow_dispatch run does not move :latest (only v* tag pushes do).

Implementation notes

  • Source reference: ../town/.github/workflows/publish-townhouse-images.yml (GitHub: toon-protocol/town). Carve the dvm slice of the build-per-platform job, but reduce it to the single linux/amd64 path: reuse its docker/metadata-action tag block and its docker/build-push-action build-and-push step. Drop preflight (connector/constants alignment), build-image-manifest, the merge-multi-arch / imagetools create step, the per-arch matrix, smoke-images's townhouse/mill/town markers, and publish-npm entirely.
  • Why amd64-only (resolved): the source workflow's header documents that arm64 emulation of Dockerfile.dvm's Solana/Rust dependency chain crashed with qemu: uncaught target signal 4 (Illegal instruction) - core dumped. The only known fix is native ubuntu-24.04-arm runners, whose cost/availability for this repo is unconfirmed. For the first iteration we ship linux/amd64 only and defer arm64; do not add a QEMU arm64 leg. (See Resolved decisions Trim store repo to dvm-only: remove leftover monorepo build contexts #2 for the path to adding arm64 later.)
  • dvm smoke marker (optional but recommended): the source smoke-images job pulls the dvm image by digest and asserts /app/entrypoint-dvm.js is present + non-empty and node --version is v20.x. Port just the dvm slice as a cheap post-push liveness gate.
  • cosign signing (optional): the source signs digests via keyless OIDC (id-token: write, sigstore/cosign-installer, COSIGN_YES: true). Keep it if signed images are wanted (then add id-token: write to permissions); otherwise it can be dropped to keep the first cut minimal. Default recommendation: keep it, to honor the README's "signed digest" wording.
  • Validation: before relying on green CI, confirm docker build -f Dockerfile.dvm . succeeds locally from this repo (it will NOT today — see Critical prerequisite / issue Trim store repo to dvm-only: remove leftover monorepo build contexts #2). A workflow_dispatch rehearsal that pushes a non-latest versioned tag is the safest end-to-end check; verify the result with docker buildx imagetools inspect ghcr.io/toon-protocol/store:<tag>.
  • GHCR package visibility: GHCR auto-creates the package on first push, scoped to this repo and initially private. If operators must pull anonymously, package visibility must be flipped to public in package settings (one-time, manual) — out of scope for the workflow file but worth noting in the PR description.

Resolved decisions

These were the publish-policy questions that could not be settled from repo evidence alone. Both are now decided — no human input remains:

  1. GHCR image name → store. Publish as ghcr.io/toon-protocol/store (hardcoded everywhere: env.IMAGE, tags, any manifest/inspect assertions). The old monorepo ghcr.io/toon-protocol/dvm image name is superseded; any consumers (e.g. townhouse's image-manifest.json / DockerOrchestrator) that still pull ghcr.io/toon-protocol/dvm must migrate to ghcr.io/toon-protocol/store — that migration is not this issue's job.
  2. Platform → linux/amd64 only (first iteration). arm64 is deferred. QEMU emulation crashes on Dockerfile.dvm's Solana/Rust chain, and native arm64 runner cost/availability for this repo is unconfirmed. arm64 can be added later via native ubuntu-24.04-arm runners (re-introducing the per-arch matrix + imagetools create manifest-merge) if a consumer needs it. Not in scope here.

(Trigger policy — v* tags + workflow_dispatch — is taken from the source workflow as the sensible default and is not blocking; raise it only if release cadence differs.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs:humanA human decision/clarification is requiredrisk:mediumMedium-risk; maybe agent-suitable with human review

    Type

    Fields

    No fields configured for Chore.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions