Repository navigation
Physical scale: detect the audited shape (no unit / [1,1,1]) and use the manifest pitch #4518
Workflow file for this run
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: Large PR review gate | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] | |
| pull_request_review: | |
| types: [submitted, edited, dismissed] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| gate: | |
| name: Gate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Block large PRs unless approved | |
| uses: actions/github-script@v7 | |
| env: | |
| LARGE_PR_FILE_THRESHOLD: "20" | |
| with: | |
| script: | | |
| const threshold = Number(process.env.LARGE_PR_FILE_THRESHOLD || "20"); | |
| const { owner, repo } = context.repo; | |
| const prNumber = context.payload.pull_request?.number ?? context.issue.number; | |
| if (!prNumber) { | |
| core.setFailed("Could not determine PR number from event payload."); | |
| return; | |
| } | |
| const prResp = await github.rest.pulls.get({ | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| }); | |
| const pr = prResp.data; | |
| const changedFiles = pr.changed_files ?? 0; | |
| const author = pr.user?.login; | |
| core.info(`PR #${prNumber}: changed_files=${changedFiles}, threshold=${threshold}, author=${author}`); | |
| if (pr.draft) { | |
| core.info("Draft PR detected — skipping gate."); | |
| return; | |
| } | |
| const reviews = await github.paginate(github.rest.pulls.listReviews, { | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| per_page: 100, | |
| }); | |
| // Sort defensively so "last review wins" is stable even if API ordering changes. | |
| reviews.sort((a, b) => { | |
| const aTime = new Date(a.submitted_at ?? 0).getTime(); | |
| const bTime = new Date(b.submitted_at ?? 0).getTime(); | |
| if (aTime !== bTime) return aTime - bTime; | |
| return (a.id ?? 0) - (b.id ?? 0); | |
| }); | |
| const latestStateByUser = new Map(); | |
| const decisionStates = new Set(["APPROVED", "CHANGES_REQUESTED", "DISMISSED"]); | |
| for (const r of reviews) { | |
| const login = r.user?.login; | |
| if (!login) continue; | |
| if (login === author) continue; | |
| if (!decisionStates.has(r.state)) continue; | |
| latestStateByUser.set(login, r.state); | |
| } | |
| let approvals = 0; | |
| for (const state of latestStateByUser.values()) { | |
| if (state === "APPROVED") approvals += 1; | |
| } | |
| core.info(`Approvals (excluding author): ${approvals}`); | |
| if (changedFiles > threshold && approvals < 1) { | |
| core.setFailed( | |
| `This PR changes ${changedFiles} files (> ${threshold}). Add at least 1 approving review to merge.` | |
| ); | |
| } else { | |
| core.info("Gate passed."); | |
| } |