diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 062d11b96..8ec574744 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,3 +1,13 @@ +# Reference: https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners + +# Conditions for pushing the image to GHCR: +# - Triggered by push to a version tag (`v*`) +# - Triggered by a scheduled run +# - Triggered manually via `workflow_dispatch` with `push: true` +# +# Conditional expression: +# startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push + name: Publish Docker image on: @@ -8,11 +18,18 @@ on: required: true type: string default: 'main' + push: + description: 'Push the image to container registry' + required: false + type: boolean + default: false push: tags: - 'v*' branches: - main + schedule: + - cron: '30 1 * * *' env: REGISTRY: ghcr.io @@ -26,12 +43,19 @@ jobs: uses: ./.github/workflows/ci.yml build: - name: Build docker image + name: Build Docker image needs: [ ci ] - timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + platform: [amd64, arm64] - runs-on: ubuntu-latest + timeout-minutes: 60 + runs-on: ${{ matrix.platform == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + + outputs: + tags: ${{ steps.meta.outputs.tags }} permissions: contents: read @@ -39,47 +63,137 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.0 with: ref: ${{ github.event.inputs.ref || github.ref }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3.10.0 - name: Log in to the container registry - uses: docker/login-action@v3 + uses: docker/login-action@v3.3.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Configure image tags + id: tag_config + shell: bash + run: | + BASE_CONFIG="type=sha,format=long" + if [[ $GITHUB_EVENT_NAME == "schedule" ]]; then + BASE_CONFIG+=$'\n'"type=schedule,pattern=nightly" + elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then + BASE_CONFIG="type=semver,pattern={{version}}" + BASE_CONFIG+=$'\n'"type=raw,value=stable" + fi + { + echo 'TAGS_SPEC<> $GITHUB_ENV + - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v5.6.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - flavor: latest=auto - tags: | - type=sha,format=long - type=semver,pattern={{version}} - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + flavor: latest=false + tags: ${{ env.TAGS_SPEC }} - - name: Build and push Docker image - uses: docker/build-push-action@v6 + - name: Publish 'linux/${{ matrix.platform }}' image by digest + uses: docker/build-push-action@v6.16.0 id: build with: context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: 'linux/amd64,linux/arm64' - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: false - # https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#adding-a-description-to-multi-arch-images - outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=A multi-arch Docker image for the Maybe Rails app build-args: BUILD_COMMIT_SHA=${{ github.sha }} + platforms: 'linux/${{ matrix.platform }}' + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform }},mode=max + labels: ${{ steps.meta.outputs.labels }} + provenance: false + push: true + # DO NOT REMOVE `oci-mediatypes=true`, fixes annotation not showing up on job.merge.steps[-1] + # ref: https://github.com/docker/build-push-action/discussions/1022 + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},name-canonical=true,push-by-digest=true,oci-mediatypes=true + + - name: Export the Docker image digest + if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }} + run: | + mkdir -p "${RUNNER_TEMP}"/digests + echo "${DIGEST#sha256:}" > "${RUNNER_TEMP}/digests/digest-${PLATFORM}" + env: + DIGEST: ${{ steps.build.outputs.digest }} + PLATFORM: ${{ matrix.platform }} + + - name: Upload the Docker image digest + if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }} + uses: actions/upload-artifact@v4.6.2 + with: + name: digest-${{ matrix.platform }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Merge multi-arch manifest & push multi-arch tag + if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }} + needs: [build] + + timeout-minutes: 60 + runs-on: 'ubuntu-24.04' + + permissions: + packages: write + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.10.0 + + - name: Download Docker image digests + uses: actions/download-artifact@v4.3.0 + with: + path: ${{ runner.temp }}/digests + pattern: digest-* + merge-multiple: true + + - name: Log in to the container registry + uses: docker/login-action@v3.3.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge and push Docker image + env: + TAGS: ${{ needs.build.outputs.tags }} + DIGESTS_DIR: ${{ runner.temp }}/digests + shell: bash -xeuo pipefail {0} + run: | + tag_args=() + while IFS=$'\n' read -r tag; do + [[ -n "${tag}" ]] || continue + tag_args+=("--tag=${tag}") + done <<< "${TAGS}" + + image_args=() + for PLATFORM in amd64 arm64; do + image_args+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:$(<"${DIGESTS_DIR}/digest-${PLATFORM}")") + done + + attempts=0 + until docker buildx imagetools create \ + --annotation "index:org.opencontainers.image.description=A multi-arch Docker image for the Sure Rails app" \ + "${tag_args[@]}" "${image_args[@]}" \ + ; do + attempts=$((attempts + 1)) + if [[ $attempts -ge 3 ]]; then + echo "[$(date -u)] ERROR: Failed after 3 attempts." >&2 + exit 1 + fi + delay=$((2 ** attempts)) + if [[ $delay -gt 15 ]]; then delay=15; fi + echo "Push failed (attempt $attempts). Retrying in ${delay} seconds..." + sleep ${delay} + done