Skip to content

Commit 1396437

Browse files
authored
ci: bootstrap trusted AI review verifier (#174)
Install the exact-head AI review verifier on the trusted default branch so product pull requests can execute governance code from their immutable base rather than from untrusted PR heads.
1 parent 39d77bc commit 1396437

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"use strict";
2+
3+
const TRUSTED_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
4+
const CODERABBIT_LOGINS = new Set(["coderabbitai[bot]", "coderabbitai"]);
5+
const CODEX_LOGINS = new Set([
6+
"chatgpt-codex-connector[bot]",
7+
"chatgpt-codex-connector",
8+
]);
9+
const CODERABBIT_STATUS_CONTEXT = "CodeRabbit";
10+
const POLL_ATTEMPTS = 45;
11+
const POLL_INTERVAL_MS = 20_000;
12+
13+
function parseTime(value) {
14+
const parsed = Date.parse(value ?? 0);
15+
return Number.isFinite(parsed) ? parsed : 0;
16+
}
17+
18+
function createdTimeOf(item) {
19+
return parseTime(item.created_at);
20+
}
21+
22+
function submittedTimeOf(review) {
23+
return parseTime(review.submitted_at);
24+
}
25+
26+
function isSubmittedActiveReview(review) {
27+
return (
28+
Boolean(review.submitted_at) &&
29+
review.state !== "PENDING" &&
30+
review.state !== "DISMISSED"
31+
);
32+
}
33+
34+
function commandLinesOf(item) {
35+
return (item.body ?? "")
36+
.toLowerCase()
37+
.split(/\r?\n/)
38+
.map((line) => line.trim())
39+
.filter(Boolean);
40+
}
41+
42+
function latestCreatedTime(items) {
43+
return items.reduce(
44+
(latest, item) => Math.max(latest, createdTimeOf(item)),
45+
0,
46+
);
47+
}
48+
49+
function isPermissionError(error) {
50+
const message = error?.message ?? "";
51+
if (error?.status === 401) return true;
52+
if (error?.status !== 403) return false;
53+
return !/rate limit|secondary rate|abuse detection/i.test(message);
54+
}
55+
56+
module.exports = async function verifyAiReviewContract({ github, context, core }) {
57+
const { owner, repo } = context.repo;
58+
const pr = context.payload.pull_request;
59+
if (!pr?.head?.sha) {
60+
core.setFailed("AI review verifier requires a pull_request event with a head SHA.");
61+
return;
62+
}
63+
64+
const currentHead = pr.head.sha.toLowerCase();
65+
let headUpdateAnchor = 0;
66+
let lastApiError = "";
67+
68+
for (let attempt = 1; attempt <= POLL_ATTEMPTS; attempt += 1) {
69+
let codeRabbitRequestAt = 0;
70+
let codeRabbitReview;
71+
let codeRabbitStatus;
72+
let nativeCodexReview;
73+
74+
try {
75+
if (headUpdateAnchor <= 0) {
76+
const run = await github.rest.actions.getWorkflowRun({
77+
owner,
78+
repo,
79+
run_id: context.runId,
80+
});
81+
headUpdateAnchor = parseTime(run.data.created_at);
82+
if (headUpdateAnchor <= 0) {
83+
throw new Error("workflow run has no immutable creation timestamp");
84+
}
85+
}
86+
87+
const [comments, reviews, statuses] = await Promise.all([
88+
github.paginate(github.rest.issues.listComments, {
89+
owner,
90+
repo,
91+
issue_number: pr.number,
92+
since: new Date(headUpdateAnchor).toISOString(),
93+
per_page: 100,
94+
}),
95+
github.paginate(github.rest.pulls.listReviews, {
96+
owner,
97+
repo,
98+
pull_number: pr.number,
99+
per_page: 100,
100+
}),
101+
github.paginate(github.rest.repos.listCommitStatusesForRef, {
102+
owner,
103+
repo,
104+
ref: currentHead,
105+
per_page: 100,
106+
}),
107+
]);
108+
109+
const freshRequest = (item, command) =>
110+
TRUSTED_ASSOCIATIONS.has(item.author_association) &&
111+
createdTimeOf(item) >= headUpdateAnchor &&
112+
commandLinesOf(item).includes(command);
113+
114+
codeRabbitRequestAt = latestCreatedTime(
115+
comments.filter((item) => freshRequest(item, "@coderabbitai review")),
116+
);
117+
const codexRequestAt = latestCreatedTime(
118+
comments.filter((item) => freshRequest(item, "@codex review")),
119+
);
120+
121+
codeRabbitReview = reviews.find(
122+
(review) =>
123+
CODERABBIT_LOGINS.has(review.user?.login) &&
124+
review.commit_id?.toLowerCase() === currentHead &&
125+
isSubmittedActiveReview(review) &&
126+
codeRabbitRequestAt > 0 &&
127+
submittedTimeOf(review) >= codeRabbitRequestAt,
128+
);
129+
130+
codeRabbitStatus = statuses.find(
131+
(status) =>
132+
status.context === CODERABBIT_STATUS_CONTEXT &&
133+
status.state === "success" &&
134+
CODERABBIT_LOGINS.has(status.creator?.login) &&
135+
codeRabbitRequestAt > 0 &&
136+
createdTimeOf(status) >= codeRabbitRequestAt,
137+
);
138+
139+
nativeCodexReview = reviews.find(
140+
(review) =>
141+
CODEX_LOGINS.has(review.user?.login) &&
142+
review.commit_id?.toLowerCase() === currentHead &&
143+
isSubmittedActiveReview(review) &&
144+
codexRequestAt > 0 &&
145+
submittedTimeOf(review) >= codexRequestAt,
146+
);
147+
} catch (error) {
148+
lastApiError = error?.message ?? String(error);
149+
if (isPermissionError(error)) {
150+
core.setFailed(
151+
"AI review verifier lacks required workflow permissions. Grant actions: read, issues: read, pull-requests: read, and statuses: read. " +
152+
`GitHub API error: ${lastApiError}`,
153+
);
154+
return;
155+
}
156+
core.warning(
157+
`Transient GitHub API error (${attempt}/${POLL_ATTEMPTS}): ${lastApiError}`,
158+
);
159+
}
160+
161+
if (codeRabbitRequestAt > 0 && (codeRabbitReview || codeRabbitStatus)) {
162+
const evidenceType = codeRabbitReview
163+
? "submitted exact-head pull-request review"
164+
: "successful exact-head commit status after the request";
165+
await core.summary
166+
.addHeading("AI review contract")
167+
.addTable([
168+
[
169+
{ data: "Lane", header: true },
170+
{ data: "Evidence", header: true },
171+
{ data: "Exact head", header: true },
172+
],
173+
["CodeRabbit (required)", evidenceType, "yes"],
174+
[
175+
"Codex (supplemental)",
176+
nativeCodexReview ? "native submitted review" : "not counted",
177+
nativeCodexReview ? "yes" : "n/a",
178+
],
179+
])
180+
.addRaw(
181+
`\nFreshness anchor: workflow run ${context.runId}; head ${pr.head.sha}. ` +
182+
"Edited requests, pending/dismissed reviews, summaries, and older-head evidence do not count.\n",
183+
)
184+
.write();
185+
core.notice(
186+
`Verified native CodeRabbit evidence for ${pr.head.sha}: ${evidenceType}.`,
187+
);
188+
return;
189+
}
190+
191+
core.info(
192+
`Waiting for native CodeRabbit evidence (${attempt}/${POLL_ATTEMPTS}); ` +
193+
`request=${codeRabbitRequestAt > 0}; review=${Boolean(codeRabbitReview)}; ` +
194+
`status=${Boolean(codeRabbitStatus)}; head=${pr.head.sha}`,
195+
);
196+
if (attempt < POLL_ATTEMPTS) {
197+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
198+
}
199+
}
200+
201+
core.setFailed(
202+
"Require a trusted @coderabbitai review request created after the immutable head-update anchor and native CodeRabbit evidence for that same head after the request: either a submitted active review or a successful bot-authored CodeRabbit commit status." +
203+
(lastApiError ? ` Last transient API error: ${lastApiError}` : ""),
204+
);
205+
};

0 commit comments

Comments
 (0)