Docker #722
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 | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| workflow_dispatch: | |
| inputs: | |
| sha: | |
| description: "Commit SHA or ref to build (e.g. a release tag's commit). Lets you re-release a signed image from an exact source commit without moving the git tag. Leave blank to build the branch you dispatched from." | |
| required: false | |
| default: '' | |
| image_tag: | |
| description: 'Image tag to publish as (e.g. 0.10.5). Leave blank to tag with the branch name (or sha-<short> when a sha is given).' | |
| required: false | |
| default: '' | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| concurrency: | |
| group: docker-${{ github.ref }}-${{ inputs.image_tag || inputs.sha || github.sha }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Build each architecture on its OWN native runner — free arm64 hosted | |
| # runners (ubuntu-24.04-arm) are GA for public repos — in parallel, pushing | |
| # each as an untagged per-arch image BY DIGEST. Native beats QEMU here: the | |
| # image runs `bun install` plus two Vite/TanStack production builds, which are | |
| # far too slow under arm64 emulation (and risk job timeouts). | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| runs-on: ${{ matrix.runner }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Prepare platform pair | |
| env: | |
| PLATFORM: ${{ matrix.platform }} | |
| run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV" | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| # On workflow_dispatch, build the requested sha/ref (re-release of an | |
| # exact source commit); otherwise build the pushed ref as usual. | |
| ref: ${{ github.event.inputs.sha || github.ref }} | |
| - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 | |
| - name: Resolve lowercase image ref | |
| run: echo "IMAGE=${REGISTRY}/$(printf '%s' "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" | |
| - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 | |
| with: | |
| context: . | |
| file: ./apps/web/Dockerfile | |
| platforms: ${{ matrix.platform }} | |
| # No tags here — the manifest list (and its tags) is assembled in the | |
| # merge job; each leg pushes an untagged per-arch image by digest. | |
| outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true | |
| # Per-arch cache scope so the two runners don't clobber each other. | |
| cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} | |
| cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} | |
| - name: Export digest | |
| env: | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| mkdir -p /tmp/digests | |
| touch "/tmp/digests/${DIGEST#sha256:}" | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: digests-${{ env.PLATFORM_PAIR }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Assemble the multi-arch manifest list (OCI image index) from the per-arch | |
| # digests, apply the real tags, then keyless-sign the index digest. | |
| merge: | |
| runs-on: ubuntu-latest | |
| needs: [build] | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write # keyless cosign signing via GitHub OIDC | |
| steps: | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| path: /tmp/digests | |
| pattern: digests-* | |
| merge-multiple: true | |
| - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 | |
| - name: Resolve lowercase image ref | |
| run: echo "IMAGE=${REGISTRY}/$(printf '%s' "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" | |
| - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 | |
| id: meta | |
| with: | |
| images: ${{ env.IMAGE }} | |
| # The branch-name tag covers branch pushes AND manual dispatches that | |
| # build the dispatched branch itself (no sha/image_tag override). | |
| tags: | | |
| type=ref,event=branch,enable=${{ github.event.inputs.sha == '' && github.event.inputs.image_tag == '' }} | |
| type=semver,pattern={{version}} | |
| type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | |
| type=raw,value=${{ github.event.inputs.image_tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag != '' }} | |
| type=sha,enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_tag == '' && github.event.inputs.sha != '' }} | |
| - name: Create manifest list and push | |
| working-directory: /tmp/digests | |
| run: | | |
| docker buildx imagetools create \ | |
| $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | |
| $(printf "${IMAGE}@sha256:%s " *) | |
| - name: Capture image index digest | |
| id: index | |
| env: | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| run: | | |
| DIGEST=$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" --format '{{json .Manifest.Digest}}' | tr -d '"') | |
| echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@f713795cb21599bc4e5c4b58cbad1da852d7eeb9 # v3 | |
| # Keyless cosign signature over the pushed index digest, required by the | |
| # cluster's Kyverno verify-images ClusterPolicy (keyless, GitHub OIDC | |
| # issuer + QuackbackIO workflow subject). --recursive signs the OCI index | |
| # AND each per-arch child manifest, so policy can verify either the index | |
| # or a resolved per-arch digest. | |
| - name: Sign image index (keyless) | |
| env: | |
| IMAGE_DIGEST: ${{ steps.index.outputs.digest }} | |
| run: cosign sign --yes --recursive "${IMAGE}@${IMAGE_DIGEST}" |