Docker Publish #84
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: Docker Publish | |
| # Image tag convention: | |
| # :latest — cutting-edge main builds, published only after the PR Quality | |
| # Gate succeeds on main (community testers track this tag). | |
| # :stable — vetted releases, published from `v*` version tags alongside the | |
| # matching semantic-version tags. | |
| on: | |
| # Gate :latest on a green CI run: publish only when the quality gate passes | |
| # on the default branch, never on a raw push. | |
| workflow_run: | |
| workflows: ["PR Quality Gate"] | |
| types: [completed] | |
| branches: [main] | |
| push: | |
| tags: ["v*"] | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| # Deny-all by default; each job opts into the narrowest scope it needs. Only the | |
| # image-publishing jobs get `packages: write`, and this workflow never runs on | |
| # pull requests, so untrusted PR code never sees a registry-write token. | |
| permissions: {} | |
| jobs: | |
| # Build each platform natively on its own runner — arm64 is a first-class | |
| # citizen built on a real arm64 host, never cross-compiled under QEMU. Each | |
| # build is pushed by digest only; the manifest list is assembled afterwards. | |
| build: | |
| name: Build (${{ matrix.platform }}) | |
| # A workflow_run gains package-write privileges, so it must only follow the | |
| # green quality gate caused by a push to this repository's default branch — | |
| # never a pull-request run. Tag and manual releases are maintainer actions. | |
| if: >- | |
| ${{ github.event_name != 'workflow_run' || | |
| (github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'push' && | |
| github.event.workflow_run.head_repository.full_name == github.repository && | |
| github.event.workflow_run.head_branch == github.event.repository.default_branch) }} | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| # For workflow_run events, build the exact commit CI validated. | |
| ref: ${{ github.event.workflow_run.head_sha || github.sha }} | |
| - name: Normalize image name | |
| # GHCR requires a lowercase repository path; github.repository may be | |
| # mixed-case (e.g. BTreeMap/Serval), so downcase it for every ref below. | |
| run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> "$GITHUB_ENV" | |
| env: | |
| IMAGE_NAME: ${{ env.IMAGE_NAME }} | |
| - name: Prepare platform pair | |
| run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV" | |
| env: | |
| PLATFORM: ${{ matrix.platform }} | |
| - name: Extract metadata (labels) | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| platforms: ${{ matrix.platform }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| # Cargo runs inside Docker, so BuildKit's container-layer cache is | |
| # intentionally separate from the host rust-cache target roots. | |
| cache-from: type=gha,scope=serval-image-${{ env.PLATFORM_PAIR }} | |
| cache-to: type=gha,mode=max,scope=serval-image-${{ env.PLATFORM_PAIR }} | |
| outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | |
| - name: Export digest | |
| run: | | |
| mkdir -p "${{ runner.temp }}/digests" | |
| digest="${{ steps.build.outputs.digest }}" | |
| touch "${{ runner.temp }}/digests/${digest#sha256:}" | |
| - name: Upload digest | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: digests-${{ env.PLATFORM_PAIR }} | |
| path: ${{ runner.temp }}/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Combine the per-architecture digests into a single multi-arch manifest list | |
| # and apply the human-facing tags (branch, semver, sha). | |
| merge: | |
| name: Publish multi-arch manifest | |
| runs-on: ubuntu-latest | |
| needs: [build] | |
| permissions: | |
| packages: write | |
| steps: | |
| - name: Normalize image name | |
| # GHCR requires a lowercase repository path; github.repository may be | |
| # mixed-case (e.g. BTreeMap/Serval), so downcase it for every ref below. | |
| run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> "$GITHUB_ENV" | |
| env: | |
| IMAGE_NAME: ${{ env.IMAGE_NAME }} | |
| - name: Download digests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: ${{ runner.temp }}/digests | |
| pattern: digests-* | |
| merge-multiple: true | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata (tags) | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=sha | |
| - name: Create and push manifest list | |
| working-directory: ${{ runner.temp }}/digests | |
| run: | | |
| docker buildx imagetools create \ | |
| $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) | |
| - name: Inspect published image | |
| run: | | |
| docker buildx imagetools inspect \ | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} |