fix(ci): authenticate proof verdict markers (#83692)

Summary:
- The branch restricts exact-head ClawSweeper proof markers to GitHub App-authored comments, adds read-only issue-comment token fallback for the proof workflow, and adds focused regression tests plus a changelog entry.
- Reproducibility: yes. Source inspection of current main shows any issue comment body with a matching `clawsw ...  SHA is accepted without author/App authentication; the PR adds focused negative tests for forged comments.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(ci): authenticate proof verdict markers

Validation:
- ClawSweeper review passed for head f4c375eaa7.
- Required merge gates passed before the squash merge.

Prepared head SHA: f4c375eaa7
Review: https://github.com/openclaw/openclaw/pull/83692#issuecomment-4479843682

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
Tak Hoffman
2026-05-18 12:42:10 -05:00
committed by GitHub
parent 0901801238
commit 06a39015f2
6 changed files with 161 additions and 32 deletions

View File

@@ -14,6 +14,41 @@ function escapeCommandValue(value) {
.replace(/:/g, "%3A");
}
async function fetchProofComments({ owner, repo, issueNumber, tokens }) {
let lastError;
for (const token of tokens.filter(Boolean)) {
const comments = [];
try {
for (let page = 1; page <= 10; page += 1) {
const url = new URL(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
);
url.searchParams.set("per_page", "100");
url.searchParams.set("page", String(page));
const response = await fetch(url, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) {
throw new Error(`comments API returned ${response.status}`);
}
const pageComments = await response.json();
comments.push(...pageComments);
if (pageComments.length < 100) {
break;
}
}
return comments;
} catch (error) {
lastError = error;
}
}
throw lastError ?? new Error("No GitHub token available for proof comment lookup.");
}
const eventPath = process.env.GITHUB_EVENT_PATH;
if (!eventPath) {
console.error("::error title=Real behavior proof failed::GITHUB_EVENT_PATH is not set.");
@@ -51,41 +86,29 @@ if (evaluation.passed) {
process.exit(0);
}
const token = appToken || process.env.GITHUB_TOKEN;
const repository = process.env.GITHUB_REPOSITORY;
if (token && repository && pullRequest.number) {
if ((appToken || process.env.GITHUB_TOKEN) && repository && pullRequest.number) {
const [owner, repo] = repository.split("/");
const comments = [];
for (let page = 1; page <= 10; page += 1) {
const url = new URL(
`https://api.github.com/repos/${owner}/${repo}/issues/${pullRequest.number}/comments`,
);
url.searchParams.set("per_page", "100");
url.searchParams.set("page", String(page));
const response = await fetch(url, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
},
try {
const comments = await fetchProofComments({
owner,
repo,
issueNumber: pullRequest.number,
tokens: [appToken, process.env.GITHUB_TOKEN],
});
if (!response.ok) {
throw new Error(`Failed to fetch PR comments for proof verdicts: ${response.status}`);
}
const pageComments = await response.json();
comments.push(...pageComments);
if (pageComments.length < 100) {
break;
}
}
const clawSweeperEvaluation = evaluateClawSweeperExactHeadProof({
pullRequest,
comments,
});
if (clawSweeperEvaluation.passed) {
console.log(clawSweeperEvaluation.reason);
process.exit(0);
const clawSweeperEvaluation = evaluateClawSweeperExactHeadProof({
pullRequest,
comments,
});
if (clawSweeperEvaluation.passed) {
console.log(clawSweeperEvaluation.reason);
process.exit(0);
}
} catch (error) {
console.warn(
`::warning title=Proof verdict comment lookup failed::${escapeCommandValue(error?.message ?? String(error))}`,
);
}
}

View File

@@ -242,6 +242,13 @@ function extractMarkerField(marker, name) {
return match?.[1] ?? "";
}
function isTrustedClawSweeperComment(comment) {
const appSlug = String(
comment?.performed_via_github_app?.slug ?? comment?.performedViaGithubApp?.slug ?? "",
).toLowerCase();
return appSlug === "clawsweeper";
}
export function hasClawSweeperExactHeadProof({ pullRequest, comments = [] } = {}) {
const pullNumber = String(pullRequest?.number ?? "");
const headSha = String(pullRequest?.head?.sha ?? pullRequest?.head_sha ?? "").toLowerCase();
@@ -250,6 +257,9 @@ export function hasClawSweeperExactHeadProof({ pullRequest, comments = [] } = {}
}
for (const comment of comments) {
if (!isTrustedClawSweeperComment(comment)) {
continue;
}
const body = String(comment?.body ?? "");
const markers = body.match(/<!--\s*clawsweeper-verdict:pass\b[\s\S]*?-->/gi) ?? [];
for (const marker of markers) {