ci: require formal kapi-agent approval #5
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: kapi-agent formal approval gate | |
| on: | |
| pull_request: | |
| types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] | |
| pull_request_review: | |
| types: [submitted, edited, dismissed] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| issues: read | |
| checks: read | |
| statuses: read | |
| jobs: | |
| require-formal-kapi-agent-approval: | |
| name: require formal kapi-agent approval | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Require formal current-head kapi-agent approval | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const reviewer = 'kapi-agent'; | |
| const requiredCheck = 'kapi-agent/review'; | |
| const pr = context.payload.pull_request; | |
| if (!pr) core.setFailed('This workflow must run on pull_request or pull_request_review events.'); | |
| const { owner, repo } = context.repo; | |
| const pull = (await github.rest.pulls.get({ owner, repo, pull_number: pr.number })).data; | |
| const reviews = await github.paginate(github.rest.pulls.listReviews, { owner, repo, pull_number: pr.number, per_page: 100 }); | |
| const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 }); | |
| const checkRuns = (await github.rest.checks.listForRef({ owner, repo, ref: pull.head.sha, per_page: 100 })).data.check_runs; | |
| const statuses = (await github.rest.repos.getCombinedStatusForRef({ owner, repo, ref: pull.head.sha })).data.statuses; | |
| const latestReview = reviews | |
| .filter((review) => review.user?.login === reviewer) | |
| .sort((a, b) => String(a.submitted_at || '').localeCompare(String(b.submitted_at || '')) || Number(a.id) - Number(b.id)) | |
| .at(-1); | |
| const checkRun = checkRuns | |
| .filter((check) => check.name === requiredCheck) | |
| .sort((a, b) => String(a.completed_at || a.started_at || '').localeCompare(String(b.completed_at || b.started_at || '')) || Number(a.id) - Number(b.id)) | |
| .at(-1); | |
| const status = statuses | |
| .filter((item) => item.context === requiredCheck) | |
| .sort((a, b) => String(a.updated_at || a.created_at || '').localeCompare(String(b.updated_at || b.created_at || '')) || Number(a.id) - Number(b.id)) | |
| .at(-1); | |
| const checkState = checkRun?.conclusion || checkRun?.status || status?.state; | |
| const approvalCommentCount = comments.filter((comment) => /^## kapi-agent review\s*\n\s*\n\*\*Verdict:\*\*\s*APPROVE(?:\s*\n|\s*$)/i.test(String(comment.body || '').trimStart())).length; | |
| const diagnostics = []; | |
| if (pull.draft) diagnostics.push('PR is draft'); | |
| if (!latestReview) diagnostics.push(`missing formal Pull Request Review by ${reviewer}`); | |
| if (latestReview && latestReview.state !== 'APPROVED') diagnostics.push(`latest formal ${reviewer} review is ${latestReview.state}`); | |
| if (latestReview && latestReview.commit_id !== pull.head.sha) diagnostics.push(`latest formal ${reviewer} review is not for current head ${pull.head.sha}`); | |
| if (!checkState) diagnostics.push(`missing ${requiredCheck} check/status on current head`); | |
| if (checkState && checkState !== 'success') diagnostics.push(`${requiredCheck} is ${checkState}`); | |
| if (approvalCommentCount) diagnostics.push(`ignored ${approvalCommentCount} approval-shaped comment(s); comments are not PR reviews`); | |
| await core.summary | |
| .addHeading('kapi-agent formal approval gate') | |
| .addRaw(`PR: #${pr.number}\n`) | |
| .addRaw(`Head: ${pull.head.sha}\n`) | |
| .addRaw(`Latest formal review: ${latestReview ? `${latestReview.user?.login} / ${latestReview.state} / ${latestReview.commit_id || 'no-commit'}` : 'missing'}\n`) | |
| .addRaw(`Required check: ${checkState || 'missing'}\n`) | |
| .addList(diagnostics.length ? diagnostics : ['pass']) | |
| .write(); | |
| if (diagnostics.length) core.setFailed(diagnostics.join('; ')); |