From b3d51aa76aef83252f11c3245ca6901589b8e993 Mon Sep 17 00:00:00 2001 From: Sebastian Fischer Date: Sat, 25 Apr 2026 06:26:31 +0200 Subject: [PATCH] Build anvl-cpu image as multi-arch (amd64 + arm64) Splits the cpu workflow across native runners (ubuntu-latest for amd64, ubuntu-24.04-arm for arm64), pushes each platform by digest, and assembles the manifest list in a separate merge job. Avoids QEMU. CUDA workflow is untouched; the existing docker-build composite action keeps its current single-arch behavior. Two new composite actions: - docker-build-by-digest: per-platform build, pushes by digest to both Docker Hub and GHCR. - docker-merge: combines uploaded digests into a tagged manifest list in both registries. Also drops the hardcoded x86_64-pc-linux-gnu in .Rprofile's libPath in favor of R.version$platform so the path is correct on arm64. Co-Authored-By: Claude Opus 4.7 (1M context) --- .Rprofile | 2 +- .../docker-build-by-digest/action.yaml | 89 ++++++++++++ .github/actions/docker-merge/action.yaml | 66 +++++++++ .github/workflows/cpu.yaml | 134 ++++++++++++++++-- 4 files changed, 278 insertions(+), 13 deletions(-) create mode 100644 .github/actions/docker-build-by-digest/action.yaml create mode 100644 .github/actions/docker-merge/action.yaml diff --git a/.Rprofile b/.Rprofile index 2bbfb5b..aa75a1a 100644 --- a/.Rprofile +++ b/.Rprofile @@ -20,4 +20,4 @@ options( ) ) -.libPaths(sprintf("/root/R/x86_64-pc-linux-gnu-library/%s.%s", R.version$major, R.version$minor)) +.libPaths(sprintf("/root/R/%s-library/%s.%s", R.version$platform, R.version$major, R.version$minor)) diff --git a/.github/actions/docker-build-by-digest/action.yaml b/.github/actions/docker-build-by-digest/action.yaml new file mode 100644 index 0000000..38d1f09 --- /dev/null +++ b/.github/actions/docker-build-by-digest/action.yaml @@ -0,0 +1,89 @@ +name: 'Docker Build (Push by Digest)' +description: 'Build a Docker image for one platform and push it by digest to Docker Hub and GHCR. Output the digest for later manifest assembly.' + +inputs: + image_name_dockerhub: + description: 'Docker Hub image (e.g., docker.io/sebffischer/anvl-cpu)' + required: true + image_name_ghcr: + description: 'GHCR image (e.g., ghcr.io/r-xla/anvl-cpu)' + required: true + dockerfile: + description: 'Path to the Dockerfile' + required: true + platform: + description: 'Target platform (e.g., linux/amd64)' + required: true + push: + description: 'Whether to push' + required: true + default: 'false' + dockerhub_username: + required: false + dockerhub_token: + required: false + ghcr_token: + required: false + anvl_ref: + description: 'Git ref for r-xla/anvl to install' + required: false + default: '' + no_cache: + required: false + default: 'false' + github_pat: + description: 'Token passed as a Docker build secret for private repo access' + required: false + default: '' + +outputs: + digest: + description: 'Image digest pushed by this build' + value: ${{ steps.build.outputs.digest }} + +runs: + using: 'composite' + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: inputs.push == 'true' + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ inputs.dockerhub_username }} + password: ${{ inputs.dockerhub_token }} + + - name: Log in to GitHub Container Registry + if: inputs.push == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ inputs.ghcr_token }} + + - name: Compute platform pair + id: pair + shell: bash + run: | + platform="${{ inputs.platform }}" + echo "pair=${platform//\//-}" >> "$GITHUB_OUTPUT" + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v5 + with: + context: . + file: ${{ inputs.dockerfile }} + platforms: ${{ inputs.platform }} + build-args: | + BUILDKIT_INLINE_CACHE=1 + CACHEBUST=${{ github.run_id }} + ANVL_REF=${{ inputs.anvl_ref }} + secrets: ${{ inputs.github_pat != '' && format('github_pat={0}', inputs.github_pat) || '' }} + no-cache: ${{ inputs.no_cache == 'true' }} + cache-from: ${{ inputs.no_cache != 'true' && format('type=registry,ref={0}:buildcache-{1}', inputs.image_name_dockerhub, steps.pair.outputs.pair) || '' }} + cache-to: ${{ inputs.push == 'true' && format('type=registry,ref={0}:buildcache-{1},image-manifest=true', inputs.image_name_dockerhub, steps.pair.outputs.pair) || '' }} + outputs: | + type=image,"name=${{ inputs.image_name_dockerhub }},${{ inputs.image_name_ghcr }}",push-by-digest=true,name-canonical=true,push=${{ inputs.push }} diff --git a/.github/actions/docker-merge/action.yaml b/.github/actions/docker-merge/action.yaml new file mode 100644 index 0000000..567afc7 --- /dev/null +++ b/.github/actions/docker-merge/action.yaml @@ -0,0 +1,66 @@ +name: 'Docker Merge Manifest' +description: 'Combine per-platform digests into a multi-arch manifest list and publish under a tag in both Docker Hub and GHCR.' + +inputs: + image_name_dockerhub: + description: 'Docker Hub image (e.g., docker.io/sebffischer/anvl-cpu)' + required: true + image_name_ghcr: + description: 'GHCR image (e.g., ghcr.io/r-xla/anvl-cpu)' + required: true + tag: + description: 'Tag to publish (e.g., latest, release)' + required: true + digests_dir: + description: 'Directory containing per-platform digest files (filename = digest hex without sha256: prefix)' + required: true + dockerhub_username: + required: true + dockerhub_token: + required: true + ghcr_token: + required: true + +runs: + using: 'composite' + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ inputs.dockerhub_username }} + password: ${{ inputs.dockerhub_token }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ inputs.ghcr_token }} + + - name: Create manifest list (Docker Hub) + shell: bash + working-directory: ${{ inputs.digests_dir }} + run: | + docker buildx imagetools create \ + -t "${{ inputs.image_name_dockerhub }}:${{ inputs.tag }}" \ + $(printf '${{ inputs.image_name_dockerhub }}@sha256:%s ' *) + + - name: Create manifest list (GHCR) + shell: bash + working-directory: ${{ inputs.digests_dir }} + run: | + docker buildx imagetools create \ + -t "${{ inputs.image_name_ghcr }}:${{ inputs.tag }}" \ + $(printf '${{ inputs.image_name_ghcr }}@sha256:%s ' *) + + - name: Inspect (Docker Hub) + shell: bash + run: docker buildx imagetools inspect "${{ inputs.image_name_dockerhub }}:${{ inputs.tag }}" + + - name: Inspect (GHCR) + shell: bash + run: docker buildx imagetools inspect "${{ inputs.image_name_ghcr }}:${{ inputs.tag }}" diff --git a/.github/workflows/cpu.yaml b/.github/workflows/cpu.yaml index 94e332c..0c51c25 100644 --- a/.github/workflows/cpu.yaml +++ b/.github/workflows/cpu.yaml @@ -12,6 +12,10 @@ on: branches: - main +env: + IMAGE_DOCKERHUB: docker.io/sebffischer/anvl-cpu + IMAGE_GHCR: ghcr.io/r-xla/anvl-cpu + jobs: resolve-anvl-release: runs-on: ubuntu-latest @@ -45,44 +49,150 @@ jobs: fi build-latest: - runs-on: ubuntu-latest + 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: - uses: actions/checkout@v4 - - uses: ./.github/actions/docker-build + - id: build + uses: ./.github/actions/docker-build-by-digest with: - image_name: sebffischer/anvl-cpu + image_name_dockerhub: ${{ env.IMAGE_DOCKERHUB }} + image_name_ghcr: ${{ env.IMAGE_GHCR }} dockerfile: ./cpu/Dockerfile + platform: ${{ matrix.platform }} push: ${{ github.event_name != 'pull_request' }} dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} ghcr_token: ${{ secrets.GITHUB_TOKEN }} github_pat: ${{ secrets.GITHUB_TOKEN }} - extra_tags: | - docker.io/sebffischer/anvl-cpu:latest - ghcr.io/${{ github.repository_owner }}/anvl-cpu:latest + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + - name: Compute platform pair + if: github.event_name != 'pull_request' + id: pair + run: | + platform="${{ matrix.platform }}" + echo "pair=${platform//\//-}" >> "$GITHUB_OUTPUT" + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: digests-latest-${{ steps.pair.outputs.pair }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge-latest: + needs: build-latest + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-latest-* + merge-multiple: true + - uses: ./.github/actions/docker-merge + with: + image_name_dockerhub: ${{ env.IMAGE_DOCKERHUB }} + image_name_ghcr: ${{ env.IMAGE_GHCR }} + tag: latest + digests_dir: /tmp/digests + dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }} build-release: needs: resolve-anvl-release if: needs.resolve-anvl-release.outputs.changed == 'true' - runs-on: ubuntu-latest + 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: - uses: actions/checkout@v4 - - uses: ./.github/actions/docker-build + - id: build + uses: ./.github/actions/docker-build-by-digest with: - image_name: sebffischer/anvl-cpu + image_name_dockerhub: ${{ env.IMAGE_DOCKERHUB }} + image_name_ghcr: ${{ env.IMAGE_GHCR }} dockerfile: ./cpu/Dockerfile + platform: ${{ matrix.platform }} push: ${{ github.event_name != 'pull_request' }} dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} ghcr_token: ${{ secrets.GITHUB_TOKEN }} github_pat: ${{ secrets.GITHUB_TOKEN }} anvl_ref: ${{ needs.resolve-anvl-release.outputs.tag }} - extra_tags: | - docker.io/sebffischer/anvl-cpu:release - ghcr.io/${{ github.repository_owner }}/anvl-cpu:release + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + - name: Compute platform pair + if: github.event_name != 'pull_request' + id: pair + run: | + platform="${{ matrix.platform }}" + echo "pair=${platform//\//-}" >> "$GITHUB_OUTPUT" + - name: Upload digest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: digests-release-${{ steps.pair.outputs.pair }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge-release: + needs: build-release + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-release-* + merge-multiple: true + - uses: ./.github/actions/docker-merge + with: + image_name_dockerhub: ${{ env.IMAGE_DOCKERHUB }} + image_name_ghcr: ${{ env.IMAGE_GHCR }} + tag: release + digests_dir: /tmp/digests + dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }}