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) and sha-<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 complytime org 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):

  1. Go to ActionsPublish Images to GHCR
  2. Click Run workflow
  3. Select branch (usually main)
  4. Optionally check Force rebuild without cache
  5. 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 copy to transfer the exact same bytes (identical sha256 digest) 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.

  1. Verify CI is green on main. Check that the latest commit on main passed all CI checks (lint, test, version sync, security scans) and that a signed image exists on GHCR for the HEAD commit.

  2. Verify proofwatch.Version() matches the intended release version. The version constant lives in proofwatch/proofwatch.go. If the intended release is v0.2.0, the function must return "0.2.0". Update and merge before tagging.

  3. Verify IMAGE_VERSION in the Containerfile matches the release version. The ARG IMAGE_VERSION= default in beacon-distro/Containerfile.collector must match the release version (without the v prefix). task release-prep updates this automatically. CI can also override it via --build-arg IMAGE_VERSION=x.y.z.

  4. Verify CHANGELOG.md has an entry for this version. At minimum, include a summary of notable changes since the last release (or since project inception for v0.1.0). Follow Keep a Changelog format.

  5. Verify dependency versions are aligned. Run task version:check to confirm Go and OTel dependency versions are consistent across all modules, Containerfiles, CI workflows, and docs.

  6. Run task check locally. 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.3

Pushing 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 SettingsSecrets and variablesActions:

SecretRequired ForDescription
QUAY_USERNAMEPromotionQuay.io robot account username
QUAY_PASSWORDPromotionQuay.io robot account token

Note: GHCR uses GITHUB_TOKEN automatically, no additional secrets needed.

3. Enable Branch Protection#

In SettingsBranchesmain:

  • Require status checks to pass
  • Require branches to be up to date

Verifying Images#

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-distro

Inspect 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-stdin

Copy 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:test

Verifying 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.com

Common 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-pr123

Scenario 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-0da6ac5

Scenario 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:

ErrorCauseSolution
manifest unknownImage doesn’t existCheck tag name, verify workflow succeeded
unauthorizedAuthentication requiredRun skopeo login ghcr.io with GitHub token
requested access to the resource is deniedNot an org member or missing permissionsVerify org membership, check token scopes
connection refusedNetwork/registry issueCheck network, retry after a moment

Quick Reference#

TaskWorkflowTriggerTags
Build & publish to GHCR (production)ci_publish_ghcr.ymlPush to main (after CI)sha-<commit>
Build & publish to GHCR (dev)ci_publish_ghcr.ymlPR from org memberdev-pr<number>, sha-<commit>
Promote to Quay.ioci_publish_quay.ymlPush 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.com

More Information#