PR Auto Approval #404
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: PR Auto Approval | |
| on: | |
| workflow_run: | |
| workflows: ["CI"] | |
| types: [completed] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| check-whether-to-approve: | |
| if: github.event.workflow_run.conclusion == 'success' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: | | |
| ./.github/actions | |
| - name: Get associated PR | |
| id: associated-pr | |
| uses: ./.github/actions/associated-pr | |
| with: | |
| ref: ${{ github.event.workflow_run.head_sha }} | |
| - name: Evaluate auto-approval criteria | |
| id: evaluate | |
| if: steps.associated-pr.outputs.number != '' && steps.associated-pr.outputs.closed != 'true' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| PR_NUMBER: ${{ steps.associated-pr.outputs.number }} | |
| PR_TITLE: ${{ steps.associated-pr.outputs.title }} | |
| PR_LABELS: ${{ steps.associated-pr.outputs.labels }} | |
| PR_AUTHOR: ${{ steps.associated-pr.outputs.author }} | |
| PR_HEAD_REPO: ${{ steps.associated-pr.outputs.head_repo }} | |
| with: | |
| script: | # js | |
| const prNumber = parseInt(process.env.PR_NUMBER || ""); | |
| const baseRepo = `${context.repo.owner}/${context.repo.repo}`; | |
| const headRepo = process.env.PR_HEAD_REPO ?? ""; | |
| if (headRepo && headRepo !== baseRepo) { | |
| core.info("Skipping: PR is from a fork"); | |
| return; | |
| } | |
| const login = process.env.PR_AUTHOR ?? ""; | |
| if (/bot/i.test(login)) { | |
| core.info(`Skipping: author "${login}" appears to be a bot`); | |
| return; | |
| } | |
| const labels = JSON.parse(process.env.PR_LABELS || "[]"); | |
| core.info(`PR labels: ${labels.join(", ")}`); | |
| if (!labels.includes("experienced-contributor")) { | |
| core.info("Skipping: missing experienced-contributor label"); | |
| return; | |
| } | |
| const allowedSizes = ["size: XS", "size: S", "size: M"]; | |
| if (!allowedSizes.some(size => labels.includes(size))) { | |
| core.info("Skipping: missing required size label (XS, S, or M)"); | |
| return; | |
| } | |
| const title = process.env.PR_TITLE ?? ""; | |
| const isChore = title.startsWith("chore:") || title.startsWith("chore("); | |
| const isTest = title.startsWith("test:") || title.startsWith("test("); | |
| const isDocs = title.startsWith("docs:") || title.startsWith("docs("); | |
| if (!isChore && !isTest && !isDocs) { | |
| core.info(`Skipping: commit is not one of these types: chore, test, or docs`); | |
| return; | |
| } | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| per_page: 100, | |
| }); | |
| const filenames = files.map(f => f.filename); | |
| if (isTest) { | |
| const testFilePattern = /(\.(spec|integration)\.(ts|tsx)$|\/tests?\/)|(jest|vitest)\.config\.[tj]s$/; | |
| const onlyTestFiles = filenames.every(f => testFilePattern.test(f)); | |
| if (!onlyTestFiles) { | |
| core.info("Skipping test: PR changes non-test files"); | |
| return; | |
| } | |
| core.setOutput("approval-reason", "all changes are related to tests"); | |
| } else if (isDocs) { | |
| const imagePattern = /\.(png|jpg|jpeg|gif|svg|webp|ico|bmp)$/i; | |
| const docsFilePattern = f => f.endsWith(".md") || imagePattern.test(f); | |
| const onlyDocFiles = filenames.every(docsFilePattern); | |
| if (!onlyDocFiles) { | |
| core.info("Skipping docs: PR changes non-documentation files"); | |
| return; | |
| } | |
| core.setOutput("approval-reason", "all changes are related to documentation"); | |
| } else if (isChore) { | |
| const sourceCodeFile = /\.(ts|tsx|js|jsx)$/; | |
| const touchesCode = filenames.some(f => { | |
| return sourceCodeFile.test(f) && (f.includes("/packages/") || f.includes("/apps/")); | |
| }); | |
| if (touchesCode) { | |
| core.info("Skipping chore: PR touches code files in packages/ or apps/"); | |
| return; | |
| } | |
| core.setOutput("approval-reason", "chore that does not touch source code files"); | |
| } | |
| - name: Approve PR | |
| if: steps.evaluate.outputs.approval-reason != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUMBER: ${{ steps.associated-pr.outputs.number }} | |
| APPROVAL_REASON: ${{ steps.evaluate.outputs.approval-reason }} | |
| run: | # shell | |
| should_skip=$(gh pr view "$PR_NUMBER" \ | |
| -R "$GITHUB_REPOSITORY" \ | |
| --json closed,reviews \ | |
| --jq '.closed or any( | |
| .reviews[]; | |
| .state == "APPROVED" | |
| and .author.login == "github-actions[bot]" | |
| )' | |
| ); | |
| if [ "$should_skip" = "true" ]; then | |
| echo "PR #$PR_NUMBER is already closed or approved, skipping approval"; | |
| else | |
| gh pr review "$PR_NUMBER" -R "$GITHUB_REPOSITORY" --approve --body "Auto-approved: $APPROVAL_REASON." | |
| fi |