Publish Image
This guide explains how to publish in GHCR and promote in Quay for container images by using the org-infra reusable workflows.
Process Overview Tl;dr#
The publishing process values security and automation to provide predictable, low-cost image releases.
Main Branch Push → Build + Scan + Sign → GHCR (sha-<commit>)
↓
Release Tag (v*) → Verify + Promote → Quay.io (v1.2.3)
PR from Org Member → Build + Scan + Sign → GHCR (dev-pr<number>)Tagging Strategy:
- Production builds (main branch):
sha-<commit>(immutable) - Dev builds (org member PRs):
dev-pr<number>+sha-<commit>(mutable + immutable) - External PRs: No images built (security)
Main Branch Pipeline (scan Source ��� Build ��� Scan Image ��� Sign)#
┌─────────────────────────────────────────────────────────────────────────────────┐
│ MAIN BRANCH PUSH │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ org-infra reusable workflows │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
╔═══════════════════════════════════════════════════════════════════════════╗
║ STAGE 1: BUILD & PUSH ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ reusable_publish_ghcr.yml │ ║
║ │ ┌───────────────────────────────────────────────────────────────┐ │ ║
║ │ │ • Checkout source │ │ ║
║ │ │ • Login to GHCR │ │ ║
║ │ │ • Build multi-platform image (Buildx) │ │ ║
║ │ │ • Push to ghcr.io/org/image:sha-<commit> │ │ ║
║ │ │ • Auto-generate SBOM + SLSA provenance (buildx attestations) │ │ ║
║ │ └───────────────────────────────────────────────────────────────┘ │ ║
║ │ │ ║
║ │ OUTPUTS: digest (sha256:...), image (ghcr.io/org/image) │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════════╝
│
│ digest, image
▼
╔═══════════════════════════════════════════════════════════════════════════╗
║ STAGE 2: VULNERABILITY SCAN ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ reusable_vuln_scan.yml │ ║
║ │ ┌───────────────────────────────────────────────────────────────┐ │ ║
║ │ │ [OSV-Scanner] Dependency CVE scan (lockfiles) │ │ ║
║ │ │ [Trivy Source] Secrets + misconfig scan │ │ ║
║ │ │ [Trivy Image] Container OS/runtime vuln scan │ │ ║
║ │ │ │ │ ║
║ │ │ • Upload SARIF results to GitHub Security tab │ │ ║
║ │ │ • Attach vuln attestation to image (cosign attest) │ │ ║
║ │ └───────────────────────────────────────────────────────────────┘ │ ║
║ │ │ ║
║ │ OUTPUTS: trivy_image_scan_passed, vuln_attestation_attached │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════════╝
│
│ digest, scan results
▼
╔═══════════════════════════════════════════════════════════════════════════╗
║ STAGE 3: SIGN & VERIFY ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ reusable_sign_and_verify.yml │ ║
║ │ ┌───────────────────────────────────────────────────────────────┐ │ ║
║ │ │ • Keyless signing via Sigstore/Fulcio (OIDC) │ │ ║
║ │ │ • cosign sign image@digest │ │ ║
║ │ │ │ │ ║
║ │ │ Verify all attestations: │ │ ║
║ │ │ ✓ Signature (identity: github.com/org/*) │ │ ║
║ │ │ ✓ SLSA Provenance │ │ ║
║ │ │ ✓ SBOM (SPDX) │ │ ║
║ │ │ ✓ Vulnerability scan attestation │ │ ║
║ │ └───────────────────────────────────────────────────────────────┘ │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════════╝
│
▼
┌───────────────────────────────┐
│ ✅ Image Ready in GHCR │
│ ghcr.io/org/image:sha-abc123 │
│ + signature │
│ + SBOM │
│ + SLSA provenance │
│ + vuln attestation │
└───────────────────────────────┘Pr Dev Build Pipeline (org Members)#
For pull requests from organization members, the same security pipeline runs to validate Containerfile changes before merge.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PULL REQUEST (from org member) │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
╔═══════════════════════════════════════════════════════════════════════════╗
║ STAGE 0: ORG MEMBERSHIP CHECK ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ • Verify PR author is a complytime org member │ ║
║ │ • External contributors → skip image build (security) │ ║
║ │ • Org members → proceed to build │ ║
║ │ • Compute tag: dev-pr<number> │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════════╝
│
▼
(same pipeline as main branch)
scan-source → build → scan-image → sign → test
│
▼
┌───────────────────────────────┐
│ ✅ Dev Image Ready in GHCR │
│ Primary: dev-pr123 │
│ Immutable: sha-abc123 │
│ + signature + SBOM │
│ + provenance + vuln report │
└───────────────────────────────┘Security Model:
- Only organization members can trigger image builds on PRs
- External contributors’ PRs do not build images (prevents resource abuse and supply chain attacks)
- Same security scanning and signing as production builds
- Images published to GHCR with
dev-pr<number>tag (mutable) andsha-<commit>tag (immutable)
Use Case:
- Validate Containerfile changes before merge
- Test integration with dev images in local or staging environments
- Share dev images with team members for review
Release Pipeline (promote To Production)#
┌─────────────────────────────────────────────────────────────────────────────────┐
│ RELEASE TAG PUSH (v1.2.3) │
└─────────────────────────────────────────────────────────────────────────────────┘
│
▼
╔═══════════════════════════════════════════════════════════════════════════╗
║ STAGE 4: PROMOTE TO PRODUCTION REGISTRY ║
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
║ │ reusable_publish_quay.yml │ ║
║ │ ┌───────────────────────────────────────────────────────────────┐ │ ║
║ │ │ 1. Lookup source: ghcr.io/org/image:sha-<commit> → digest │ │ ║
║ │ │ │ │ ║
║ │ │ 2. Pre-promotion verification: │ │ ║
║ │ │ ✓ Verify source signature │ │ ║
║ │ │ │ │ ║
║ │ │ 3. cosign copy (preserves all signatures + attestations) │ │ ║
║ │ │ ghcr.io/org/image → quay.io/complytime/beacon-collector │ │ ║
║ │ │ │ │ ║
║ │ │ 4. Apply tags: v1.2.3, latest, sha-<full>, sha-<short> │ │ ║
║ │ │ │ │ ║
║ │ │ 5. Post-promotion verification (destination registry) │ │ ║
║ │ └───────────────────────────────────────────────────────────────┘ │ ║
║ │ │ ║
║ │ OUTPUTS: digest, dest_image_full │ ║
║ └─────────────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════════╝
│
▼
┌───────────────────────────────┐
│ ✅ Production Image Ready │
│ quay.io/complytime/ │
│ beacon-collector:v1.2.3 │
│ (all with preserved certs) │
└───────────────────────────────┘Publishing Images#
Images are automatically built and published in two scenarios:
1. Production Builds (main Branch)#
When changes are merged to main, the workflow:
- Waits for CI to complete successfully
- Checks if Containerfiles changed
- If yes: builds, scans, signs, and publishes to GHCR with tag
sha-<commit> - Runs integration tests against the published image
Trigger: Push to main branch after CI passes
2. Dev Builds (pull Requests)#
When an org member opens or updates a PR that touches:
- Any
Containerfile* - Source code in
beacon-distro/,proofwatch/,truthbeam/ - The workflow file itself
The workflow:
- Verifies PR author is a
complytimeorg member - If yes: builds, scans, signs, and publishes to GHCR with tags:
dev-pr<number>(mutable, updates on each push)sha-<commit>(immutable)
- Runs integration tests against the published image
Security: External contributors’ PRs do not trigger image builds.
Example:
## Pr #123 Opened By Org Member
## First Push (commit Abc123) Creates:
## - Dev-pr123 ��� Sha256:abc...
## - Sha-abc123 ��� Sha256:abc...
## ## Second Push (commit Def456) Updates:
## - Dev-pr123 ��� Sha256:def... (mutable, Now Points To New Image)
## - Sha-def456 ��� Sha256:def... (new Immutable Tag)
## - Sha-abc123 ��� Sha256:abc... (old Immutable Tag Still Exists)Manual Trigger#
To manually trigger a build (e.g., for base image updates or testing):
- Go to Actions → Publish Images to GHCR
- Click Run workflow
- Select branch (usually
main) - Optionally check Force rebuild without cache
- Click Run workflow
The manual trigger uses the same security pipeline (scan, sign, test) as automatic builds.
Promoting To Quay.io#
Promotion copies signed images from GHCR to Quay.io for public distribution.
Key Point: Promotion does not rebuild the image. It uses
cosign copyto transfer the exact same bytes (identicalsha256digest) from GHCR to Quay, preserving all signatures and attestations. This guarantees the image you tested in GHCR is identical to what’s released on Quay.
Pre-release Checklist#
Before tagging a release, run through these steps to ensure everything is aligned.
You can automate most of this with task dev:release-prep.
Verify CI is green on
main. Check that the latest commit onmainpassed all CI checks (lint, test, version sync, security scans) and that a signed image exists on GHCR for the HEAD commit.Verify
proofwatch.Version()matches the intended release version. The version constant lives inproofwatch/proofwatch.go. If the intended release isv0.2.0, the function must return"0.2.0". Update and merge before tagging.Verify
IMAGE_VERSIONin the Containerfile matches the release version. TheARG IMAGE_VERSION=default inbeacon-distro/Containerfile.collectormust match the release version (without thevprefix).task release-prepupdates this automatically. CI can also override it via--build-arg IMAGE_VERSION=x.y.z.Verify
CHANGELOG.mdhas an entry for this version. At minimum, include a summary of notable changes since the last release (or since project inception forv0.1.0). Follow Keep a Changelog format.Verify dependency versions are aligned. Run
task version:checkto confirm Go and OTel dependency versions are consistent across all modules, Containerfiles, CI workflows, and docs.Run
task checklocally. This runs all quality gates (lint + test) against your local checkout.
Creating A Release#
After the pre-release checklist passes:
git checkout main
git pull origin main
## Confirm The Ghcr Image Exists For Head
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:sha-$(git rev-parse --short HEAD)
## Create A Signed Tag
git tag -s v1.2.3 -m "Release v1.2.3"
git push origin v1.2.3Pushing the tag triggers ci_publish_quay.yml, which looks up the sha-<commit> image
already on GHCR and promotes it to quay.io/complytime/beacon-collector with the version
tag, latest, and SHA tags. No rebuild occurs – the promoted image has the exact same
sha256 digest as the GHCR source.
Post-release Verification#
After the Quay promotion workflow completes:
## Verify The Image Landed On Quay
skopeo inspect docker://quay.io/complytime/beacon-collector:v1.2.3
## Verify Signature On The Promoted Image
cosign verify quay.io/complytime/beacon-collector:v1.2.3 \
--certificate-identity-regexp='https://github.com/complytime/.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
## Confirm Digests Match Between Ghcr Source And Quay Destination
GHCR_DIGEST=$(skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:sha-$(git rev-parse --short HEAD) --format "{{.Digest}}")
QUAY_DIGEST=$(skopeo inspect docker://quay.io/complytime/beacon-collector:v1.2.3 --format "{{.Digest}}")
[ "$GHCR_DIGEST" = "$QUAY_DIGEST" ] && echo "Digests match" || echo "MISMATCH - investigate"Release Cadence#
Releases are created as needed. Maintainers coordinate releases via issues or discussions.
Setting Up The Repository#
1. Create Caller Workflows#
- Create a workflow for publishing to GHCR as
ci_publish_ghcr.yml - Create a workflow for publishing to Quay as
ci_publish_quay.yml - Get the commit SHA from the successful run of ci_publish_ghcr.yml
- Then tag the built commit and push trigger the Quay
2. Configure Secrets#
Add these secrets in Settings → Secrets and variables → Actions:
| Secret | Required For | Description |
|---|---|---|
QUAY_USERNAME | Promotion | Quay.io robot account username |
QUAY_PASSWORD | Promotion | Quay.io robot account token |
Note: GHCR uses
GITHUB_TOKENautomatically, no additional secrets needed.
3. Enable Branch Protection#
In Settings → Branches → main:
- Require status checks to pass
- Require branches to be up to date
Verifying Images#
Using Skopeo (recommended For Quick Checks)#
Skopeo is a command-line tool for inspecting and copying container images without pulling them. This is faster and more efficient than pulling images with podman or docker when you just want to verify publication.
Install Skopeo:
## Macos
brew install skopeo
## Fedora/rhel/centos
dnf install skopeo
## Ubuntu/debian
apt-get install skopeo
## Or Use Podman Alias (skopeo Is Bundled With Podman)
alias skopeo='podman run --rm quay.io/skopeo/stable'List all tags:
## List All Tags For Beacon-distro
skopeo list-tags docker://ghcr.io/complytime/complybeacon-beacon-distroInspect a specific image:
## Inspect Production Image (main Branch)
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:sha-0da6ac5
## Inspect Dev Image (pr Build)
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123
## Get Just The Digest
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123 \
--format "{{.Digest}}"
## Get Creation Timestamp
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123 \
--format "{{.Created}}"Authentication for private images:
If the repository is private, authenticate first:
## Login With Github Personal Access Token (requires 'read:packages' Scope)
skopeo login ghcr.io
## Username: Your-github-username
## Password: Ghp_yourpersonalaccesstoken
## Or Use Environment Variable
echo $GITHUB_TOKEN | skopeo login ghcr.io -u your-github-username --password-stdinCopy image between registries:
## Copy Dev Image To Local Testing Registry
skopeo copy \
docker://ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123 \
docker://localhost:5000/beacon-distro:testVerifying Signatures (cosign)#
After confirming the image exists with skopeo, verify cryptographic signatures:
## Verify Ghcr Image (beacon-distro)
cosign verify ghcr.io/complytime/complybeacon-beacon-distro:sha-0da6ac5 \
--certificate-identity-regexp='https://github.com/complytime/.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
## Verify Dev Image
cosign verify ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123 \
--certificate-identity-regexp='https://github.com/complytime/.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
## Verify Quay Image (production Release)
cosign verify quay.io/complytime/beacon-collector:v1.2.3 \
--certificate-identity-regexp='https://github.com/complytime/.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.comCommon Verification Scenarios#
Scenario 1: Check if my PR’s image was published
## Find Your Pr Number (e.g., Pr #123)
## Then Check For The Dev Tag
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123Scenario 2: Verify main branch image exists for a commit
## Get The Short Commit Sha (e.g., 0da6ac5)
git rev-parse --short HEAD
## Inspect The Image
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:sha-0da6ac5Scenario 3: Check when an image was last updated
## Dev Images Are Mutable And Update On Each Pr Push
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123 \
--format "{{.Created}}"Scenario 4: Verify dev and production images are identical
## Compare Digests (should Match If Built From Same Commit)
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:dev-pr123 \
--format "{{.Digest}}"
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:sha-abc123 \
--format "{{.Digest}}"Troubleshooting:
| Error | Cause | Solution |
|---|---|---|
manifest unknown | Image doesn’t exist | Check tag name, verify workflow succeeded |
unauthorized | Authentication required | Run skopeo login ghcr.io with GitHub token |
requested access to the resource is denied | Not an org member or missing permissions | Verify org membership, check token scopes |
connection refused | Network/registry issue | Check network, retry after a moment |
Quick Reference#
| Task | Workflow | Trigger | Tags |
|---|---|---|---|
| Build & publish to GHCR (production) | ci_publish_ghcr.yml | Push to main (after CI) | sha-<commit> |
| Build & publish to GHCR (dev) | ci_publish_ghcr.yml | PR from org member | dev-pr<number>, sha-<commit> |
| Promote to Quay.io | ci_publish_quay.yml | Push tag v*.*.* | v1.2.3, latest, sha-<commit> |
Verification Commands:
## List All Tags
skopeo list-tags docker://ghcr.io/complytime/complybeacon-beacon-distro
## Inspect Specific Image
skopeo inspect docker://ghcr.io/complytime/complybeacon-beacon-distro:TAG
## Verify Signature
cosign verify ghcr.io/complytime/complybeacon-beacon-distro:TAG \
--certificate-identity-regexp='https://github.com/complytime/.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.comMore Information#
- README Container Image Section — Quick skopeo examples
- Sigstore Documentation — Keyless signing details