Archived
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import type { Job } from "../../../../adapters/database/store.js";
|
|
import {
|
|
parseUnifiedDiff,
|
|
renderPullReviewComments,
|
|
validateStructuredFindings,
|
|
} from "../../../../adapters/git/diff.js";
|
|
import { workspaceDiff } from "../../../../adapters/git/repository/changes.js";
|
|
import { headSha } from "../../../../adapters/git/repository/checkout.js";
|
|
import {
|
|
marker,
|
|
parseMarker,
|
|
protocolVersion,
|
|
sha256,
|
|
} from "../../../../core/contracts.js";
|
|
import {
|
|
type PublicationContext,
|
|
type PublicationOutcome,
|
|
upsertJobStatus,
|
|
} from "../../status.js";
|
|
|
|
export async function publishPullReview(
|
|
context: PublicationContext,
|
|
job: Job,
|
|
): Promise<PublicationOutcome> {
|
|
const review = job.result?.review;
|
|
const workspace = job.workspace;
|
|
if (review?.kind !== "pull" || !workspace)
|
|
throw new Error("Pull review result is incomplete");
|
|
const pull = await context.client.getPullRequest(job.targetNumber);
|
|
if (
|
|
pull.state !== "open" ||
|
|
pull.number !== review.pullRequestNumber ||
|
|
pull.head.sha !== review.headSha ||
|
|
pull.merge_base !== review.mergeBaseSha
|
|
)
|
|
throw new Error(
|
|
"Pull request changed while its review was being published",
|
|
);
|
|
const options = context.signal ? { signal: context.signal } : {};
|
|
if ((await headSha(workspace, options)) !== review.headSha)
|
|
throw new Error(
|
|
"Review workspace does not match the pull request head",
|
|
);
|
|
const diff = await workspaceDiff(workspace, review.mergeBaseSha, options);
|
|
if (sha256(diff) !== review.diffDigest)
|
|
throw new Error("Pull-request diff changed after review");
|
|
const parsed = parseUnifiedDiff(diff);
|
|
const findings = validateStructuredFindings(review.findings, parsed);
|
|
const reviewMarker = {
|
|
v: protocolVersion,
|
|
kind: "review" as const,
|
|
issue: job.issueNumber,
|
|
mode: "implement" as const,
|
|
request: job.id,
|
|
pullRequest: job.targetNumber,
|
|
};
|
|
const reviews = await context.client.listPullReviews(job.targetNumber);
|
|
const existing = reviews.find((candidate) => {
|
|
if (
|
|
candidate.user.login.toLowerCase() !==
|
|
context.botLogin.toLowerCase()
|
|
)
|
|
return false;
|
|
const found = parseMarker(candidate.body, "review");
|
|
return (
|
|
found?.request === job.id && found.pullRequest === job.targetNumber
|
|
);
|
|
});
|
|
const pending = reviews.filter(
|
|
(candidate) =>
|
|
candidate.user.login.toLowerCase() ===
|
|
context.botLogin.toLowerCase() &&
|
|
candidate.state.toUpperCase() === "PENDING" &&
|
|
(!candidate.commit_id || candidate.commit_id === review.headSha),
|
|
);
|
|
for (const candidate of pending)
|
|
await context.client.deletePullReview(job.targetNumber, candidate.id);
|
|
if (existing && existing.state.toUpperCase() !== "PENDING") {
|
|
await publishStatus(context, job, findings.length);
|
|
return { terminal: "succeeded", reviewId: existing.id };
|
|
}
|
|
const general = review.generalFindings.length
|
|
? `\n\n## General findings\n\n${review.generalFindings.map((item) => `- ${item}`).join("\n")}`
|
|
: "";
|
|
const body = `${marker(reviewMarker)}\n${review.summary}${general}`;
|
|
const published = await context.client.createPullReview(job.targetNumber, {
|
|
event: "COMMENT",
|
|
body,
|
|
commit_id: review.headSha,
|
|
comments: renderPullReviewComments(findings),
|
|
});
|
|
await publishStatus(context, job, findings.length);
|
|
return { terminal: "succeeded", reviewId: published.id };
|
|
}
|
|
|
|
async function publishStatus(
|
|
context: PublicationContext,
|
|
job: Job,
|
|
findingCount: number,
|
|
): Promise<void> {
|
|
await upsertJobStatus(
|
|
context.client,
|
|
context.botLogin,
|
|
job,
|
|
"Agent pull request review completed",
|
|
findingCount
|
|
? `${findingCount} inline finding(s) were published.`
|
|
: "No blocking inline findings were reported.",
|
|
);
|
|
}
|