diff --git a/.github/actions/image-build/action.yml b/.github/actions/image-build/action.yml new file mode 100644 index 00000000000..64a06ec141e --- /dev/null +++ b/.github/actions/image-build/action.yml @@ -0,0 +1,118 @@ +name: 'Single arch image build' +description: 'Build single-arch image on platform appropriate runner' +inputs: + image: + description: 'Name of the image to build' + required: true + ghcr-token: + description: 'GitHub Container Registry token' + required: true + platform: + description: 'Platform to build for' + required: true + artifact-key-base: + description: 'Base key for artifact name' + required: true + context: + description: 'Path to build context' + required: true + dockerfile: + description: 'Path to Dockerfile' + required: true + build-args: + description: 'Docker build arguments' + required: false +runs: + using: 'composite' + steps: + - name: Prepare + id: prepare + shell: bash + env: + PLATFORM: ${{ inputs.platform }} + run: | + echo "platform-pair=${PLATFORM//\//-}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + if: ${{ !github.event.pull_request.head.repo.fork }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ inputs.ghcr-token }} + + - name: Generate cache key suffix + id: cache-key-suffix + shell: bash + env: + REF: ${{ github.ref_name }} + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "cache-key-suffix=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT + else + SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') + echo "suffix=${SUFFIX}" >> $GITHUB_OUTPUT + fi + + - name: Generate cache target + id: cache-target + shell: bash + env: + BUILD_ARGS: ${{ inputs.build-args }} + IMAGE: ${{ inputs.image }} + SUFFIX: ${{ steps.cache-key-suffix.outputs.suffix }} + PLATFORM_PAIR: ${{ steps.prepare.outputs.platform-pair }} + run: | + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + # Essentially just ignore the cache output (forks can't write to registry cache) + echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT + else + HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1) + CACHE_KEY="${PLATFORM_PAIR}-${HASH}" + echo "cache-key-base=${CACHE_KEY}" >> $GITHUB_OUTPUT + echo "cache-to=type=registry,ref=${IMAGE}-build-cache:${CACHE_KEY}-${SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT + fi + + - name: Generate docker image tags + id: meta + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 + env: + DOCKER_METADATA_PR_HEAD_SHA: 'true' + + - name: Build and push image + id: build + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 + with: + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + platforms: ${{ inputs.platform }} + labels: ${{ steps.meta.outputs.labels }} + cache-to: ${{ steps.cache-target.outputs.cache-to }} + cache-from: | + type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-${{ env.CACHE_KEY_SUFFIX }} + type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-main + outputs: type=image,"name=${{ inputs.image }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} + build-args: | + BUILD_ID=${{ github.run_id }} + BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.meta.outputs.tags }} + BUILD_SOURCE_REF=${{ github.ref_name }} + BUILD_SOURCE_COMMIT=${{ github.sha }} + ${{ inputs.build-args }} + + - name: Export digest + shell: bash + run: | # zizmor: ignore[template-injection] + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: ${{ inputs.artifact-key-base }}-${{ steps.cache-target.outputs.cache-key-base }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1746c93bc74..056aa7e6f41 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,6 +40,8 @@ jobs: - 'machine-learning/**' workflow: - '.github/workflows/docker.yml' + - '.github/workflows/multi-runner-build.yml' + - '.github/actions/image-build' - name: Check if we should force jobs to run id: should_force @@ -103,429 +105,74 @@ jobs: docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" - build_and_push_ml: + machine-learning: name: Build and Push ML needs: pre-job - permissions: - contents: read - packages: write if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} - runs-on: ${{ matrix.runner }} - env: - image: immich-machine-learning - context: machine-learning - file: machine-learning/Dockerfile - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning strategy: - # Prevent a failure in one image from stopping the other builds fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - device: cpu - - - platform: linux/arm64 - runner: ubuntu-24.04-arm - device: cpu - - - platform: linux/amd64 - runner: ubuntu-latest - device: cuda - suffix: -cuda - - - platform: linux/amd64 - runner: mich - device: rocm - suffix: -rocm - - - platform: linux/amd64 - runner: ubuntu-latest - device: openvino - suffix: -openvino - - - platform: linux/arm64 - runner: ubuntu-24.04-arm - device: armnn - suffix: -armnn - - - platform: linux/arm64 - runner: ubuntu-24.04-arm - device: rknn - suffix: -rknn - - steps: - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - persist-credentials: false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Login to GitHub Container Registry - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate cache key suffix - env: - REF: ${{ github.ref_name }} - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV - else - SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') - echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV - fi - - - name: Generate cache target - id: cache-target - run: | - if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then - # Essentially just ignore the cache output (forks can't write to registry cache) - echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT - else - echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${{ matrix.device }}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT - fi - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - - - name: Build and push image - id: build - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 - with: - context: ${{ env.context }} - file: ${{ env.file }} - platforms: ${{ matrix.platforms }} - labels: ${{ steps.meta.outputs.labels }} - cache-to: ${{ steps.cache-target.outputs.cache-to }} - cache-from: | - type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }} - type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main - outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} - build-args: | - DEVICE=${{ matrix.device }} - BUILD_ID=${{ github.run_id }} - BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }} - BUILD_SOURCE_REF=${{ github.ref_name }} - BUILD_SOURCE_COMMIT=${{ github.sha }} - - - name: Export digest - run: | # zizmor: ignore[template-injection] - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - - merge_ml: - name: Merge & Push ML - runs-on: ubuntu-latest - permissions: - contents: read - actions: read - packages: write - if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }} - env: - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning - DOCKER_REPO: altran1502/immich-machine-learning - strategy: matrix: include: - device: cpu + tag-suffix: '' - device: cuda - suffix: -cuda - - device: rocm - suffix: -rocm + tag-suffix: '-cuda' + platforms: linux/amd64 - device: openvino - suffix: -openvino + tag-suffix: '-openvino' + platforms: linux/amd64 - device: armnn - suffix: -armnn + tag-suffix: '-armnn' + platforms: linux/arm64 - device: rknn - suffix: -rknn - needs: - - build_and_push_ml - steps: - - name: Download digests - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - path: ${{ runner.temp }}/digests - pattern: ml-digests-${{ matrix.device }}-* - merge-multiple: true - - - name: Login to Docker Hub - if: ${{ github.event_name == 'release' }} - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GHCR - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - with: - flavor: | - # Disable latest tag - latest=false - suffix=${{ matrix.suffix }} - images: | - name=${{ env.GHCR_REPO }} - name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }} - tags: | - # Tag with branch name - type=ref,event=branch - # Tag with pr-number - type=ref,event=pr - # Tag with long commit sha hash - type=sha,format=long,prefix=commit- - # Tag with git tag on release - type=ref,event=tag - type=raw,value=release,enable=${{ github.event_name == 'release' }} - - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - # Process annotations - declare -a ANNOTATIONS=() - if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then - while IFS= read -r annotation; do - # Extract key and value by removing the manifest: prefix - if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then - key="${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - # Use array to properly handle arguments with spaces - ANNOTATIONS+=(--annotation "index:$key=$value") - fi - done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") - fi - - TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *) - - docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS - - build_and_push_server: - name: Build and Push Server - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - needs: pre-job - if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} - env: - image: immich-server - context: . - file: server/Dockerfile - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - steps: - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - persist-credentials: false - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate cache key suffix - env: - REF: ${{ github.ref_name }} - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV - else - SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g') - echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV - fi - - - name: Generate cache target - id: cache-target - run: | - if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then - # Essentially just ignore the cache output (forks can't write to registry cache) - echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT - else - echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT - fi - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - - - name: Build and push image - id: build - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 - with: - context: ${{ env.context }} - file: ${{ env.file }} - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - cache-to: ${{ steps.cache-target.outputs.cache-to }} - cache-from: | - type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }} - type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main - outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }} - build-args: | - DEVICE=cpu - BUILD_ID=${{ github.run_id }} - BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }} - BUILD_SOURCE_REF=${{ github.ref_name }} - BUILD_SOURCE_COMMIT=${{ github.sha }} - - - name: Export digest - run: | # zizmor: ignore[template-injection] - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: server-digests-${{ env.PLATFORM_PAIR }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - - merge_server: - name: Merge & Push Server - runs-on: ubuntu-latest + tag-suffix: '-rknn' + platforms: linux/arm64 + - device: rocm + tag-suffix: '-rocm' + platforms: linux/amd64 + runner-mapping: '{"linux/amd64": "mich"}' + uses: ./.github/workflows/multi-runner-build.yml permissions: contents: read actions: read packages: write - if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }} - env: - GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server - DOCKER_REPO: altran1502/immich-server - needs: - - build_and_push_server - steps: - - name: Download digests - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - path: ${{ runner.temp }}/digests - pattern: server-digests-* - merge-multiple: true + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + with: + image: immich-machine-learning + context: machine-learning + dockerfile: machine-learning/Dockerfile + platforms: ${{ matrix.platforms }} + runner-mapping: ${{ matrix.runner-mapping }} + tag-suffix: ${{ matrix.tag-suffix }} + dockerhub-push: ${{ github.event_name == 'release' }} + build-args: | + DEVICE=${{ matrix.device }} - - name: Login to Docker Hub - if: ${{ github.event_name == 'release' }} - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GHCR - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 - - - name: Generate docker image tags - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 - env: - DOCKER_METADATA_PR_HEAD_SHA: 'true' - with: - flavor: | - # Disable latest tag - latest=false - suffix=${{ matrix.suffix }} - images: | - name=${{ env.GHCR_REPO }} - name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }} - tags: | - # Tag with branch name - type=ref,event=branch - # Tag with pr-number - type=ref,event=pr - # Tag with long commit sha hash - type=sha,format=long,prefix=commit- - # Tag with git tag on release - type=ref,event=tag - type=raw,value=release,enable=${{ github.event_name == 'release' }} - - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - # Process annotations - declare -a ANNOTATIONS=() - if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then - while IFS= read -r annotation; do - # Extract key and value by removing the manifest: prefix - if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then - key="${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - # Use array to properly handle arguments with spaces - ANNOTATIONS+=(--annotation "index:$key=$value") - fi - done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") - fi - - TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *) - - docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS + server: + name: Build and Push Server + needs: pre-job + if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} + uses: ./.github/workflows/multi-runner-build.yml + permissions: + contents: read + actions: read + packages: write + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + with: + image: immich-server + context: . + dockerfile: server/Dockerfile + dockerhub-push: ${{ github.event_name == 'release' }} + build-args: | + DEVICE=cpu success-check-server: name: Docker Build & Push Server Success - needs: [merge_server, retag_server] + needs: [server, retag_server] permissions: {} runs-on: ubuntu-latest if: always() @@ -540,7 +187,7 @@ jobs: success-check-ml: name: Docker Build & Push ML Success - needs: [merge_ml, retag_ml] + needs: [machine-learning, retag_ml] permissions: {} runs-on: ubuntu-latest if: always() diff --git a/.github/workflows/multi-runner-build.yml b/.github/workflows/multi-runner-build.yml new file mode 100644 index 00000000000..17eceb7e8f8 --- /dev/null +++ b/.github/workflows/multi-runner-build.yml @@ -0,0 +1,185 @@ +name: 'Multi-runner container image build' +on: + workflow_call: + inputs: + image: + description: 'Name of the image' + type: string + required: true + context: + description: 'Path to build context' + type: string + required: true + dockerfile: + description: 'Path to Dockerfile' + type: string + required: true + tag-suffix: + description: 'Suffix to append to the image tag' + type: string + default: '' + dockerhub-push: + description: 'Push to Docker Hub' + type: boolean + default: false + build-args: + description: 'Docker build arguments' + type: string + required: false + platforms: + description: 'Platforms to build for' + type: string + runner-mapping: + description: 'Mapping from platforms to runners' + type: string + secrets: + DOCKERHUB_USERNAME: + required: false + DOCKERHUB_TOKEN: + required: false + +env: + GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ inputs.image }} + DOCKERHUB_IMAGE: altran1502/${{ inputs.image }} + +jobs: + matrix: + name: 'Generate matrix' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + key: ${{ steps.artifact-key.outputs.base }} + steps: + - name: Generate build matrix + id: matrix + shell: bash + env: + PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }} + RUNNER_MAPPING: ${{ inputs.runner-mapping || '{"linux/amd64":"ubuntu-latest","linux/arm64":"ubuntu-24.04-arm"}' }} + run: | + matrix=$(jq -R -c \ + --argjson runner_mapping "${RUNNER_MAPPING}" \ + 'split(",") | map({platform: ., runner: $runner_mapping[.]})' \ + <<< "${PLATFORMS}") + echo "${matrix}" + echo "matrix=${matrix}" >> $GITHUB_OUTPUT + + - name: Determine artifact key + id: artifact-key + shell: bash + env: + IMAGE: ${{ inputs.image }} + SUFFIX: ${{ inputs.tag-suffix }} + run: | + if [[ -n "${SUFFIX}" ]]; then + base="${IMAGE}${SUFFIX}-digests" + else + base="${IMAGE}-digests" + fi + echo "${base}" + echo "base=${base}" >> $GITHUB_OUTPUT + + build: + needs: matrix + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.matrix.outputs.matrix) }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + persist-credentials: false + + - uses: ./.github/actions/image-build + with: + context: ${{ inputs.context }} + dockerfile: ${{ inputs.dockerfile }} + image: ${{ env.GHCR_IMAGE }} + ghcr-token: ${{ secrets.GITHUB_TOKEN }} + platform: ${{ matrix.platform }} + artifact-key-base: ${{ needs.matrix.outputs.key }} + build-args: ${{ inputs.build-args }} + + merge: + needs: [matrix, build] + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.head.repo.fork }} + permissions: + contents: read + actions: read + packages: write + steps: + - name: Download digests + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 + with: + path: ${{ runner.temp }}/digests + pattern: ${{ needs.matrix.outputs.key }}-* + merge-multiple: true + + - name: Login to Docker Hub + if: ${{ inputs.dockerhub-push }} + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GHCR + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + + - name: Generate docker image tags + id: meta + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 + env: + DOCKER_METADATA_PR_HEAD_SHA: 'true' + with: + flavor: | + # Disable latest tag + latest=false + suffix=${{ inputs.tag-suffix }} + images: | + name=${{ env.GHCR_IMAGE }} + name=${{ env.DOCKERHUB_IMAGE }},enable=${{ inputs.dockerhub-push }} + tags: | + # Tag with branch name + type=ref,event=branch + # Tag with pr-number + type=ref,event=pr + # Tag with long commit sha hash + type=sha,format=long,prefix=commit- + # Tag with git tag on release + type=ref,event=tag + type=raw,value=release,enable=${{ github.event_name == 'release' }} + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + # Process annotations + declare -a ANNOTATIONS=() + if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then + while IFS= read -r annotation; do + # Extract key and value by removing the manifest: prefix + if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then + key="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + # Use array to properly handle arguments with spaces + ANNOTATIONS+=(--annotation "index:$key=$value") + fi + done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON") + fi + + TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + SOURCE_ARGS=$(printf "${GHCR_IMAGE}@sha256:%s " *) + + docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS