mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 07:59:49 +00:00
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 headf4c375eaa7. - Required merge gates passed before the squash merge. Prepared head SHA:f4c375eaa7Review: 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:
@@ -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))}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user