-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkapi-agent-formal-approval-gate.yml
More file actions
87 lines (84 loc) · 4.66 KB
/
kapi-agent-formal-approval-gate.yml
File metadata and controls
87 lines (84 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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 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: pr.number });
const prNode = data.repository.pullRequest;
const reviews = prNode.reviews.nodes || [];
const comments = prNode.comments.nodes || [];
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.author?.login === reviewer)
.sort((a, b) => String(a.submittedAt || '').localeCompare(String(b.submittedAt || '')))
.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?.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`);
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: #${pr.number}\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('; '));