From cb300d252cba152235239a9ad5dbba4928467cd4 Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Sat, 15 Nov 2025 06:43:52 +0700 Subject: [PATCH 1/4] Distributed multiarch builds Closes #2 --- .github/workflows/docker.yml | 142 +++++++++++++++++++++++++---------- 1 file changed, 103 insertions(+), 39 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cb54c98..9100bbe 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,19 +4,19 @@ on: workflow_call: inputs: publish: - description: 'Whether to sign and publish the image using cosign and GHCR' + description: "Whether to sign and publish the image using cosign and GHCR" required: true type: boolean dockerfile: - description: 'Path to the Dockerfile to build' + description: "Path to the Dockerfile to build" required: false type: string context: - description: 'Path to the build context' + description: "Path to the build context" required: false type: string image_name: - description: 'Name of the image to build' + description: "Name of the image to build" required: false type: string @@ -27,38 +27,36 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: - build: - runs-on: ubuntu-latest + # Build docker image for each architecture in parallel + build-arch: + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + + runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} permissions: contents: read packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 - # Install the cosign tool except on PR - # https://github.com/sigstore/cosign-installer - - name: Install cosign - if: inputs.publish - uses: sigstore/cosign-installer@v3 - + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 - # Workaround: https://github.com/docker/build-push-action/issues/461 - - name: Setup Docker buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64 - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: inputs.publish uses: docker/login-action@v3 @@ -67,8 +65,6 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 @@ -76,32 +72,100 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} images: ${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }} - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v5 + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 with: context: ${{ inputs.context || '.' }} - push: ${{ inputs.publish }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 file: ${{ inputs.dockerfile || 'Dockerfile' }} + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: | + type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ inputs.publish }} + cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} + + - name: Export digest + if: inputs.publish + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + if: inputs.publish + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + # Merge all architecture images into a single manifest + merge: + runs-on: ubuntu-latest + if: inputs.publish + needs: + - build-arch + permissions: + contents: read + packages: write + id-token: write + + steps: + - 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 into registry ${{ env.REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + images: ${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }} + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }}@sha256:%s ' *) - # Sign the resulting Docker image digest except on PRs. + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + + # Install the cosign tool + # https://github.com/sigstore/cosign-installer + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + # Sign the resulting Docker image digest. # This will only write to the public Rekor transparency log when the Docker # repository is public to avoid leaking data. If you would like to publish # transparency data even for private images, pass --force to cosign below. # https://github.com/sigstore/cosign - name: Sign the published Docker image - if: inputs.publish env: # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} \ No newline at end of file + run: | + images="" + for tag in ${TAGS}; do + images+="${tag}@${{ steps.meta.outputs.digest }} " + done + echo "${images}" | xargs -n 1 cosign sign --yes From 40de43f740e38e4b8f332c55fc78eacdab8c6918 Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Sat, 15 Nov 2025 06:54:58 +0700 Subject: [PATCH 2/4] force lowercase name --- .github/workflows/docker.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9100bbe..23776d9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,6 +25,7 @@ env: REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} + IMAGE_NAME_LOWER: "" jobs: # Build docker image for each architecture in parallel @@ -50,6 +51,7 @@ jobs: run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + echo "IMAGE_NAME_LOWER=$(echo '${{ inputs.image_name || env.IMAGE_NAME }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -70,7 +72,7 @@ jobs: uses: docker/metadata-action@v5 with: github-token: ${{ secrets.GITHUB_TOKEN }} - images: ${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }} - name: Build and push by digest id: build @@ -80,8 +82,7 @@ jobs: file: ${{ inputs.dockerfile || 'Dockerfile' }} platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} - outputs: | - type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ inputs.publish }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }},push-by-digest=true,name-canonical=true,push=${{ inputs.publish }} cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} @@ -113,6 +114,10 @@ jobs: id-token: write steps: + - name: Prepare + run: | + echo "IMAGE_NAME_LOWER=$(echo '${{ inputs.image_name || env.IMAGE_NAME }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + - name: Download digests uses: actions/download-artifact@v4 with: @@ -135,17 +140,17 @@ jobs: uses: docker/metadata-action@v5 with: github-token: ${{ secrets.GITHUB_TOKEN }} - images: ${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }}@sha256:%s ' *) + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}@sha256:%s ' *) - name: Inspect image run: | - docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name || env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}:${{ steps.meta.outputs.version }} # Install the cosign tool # https://github.com/sigstore/cosign-installer From 1daf640ab71657ce875f949852b27ffe4e490ced Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Sat, 15 Nov 2025 07:09:44 +0700 Subject: [PATCH 3/4] take digests from manifest list --- .github/workflows/docker.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 23776d9..7a66145 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -83,8 +83,8 @@ jobs: platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }},push-by-digest=true,name-canonical=true,push=${{ inputs.publish }} - cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }} - cache-to: type=gha,mode=max,scope=build-${{ env.PLATFORM_PAIR }} + cache-from: type=gha + cache-to: type=gha,mode=max - name: Export digest if: inputs.publish @@ -143,14 +143,18 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }} - name: Create manifest list and push + id: manifest 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_LOWER }}@sha256:%s ' *) - name: Inspect image + id: inspect run: | - docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}:${{ steps.meta.outputs.version }} + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}:${{ steps.meta.outputs.version }} --raw | jq -r '.manifests[0].digest' + digest=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}:${{ steps.meta.outputs.version }} --raw | sha256sum | awk '{print "sha256:" $1}') + echo "digest=${digest}" >> $GITHUB_OUTPUT # Install the cosign tool # https://github.com/sigstore/cosign-installer @@ -166,11 +170,12 @@ jobs: env: # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.inspect.outputs.digest }} # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. run: | images="" for tag in ${TAGS}; do - images+="${tag}@${{ steps.meta.outputs.digest }} " + images+="${tag}@${DIGEST} " done echo "${images}" | xargs -n 1 cosign sign --yes From d5d6642112135d63e74065b0d9364f30776c5ab6 Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Sat, 15 Nov 2025 07:17:40 +0700 Subject: [PATCH 4/4] Allow specifying custom digest artifact name prefix to prevent collisions --- .github/workflows/docker.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7a66145..497afb2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -19,6 +19,11 @@ on: description: "Name of the image to build" required: false type: string + artifact_name: + description: "Name prefix for digest artifacts (defaults to 'digests'). Useful when running multiple docker builds in the same workflow." + required: false + type: string + default: "digests" env: # Use docker.io for Docker Hub if empty @@ -97,7 +102,7 @@ jobs: if: inputs.publish uses: actions/upload-artifact@v4 with: - name: digests-${{ env.PLATFORM_PAIR }} + name: ${{ inputs.artifact_name }}-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 @@ -122,7 +127,7 @@ jobs: uses: actions/download-artifact@v4 with: path: ${{ runner.temp }}/digests - pattern: digests-* + pattern: ${{ inputs.artifact_name }}-* merge-multiple: true - name: Set up Docker Buildx