diff --git a/.github/workflows/pr-merge-gatekeeper.yml b/.github/workflows/pr-merge-gatekeeper.yml index e72e7ce..6d83503 100644 --- a/.github/workflows/pr-merge-gatekeeper.yml +++ b/.github/workflows/pr-merge-gatekeeper.yml @@ -16,4 +16,4 @@ jobs: uses: upsidr/merge-gatekeeper@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - ignored: "require-review / check-approvals" \ No newline at end of file + ignored: "check-approvals" \ No newline at end of file diff --git a/.github/workflows/pr-require-bot-approvals.yml b/.github/workflows/pr-require-bot-approvals.yml index 48739e0..f383ea1 100644 --- a/.github/workflows/pr-require-bot-approvals.yml +++ b/.github/workflows/pr-require-bot-approvals.yml @@ -1,3 +1,11 @@ +# This is an inlined copy of +# censys/shared-actions/.github/workflows/reusable-require-bot-pr-approvals.yaml +# because this repository is public and cannot reference the private +# shared-actions repository. Keep the two in sync when either changes. +# +# The reusable workflow's inputs are resolved here to the values this repo +# needs; the censys-org runner setup is dropped since we run on ubuntu-latest, +# where the GitHub CLI is preinstalled. name: PR - Require Bot Approvals on: @@ -7,5 +15,85 @@ on: types: [ submitted, dismissed ] jobs: - require-review: - uses: censys/shared-actions/.github/workflows/reusable-require-bot-pr-approvals.yaml@main + check-approvals: + runs-on: ubuntu-latest + permissions: + pull-requests: read + statuses: write + env: + # Comma-separated list of GitHub logins that require the extra approval count. + BOT_LOGINS: 'claude[bot]' + # Number of approving reviews required on PRs opened by one of BOT_LOGINS. + REQUIRED_APPROVALS: 2 + # Name of the commit status this workflow sets to reflect the actual + # gating decision (success/pending). This exact name is what branch + # protection must require - the job itself always succeeds. + STATUS_CONTEXT: 'Additional Reviewers Required' + steps: + # This step always exits 0 - it never blocks merge directly. It only + # computes the gating decision; the actual required-status-check is the + # commit status set by the "Set commit status" step below, which is what + # branch protection should be configured against (see STATUS_CONTEXT). + - name: Check approving review count + id: check + env: + GITHUB_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + run: | + set -euo pipefail + + is_gated_author=false + IFS=',' read -ra BOTS <<< "$BOT_LOGINS" + for bot in "${BOTS[@]}"; do + bot_trimmed="$(echo "$bot" | xargs)" + if [ "$bot_trimmed" = "$PR_AUTHOR" ]; then + is_gated_author=true + break + fi + done + + if [ "$is_gated_author" != "true" ]; then + echo "PR author '$PR_AUTHOR' is not in the gated BOT_LOGINS list ($BOT_LOGINS) - no extra approvals required." + echo "state=success" >> "$GITHUB_OUTPUT" + echo "description=PR author is not a gated bot login; no extra approvals required." >> "$GITHUB_OUTPUT" + exit 0 + fi + + approval_count=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUMBER/reviews" --paginate | \ + jq '[.[] | {user: .user.login, state: .state, id: .id}] | group_by(.user) | map(max_by(.id)) | map(select(.state == "APPROVED")) | length') + + if ! [[ "$approval_count" =~ ^[0-9]+$ ]]; then + echo "::error::Could not determine approving review count for PR #$PR_NUMBER (got '$approval_count'); failing closed via pending status." + echo "state=pending" >> "$GITHUB_OUTPUT" + echo "description=Could not determine approving review count; treating as not yet satisfied." >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "PR #$PR_NUMBER opened by '$PR_AUTHOR' currently has $approval_count approving review(s); $REQUIRED_APPROVALS required." + + if [ "$approval_count" -lt "$REQUIRED_APPROVALS" ]; then + echo "state=pending" >> "$GITHUB_OUTPUT" + echo "description=$approval_count/$REQUIRED_APPROVALS approvals required." >> "$GITHUB_OUTPUT" + else + echo "state=success" >> "$GITHUB_OUTPUT" + echo "description=Required approvals met ($approval_count/$REQUIRED_APPROVALS)." >> "$GITHUB_OUTPUT" + fi + + - name: Set commit status + uses: actions/github-script@v7 + env: + STATE: ${{ steps.check.outputs.state }} + DESCRIPTION: ${{ steps.check.outputs.description }} + with: + script: | + const sha = context.payload.pull_request.head.sha; + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha, + state: process.env.STATE, + context: process.env.STATUS_CONTEXT, + description: process.env.DESCRIPTION, + target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + });