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 { 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 { 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.", ); }