Skip to content

kapi-agent formal approval gate #385

kapi-agent formal approval gate

kapi-agent formal approval gate #385

name: kapi-agent formal approval gate
on:
check_run:
types: [completed]
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
if: >
github.event_name == 'pull_request_review' ||
(
github.event_name == 'check_run' &&
github.event.check_run.name == 'kapi-agent/review'
)
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 maxAttempts = 12;
const retryDelayMs = 10_000;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request?.number || context.payload.check_run?.pull_requests?.[0]?.number;
if (!prNumber) {
core.setFailed('Could not determine pull request number from event payload.');
return;
}
if (context.eventName === 'check_run' && context.payload.check_run?.name !== requiredCheck) {
core.info(`Ignoring unrelated check_run: ${context.payload.check_run?.name || 'unknown'}`);
return;
}
const pull = (await github.rest.pulls.get({ owner, repo, pull_number: prNumber })).data;
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
comments(last: 100) { nodes { body } }
reviews(last: 100) {
nodes {
author { login }
state
submittedAt
commit { oid }
}
}
}
}
}
`;
const data = await github.graphql(query, { owner, repo, number: prNumber });
const prNode = data.repository.pullRequest;
const reviews = prNode.reviews.nodes || [];
const comments = prNode.comments.nodes || [];
const latestReview = reviews
.filter((review) => review.author?.login === reviewer)
.sort((a, b) => String(a.submittedAt || '').localeCompare(String(b.submittedAt || '')))
.at(-1);
async function getRequiredCheckState() {
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 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);
return checkRun?.conclusion || checkRun?.status || status?.state;
}
let checkState;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
checkState = await getRequiredCheckState();
if (checkState === 'success' || checkState === 'failure' || checkState === 'error' || checkState === 'cancelled' || checkState === 'skipped') {
break;
}
core.info(`${requiredCheck} is ${checkState || 'missing'}; waiting before final approval gate attempt ${attempt}/${maxAttempts}.`);
if (attempt < maxAttempts) await sleep(retryDelayMs);
}
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?.oid !== 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 after waiting`);
if (checkState && checkState !== 'success') diagnostics.push(`${requiredCheck} is ${checkState}`);
const summaryItems = diagnostics.length ? [...diagnostics] : ['pass'];
if (approvalCommentCount) {
summaryItems.push(`ignored ${approvalCommentCount} approval-shaped comment(s); comments are not PR reviews`);
}
await core.summary
.addHeading('kapi-agent formal approval gate')
.addRaw(`PR: #${prNumber}\n`)
.addRaw(`Head: ${pull.head.sha}\n`)
.addRaw(`Latest formal review: ${latestReview ? `${latestReview.author?.login} / ${latestReview.state} / ${latestReview.commit?.oid || 'no-commit'}` : 'missing'}\n`)
.addRaw(`Required check: ${checkState || 'missing'}\n`)
.addList(summaryItems)
.write();
if (diagnostics.length) core.setFailed(diagnostics.join('; '));