From a0744f179fb1433fc30987f309d2dfe03ce30dac Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:46:42 -0800 Subject: [PATCH] Chore: Build the ARM64 image using the native ARM64 runner (#11720) --- .github/workflows/ci-docker.yml | 225 ++++++++++++++++++++++++-------- 1 file changed, 172 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 9def58787..c91b2b05c 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -15,100 +15,219 @@ on: concurrency: group: docker-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + REGISTRY: ghcr.io jobs: - build-docker-image: - name: Build Docker Image - runs-on: ubuntu-24.04 - env: - CAN_PUSH: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} + build-arch: + name: Build ${{ matrix.arch }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + arch: amd64 + platform: linux/amd64 + - runner: ubuntu-24.04-arm + arch: arm64 + platform: linux/arm64 + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + outputs: + can-push: ${{ steps.check-push.outputs.can-push }} + push-external: ${{ steps.check-push.outputs.push-external }} + repository: ${{ steps.repo.outputs.name }} + ref-name: ${{ steps.ref.outputs.name }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v6.0.1 - name: Determine ref name id: ref run: | - echo "name=${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" >> $GITHUB_OUTPUT - - name: Check external registry push - id: push-external + ref_name="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" + # Sanitize by replacing / with - for cache keys + cache_ref="${ref_name//\//-}" + + echo "ref_name=${ref_name}" + echo "cache_ref=${cache_ref}" + + echo "name=${ref_name}" >> $GITHUB_OUTPUT + echo "cache-ref=${cache_ref}" >> $GITHUB_OUTPUT + - name: Check push permissions + id: check-push env: REF_NAME: ${{ steps.ref.outputs.name }} run: | - # If we cannot push (e.g. fork PR), explicitly disable external push and exit - if [[ "$CAN_PUSH" != "true" ]]; then - echo "enable=false" >> $GITHUB_OUTPUT - exit 0 - fi + # can-push: Can we push to GHCR? + # True for: pushes, or PRs from the same repo (not forks) + can_push=${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} + echo "can-push=${can_push}" + echo "can-push=${can_push}" >> $GITHUB_OUTPUT - # Only push to Docker Hub / Quay from main repo on specific branches/tags - if [[ "${{ github.repository_owner }}" == "paperless-ngx" ]]; then - case "$REF_NAME" in + # push-external: Should we also push to Docker Hub and Quay.io? + # Only for main repo on dev/beta branches or version tags + push_external="false" + if [[ "${can_push}" == "true" && "${{ github.repository_owner }}" == "paperless-ngx" ]]; then + case "${REF_NAME}" in dev|beta) - echo "enable=true" >> $GITHUB_OUTPUT - exit 0 + push_external="true" ;; esac - case "$GITHUB_REF" in + case "${{ github.ref }}" in refs/tags/v*|*beta.rc*) - echo "enable=true" >> $GITHUB_OUTPUT - exit 0 + push_external="true" ;; esac fi - echo "enable=false" >> $GITHUB_OUTPUT + echo "push-external=${push_external}" + echo "push-external=${push_external}" >> $GITHUB_OUTPUT + - name: Set repository name + id: repo + run: | + repo_name="${{ github.repository }}" + repo_name="${repo_name,,}" + + echo "repository=${repo_name}" + echo "name=${repo_name}" >> $GITHUB_OUTPUT + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.12.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Docker metadata id: docker-meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v5.10.0 with: images: | - ghcr.io/${{ github.repository }} - name=paperlessngx/paperless-ngx,enable=${{ steps.push-external.outputs.enable }} - name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.push-external.outputs.enable }} + ${{ env.REGISTRY }}/${{ steps.repo.outputs.name }} tags: | type=ref,event=branch type=raw,value=${{ steps.ref.outputs.name }},enable=${{ github.event_name == 'pull_request' }} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6.18.0 + with: + context: . + file: ./Dockerfile + platforms: ${{ matrix.platform }} + labels: ${{ steps.docker-meta.outputs.labels }} + build-args: | + PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.can-push }} + cache-from: | + type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.cache-ref }}-${{ matrix.arch }} + type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:dev-${{ matrix.arch }} + cache-to: ${{ steps.check-push.outputs.can-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.cache-ref, matrix.arch) || '' }} + - name: Export digest + if: steps.check-push.outputs.can-push == 'true' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + echo "digest=${digest}" + touch "/tmp/digests/${digest#sha256:}" + - name: Upload digest + if: steps.check-push.outputs.can-push == 'true' + uses: actions/upload-artifact@v6.0.0 + with: + name: digests-${{ matrix.arch }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + merge-and-push: + name: Merge and Push Manifest + runs-on: ubuntu-24.04 + needs: build-arch + if: needs.build-arch.outputs.can-push == 'true' + permissions: + contents: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@v7.0.0 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + - name: List digests + run: | + echo "Downloaded digests:" + ls -la /tmp/digests/ - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 + uses: docker/setup-buildx-action@v3.12.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v3.6.0 with: - registry: ghcr.io + registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub - if: steps.push-external.outputs.enable == 'true' - uses: docker/login-action@v3 + if: needs.build-arch.outputs.push-external == 'true' + uses: docker/login-action@v3.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to Quay.io - if: steps.push-external.outputs.enable == 'true' - uses: docker/login-action@v3 + if: needs.build-arch.outputs.push-external == 'true' + uses: docker/login-action@v3.6.0 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_ROBOT_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v6 + - name: Docker metadata + id: docker-meta + uses: docker/metadata-action@v5.10.0 with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: ${{ env.CAN_PUSH == 'true' }} - tags: ${{ steps.docker-meta.outputs.tags }} - labels: ${{ steps.docker-meta.outputs.labels }} - build-args: | - PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }} - cache-from: | - type=registry,ref=ghcr.io/${{ github.repository }}/cache/app:${{ steps.ref.outputs.name }} - type=registry,ref=ghcr.io/${{ github.repository }}/cache/app:dev - cache-to: ${{ env.CAN_PUSH == 'true' && format('type=registry,mode=max,ref=ghcr.io/{0}/cache/app:{1}', github.repository, steps.ref.outputs.name) || '' }} + images: | + ${{ env.REGISTRY }}/${{ needs.build-arch.outputs.repository }} + tags: | + type=ref,event=branch + type=raw,value=${{ needs.build-arch.outputs.ref-name }},enable=${{ github.event_name == 'pull_request' }} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - name: Create manifest list and push + working-directory: /tmp/digests + env: + REPOSITORY: ${{ needs.build-arch.outputs.repository }} + run: | + tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${DOCKER_METADATA_OUTPUT_JSON}") + + digests="" + for digest in *; do + digests+="${{ env.REGISTRY }}/${REPOSITORY}@sha256:${digest} " + done + + echo "Creating manifest with tags: ${tags}" + echo "From digests: ${digests}" + + docker buildx imagetools create ${tags} ${digests} - name: Inspect image - if: env.CAN_PUSH == 'true' run: | docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }} + - name: Copy to Docker Hub + if: needs.build-arch.outputs.push-external == 'true' + env: + TAGS: ${{ steps.docker-meta.outputs.tags }} + GHCR_REPO: ${{ env.REGISTRY }}/${{ needs.build-arch.outputs.repository }} + run: | + for tag in ${TAGS}; do + dockerhub_tag="${tag/${GHCR_REPO}/paperlessngx/paperless-ngx}" + echo "Copying ${tag} to ${dockerhub_tag}" + docker buildx imagetools create --tag "${dockerhub_tag}" "${tag}" + done + - name: Copy to Quay.io + if: needs.build-arch.outputs.push-external == 'true' + env: + TAGS: ${{ steps.docker-meta.outputs.tags }} + GHCR_REPO: ${{ env.REGISTRY }}/${{ needs.build-arch.outputs.repository }} + run: | + for tag in ${TAGS}; do + quay_tag="${tag/${GHCR_REPO}/quay.io/paperlessngx/paperless-ngx}" + echo "Copying ${tag} to ${quay_tag}" + docker buildx imagetools create --tag "${quay_tag}" "${tag}" + done