shortener 0.2.0

A simple URL shortener.
Documentation
name: Docker

on:
  push:
    branches:
      - main
    tags:
      - "**"
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions: {}

defaults:
  run:
    shell: bash -euxo pipefail {0}

env:
  DOCKERHUB_USERNAME: zhongruoyu
  DOCKERHUB_REPOSITORY: zhongruoyu/shortener
  GHCR_USERNAME: zhongruoyu
  GHCR_REPOSITORY: ghcr.io/zhongruoyu/shortener
  BUILD_CACHE_REPOSITORY: ghcr.io/zhongruoyu/shortener/cache

jobs:
  metadata:
    name: Determine metadata
    runs-on: ubuntu-latest
    outputs:
      tags: ${{ steps.metadata.outputs.tags }}
      labels: ${{ steps.metadata.outputs.labels }}
    steps:
      - name: Determine metadata
        id: metadata
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
        with:
          images: |
            ${{ env.DOCKERHUB_REPOSITORY }}
            ${{ env.GHCR_REPOSITORY }}
          flavor: |
            latest=${{ github.event_name == 'push' && github.ref_type == 'tag' }}
          tags: |
            type=ref,event=branch
            type=ref,event=tag
            type=ref,event=pr
            type=sha,enable=${{ github.event_name == 'push' && github.ref_type == 'branch' }},prefix=,format=long

  build:
    needs: metadata
    strategy:
      fail-fast: false
      matrix:
        arch:
          - x86_64
          - aarch64
        include:
          - { arch: x86_64, runs-on: ubuntu-latest }
          - { arch: aarch64, runs-on: ubuntu-24.04-arm }
    name: Build (${{ matrix.arch }})
    runs-on: ${{ matrix.runs-on }}
    environment:
      name: docker
      deployment: false
    permissions:
      contents: read
      packages: write # for pushing image and cache
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false
      - name: Cache Cargo registry
        # zizmor: ignore[cache-poisoning] We only save cache for pushes to the main branch.
        id: cache-cargo
        if: (github.event_name == 'push' && github.ref_type == 'branch') || github.event_name == 'pull_request'
        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
        with:
          path: |
            cargo-registry-index
            cargo-registry-cache
            cargo-git-db
          key: docker-cargo-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            docker-cargo-${{ matrix.arch }}-
          lookup-only: ${{ !(github.event_name == 'push' && github.ref == 'refs/heads/main') }}
      - name: Cache build artifacts
        # zizmor: ignore[cache-poisoning] We only save cache for pushes to the main branch.
        id: cache-target
        if: (github.event_name == 'push' && github.ref_type == 'branch') || github.event_name == 'pull_request'
        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
        with:
          path: |
            target
          key: docker-target-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            docker-target-${{ matrix.arch }}-
          lookup-only: ${{ !(github.event_name == 'push' && github.ref == 'refs/heads/main') }}
      - name: Set up Docker Buildx
        id: setup-buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
      - name: Restore Docker cache mount for Cargo registry
        if: (github.event_name == 'push' && github.ref_type == 'branch') || github.event_name == 'pull_request'
        uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0
        with:
          builder: ${{ steps.setup-buildx.outputs.name }}
          cache-map: |
            {
              "cargo-registry-index": "/usr/local/cargo/registry/index",
              "cargo-registry-cache": "/usr/local/cargo/registry/cache",
              "cargo-git-db": "/usr/local/cargo/git/db"
            }
          skip-extraction: ${{ steps.cache-cargo.outputs.cache-hit }}
      - name: Restore Docker cache mount for build artifacts
        if: (github.event_name == 'push' && github.ref_type == 'branch') || github.event_name == 'pull_request'
        uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0
        with:
          builder: ${{ steps.setup-buildx.outputs.name }}
          cache-map: |
            {
              "target": "/app/target"
            }
          skip-extraction: ${{ steps.cache-target.outputs.cache-hit }}
      - name: Login to Docker Hub
        if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag')
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          username: ${{ env.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Login to GitHub Container Registry
        if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag')
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          registry: ghcr.io
          username: ${{ env.GHCR_USERNAME }}
          password: ${{ github.token }}
      - name: Set up build
        id: setup
        run: |
          rust_version="$(gh api /repos/rust-lang/rust/releases/latest --jq '.tag_name')"
          cache_from=""
          cache_to=""
          outputs=()
          if [[ ("$GITHUB_EVENT_NAME" = "push" && "$GITHUB_REF_TYPE" = "branch") ||
                "$GITHUB_EVENT_NAME" = "pull_request" ]]; then
            cache_from="type=registry,ref=$BUILD_CACHE_REPOSITORY:$ARCH"
          fi
          if [[ "$GITHUB_EVENT_NAME" = "push" ]]; then
            if [[ "$GITHUB_REF_TYPE" = "branch" && "$GITHUB_REF" = "refs/heads/main" ]]; then
              cache_to="type=registry,ref=$BUILD_CACHE_REPOSITORY:$ARCH,mode=max"
            fi
            if [[ "$GITHUB_REF" = "refs/heads/main" || "$GITHUB_REF_TYPE" = "tag" ]]; then
              outputs+=(
                "type=image,name=$DOCKERHUB_REPOSITORY,name-canonical=true,push=true,push-by-digest=true"
                "type=image,name=$GHCR_REPOSITORY,name-canonical=true,push=true,push-by-digest=true"
              )
            fi
          fi
          {
            echo "rust_version=$rust_version"
            echo "cache_from=$cache_from"
            echo "cache_to=$cache_to"
            echo "outputs<<OUTPUTS"
            if [[ ${#outputs[@]} -gt 0 ]]; then
              printf '%s\n' "${outputs[@]}"
            fi
            echo "OUTPUTS"
          } >> "$GITHUB_OUTPUT"
        env:
          GH_TOKEN: ${{ github.token }}
          ARCH: ${{ matrix.arch }}
      - name: Build and push Docker image by digest
        id: build
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          context: .
          platforms: linux/${{ matrix.arch }}
          labels: ${{ needs.metadata.outputs.labels }}
          build-args: |
            RUST_VERSION=${{ steps.setup.outputs.rust_version }}
          cache-from: ${{ steps.setup.outputs.cache_from }}
          cache-to: ${{ steps.setup.outputs.cache_to }}
          outputs: ${{ steps.setup.outputs.outputs }}
      - name: Export digest
        run: |
          mkdir -p "$RUNNER_TEMP"/digests
          echo "${DIGEST#sha256:}" > "$RUNNER_TEMP"/digests/"$ARCH"
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
          ARCH: ${{ matrix.arch }}
      - name: Upload digest
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: digests-${{ matrix.arch }}
          path: ${{ runner.temp }}/digests/*

  merge:
    name: Merge
    if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag')
    needs: [metadata, build]
    runs-on: ubuntu-latest
    environment:
      name: docker
      deployment: false
    permissions:
      contents: read
      packages: write # for pushing image
    steps:
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
      - name: Login to Docker Hub
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          username: ${{ env.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Login to GitHub Container Registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          registry: ghcr.io
          username: ${{ env.GHCR_USERNAME }}
          password: ${{ github.token }}
      - name: Download digests
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          path: ${{ runner.temp }}/digests
          pattern: digests-*
          merge-multiple: true
      - name: Create manifest list and push
        run: |
          mapfile -t tags <<< "$TAGS"
          tag_args=()
          for tag in "${tags[@]}"; do
            tag_args+=(--tag="$tag")
          done
          images=(
            "$GHCR_REPOSITORY@sha256:$(cat "$RUNNER_TEMP"/digests/x86_64)"
            "$GHCR_REPOSITORY@sha256:$(cat "$RUNNER_TEMP"/digests/aarch64)"
          )
          docker buildx imagetools create "${tag_args[@]}" "${images[@]}"
        env:
          TAGS: ${{ needs.metadata.outputs.tags }}