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:
- checkout —
actions/checkout@v4, fetch-depth: 0 (metadata-action reads tags).
- setup-qemu —
docker/setup-qemu-action@v3, platforms: arm64 (amd64 is native).
- setup-buildx —
docker/setup-buildx-action@v3.
- login-ghcr —
docker/login-action@v3 with registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }}.
- metadata —
docker/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.)
- build-and-push —
docker/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 }}.
- cosign-install —
sigstore/cosign-installer@v3.
- 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:
- setup-qemu (same as above — arm64 emulation).
- cosign-install.
- 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 }}
- smoke-test amd64:
docker run --rm --platform linux/amd64 \
ghcr.io/${{ github.repository_owner }}/chartr@${{ needs.build-and-publish.outputs.digest }} \
chartr --help
- smoke-test arm64 — same,
--platform linux/arm64, under qemu (~1 min).
- 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.
Part of #2
D7 — GHCR publish workflow
Labels:
wayfinder:grillingstatus:closedBlocked by: D6 (closed)
Resolution
Annotated GitHub Actions workflow outline. Not runnable YAML — implementers convert.
File:
.github/workflows/docker-publish.ymlTriggers:
No cron rebuild.
:edgerefreshes 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):
Jobs:
1.
build-and-publish— single job, multi-arch via buildx matrix (platforms:list, not job matrix — one manifest list)Steps in order:
actions/checkout@v4,fetch-depth: 0(metadata-action reads tags).docker/setup-qemu-action@v3,platforms: arm64(amd64 is native).docker/setup-buildx-action@v3.docker/login-action@v3withregistry: ghcr.io,username: ${{ github.actor }},password: ${{ secrets.GITHUB_TOKEN }}.docker/metadata-action@v5:type=shashort-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.)docker/build-push-action@v6:${{ steps.build.outputs.digest }}.sigstore/cosign-installer@v3.2.
verify— depends onbuild-and-publish; runs the same trigger setSteps:
--platform linux/arm64, under qemu (~1 min).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 ... DELETEthe offending version. Documented as a known-limitation troubleshooting entry (not worth automating for v1).Concurrency:
Expected build time:
Downstream impact:
ghcr.io/<owner>/chartr:latest(or:edgefor tracking main). Docs notecosign verifyincantation for users who want to verify before pulling.Question
Spec the GitHub Actions workflow that builds and publishes the image to
ghcr.io/<owner>/chartr. Decisions:pushtomain(→edgeand/ormain-<sha>tags) ANDpushof tags matchingv*.*.*(→ semver tags +latest). Exact tag matrix viadocker/metadata-action.linux/amd64+linux/arm64viadocker/setup-qemu-action+docker/setup-buildx-action; single manifest list.GITHUB_TOKENwithpackages: write. Confirm minimum permissions block.cosign sign --keylesson the published digest.--provenance=true+--sbom=trueattestations; note the tradeoff with build time.type=ghacache for buildx layers; expected cold vs. warm build times.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.