Skip to content

ci(release): publish multi-arch Docker image to GHCR on tag #1

ci(release): publish multi-arch Docker image to GHCR on tag

ci(release): publish multi-arch Docker image to GHCR on tag #1

Workflow file for this run

name: Build and publish Docker image
# Trigger on annotated semver tags (v1.2.3 / v1.2.3-rc.1) and on manual dispatch.
# We deliberately don't build on every push to main — that would inflate the
# registry with throwaway tags. Use a tag to ship.
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
inputs:
ref:
description: 'Ref to build (branch/tag/sha). Defaults to current.'
required: false
default: ''
permissions:
contents: read
packages: write
# Required for SLSA build provenance attestation produced by docker/build-push-action.
id-token: write
attestations: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
name: build-and-push (${{ matrix.platform }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner-arch: amd64
- platform: linux/arm64
runner-arch: arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Only emit a `latest` tag for the amd64 leg so we don't end up with
# two single-arch latest manifests fighting each other. The per-arch
# build pushes by digest; the merge step below stitches a multi-arch
# manifest carrying all the human-readable tags.
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
labels: |
org.opencontainers.image.title=fronius-modbus-mqtt
org.opencontainers.image.description=Fronius inverter/meter Modbus TCP -> MQTT/InfluxDB bridge
org.opencontainers.image.vendor=Stefan M
org.opencontainers.image.licenses=PolyForm-Noncommercial-1.0.0
- name: Build and push (per-arch digest)
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
push: true
labels: ${{ steps.meta.outputs.labels }}
# We push by digest only; the manifest tags are stitched in the merge job.
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.runner-arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.runner-arch }}
- name: Export digest
run: |
mkdir -p /tmp/digests
echo "${{ steps.build.outputs.digest }}" > "/tmp/digests/${{ matrix.runner-arch }}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.runner-arch }}
path: /tmp/digests/${{ matrix.runner-arch }}
if-no-files-found: error
retention-days: 1
merge:
name: merge multi-arch manifest
runs-on: ubuntu-24.04
needs: build-and-push
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ !contains(github.ref, '-') }}
- name: Create and push multi-arch manifest
working-directory: /tmp/digests
run: |
# Build a single buildx imagetools `create` command that:
# - applies every tag in $DOCKER_METADATA_OUTPUT_TAGS
# - references every per-arch digest collected above
# The digests were written by the matrix as plain `sha256:...` strings.
IMAGE_REF="${REGISTRY}/${IMAGE_NAME}"
DIGEST_ARGS=""
for f in *; do
digest=$(cat "$f")
DIGEST_ARGS="$DIGEST_ARGS ${IMAGE_REF}@${digest}"
done
# shellcheck disable=SC2086 # word splitting is intentional here
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<<"$DOCKER_METADATA_OUTPUT_JSON") \
$DIGEST_ARGS
- name: Inspect resulting image
run: |
docker buildx imagetools inspect \
"${REGISTRY}/${IMAGE_NAME}:${DOCKER_METADATA_OUTPUT_VERSION}"