Publish Docker image #16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 default branch (`main`) | |
| # - Triggered by push to a version tag (`v*`) | |
| # - Triggered by a scheduled run | |
| # - Triggered manually via `workflow_dispatch` with `push: true` | |
| # | |
| # Conditional expression: | |
| # github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push | |
| name: Publish Docker image | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: 'Git ref (tag or commit SHA) to build' | |
| 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 | |
| IMAGE_NAME: ${{ github.repository }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| ci: | |
| uses: ./.github/workflows/ci.yml | |
| build: | |
| name: Build Docker image | |
| needs: [ ci ] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| platform: [amd64, arm64] | |
| timeout-minutes: 60 | |
| runs-on: ${{ matrix.platform == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} | |
| outputs: | |
| tags: ${{ steps.meta.outputs.tags }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Check out the repo | |
| uses: actions/checkout@v4.2.0 | |
| with: | |
| ref: ${{ github.event.inputs.ref || github.ref }} | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3.10.0 | |
| - 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: 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" | |
| BASE_CONFIG+=$'\n'"type=schedule,pattern=nightly-{{date 'ddd'}}" | |
| elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then | |
| BASE_CONFIG="type=semver,pattern={{version}}" | |
| BASE_CONFIG+=$'\n'"type=raw,value=stable" | |
| fi | |
| { | |
| echo 'TAGS_SPEC<<EOF' | |
| echo "$BASE_CONFIG" | |
| echo EOF | |
| } >> $GITHUB_ENV | |
| - name: Get current date (RFC 3339 format) | |
| id: date | |
| run: echo "date=$(date -Iseconds)" >> $GITHUB_OUTPUT | |
| - name: Extract metadata for Docker | |
| id: meta | |
| uses: docker/metadata-action@v5.6.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| flavor: latest=false | |
| tags: ${{ env.TAGS_SPEC }} | |
| labels: | | |
| org.opencontainers.image.version=${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }} | |
| org.opencontainers.image.created=${{ steps.date.outputs.date }} | |
| org.opencontainers.image.ref.name=${{ github.ref_name }} | |
| org.opencontainers.image.vendor=we-promise | |
| org.opencontainers.image.title=Sure | |
| org.opencontainers.image.description=A multi-arch Docker image for the Sure Rails app | |
| - name: Publish 'linux/${{ matrix.platform }}' image by digest | |
| uses: docker/build-push-action@v6.16.0 | |
| id: build | |
| with: | |
| context: . | |
| 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: ${{ github.ref == 'refs/heads/main' || 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: ${{ github.ref == 'refs/heads/main' || 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: ${{ github.ref == 'refs/heads/main' || 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 | |
| annotations=( | |
| "index:org.opencontainers.image.created=$(date -Iseconds)" | |
| 'index:org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}' | |
| 'index:org.opencontainers.image.revision=${{ github.sha }}' | |
| 'index:org.opencontainers.image.ref.name=${{ github.ref_name }}' | |
| 'index:org.opencontainers.image.vendor=we-promise' | |
| 'index:org.opencontainers.image.licenses=AGPL-3.0' | |
| 'index:org.opencontainers.image.title=Sure' | |
| 'index:org.opencontainers.image.description=A multi-arch Docker image for the Sure Rails app' | |
| ) | |
| annotation_args=() | |
| for annotation in "${annotations[@]}"; do | |
| annotation_args+=("--annotation=${annotation}") | |
| done | |
| if [[ $GITHUB_REF_TYPE == "tag" ]]; then | |
| annotation_args+=("--annotation=index:org.opencontainers.image.version=$GITHUB_REF_NAME") | |
| fi | |
| attempts=0 | |
| until docker buildx imagetools create \ | |
| "${annotation_args[@]}" \ | |
| "${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 |