github-actions-maintainer 0.7.0

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

# Callable release workflow. It owns the common CI gate and optional
# workflow-dispatch chaining so consuming repositories only declare their
# triggers and repository-specific pipeline names.

on:
  workflow_call:
    inputs:
      bump:
        description: Version bump strategy (auto, major, minor, patch).
        required: false
        default: auto
        type: string
      tag-prefix:
        description: Prefix for release tags.
        required: false
        default: v
        type: string
      update-major-alias:
        description: Also move the moving major alias tag to the new release.
        required: false
        default: false
        type: boolean
      notes-file:
        description: Path where generated release notes are written.
        required: false
        default: release_notes.md
        type: string
      dry-run:
        description: Analyze and report without creating any commit, tag, or release.
        required: false
        default: false
        type: boolean
      create-pr:
        description: Create or update an automated release branch and pull request instead of publishing directly.
        required: false
        default: false
        type: boolean
      release-branch:
        description: Automation-owned release branch (automation/release or automation/release/*).
        required: false
        default: automation/release
        type: string
      required-workflows:
        description: Comma-separated workflow names that must pass for the target commit.
        required: false
        default: ""
        type: string
      dispatch-workflows:
        description: Comma-separated workflow files to dispatch after a release.
        required: false
        default: ""
        type: string
      dispatch-version-workflow:
        description: Optional workflow file that receives the released version as a version input.
        required: false
        default: ""
        type: string
      github-app-id:
        description: Optional GitHub App installation ID source. Pair with github-app-private-key to authenticate release commits and PRs as the App.
        required: false
        default: ""
        type: string
    secrets:
      release-token:
        description: Optional GitHub App or PAT token; falls back to GITHUB_TOKEN. Required if the created tag must trigger tag pipelines natively.
        required: false
      github-app-private-key:
        description: Private key for the GitHub App identified by github-app-id. Store it as an organization or repository secret.
        required: false
    outputs:
      released:
        description: Whether a release was created (true or false).
        value: ${{ jobs.release.outputs.released }}
      version:
        description: The released version without the tag prefix.
        value: ${{ jobs.release.outputs.version }}
      tag:
        description: The created release tag.
        value: ${{ jobs.release.outputs.tag }}
      release-url:
        description: URL of the created GitHub Release.
        value: ${{ jobs.release.outputs.release-url }}
      release-pr-number:
        description: Number of the created or updated automated release pull request.
        value: ${{ jobs.release.outputs.release-pr-number }}
      release-pr-url:
        description: URL of the created or updated automated release pull request.
        value: ${{ jobs.release.outputs.release-pr-url }}
      release-branch:
        description: Branch used for the automated release pull request.
        value: ${{ jobs.release.outputs.release-branch }}

permissions:
  contents: read
  pull-requests: write

jobs:
  check:
    name: Check release prerequisites
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
    outputs:
      should-release: ${{ steps.gate.outputs.should-release }}
      target-sha: ${{ steps.target.outputs.sha }}
    steps:
      - name: Checkout target commit
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
          ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
      - name: Resolve target commit
        id: target
        env:
          EVENT_NAME: ${{ github.event_name }}
          WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
        run: |
          set -euo pipefail
          if [[ "$EVENT_NAME" == workflow_run ]]; then
            [[ "$WORKFLOW_RUN_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "Invalid workflow-run head SHA" >&2; exit 1; }
            echo "sha=$WORKFLOW_RUN_HEAD_SHA" >> "$GITHUB_OUTPUT"
          else
            echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
          fi
      - name: Verify required workflows
        id: gate
        env:
          GH_TOKEN: ${{ github.token }}
          TARGET_SHA: ${{ steps.target.outputs.sha }}
          REQUIRED_WORKFLOWS: ${{ inputs.required-workflows }}
          EVENT_NAME: ${{ github.event_name }}
          TRIGGER_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
          TRIGGER_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
        run: |
          set -euo pipefail
          if [[ "$EVENT_NAME" == workflow_run && ( "$TRIGGER_CONCLUSION" != success || "$TRIGGER_REPOSITORY" != "$GITHUB_REPOSITORY" ) ]]; then
            echo "should-release=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          if [[ -z "$REQUIRED_WORKFLOWS" ]]; then
            echo "should-release=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          IFS=',' read -ra workflows <<< "$REQUIRED_WORKFLOWS"
          for workflow in "${workflows[@]}"; do
            workflow="$(xargs <<< "$workflow")"
            [[ -n "$workflow" ]] || continue
            conclusion="$(gh run list --workflow="$workflow" --commit="$TARGET_SHA" --limit=1 --json conclusion --jq '.[0].conclusion // ""')"
            if [[ "$conclusion" != success ]]; then
              echo "Required workflow '$workflow' is '$conclusion' for $TARGET_SHA"
              echo "should-release=false" >> "$GITHUB_OUTPUT"
              exit 0
            fi
          done
          echo "should-release=true" >> "$GITHUB_OUTPUT"
  release:
    name: Auto Release
    needs: check
    if: needs.check.outputs.should-release == 'true'
    runs-on: ubuntu-latest
    permissions:
      actions: write
      contents: write
      pull-requests: write
    outputs:
      released: ${{ steps.release.outputs.released }}
      version: ${{ steps.release.outputs.version }}
      tag: ${{ steps.release.outputs.tag }}
      release-url: ${{ steps.release.outputs.release-url }}
      release-pr-number: ${{ steps.release.outputs.release-pr-number }}
      release-pr-url: ${{ steps.release.outputs.release-pr-url }}
      release-branch: ${{ steps.release.outputs.release-branch }}
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
          ref: ${{ needs.check.outputs.target-sha }}

      # Fetch the maintainer action at the exact commit of this reusable
      # workflow (job.workflow_sha), so the action version always matches the
      # workflow ref the caller pinned — no mutable cross-repo action
      # reference.
      - name: Checkout the action at the workflow ref
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          repository: ${{ job.workflow_repository || 'ThreatFlux/github_actions' }}
          ref: ${{ job.workflow_sha }}
          path: .release-action
          persist-credentials: false

      - name: Create GitHub App installation token
        id: github-app-token
        if: inputs.github-app-id != '' && secrets.github-app-private-key != ''
        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
        with:
          app-id: ${{ inputs.github-app-id }}
          private-key: ${{ secrets.github-app-private-key }}
          owner: ${{ github.repository_owner }}
          repositories: ${{ github.event.repository.name }}

      - name: Validate GitHub App configuration
        if: (inputs.github-app-id == '') != (secrets.github-app-private-key == '')
        run: |
          echo "github-app-id and github-app-private-key must be configured together" >&2
          exit 1

      - name: Release
        id: release
        uses: ./.release-action
        with:
          command: release
          token: ${{ steps.github-app-token.outputs.token || secrets.release-token || github.token }}
          bump: ${{ inputs.bump }}
          tag-prefix: ${{ inputs.tag-prefix }}
          update-major-alias: ${{ inputs.update-major-alias }}
          notes-file: ${{ inputs.notes-file }}
          dry-run: ${{ inputs.dry-run }}
          create-pr: ${{ inputs.create-pr }}
          release-branch: ${{ inputs.release-branch }}
      - name: Dispatch downstream release workflows
        if: steps.release.outputs.released == 'true' && inputs.dispatch-workflows != ''
        env:
          GH_TOKEN: ${{ steps.github-app-token.outputs.token || secrets.release-token || github.token }}
          TAG: ${{ steps.release.outputs.tag }}
          VERSION: ${{ steps.release.outputs.version }}
          DISPATCH_WORKFLOWS: ${{ inputs.dispatch-workflows }}
          VERSION_WORKFLOW: ${{ inputs.dispatch-version-workflow }}
        run: |
          set -euo pipefail
          IFS=',' read -ra workflows <<< "$DISPATCH_WORKFLOWS"
          for workflow in "${workflows[@]}"; do
            workflow="$(xargs <<< "$workflow")"
            [[ -n "$workflow" ]] || continue
            if [[ "$workflow" == "$VERSION_WORKFLOW" ]]; then
              gh workflow run "$workflow" --repo "$GITHUB_REPOSITORY" --ref "$TAG" -f version="$VERSION"
            else
              gh workflow run "$workflow" --repo "$GITHUB_REPOSITORY" --ref "$TAG"
            fi
          done