macro_paste 1.1.7

Macros for all your token pasting needs. Maintained, drop-in replacement for paste.
Documentation
name: Validate and auto-merge bot PRs

on:
  workflow_dispatch:
  pull_request:
    branches:
      - "*"

permissions:
  contents: read

jobs:
  dependabot:
    name: Validate and auto-merge bot PRs
    runs-on: ubuntu-latest

    permissions:
      contents: write
      pull-requests: write
      checks: read
      actions: write

    if: |
      github.event.pull_request.user.login == 'dependabot[bot]' ||
      github.event.pull_request.user.login == 'butlergroup-automerge-token-issuer[bot]' ||
      github.event.pull_request.user.login == 'github-actions[bot]'

    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411
        with:
          egress-policy: audit

      - name: Fetch Dependabot metadata
        if: github.event.pull_request.user.login == 'dependabot[bot]'
        id: metadata
        uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Fetch bot PR metadata
        id: bot-metadata
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          BOT_LOGIN: ${{ github.event.pull_request.user.login }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BRANCH: ${{ github.event.pull_request.head.ref }}
        run: |
          set -euo pipefail

          labels=$(gh api "repos/$REPO/issues/$PR_NUMBER/labels" \
            --jq '[.[].name] | join(",")')

          if [[ "$BOT_LOGIN" == "dependabot[bot]" ]]; then
            bot_type="dependabot"
          elif [[ "$BOT_LOGIN" == "github-actions[bot]" ]]; then
            bot_type="github-actions"
          elif [[ "$BOT_LOGIN" == "butlergroup-automerge-token-issuer[bot]" ]]; then
            bot_type="github-app"
          else
            bot_type="unknown"
          fi

          {
            echo "bot-login=$BOT_LOGIN"
            echo "pr-title=$PR_TITLE"
            echo "pr-branch=$PR_BRANCH"
            echo "labels=$labels"
            echo "bot-type=$bot_type"
          } >> "$GITHUB_OUTPUT"

      - name: Wait for all checks to complete successfully
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO: ${{ github.repository }}
          SHA: ${{ github.event.pull_request.head.sha }}
        run: |
          set -euo pipefail

          # Timeout after 30 minutes
          timeout_seconds=1800
          start_time=$(date +%s)

          # Checks to ignore
          EXCLUDED_PATTERNS=(
            "Validate and auto-merge bot PRs"
            "Greet First-Time Contributors"
            "code/snyk"
          )

          echo "Waiting for checks on commit: $SHA"

          while true; do
            now=$(date +%s)
            elapsed=$((now - start_time))

            if [[ $elapsed -ge $timeout_seconds ]]; then
              echo "Timed out waiting for checks."
              exit 1
            fi

            echo "Fetching check runs..."

            checks=$(gh api \
              "repos/$REPO/commits/$SHA/check-runs" \
              --jq '.check_runs[] | [
                .name,
                .status,
                .conclusion
              ] | @tsv')

            if [[ -z "$checks" ]]; then
              echo "No checks registered yet..."
              sleep 15
              continue
            fi

            pending=0
            failed=0
            actionable_checks=0

            while IFS=$'\t' read -r name status conclusion; do

              skip=false

              for pattern in "${EXCLUDED_PATTERNS[@]}"; do
                if [[ "$name" == *"$pattern"* ]]; then
                  echo "Skipping excluded check: $name"
                  skip=true
                  break
                fi
              done

              if [[ "$skip" == true ]]; then
                continue
              fi

              echo "Check: $name"
              echo "  Status: $status"
              echo "  Conclusion: $conclusion"

              actionable_checks=$((actionable_checks + 1))

              #
              # Pending states
              #
              if [[ "$status" != "completed" ]]; then
                pending=1
                continue
              fi

              #
              # Failed states
              #
              case "$conclusion" in
                success|neutral|skipped)
                  ;;
                *)
                  echo "Check failed: $name"
                  failed=1
                  ;;
              esac

            done <<< "$checks"

            #
            # Prevent accidental merges if no real checks exist
            #
            if [[ $actionable_checks -eq 0 ]]; then
              echo "No actionable checks found yet..."
              pending=1
            fi

            #
            # Fail immediately if any check failed
            #
            if [[ $failed -eq 1 ]]; then
              echo "One or more checks failed."
              exit 1
            fi

            #
            # Success condition
            #
            if [[ $pending -eq 0 ]]; then
              echo "All checks completed successfully."

              #
              # Extra stabilization delay to avoid race conditions
              #
              echo "Waiting 20 seconds for GitHub state stabilization..."
              sleep 20

              break
            fi

            echo "Checks still pending..."
            sleep 15
          done

      - name: Verify PR mergeability
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_URL: ${{ github.event.pull_request.html_url }}
        run: |
          set -euo pipefail

          for _ in {1..20}; do
            state=$(gh pr view "$PR_URL" \
              --json mergeable \
              --jq '.mergeable')

            echo "Mergeable state: $state"

            if [[ "$state" == "MERGEABLE" ]]; then
              exit 0
            fi

            if [[ "$state" == "CONFLICTING" ]]; then
              echo "PR has merge conflicts."
              exit 1
            fi

            sleep 10
          done

          echo "PR never became mergeable."
          exit 1

      - name: Verify GitHub App secret availability
        shell: bash
        env:
          APP_ID: ${{ vars.APP_ID }}
          APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
        run: |
          set -euo pipefail

          if [[ -z "$APP_ID" ]]; then
            echo "::error::APP_ID is empty"
            exit 1
          fi

          if [[ -z "$APP_PRIVATE_KEY" ]]; then
            echo "::error::APP_PRIVATE_KEY is empty/unavailable in this workflow context"
            exit 1
          fi

          if ! grep -q "BEGIN .*PRIVATE KEY" <<< "$APP_PRIVATE_KEY"; then
            echo "::error::APP_PRIVATE_KEY does not look like a PEM private key"
            exit 1
          fi

          echo "GitHub App inputs are present"

      - name: Generate GitHub App installation token manually
        id: app-token
        shell: bash
        env:
          APP_ID: ${{ vars.APP_ID }}
          APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
          REPO: ${{ github.repository }}
        run: |
          set -euo pipefail

          private_key_file="$(mktemp)"
          trap 'rm -f "$private_key_file"' EXIT
          printf '%s\n' "$APP_PRIVATE_KEY" > "$private_key_file"

          now="$(date +%s)"
          iat="$((now - 60))"
          exp="$((now + 540))"

          b64url() {
            openssl base64 -A | tr '+/' '-_' | tr -d '='
          }

          header="$(printf '{"alg":"RS256","typ":"JWT"}' | b64url)"
          payload="$(printf '{"iat":%s,"exp":%s,"iss":"%s"}' "$iat" "$exp" "$APP_ID" | b64url)"
          unsigned_token="${header}.${payload}"

          signature="$(
            printf '%s' "$unsigned_token" |
              openssl dgst -sha256 -sign "$private_key_file" -binary |
              b64url
          )"

          jwt="${unsigned_token}.${signature}"

          owner="${REPO%%/*}"
          repo_name="${REPO#*/}"

          echo "Looking up GitHub App installation for $REPO"

          installation_json="$(
            curl -sS \
              -H "Accept: application/vnd.github+json" \
              -H "Authorization: Bearer $jwt" \
              -H "X-GitHub-Api-Version: 2022-11-28" \
              "https://api.github.com/repos/$REPO/installation"
          )"

          installation_id="$(jq -r '.id // empty' <<< "$installation_json")"

          if [[ -z "$installation_id" ]]; then
            echo "Repo installation lookup did not return an installation ID."
            echo "Trying owner-level installation lookup for $owner"

            installation_json="$(
              curl -sS \
                -H "Accept: application/vnd.github+json" \
                -H "Authorization: Bearer $jwt" \
                -H "X-GitHub-Api-Version: 2022-11-28" \
                "https://api.github.com/orgs/$owner/installation"
            )"

            installation_id="$(jq -r '.id // empty' <<< "$installation_json")"
          fi

          if [[ -z "$installation_id" ]]; then
            echo "::error::Could not determine GitHub App installation ID for $REPO. Confirm the app is installed on owner '$owner' and has access to repo '$repo_name'."
            echo "$installation_json" | jq -r '.message // empty'
            exit 1
          fi

          token_json="$(
            curl -sS \
              -X POST \
              -H "Accept: application/vnd.github+json" \
              -H "Authorization: Bearer $jwt" \
              -H "X-GitHub-Api-Version: 2022-11-28" \
              "https://api.github.com/app/installations/$installation_id/access_tokens" \
              -d "$(jq -nc \
                --arg repo "$repo_name" \
                '{
                  repositories: [$repo],
                  permissions: {
                    contents: "write",
                    pull_requests: "write",
                    checks: "read",
                    actions: "write"
                  }
                }')"
          )"

          token="$(jq -r '.token // empty' <<< "$token_json")"

          if [[ -z "$token" ]]; then
            echo "::error::GitHub App installation token was empty"
            echo "$token_json" | jq -r '.message // empty'
            exit 1
          fi

          echo "::add-mask::$token"
          echo "token=$token" >> "$GITHUB_OUTPUT"

      - name: Merge bot PR
        env:
          GH_TOKEN: ${{ steps.app-token.outputs.token }}
          REPO: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          PR_TITLE: ${{ github.event.pull_request.title }}
        run: |
          set -euo pipefail

          gh pr view "$PR_NUMBER" \
            --repo "$REPO" \
            --json body \
            --jq '.body' > pr-body.md

          gh pr merge "$PR_NUMBER" \
            --repo "$REPO" \
            --merge \
            --delete-branch \
            --subject "$PR_TITLE" \
            --body-file pr-body.md

      - name: Notify on failure
        if: failure()
        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
        with:
          script: |
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `❌ Bot PR auto-merge failed

              PR: ${context.payload.pull_request.html_url}

              Workflow:
              ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
            })