Skip to content

D7: GHCR publish workflow #12

Description

@Teagan42

Part of #2

D7 — GHCR publish workflow

Labels: wayfinder:grilling status:closed
Blocked by: D6 (closed)

Resolution

Annotated GitHub Actions workflow outline. Not runnable YAML — implementers convert.

File: .github/workflows/docker-publish.yml

Triggers:

on:
  push:
    branches: [main]           # → :edge
    tags:     ['v*.*.*']       # → :1.2.3, :1.2, :1, :latest
  workflow_dispatch:            # manual re-run for hotfix rebuilds

No cron rebuild. :edge refreshes agents whenever main moves; long-lived tags stay pinned to their release commit. Users needing fresher baked agents rebuild locally or wait for the next release.

Permissions block (minimum required):

permissions:
  contents:     read            # actions/checkout
  packages:     write           # GHCR push
  id-token:     write           # OIDC → cosign keyless
  attestations: write           # SLSA build provenance attestations

Jobs:

1. build-and-publish — single job, multi-arch via buildx matrix (platforms: list, not job matrix — one manifest list)

Steps in order:

  1. checkoutactions/checkout@v4, fetch-depth: 0 (metadata-action reads tags).
  2. setup-qemudocker/setup-qemu-action@v3, platforms: arm64 (amd64 is native).
  3. setup-buildxdocker/setup-buildx-action@v3.
  4. login-ghcrdocker/login-action@v3 with registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }}.
  5. metadatadocker/metadata-action@v5:
    images: ghcr.io/${{ github.repository_owner }}/chartr
    tags: |
      type=edge,branch=main            # main → :edge
      type=semver,pattern={{version}}  # v1.2.3 → :1.2.3
      type=semver,pattern={{major}}.{{minor}}
      type=semver,pattern={{major}}
      # `latest` is implicit for semver tags — metadata-action adds it when the tag is stable (no -pre).
    labels: |
      org.opencontainers.image.version={{version}}
      org.opencontainers.image.revision={{sha}}
    (type=sha short-sha tags — considered and skipped for v1; digest-pinning covers the same use case with less tag noise. Can be added later without breaking anything.)
  6. build-and-pushdocker/build-push-action@v6:
    context: .
    file: packaging/docker/Dockerfile
    platforms: linux/amd64,linux/arm64
    push: true
    tags:   ${{ steps.metadata.outputs.tags }}
    labels: ${{ steps.metadata.outputs.labels }}
    build-args: |
      VERSION=${{ steps.metadata.outputs.version }}
      COMMIT=${{ github.sha }}
    cache-from: type=gha
    cache-to:   type=gha,mode=max     # rehydrates D6's cache mounts (npm, go-build, go-mod, pip)
    provenance: mode=min              # SLSA v1 provenance, minimal shape (smaller, faster builds)
    sbom: true                        # SPDX SBOM attached to the digest
    outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=chartr — AI workspace with a map of your work
    Emits a manifest list digest — captured as ${{ steps.build.outputs.digest }}.
  7. cosign-installsigstore/cosign-installer@v3.
  8. cosign-sign — keyless, on the manifest-list digest:
    cosign sign --yes ghcr.io/${{ github.repository_owner }}/chartr@${{ steps.build.outputs.digest }}
    Signs both per-arch descriptors implicitly via the index digest.

2. verify — depends on build-and-publish; runs the same trigger set

Steps:

  1. setup-qemu (same as above — arm64 emulation).
  2. cosign-install.
  3. cosign verify — keyless, matching the workflow's own OIDC identity:
    cosign verify \
      --certificate-identity-regexp "https://github.com/${{ github.repository }}/.github/workflows/docker-publish.yml@.*" \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com \
      ghcr.io/${{ github.repository_owner }}/chartr@${{ needs.build-and-publish.outputs.digest }}
  4. smoke-test amd64:
    docker run --rm --platform linux/amd64 \
      ghcr.io/${{ github.repository_owner }}/chartr@${{ needs.build-and-publish.outputs.digest }} \
      chartr --help
  5. smoke-test arm64 — same, --platform linux/arm64, under qemu (~1 min).
  6. smoke-test agents list-baked on amd64 — runs the new subcommand from D9; confirms the manifest file was baked correctly. Guard with a continue-on-error: false — a failure here means D6 or D9 shipped incorrectly and the release should NOT be considered good.

No cleanup / no tag-move on failure: if verify fails, the digest and tags are already published. Investigator must manually gh api ... DELETE the offending version. Documented as a known-limitation troubleshooting entry (not worth automating for v1).

Concurrency:

concurrency:
  group: docker-publish-${{ github.ref }}
  cancel-in-progress: false        # never cancel a publish in flight

Expected build time:

  • Cold: ~15-20 min (arm64 buildx cross-compile via qemu + apt-get + npm agents install).
  • Warm (GHA cache hit): ~5-8 min.

Downstream impact:

  • D8 (compose + docs) — image ref in examples is ghcr.io/<owner>/chartr:latest (or :edge for tracking main). Docs note cosign verify incantation for users who want to verify before pulling.
  • No new tickets — this closes the last spec question for the destination.

Question

Spec the GitHub Actions workflow that builds and publishes the image to ghcr.io/<owner>/chartr. Decisions:

  • Triggers: push to main (→ edge and/or main-<sha> tags) AND push of tags matching v*.*.* (→ semver tags + latest). Exact tag matrix via docker/metadata-action.
  • Multi-arch: linux/amd64 + linux/arm64 via docker/setup-qemu-action + docker/setup-buildx-action; single manifest list.
  • Auth: OIDC token vs. GITHUB_TOKEN with packages: write. Confirm minimum permissions block.
  • Signing: cosign sign --keyless on the published digest.
  • Provenance / SBOM: buildx --provenance=true + --sbom=true attestations; note the tradeoff with build time.
  • Caching: type=gha cache for buildx layers; expected cold vs. warm build times.
  • Verification step: a post-publish job that pulls the digest, cosign verify, and smoke-runs --version (or equivalent) on both arches (arm64 under qemu).

Deliverable: an annotated workflow outline (job names, steps, key with: values, permissions block) — NOT a runnable YAML file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions