Auto-Approve Clean PRs #165
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Auto-Approve Clean PRs | |
| on: | |
| workflow_run: | |
| workflows: [".github/workflows/base.yml", "PyDeequ Bot"] | |
| types: [completed] | |
| permissions: | |
| pull-requests: write | |
| actions: read | |
| jobs: | |
| approve: | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.event == 'pull_request' || github.event.workflow_run.event == 'pull_request_target' | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Find PR and check both conditions | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const sha = context.payload.workflow_run.head_sha; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // Find the PR for this SHA | |
| let prNumber = null; | |
| const prs = context.payload.workflow_run.pull_requests; | |
| if (prs && prs.length > 0) { | |
| prNumber = prs[0].number; | |
| } else { | |
| const {data: searchResult} = await github.rest.pulls.list({ | |
| owner, repo, state: 'open', sort: 'updated', direction: 'desc', per_page: 30 | |
| }); | |
| const match = searchResult.find(pr => pr.head.sha === sha); | |
| if (match) { | |
| prNumber = match.number; | |
| } | |
| } | |
| if (!prNumber) { | |
| core.info(`No open PR found for SHA ${sha}, skipping`); | |
| return; | |
| } | |
| core.info(`Found PR #${prNumber} for SHA ${sha}`); | |
| // Verify the PR head SHA still matches (no new push since trigger) | |
| const {data: pr} = await github.rest.pulls.get({ | |
| owner, repo, pull_number: prNumber | |
| }); | |
| if (pr.head.sha !== sha) { | |
| core.info(`PR head ${pr.head.sha} differs from trigger SHA ${sha} — new push arrived, skipping`); | |
| return; | |
| } | |
| // Condition 1: CI must have passed for this SHA | |
| const {data: workflowRuns} = await github.rest.actions.listWorkflowRunsForRepo({ | |
| owner, repo, head_sha: sha, status: 'completed' | |
| }); | |
| const ciRun = workflowRuns.workflow_runs.find(r => | |
| r.name === '.github/workflows/base.yml' && r.conclusion === 'success' | |
| ); | |
| if (!ciRun) { | |
| core.info(`CI has not passed for SHA ${sha}, skipping`); | |
| return; | |
| } | |
| // Condition 2: Bot must have posted a clean review for this SHA | |
| const {data: reviews} = await github.rest.pulls.listReviews({ | |
| owner, repo, pull_number: prNumber | |
| }); | |
| const CLEAN_MARKER = '<!-- deequ-bot:clean -->'; | |
| const latestBot = reviews | |
| .filter(r => r.user.login === 'github-actions[bot]') | |
| .sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at))[0]; | |
| if (!latestBot || !latestBot.body.includes(CLEAN_MARKER) || latestBot.commit_id !== sha) { | |
| core.info('Bot has not posted a clean review for this SHA, skipping'); | |
| return; | |
| } | |
| // Both conditions met — check for existing approval to prevent doubles | |
| const botApprovals = reviews.filter(r => | |
| r.user.login === 'github-actions[bot]' && r.state === 'APPROVED' | |
| ); | |
| if (botApprovals.length > 0) { | |
| core.info('Bot already approved this PR, skipping'); | |
| return; | |
| } | |
| // Approve | |
| core.info(`Approving PR #${prNumber}: bot review clean + CI passed for SHA ${sha}`); | |
| await github.rest.pulls.createReview({ | |
| owner, repo, pull_number: prNumber, | |
| event: 'APPROVE', | |
| body: `No issues found and CI is passing. Auto-approved.\n\n---\n*Generated by AI — human merge required.*` | |
| }); |