Steps to reproduce the GHCR build/push setup for a new repo (based on yyc-pc-health-ping).
Copy both workflow files into .github/workflows/ in the new repo:
test-build.yml — CI workflow. Checks out the repo, sets up Buildx, and runs a build-only validation (push: false) against the Dockerfile. Confirms the image builds cleanly without touching the registry.publish-image.yml — Build and Push workflow. Triggered via workflow_run after test-build.yml (the workflow named "CI") completes. Only proceeds if github.event.workflow_run.conclusion == 'success'. Checks out the commit at head_sha, logs into GHCR, and builds + pushes with both :latest and a SHA tag.Update the workflows: ["CI"] reference in publish-image.yml if the CI workflow's name: differs from "CI" in the new repo.
publish-image.yml:
name: Publish to GHCR
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
jobs:
build-and-push:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Set lowercase image name
run: echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ github.event.workflow_run.head_sha }}
test-build.yml:
name: CI
on:
push:
branches: [main]
jobs:
build-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set lowercase image name
run: echo "IMAGE_NAME=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- name: Validate Docker build
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: ${{ env.IMAGE_NAME }}:ci-check
test-build.yml and publish-image.yml completed successfully.docker manifest inspect ghcr.io/OWNER/IMAGE_NAME:latest returns a valid manifest.latest is a moving tag, not a separate image. Pushing a new :latest just repoints it; the previous image becomes untagged/dangling unless it also has a SHA tag. GHCR doesn't auto-delete dangling versions.publish-image.yml runs via workflow_run (a separate workflow), a failure there does not turn the CI badge red. This is a known limitation, not a bug — needs: only chains jobs within a single workflow file, and cross-file chaining isn't natively supported.