ci(release): publish multi-arch Docker image to GHCR on tag #1
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}" |