Skip to content

ci: require formal kapi-agent approval #5

ci: require formal kapi-agent approval

ci: require formal kapi-agent approval #5

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('; '));