feat: introduce multi-arch build matrix and OCI-compliant multi-arch images (#46)

* chore: update Docker image description to 'Sure'

* chore: adjust Docker metadata tags in publish workflow

- Disable automatic 'latest' tag generation
- Add 'nightly' tag for scheduled builds

* feat: add optional 'push' input to control image publishing

- Allow toggling push behavior via workflow input
- Allow push to 'ghcr.io' on tags, scheduled events, or when
	input is true

* chore: switch Docker cache to use registry-based build cache

* feat(ci): add multi-arch matrix image build & manifest merge

- Build and push images for amd64 and arm64 with platform-specific cache
- Export and upload image digests for each platform
- Merge digests into multi-arch manifest with retry logic
- Remove QEMU setup, use platform-specific runners
- Dynamic tag configuration for nightly, stable, and SHA tags

* chore(deps): pin external actions to specific minor versions

* Bump actions/checkout to v4.2.0
* Bump docker/setup-buildx-action to v3.10.0
* Bump docker/login-action to v3.3.0
* Bump docker/metadata-action to v5.6.0
* Bump docker/build-push-action to v6.16.0

* chore(ci): improve publish workflow robustness

- Set artifact upload to error if no files found & limit retention to 1 day

* chore(ci): enable OCI media types in Docker build outputs

* feat(ci): add scheduled nightly publish at 01:30 UTC

* chore(ci): refine image publish workflow and clarify multi-arch steps

- Add reference and conditions for push in workflow comments
- Rename build step for clarity on published platform target
- Ensure oci-mediatypes is preserved for annotations to show
- Rename merge job to indicate pushing multi-arch manifest tag
This commit is contained in:
Himank Dave
2025-08-03 02:23:49 -04:00
committed by GitHub
parent 25ccb8471c
commit a14b0535ec

View File

@@ -1,3 +1,13 @@
# Reference: https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
# Conditions for pushing the image to GHCR:
# - Triggered by push to a version tag (`v*`)
# - Triggered by a scheduled run
# - Triggered manually via `workflow_dispatch` with `push: true`
#
# Conditional expression:
# startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push
name: Publish Docker image
on:
@@ -8,11 +18,18 @@ on:
required: true
type: string
default: 'main'
push:
description: 'Push the image to container registry'
required: false
type: boolean
default: false
push:
tags:
- 'v*'
branches:
- main
schedule:
- cron: '30 1 * * *'
env:
REGISTRY: ghcr.io
@@ -26,12 +43,19 @@ jobs:
uses: ./.github/workflows/ci.yml
build:
name: Build docker image
name: Build Docker image
needs: [ ci ]
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
platform: [amd64, arm64]
runs-on: ubuntu-latest
timeout-minutes: 60
runs-on: ${{ matrix.platform == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
outputs:
tags: ${{ steps.meta.outputs.tags }}
permissions:
contents: read
@@ -39,47 +63,137 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@v4.2.0
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v3.10.0
- name: Log in to the container registry
uses: docker/login-action@v3
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Configure image tags
id: tag_config
shell: bash
run: |
BASE_CONFIG="type=sha,format=long"
if [[ $GITHUB_EVENT_NAME == "schedule" ]]; then
BASE_CONFIG+=$'\n'"type=schedule,pattern=nightly"
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
BASE_CONFIG="type=semver,pattern={{version}}"
BASE_CONFIG+=$'\n'"type=raw,value=stable"
fi
{
echo 'TAGS_SPEC<<EOF'
echo "$BASE_CONFIG"
echo EOF
} >> $GITHUB_ENV
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v5.6.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: latest=auto
tags: |
type=sha,format=long
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
flavor: latest=false
tags: ${{ env.TAGS_SPEC }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
- name: Publish 'linux/${{ matrix.platform }}' image by digest
uses: docker/build-push-action@v6.16.0
id: build
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: 'linux/amd64,linux/arm64'
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
# https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#adding-a-description-to-multi-arch-images
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=A multi-arch Docker image for the Maybe Rails app
build-args: BUILD_COMMIT_SHA=${{ github.sha }}
platforms: 'linux/${{ matrix.platform }}'
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform }}
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform }},mode=max
labels: ${{ steps.meta.outputs.labels }}
provenance: false
push: true
# DO NOT REMOVE `oci-mediatypes=true`, fixes annotation not showing up on job.merge.steps[-1]
# ref: https://github.com/docker/build-push-action/discussions/1022
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},name-canonical=true,push-by-digest=true,oci-mediatypes=true
- name: Export the Docker image digest
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }}
run: |
mkdir -p "${RUNNER_TEMP}"/digests
echo "${DIGEST#sha256:}" > "${RUNNER_TEMP}/digests/digest-${PLATFORM}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
PLATFORM: ${{ matrix.platform }}
- name: Upload the Docker image digest
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }}
uses: actions/upload-artifact@v4.6.2
with:
name: digest-${{ matrix.platform }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: Merge multi-arch manifest & push multi-arch tag
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }}
needs: [build]
timeout-minutes: 60
runs-on: 'ubuntu-24.04'
permissions:
packages: write
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
- name: Download Docker image digests
uses: actions/download-artifact@v4.3.0
with:
path: ${{ runner.temp }}/digests
pattern: digest-*
merge-multiple: true
- name: Log in to the container registry
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Merge and push Docker image
env:
TAGS: ${{ needs.build.outputs.tags }}
DIGESTS_DIR: ${{ runner.temp }}/digests
shell: bash -xeuo pipefail {0}
run: |
tag_args=()
while IFS=$'\n' read -r tag; do
[[ -n "${tag}" ]] || continue
tag_args+=("--tag=${tag}")
done <<< "${TAGS}"
image_args=()
for PLATFORM in amd64 arm64; do
image_args+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:$(<"${DIGESTS_DIR}/digest-${PLATFORM}")")
done
attempts=0
until docker buildx imagetools create \
--annotation "index:org.opencontainers.image.description=A multi-arch Docker image for the Sure Rails app" \
"${tag_args[@]}" "${image_args[@]}" \
; do
attempts=$((attempts + 1))
if [[ $attempts -ge 3 ]]; then
echo "[$(date -u)] ERROR: Failed after 3 attempts." >&2
exit 1
fi
delay=$((2 ** attempts))
if [[ $delay -gt 15 ]]; then delay=15; fi
echo "Push failed (attempt $attempts). Retrying in ${delay} seconds..."
sleep ${delay}
done