Skip to content
Latchkey

[ruff] Build Docker image workflow (astral-sh/ruff)

The [ruff] Build Docker image workflow from astral-sh/ruff, explained and optimized by Latchkey.

C

CI health: C - fair

The optimized version below adds run de-duplication, job timeouts.

Source: astral-sh/ruff.github/workflows/build-docker.ymlLicense MITView source

What it does

This is the [ruff] Build Docker image workflow from the astral-sh/ruff repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
# Build and publish a Docker image.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist`.
#
# TODO(charlie): Ideally, the publish step would happen as a publish job within `cargo-dist`, but
# sharing the built image as an artifact between jobs is challenging.
name: "[ruff] Build Docker image"

on:
  workflow_call:
    inputs:
      plan:
        required: true
        type: string
  pull_request:
    paths:
      - .github/workflows/build-docker.yml

env:
  RUFF_BASE_IMG: ghcr.io/${{ github.repository_owner }}/ruff

permissions:
  contents: read
  # TODO(zanieb): Ideally, this would be `read` on dry-run but that will require
  # significant changes to the workflow.
  packages: write # zizmor: ignore[excessive-permissions]

jobs:
  docker-build:
    name: Build Docker image (ghcr.io/astral-sh/ruff) for ${{ matrix.platform }}
    runs-on: ubuntu-latest
    environment:
      name: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit && 'release' || '' }}
    strategy:
      fail-fast: false
      matrix:
        platform:
          - linux/amd64
          - linux/arm64
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          submodules: recursive
          persist-credentials: false

      - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

      - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Check tag consistency
        if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
        env:
          TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
        run: |
          version=$(grep -m 1 "^version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
          if [ "${TAG}" != "${version}" ]; then
            echo "The input tag does not match the version from pyproject.toml:" >&2
            echo "${TAG}" >&2
            echo "${version}" >&2
            exit 1
          else
            echo "Releasing ${version}"
          fi

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
        with:
          images: ${{ env.RUFF_BASE_IMG }}
          # Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
          tags: |
            type=raw,value=dry-run,enable=${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
            type=pep440,pattern={{ version }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}

      - name: Normalize Platform Pair (replace / with -)
        run: |
          platform=${{ matrix.platform }}
          echo "PLATFORM_TUPLE=${platform//\//-}" >> "$GITHUB_ENV"

      # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
        with:
          context: .
          platforms: ${{ matrix.platform }}
          cache-from: type=gha,scope=ruff-${{ env.PLATFORM_TUPLE }}
          cache-to: type=gha,mode=min,scope=ruff-${{ env.PLATFORM_TUPLE }}
          labels: ${{ steps.meta.outputs.labels }}
          outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}

      - name: Export digests
        env:
          digest: ${{ steps.build.outputs.digest }}
        run: |
          mkdir -p /tmp/digests
          touch "/tmp/digests/${digest#sha256:}"

      - name: Upload digests
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: digests-${{ env.PLATFORM_TUPLE }}
          path: /tmp/digests/*
          if-no-files-found: error
          retention-days: 1

  docker-publish:
    name: Publish Docker image (ghcr.io/astral-sh/ruff)
    runs-on: ubuntu-latest
    environment:
      name: release
    needs:
      - docker-build
    if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
    permissions:
      attestations: write
      id-token: write
      packages: write
    steps:
      - name: Download digests
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          path: /tmp/digests
          pattern: digests-*
          merge-multiple: true

      - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
        with:
          images: ${{ env.RUFF_BASE_IMG }}
          # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
          tags: |
            type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
            type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}

      - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
      - name: Create manifest list and push
        working-directory: /tmp/digests
        # The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
        # The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
        # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
        run: |
          # shellcheck disable=SC2046
          docker buildx imagetools create \
            $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
            $(printf "${RUFF_BASE_IMG}@sha256:%s " *)

      - name: Export manifest digest
        id: manifest-digest
        env:
          IMAGE: ${{ env.RUFF_BASE_IMG }}
          VERSION: ${{ steps.meta.outputs.version }}
        run: |
          digest="$(
            docker buildx imagetools inspect \
              "${IMAGE}:${VERSION}" \
              --format '{{json .Manifest}}' \
            | jq -r '.digest'
          )"
          echo "digest=${digest}" >> "$GITHUB_OUTPUT"

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
        with:
          subject-name: ${{ env.RUFF_BASE_IMG }}
          subject-digest: ${{ steps.manifest-digest.outputs.digest }}

  docker-publish-extra:
    name: Publish additional Docker image based on ${{ matrix.image-mapping }}
    runs-on: ubuntu-latest
    environment:
      name: release
    needs:
      - docker-publish
    if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
    permissions:
      attestations: write
      id-token: write
      packages: write
    strategy:
      fail-fast: false
      matrix:
        # Mapping of base image followed by a comma followed by one or more base tags (comma separated)
        # Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first)
        image-mapping:
          - alpine:3.23,alpine3.23,alpine
          - debian:trixie-slim,trixie-slim,debian-slim
          - buildpack-deps:trixie,trixie,debian
    steps:
      - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

      - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Generate Dynamic Dockerfile Tags
        shell: bash
        env:
          TAG_VALUE: ${{ fromJson(inputs.plan).announcement_tag }}
        run: |
          set -euo pipefail

          # Extract the image and tags from the matrix variable
          IFS=',' read -r BASE_IMAGE BASE_TAGS <<< "${{ matrix.image-mapping }}"

          # Generate Dockerfile content
          cat <<EOF > Dockerfile
          FROM ${BASE_IMAGE}
          COPY --from=${RUFF_BASE_IMG}:latest /ruff /usr/local/bin/ruff
          ENTRYPOINT []
          CMD ["/usr/local/bin/ruff"]
          EOF

          # Initialize a variable to store all tag docker metadata patterns
          TAG_PATTERNS=""

          # Loop through all base tags and append its docker metadata pattern to the list
          # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
          IFS=','; for TAG in ${BASE_TAGS}; do
            TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${TAG_VALUE}\n"
            TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${TAG_VALUE}\n"
            TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"
          done

          # Remove the trailing newline from the pattern list
          TAG_PATTERNS="${TAG_PATTERNS%\\n}"

          # Export image cache name
          echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> "$GITHUB_ENV"

          # Export tag patterns using the multiline env var syntax
          {
            echo "TAG_PATTERNS<<EOF"
            echo -e "${TAG_PATTERNS}"
            echo EOF
          } >> "$GITHUB_ENV"

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
        # ghcr.io prefers index level annotations
        env:
          DOCKER_METADATA_ANNOTATIONS_LEVELS: index
        with:
          images: ${{ env.RUFF_BASE_IMG }}
          flavor: |
            latest=false
          tags: |
            ${{ env.TAG_PATTERNS }}

      - name: Build and push
        id: build-and-push
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          # We do not really need to cache here as the Dockerfile is tiny
          #cache-from: type=gha,scope=ruff-${{ env.IMAGE_REF }}
          #cache-to: type=gha,mode=min,scope=ruff-${{ env.IMAGE_REF }}
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          annotations: ${{ steps.meta.outputs.annotations }}

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
        with:
          subject-name: ${{ env.RUFF_BASE_IMG }}
          subject-digest: ${{ steps.build-and-push.outputs.digest }}

  # This is effectively a duplicate of `docker-publish` to make https://github.com/astral-sh/ruff/pkgs/container/ruff
  # show the ruff base image first since GitHub always shows the last updated image digests
  # This works by annotating the original digests (previously non-annotated) which triggers an update to ghcr.io
  docker-republish:
    name: Annotate Docker image (ghcr.io/astral-sh/ruff)
    runs-on: ubuntu-latest
    environment:
      name: release
    needs:
      - docker-publish-extra
    if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
    permissions:
      attestations: write
      id-token: write
      packages: write
    steps:
      - name: Download digests
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          path: /tmp/digests
          pattern: digests-*
          merge-multiple: true

      - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
        env:
          DOCKER_METADATA_ANNOTATIONS_LEVELS: index
        with:
          images: ${{ env.RUFF_BASE_IMG }}
          # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
          tags: |
            type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
            type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}

      - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
      - name: Create manifest list and push
        working-directory: /tmp/digests
        # The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces)
        # The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array
        # The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
        # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
        run: |
          readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done

          # shellcheck disable=SC2046
          docker buildx imagetools create \
            "${annotations[@]}" \
            $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
            $(printf "${RUFF_BASE_IMG}@sha256:%s " *)

      - name: Export manifest digest
        id: manifest-digest
        env:
          IMAGE: ${{ env.RUFF_BASE_IMG }}
          VERSION: ${{ steps.meta.outputs.version }}
        run: |
          digest="$(
            docker buildx imagetools inspect \
              "${IMAGE}:${VERSION}" \
              --format '{{json .Manifest}}' \
            | jq -r '.digest'
          )"
          echo "digest=${digest}" >> "$GITHUB_OUTPUT"

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
        with:
          subject-name: ${{ env.RUFF_BASE_IMG }}
          subject-digest: ${{ steps.manifest-digest.outputs.digest }}

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

# Build and publish a Docker image.## Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local# artifacts job within `cargo-dist`.## TODO(charlie): Ideally, the publish step would happen as a publish job within `cargo-dist`, but# sharing the built image as an artifact between jobs is challenging.name: "[ruff] Build Docker image" on:  workflow_call:    inputs:      plan:        required: true        type: string  pull_request:    paths:      - .github/workflows/build-docker.yml env:  RUFF_BASE_IMG: ghcr.io/${{ github.repository_owner }}/ruff permissions:  contents: read  # TODO(zanieb): Ideally, this would be `read` on dry-run but that will require  # significant changes to the workflow.  packages: write # zizmor: ignore[excessive-permissions] concurrency:  group: ${{ github.workflow }}-${{ github.ref }}  cancel-in-progress: true jobs:  docker-build:    timeout-minutes: 30    name: Build Docker image (ghcr.io/astral-sh/ruff) for ${{ matrix.platform }}    runs-on: latchkey-small    environment:      name: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit && 'release' || '' }}    strategy:      fail-fast: false      matrix:        platform:          - linux/amd64          - linux/arm64    steps:      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0        with:          submodules: recursive          persist-credentials: false       - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0       - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0        if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}        with:          registry: ghcr.io          username: ${{ github.repository_owner }}          password: ${{ secrets.GITHUB_TOKEN }}       - name: Check tag consistency        if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}        env:          TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}        run: |          version=$(grep -m 1 "^version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')          if [ "${TAG}" != "${version}" ]; then            echo "The input tag does not match the version from pyproject.toml:" >&2            echo "${TAG}" >&2            echo "${version}" >&2            exit 1          else            echo "Releasing ${version}"          fi       - name: Extract metadata (tags, labels) for Docker        id: meta        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0        with:          images: ${{ env.RUFF_BASE_IMG }}          # Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name          tags: |            type=raw,value=dry-run,enable=${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}            type=pep440,pattern={{ version }},value=${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }},enable=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}       - name: Normalize Platform Pair (replace / with -)        run: |          platform=${{ matrix.platform }}          echo "PLATFORM_TUPLE=${platform//\//-}" >> "$GITHUB_ENV"       # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/      - name: Build and push by digest        id: build        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0        with:          context: .          platforms: ${{ matrix.platform }}          cache-from: type=gha,scope=ruff-${{ env.PLATFORM_TUPLE }}          cache-to: type=gha,mode=min,scope=ruff-${{ env.PLATFORM_TUPLE }}          labels: ${{ steps.meta.outputs.labels }}          outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}       - name: Export digests        env:          digest: ${{ steps.build.outputs.digest }}        run: |          mkdir -p /tmp/digests          touch "/tmp/digests/${digest#sha256:}"       - name: Upload digests        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1        with:          name: digests-${{ env.PLATFORM_TUPLE }}          path: /tmp/digests/*          if-no-files-found: error          retention-days: 1   docker-publish:    timeout-minutes: 30    name: Publish Docker image (ghcr.io/astral-sh/ruff)    runs-on: latchkey-small    environment:      name: release    needs:      - docker-build    if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}    permissions:      attestations: write      id-token: write      packages: write    steps:      - name: Download digests        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1        with:          path: /tmp/digests          pattern: digests-*          merge-multiple: true       - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0       - name: Extract metadata (tags, labels) for Docker        id: meta        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0        with:          images: ${{ env.RUFF_BASE_IMG }}          # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version          tags: |            type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}            type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}       - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0        with:          registry: ghcr.io          username: ${{ github.repository_owner }}          password: ${{ secrets.GITHUB_TOKEN }}       # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/      - name: Create manifest list and push        working-directory: /tmp/digests        # The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array        # The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory        # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`        run: |          # shellcheck disable=SC2046          docker buildx imagetools create \            $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \            $(printf "${RUFF_BASE_IMG}@sha256:%s " *)       - name: Export manifest digest        id: manifest-digest        env:          IMAGE: ${{ env.RUFF_BASE_IMG }}          VERSION: ${{ steps.meta.outputs.version }}        run: |          digest="$(            docker buildx imagetools inspect \              "${IMAGE}:${VERSION}" \              --format '{{json .Manifest}}' \            | jq -r '.digest'          )"          echo "digest=${digest}" >> "$GITHUB_OUTPUT"       - name: Generate artifact attestation        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1        with:          subject-name: ${{ env.RUFF_BASE_IMG }}          subject-digest: ${{ steps.manifest-digest.outputs.digest }}   docker-publish-extra:    timeout-minutes: 30    name: Publish additional Docker image based on ${{ matrix.image-mapping }}    runs-on: latchkey-small    environment:      name: release    needs:      - docker-publish    if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}    permissions:      attestations: write      id-token: write      packages: write    strategy:      fail-fast: false      matrix:        # Mapping of base image followed by a comma followed by one or more base tags (comma separated)        # Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first)        image-mapping:          - alpine:3.23,alpine3.23,alpine          - debian:trixie-slim,trixie-slim,debian-slim          - buildpack-deps:trixie,trixie,debian    steps:      - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0       - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0        with:          registry: ghcr.io          username: ${{ github.repository_owner }}          password: ${{ secrets.GITHUB_TOKEN }}       - name: Generate Dynamic Dockerfile Tags        shell: bash        env:          TAG_VALUE: ${{ fromJson(inputs.plan).announcement_tag }}        run: |          set -euo pipefail           # Extract the image and tags from the matrix variable          IFS=',' read -r BASE_IMAGE BASE_TAGS <<< "${{ matrix.image-mapping }}"           # Generate Dockerfile content          cat <<EOF > Dockerfile          FROM ${BASE_IMAGE}          COPY --from=${RUFF_BASE_IMG}:latest /ruff /usr/local/bin/ruff          ENTRYPOINT []          CMD ["/usr/local/bin/ruff"]          EOF           # Initialize a variable to store all tag docker metadata patterns          TAG_PATTERNS=""           # Loop through all base tags and append its docker metadata pattern to the list          # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version          IFS=','; for TAG in ${BASE_TAGS}; do            TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${TAG_VALUE}\n"            TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${TAG_VALUE}\n"            TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"          done           # Remove the trailing newline from the pattern list          TAG_PATTERNS="${TAG_PATTERNS%\\n}"           # Export image cache name          echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> "$GITHUB_ENV"           # Export tag patterns using the multiline env var syntax          {            echo "TAG_PATTERNS<<EOF"            echo -e "${TAG_PATTERNS}"            echo EOF          } >> "$GITHUB_ENV"       - name: Extract metadata (tags, labels) for Docker        id: meta        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0        # ghcr.io prefers index level annotations        env:          DOCKER_METADATA_ANNOTATIONS_LEVELS: index        with:          images: ${{ env.RUFF_BASE_IMG }}          flavor: |            latest=false          tags: |            ${{ env.TAG_PATTERNS }}       - name: Build and push        id: build-and-push        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0        with:          context: .          platforms: linux/amd64,linux/arm64          # We do not really need to cache here as the Dockerfile is tiny          #cache-from: type=gha,scope=ruff-${{ env.IMAGE_REF }}          #cache-to: type=gha,mode=min,scope=ruff-${{ env.IMAGE_REF }}          push: true          tags: ${{ steps.meta.outputs.tags }}          labels: ${{ steps.meta.outputs.labels }}          annotations: ${{ steps.meta.outputs.annotations }}       - name: Generate artifact attestation        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1        with:          subject-name: ${{ env.RUFF_BASE_IMG }}          subject-digest: ${{ steps.build-and-push.outputs.digest }}   # This is effectively a duplicate of `docker-publish` to make https://github.com/astral-sh/ruff/pkgs/container/ruff  # show the ruff base image first since GitHub always shows the last updated image digests  # This works by annotating the original digests (previously non-annotated) which triggers an update to ghcr.io  docker-republish:    timeout-minutes: 30    name: Annotate Docker image (ghcr.io/astral-sh/ruff)    runs-on: latchkey-small    environment:      name: release    needs:      - docker-publish-extra    if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}    permissions:      attestations: write      id-token: write      packages: write    steps:      - name: Download digests        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1        with:          path: /tmp/digests          pattern: digests-*          merge-multiple: true       - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0       - name: Extract metadata (tags, labels) for Docker        id: meta        uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0        env:          DOCKER_METADATA_ANNOTATIONS_LEVELS: index        with:          images: ${{ env.RUFF_BASE_IMG }}          # Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version          tags: |            type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}            type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}       - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0        with:          registry: ghcr.io          username: ${{ github.repository_owner }}          password: ${{ secrets.GITHUB_TOKEN }}       # Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/      - name: Create manifest list and push        working-directory: /tmp/digests        # The readarray part is used to make sure the quoting and special characters are preserved on expansion (e.g. spaces)        # The jq command expands the docker/metadata json "tags" array entry to `-t tag1 -t tag2 ...` for each tag in the array        # The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory        # The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`        run: |          readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done           # shellcheck disable=SC2046          docker buildx imagetools create \            "${annotations[@]}" \            $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \            $(printf "${RUFF_BASE_IMG}@sha256:%s " *)       - name: Export manifest digest        id: manifest-digest        env:          IMAGE: ${{ env.RUFF_BASE_IMG }}          VERSION: ${{ steps.meta.outputs.version }}        run: |          digest="$(            docker buildx imagetools inspect \              "${IMAGE}:${VERSION}" \              --format '{{json .Manifest}}' \            | jq -r '.digest'          )"          echo "digest=${digest}" >> "$GITHUB_OUTPUT"       - name: Generate artifact attestation        uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1        with:          subject-name: ${{ env.RUFF_BASE_IMG }}          subject-digest: ${{ steps.manifest-digest.outputs.digest }} 

What changed

  • Run on Latchkey managed runners with one line (runs-on), which apply the fixes below automatically and self-heal transient failures. This example uses latchkey-small; pick the runner size that fits the job.
  • Cancel superseded runs when a branch or PR gets a newer push.
  • Add a job timeout so a hung step cannot burn hours of runner time.

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

  • Container pulls and builds

This workflow runs 4 jobs (7 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow

Frequently asked questions

What does the [ruff] Build Docker image workflow (astral-sh/ruff) workflow do?
This is the [ruff] Build Docker image workflow from the astral-sh/ruff repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
What CI health grade does this workflow get?
This Docker workflow grades C. Paste your own workflow into the Latchkey grader to see its grade and the exact fixes.
How can I improve this Docker workflow?
Apply run de-duplication, job timeouts. Latchkey applies these automatically on managed runners when you point runs-on at Latchkey.

References