github-actions-maintainer 0.7.2

General-purpose GitHub Actions maintenance toolkit with secure workflow pinning
Documentation
name: Auto Release

concurrency:
  group: auto-release-${{ github.event.workflow_run.head_branch || github.ref_name || 'main' }}
  cancel-in-progress: true

on:
  workflow_run:
    # Docker is included because the release pins the runtime image built from
    # the release target commit, so that publish has to land first. Whichever
    # of the three finishes last triggers the run that actually releases.
    workflows: ["CI", "Security", "Docker"]
    types: [completed]
    branches: [main]
  schedule:
    - cron: "0 9 * * 1"
  workflow_dispatch:
    inputs:
      version_bump:
        description: Version bump type
        required: true
        default: auto
        type: choice
        options:
          - auto
          - patch
          - minor
          - major

permissions:
  contents: read

jobs:
  check:
    name: Check for Release
    runs-on: ${{ vars.RUST_TEMPLATE_RUNNER_UBUNTU || 'ubuntu-latest' }}
    if: >-
      (github.event_name == 'workflow_run' &&
      github.event.workflow_run.event == 'push' &&
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.head_repository.full_name == github.repository) ||
      github.event_name == 'schedule' ||
      github.event_name == 'workflow_dispatch'
    permissions:
      actions: read
      contents: read
    outputs:
      should_release: ${{ steps.decide.outputs.should_release }}
      target_sha: ${{ steps.target.outputs.sha }}
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
          ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref_name }}

      - name: Determine target SHA
        id: target
        env:
          EVENT_NAME: ${{ github.event_name }}
          WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
        run: |
          if [[ "${EVENT_NAME}" == "workflow_run" ]]; then
            if [[ ! "${WORKFLOW_RUN_HEAD_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
              echo "Invalid workflow-run head SHA" >&2
              exit 1
            fi
            echo "sha=${WORKFLOW_RUN_HEAD_SHA}" >> "$GITHUB_OUTPUT"
          else
            echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
          fi

      - name: Check CI status
        id: ci_status
        run: |
          TARGET_SHA="${{ steps.target.outputs.sha }}"
          CI_STATUS=$(gh run list --workflow=ci.yml --branch=main --commit="$TARGET_SHA" --event=push --limit=1 --json conclusion --jq '.[0].conclusion // ""')
          SECURITY_STATUS=$(gh run list --workflow=security.yml --branch=main --commit="$TARGET_SHA" --event=push --limit=1 --json conclusion --jq '.[0].conclusion // ""')
          # The runtime pin resolves the image docker.yml publishes for this
          # commit, so a release before that publish would have nothing to pin.
          DOCKER_STATUS=$(gh run list --workflow=docker.yml --branch=main --commit="$TARGET_SHA" --event=push --limit=1 --json conclusion --jq '.[0].conclusion // ""')
          echo "ci=$CI_STATUS security=$SECURITY_STATUS docker=$DOCKER_STATUS"
          if [[ "$CI_STATUS" == "success" ]] && [[ "$SECURITY_STATUS" == "success" ]] && [[ "$DOCKER_STATUS" == "success" ]]; then
            echo "all_passed=true" >> "$GITHUB_OUTPUT"
          else
            echo "all_passed=false" >> "$GITHUB_OUTPUT"
          fi
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Decide whether to attempt a release
        id: decide
        env:
          ALL_PASSED: ${{ steps.ci_status.outputs.all_passed }}
        run: |
          # The release CLI itself no-ops when no conventional commit warrants
          # a release, so this gate only enforces green CI and Security runs —
          # including for manual dispatches.
          if [[ "${ALL_PASSED}" == "true" ]]; then
            echo "should_release=true" >> "$GITHUB_OUTPUT"
          else
            echo "should_release=false" >> "$GITHUB_OUTPUT"
          fi

  release:
    name: Create Release
    needs: check
    if: needs.check.outputs.should_release == 'true'
    runs-on: ${{ vars.RUST_TEMPLATE_RUNNER_UBUNTU || 'ubuntu-latest' }}
    permissions:
      contents: write
      actions: write
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
          ref: ${{ needs.check.outputs.target_sha }}

      # The published version-tagged image only exists after the release tag is
      # pushed, so a Dockerfile can never pin its own release. The docker.yml
      # run for this same commit already published a short-SHA image though, so
      # pin that: it is built from exactly the code being released, and staging
      # the rewrite into the release commit makes the tag self-consistent.
      # Without this the tag ships whatever older image the pin happened to
      # name, which is how v0.7.0 shipped a 0.6.2 binary. The short-SHA tag is
      # mutable — scheduled rebuilds re-push it — so the resolved digest is
      # what gets written, which is immutable once committed.
      - name: Pin runtime images to this commit
        id: runtime_pin
        env:
          TARGET_SHA: ${{ needs.check.outputs.target_sha }}
          PIN_FILES: runtime/Dockerfile release/Dockerfile
        run: |
          set -euo pipefail
          repo_path="$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
          short="${TARGET_SHA:0:7}"
          token="$(curl -fsSL "https://ghcr.io/token?scope=repository:${repo_path}:pull&service=ghcr.io" | jq -r .token)"
          digest="$(
            curl -fsSL -o /dev/null -D - \
              -H "Authorization: Bearer ${token}" \
              -H 'Accept: application/vnd.oci.image.index.v1+json' \
              -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
              -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
              "https://ghcr.io/v2/${repo_path}/manifests/${short}" \
              | tr -d '\r' | awk 'tolower($1) == "docker-content-digest:" { print $2 }'
          )"
          if [[ -z "${digest}" ]]; then
            echo "no published ghcr.io/${repo_path} image tagged ${short}; refusing to release a mismatched runtime" >&2
            exit 1
          fi
          for file in ${PIN_FILES}; do
            [[ -f "${file}" ]] || { echo "missing ${file}" >&2; exit 1; }
            sed -i -E "s|^FROM [^[:space:]]+|FROM ghcr.io/${repo_path}:${short}@${digest}|" "${file}"
            grep -m1 '^FROM ' "${file}"
          done
          echo "files=${PIN_FILES// /,}" >> "$GITHUB_OUTPUT"

      - name: Release
        id: release
        uses: ./
        with:
          command: release
          token: ${{ secrets.GITHUB_TOKEN }}
          bump: ${{ github.event.inputs.version_bump || 'auto' }}
          update-major-alias: "true"
          extra-files: ${{ steps.runtime_pin.outputs.files }}

      # Tags created with GITHUB_TOKEN do not trigger tag-push workflows, so
      # dispatch the tag pipelines explicitly (workflow_dispatch is exempt
      # from that suppression).
      - name: Trigger tag pipelines
        if: steps.release.outputs.released == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          VERSION: ${{ steps.release.outputs.version }}
          TAG: ${{ steps.release.outputs.tag }}
        run: |
          gh workflow run release.yml --repo "$GITHUB_REPOSITORY" --ref "$TAG" -f version="$VERSION"
          gh workflow run docker.yml --repo "$GITHUB_REPOSITORY" --ref "$TAG"