cargo-chef 0.1.77

A cargo sub-command to build project dependencies for optimal Docker layer caching.
Documentation
name: Build Docker images
on:
  push:
    tags:
      - "**[0-9]+.[0-9]+.[0-9]+*"
  schedule:
    - cron: "42 7 * * *" # run at 7:42 UTC (morning) every day
  workflow_dispatch:

permissions:
  contents: read

env:
  DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
  DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
  DOCKER_REPO: ${{ secrets.DOCKERHUB_USERNAME }}/cargo-chef

jobs:
  resolve_inputs:
    name: Resolve release version and Rust tag groups
    runs-on: ubuntu-latest
    outputs:
      package_version: ${{ steps.collect.outputs.package_version }}
      is_release_version: ${{ steps.collect.outputs.is_release_version }}
      group_matrix: ${{ steps.collect.outputs.group_matrix }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - id: collect
        run: |
          set -euo pipefail

          LATEST_TAG=$(
            git ls-remote --tags --refs origin \
              | awk -F/ '{print $NF}' \
              | sort -V \
              | tail -n 1
          )
          CHEF_PACKAGE_VERSION=${LATEST_TAG#v}
          echo "package_version=$CHEF_PACKAGE_VERSION" >> "$GITHUB_OUTPUT"

          if ! [[ "$CHEF_PACKAGE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Version '$CHEF_PACKAGE_VERSION' is not a release semver. Skipping image publish."
            echo "is_release_version=false" >> "$GITHUB_OUTPUT"
            echo 'group_matrix=[]' >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "Detected release version $CHEF_PACKAGE_VERSION"
          echo "is_release_version=true" >> "$GITHUB_OUTPUT"

          # Build one matrix row per official-images source group (GitCommit + Directory).
          # All aliases in the same group are attached to the same canonical image.
          GROUP_MATRIX=$(
            curl --silent https://raw.githubusercontent.com/docker-library/official-images/master/library/rust \
              | awk '
                /^Tags:/ {
                  tags = substr($0, 7)
                  gsub(/, /, "\n", tags)
                  next
                }
                /^Architectures:/ {
                  arches = substr($0, 16)
                  gsub(/, /, ",", arches)
                  next
                }
                /^GitCommit:/ {
                  git = substr($0, 12)
                  next
                }
                /^Directory:/ {
                  dir = substr($0, 11)
                  n = split(tags, arr, "\n")
                  for (i = 1; i <= n; i++) {
                    tag = arr[i]
                    print git ":" dir "\t" tag "\t" arches
                  }
                  tags = ""
                  arches = ""
                  git = ""
                  dir = ""
                }
              ' \
              | sort -u \
              | jq -R -s -c '
                  split("\n")
                  | map(select(length > 0))
                  | map(split("\t"))
                  | map({group_key: .[0], rust_image_tag: .[1], architectures: (.[2] // "" | split(",") | map(select(length > 0)))})
                  | group_by(.group_key)
                  | map({
                      group_key: .[0].group_key,
                      rust_aliases: (map(.rust_image_tag) | sort),
                      architectures: (map(.architectures) | add | unique | sort),
                      representative_rust_tag: (
                        map(.rust_image_tag)
                        | sort_by(
                            if test("^latest$") then [5, .]
                            elif test("^[0-9]+\\.[0-9]+\\.[0-9]+([-.].+)?$") then [1, .]
                            elif test("^[0-9]+\\.[0-9]+([-.].+)?$") then [2, .]
                            elif test("^[0-9]+([-.].+)?$") then [3, .]
                            else [4, .]
                            end
                          )
                        | .[0]
                      ),
                      platforms: (
                        map(.architectures)
                        | add
                        | unique
                        | map(
                            if . == "amd64" then "linux/amd64"
                            elif . == "arm64v8" then "linux/arm64"
                            elif . == "arm32v7" then "linux/arm/v7"
                            elif . == "i386" then "linux/386"
                            else empty
                            end
                          )
                        | unique
                        | sort
                      )
                    })
                '
          )

          if [ "$GROUP_MATRIX" = "[]" ] || [ -z "$GROUP_MATRIX" ]; then
            echo "Failed to generate Rust image group matrix" >&2
            exit 1
          fi

          echo "group_matrix=$GROUP_MATRIX" >> "$GITHUB_OUTPUT"
  build_unique_images:
    name: Build unique group images
    needs: [resolve_inputs]
    if: ${{ needs.resolve_inputs.outputs.is_release_version == 'true' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        group_entry: ${{fromJSON(needs.resolve_inputs.outputs.group_matrix)}}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Determine if canonical image exists
        id: canonical_status
        run: |
          CHEF_PACKAGE_VERSION=${{ needs.resolve_inputs.outputs.package_version }}
          GROUP_KEY='${{ matrix.group_entry.group_key }}'
          GROUP_KEY_TAG=$(printf '%s' "$GROUP_KEY" | shasum -a 256 | cut -c1-16)
          CANONICAL_IMAGE=$DOCKER_REPO:$CHEF_PACKAGE_VERSION-base-$GROUP_KEY_TAG

          if DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect "$CANONICAL_IMAGE" >/dev/null 2>&1; then
            echo "Canonical image already exists for group $GROUP_KEY_TAG. Skipping build."
            echo "result=skip" >> "$GITHUB_OUTPUT"
          else
            echo "Canonical image does not exist for group $GROUP_KEY_TAG. Building."
            echo "result=true" >> "$GITHUB_OUTPUT"
          fi
      - name: Build and push canonical image
        if: ${{ steps.canonical_status.outputs.result == 'true' }}
        run: |
          CHEF_PACKAGE_VERSION=${{ needs.resolve_inputs.outputs.package_version }}
          RUST_IMAGE_TAG=${{ matrix.group_entry.representative_rust_tag }}
          GROUP_KEY='${{ matrix.group_entry.group_key }}'
          GROUP_KEY_TAG=$(printf '%s' "$GROUP_KEY" | shasum -a 256 | cut -c1-16)
          CANONICAL_IMAGE=$DOCKER_REPO:$CHEF_PACKAGE_VERSION-base-$GROUP_KEY_TAG
          PLATFORMS_JSON='${{ toJSON(matrix.group_entry.platforms) }}'
          PLATFORMS=$(jq -r 'join(",")' <<< "$PLATFORMS_JSON")
          if [ -z "$PLATFORMS" ] || [ "$PLATFORMS" = "null" ]; then
            echo "No supported platforms found for group $GROUP_KEY_TAG. Skipping."
            exit 0
          fi

          docker buildx build \
            --tag $CANONICAL_IMAGE \
            --build-arg=BASE_IMAGE=rust:$RUST_IMAGE_TAG \
            --build-arg=CHEF_TAG=$CHEF_PACKAGE_VERSION \
            --platform "$PLATFORMS" \
            --push \
            ./docker
  publish_group_aliases:
    name: Publish group aliases
    needs: [resolve_inputs, build_unique_images]
    if: ${{ needs.resolve_inputs.outputs.is_release_version == 'true' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        group_entry: ${{fromJSON(needs.resolve_inputs.outputs.group_matrix)}}
    steps:
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Publish aliases for Rust group
        run: |
          set -euo pipefail

          CHEF_PACKAGE_VERSION=${{ needs.resolve_inputs.outputs.package_version }}
          GROUP_KEY='${{ matrix.group_entry.group_key }}'
          GROUP_KEY_TAG=$(printf '%s' "$GROUP_KEY" | shasum -a 256 | cut -c1-16)
          CANONICAL_IMAGE=$DOCKER_REPO:$CHEF_PACKAGE_VERSION-base-$GROUP_KEY_TAG
          RUST_ALIASES_JSON='${{ toJSON(matrix.group_entry.rust_aliases) }}'

          mapfile -t RUST_ALIASES < <(jq -r '.[]' <<< "$RUST_ALIASES_JSON")
          if [ "${#RUST_ALIASES[@]}" -eq 0 ]; then
            echo "No aliases found for group $GROUP_KEY_TAG. Skipping."
            exit 0
          fi

          tag_args=()
          has_latest_alias=false
          for RUST_IMAGE_TAG in "${RUST_ALIASES[@]}"; do
            CHEF_IMAGE=$DOCKER_REPO:$CHEF_PACKAGE_VERSION-rust-$RUST_IMAGE_TAG
            CHEF_IMAGE_LATEST=$DOCKER_REPO:latest-rust-$RUST_IMAGE_TAG
            tag_args+=(--tag "$CHEF_IMAGE")
            tag_args+=(--tag "$CHEF_IMAGE_LATEST")

            if [ "$RUST_IMAGE_TAG" = "latest" ]; then
              has_latest_alias=true
            fi
          done
          if [ "$has_latest_alias" = "true" ]; then
            tag_args+=(--tag "$DOCKER_REPO:latest")
          fi

          docker buildx imagetools create "${tag_args[@]}" "$CANONICAL_IMAGE"