import { Buffer } from "node:buffer"; import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; export type PublishMode = "auto" | "force" | "disabled"; export type AutoPostSource = "default" | "user" | "project"; export type CompletionAction = "continue_tools" | "accept_final" | "clear_invocation"; /** Maximum severity that may appear in a review for auto-APPROVE to be granted. */ export type ApproveMaxPriorityLevel = "off" | "P2" | "P3" | "nit"; const APPROVE_PRIORITY_LEVELS = ["P2", "P3", "nit"] as const; const APPROVE_PRIORITY_RANK: Record = { P0: 4, P1: 3, P2: 2, P3: 1, nit: 0, NIT: 0, }; export interface ApproveMaxPriorityLevelResolution { readonly value: ApproveMaxPriorityLevel; readonly valid: boolean; readonly source: AutoPostSource; readonly error?: string; } function isValidApproveLevel(value: unknown): value is ApproveMaxPriorityLevel { return value === "off" || (typeof value === "string" && APPROVE_PRIORITY_LEVELS.includes(value as (typeof APPROVE_PRIORITY_LEVELS)[number])); } /** Resolve the auto-approve priority gate with trusted project config overlaying user config. */ export function resolveApproveMaxPriorityLevelSetting( user: unknown, trustedProject?: unknown, ): ApproveMaxPriorityLevelResolution { if (hasOwn(trustedProject, "approveMaxPriorityLevel")) { const value = (trustedProject as { approveMaxPriorityLevel?: unknown }).approveMaxPriorityLevel; return isValidApproveLevel(value) ? { value, valid: true, source: "project" } : { value: "off", valid: false, source: "project", error: `project approveMaxPriorityLevel must be one of: off, ${APPROVE_PRIORITY_LEVELS.join(", ")}`, }; } if (hasOwn(user, "approveMaxPriorityLevel")) { const value = (user as { approveMaxPriorityLevel?: unknown }).approveMaxPriorityLevel; return isValidApproveLevel(value) ? { value, valid: true, source: "user" } : { value: "off", valid: false, source: "user", error: `user approveMaxPriorityLevel must be one of: off, ${APPROVE_PRIORITY_LEVELS.join(", ")}`, }; } return { value: "off", valid: true, source: "default" }; } /** Whether all findings in a review are at or below the configured maximum priority. */ export function findingsWithinApproveMaxPriority( review: ReviewLike, level: ApproveMaxPriorityLevel, ): boolean { if (level === "off") return false; const maxRank = APPROVE_PRIORITY_RANK[level]; if (maxRank === undefined) return false; const findings = Array.isArray(review.findings) ? review.findings : []; return findings.every( (finding) => (APPROVE_PRIORITY_RANK[String(finding.severity ?? "").toUpperCase()] ?? Infinity) <= maxRank, ); } /** Decide whether a review should be published as APPROVE instead of COMMENT. */ export function shouldApproveReview( review: ReviewLike, approveMaxPriorityLevel: ApproveMaxPriorityLevel, ): boolean { const findings = Array.isArray(review.findings) ? review.findings : []; return ( review.verdict === "approve" && approveMaxPriorityLevel !== "off" && findings.every((finding) => !finding.blocking) && findingsWithinApproveMaxPriority(review, approveMaxPriorityLevel) ); } export function classifyAssistantCompletion( stopReason: string | undefined, hasToolCall: boolean, ): CompletionAction { if (stopReason === "toolUse" && hasToolCall) return "continue_tools"; if (stopReason === "stop" && !hasToolCall) return "accept_final"; return "clear_invocation"; } export interface AutoPostResolution { readonly value: boolean; readonly valid: boolean; readonly source: AutoPostSource; readonly error?: string; } function hasOwn(value: unknown, key: string): boolean { return !!value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key); } /** Resolve a strict boolean with trusted project config overlaying user config. */ export function resolveAutoPostSetting(user: unknown, trustedProject?: unknown): AutoPostResolution { if (hasOwn(trustedProject, "autoPostReviews")) { const value = (trustedProject as { autoPostReviews?: unknown }).autoPostReviews; return typeof value === "boolean" ? { value, valid: true, source: "project" } : { value: false, valid: false, source: "project", error: "project autoPostReviews must be a boolean", }; } if (hasOwn(user, "autoPostReviews")) { const value = (user as { autoPostReviews?: unknown }).autoPostReviews; return typeof value === "boolean" ? { value, valid: true, source: "user" } : { value: false, valid: false, source: "user", error: "user autoPostReviews must be a boolean", }; } return { value: false, valid: true, source: "default" }; } /** Resolve whether stale cached reviews may publish, enabled by default. */ export function resolveAllowStalePublishSetting( user: unknown, trustedProject?: unknown, ): AutoPostResolution { if (hasOwn(trustedProject, "allowStalePublish")) { const value = (trustedProject as { allowStalePublish?: unknown }).allowStalePublish; return typeof value === "boolean" ? { value, valid: true, source: "project" } : { value: false, valid: false, source: "project", error: "project allowStalePublish must be a boolean", }; } if (hasOwn(user, "allowStalePublish")) { const value = (user as { allowStalePublish?: unknown }).allowStalePublish; return typeof value === "boolean" ? { value, valid: true, source: "user" } : { value: false, valid: false, source: "user", error: "user allowStalePublish must be a boolean", }; } return { value: true, valid: true, source: "default" }; } /** Resolve whether an otherwise-qualified stale review may record APPROVE. Disabled by default. */ export function resolveAllowStaleApprovalsSetting( user: unknown, trustedProject?: unknown, ): AutoPostResolution { if (hasOwn(trustedProject, "allowStaleApprovals")) { const value = (trustedProject as { allowStaleApprovals?: unknown }).allowStaleApprovals; return typeof value === "boolean" ? { value, valid: true, source: "project" } : { value: false, valid: false, source: "project", error: "project allowStaleApprovals must be a boolean", }; } if (hasOwn(user, "allowStaleApprovals")) { const value = (user as { allowStaleApprovals?: unknown }).allowStaleApprovals; return typeof value === "boolean" ? { value, valid: true, source: "user" } : { value: false, valid: false, source: "user", error: "user allowStaleApprovals must be a boolean", }; } return { value: false, valid: true, source: "default" }; } export interface PublishModeParseResult { matched: boolean; mode?: PublishMode; prNumber?: number; allowNonOpen?: boolean; error?: string; } /** Parse trusted raw prompt-template invocation flags before template expansion. */ export function parsePublishMode(input: string): PublishModeParseResult { const trimmed = input.trim(); if (!/^\/pr-review(?:\s|$)/.test(trimmed)) return { matched: false }; const tokens = trimmed.split(/\s+/); const requested = Number(tokens[1]); if (!Number.isInteger(requested) || requested <= 0) { return { matched: true, error: "a positive PR number must be the first argument" }; } const force = tokens.includes("--comment"); const disabled = tokens.includes("--no-comment"); const full = tokens.includes("--full"); const majorOnly = tokens.includes("--major-only"); const balanced = tokens.includes("--balanced"); if (force && disabled) { return { matched: true, error: "--comment and --no-comment cannot be used together" }; } if ([full, majorOnly, balanced].filter(Boolean).length > 1) { return { matched: true, error: "--full, --major-only, and --balanced cannot be used together" }; } return { matched: true, mode: disabled ? "disabled" : force ? "force" : "auto", prNumber: requested, allowNonOpen: tokens.includes("--include-closed") || tokens.includes("--review-closed"), }; } export interface ReviewInvocation { readonly mode: PublishMode; readonly prNumber: number; readonly allowNonOpen: boolean; /** Trusted stale-publication setting captured before review execution begins. */ readonly allowStalePublish: boolean; /** Trusted stale-approval setting captured before review execution begins. */ readonly allowStaleApprovals: boolean; /** Trusted automatic-posting decision captured before review execution begins. */ readonly autoPost: Readonly; /** Trusted auto-approve priority gate captured before review execution begins. */ readonly approveMaxPriorityLevel: ApproveMaxPriorityLevel; } export interface ReviewPublicationDecision { readonly publish: boolean; readonly source?: "--comment" | `${AutoPostSource} config`; readonly error?: string; } /** Derive write authority exclusively from invocation flags and its frozen config snapshot. */ export function decideReviewPublication(invocation: ReviewInvocation): ReviewPublicationDecision { if (invocation.mode === "disabled") return { publish: false }; if (invocation.mode === "force") return { publish: true, source: "--comment" }; if (!invocation.autoPost.valid) { return { publish: false, error: invocation.autoPost.error ?? `${invocation.autoPost.source} autoPostReviews is invalid`, }; } return invocation.autoPost.value ? { publish: true, source: `${invocation.autoPost.source} config` } : { publish: false }; } export interface DirectPublishRequestParseResult { matched: boolean; prNumber?: number; } /** Narrow whole-input matcher for direct natural-language cached publish requests. */ export function parseDirectPublishRequest(input: string): DirectPublishRequestParseResult { const trimmed = input.trim(); if (!trimmed || /[\r\n]/.test(trimmed)) return { matched: false }; const match = trimmed.match( /^(?:(?:please|kindly)\s+|(?:(?:can|could|would|will)\s+you\s+))?(?:post|publish|submit)\s+(?:(?:(?:the|this|that|these|those|my|our)\s+)?(?:(?:cached|completed|current|latest|inline|github|pr|pull[\s-]?request|review)\s+)*(?:reviews?|comments|(?:inline|review)\s+comment)|(?:it|this|that)\s+as\s+(?:(?:an?|the)\s+)?(?:(?:cached|completed|current|latest|inline|github|pr|pull[\s-]?request|review)\s+)*(?:reviews?|comments|(?:inline|review)\s+comment))(?:\s+(?:for|on|to)\s+(?:(?:the\s+)?(?:pull\s+request|pr)\s*)?#?(\d+))?(?:\s+please)?[.!?]*$/i, ); if (!match) return { matched: false }; if (match[1] === undefined) return { matched: true }; const prNumber = Number(match[1]); return Number.isInteger(prNumber) && prNumber > 0 ? { matched: true, prNumber } : { matched: false }; } export interface PublishExistingParseResult { prNumber?: number; allowStale: boolean; error?: string; } /** Parse the direct, model-free `/pr-review-publish` command arguments. */ export function parsePublishExistingArgs(input: string): PublishExistingParseResult { const tokens = input.trim().split(/\s+/).filter(Boolean); const requested = Number(tokens[0]); if (!Number.isInteger(requested) || requested <= 0) { return { allowStale: false, error: "a positive PR number must be the first argument" }; } const unknown = tokens.slice(1).filter((token) => token !== "--allow-stale"); if (unknown.length > 0) { return { allowStale: false, error: `unknown argument${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}` }; } return { prNumber: requested, allowStale: tokens.includes("--allow-stale") }; } export type ReviewInvocationPhase = "reviewing" | "awaiting_confirmation" | "confirmed"; export function isNonOpenConfirmationPrompt(text: string, prNumber: number): boolean { const escaped = String(prNumber).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const match = text.trim().match( new RegExp( `^PR #${escaped} is ([A-Z_]+) \\(head [0-9a-f]{40}(?:[0-9a-f]{24})?\\)\\. Review it anyway\\? Reply yes, or rerun with --include-closed to proceed non-interactively\\.$`, "i", ), ); return !!match && ["CLOSED", "MERGED"].includes(match[1]?.toUpperCase() ?? ""); } export function isAffirmativeReviewConfirmation(text: string): boolean { return /^(?:y|yes)[.!]?$/i.test(text.trim()); } export function validateReviewInvocation(review: ReviewLike, invocation: ReviewInvocation): string | undefined { return review.pr?.number === invocation.prNumber ? undefined : `final JSON PR #${review.pr?.number ?? "?"} does not match requested PR #${invocation.prNumber}`; } /** One active invocation per extension session; queued reviews cannot overwrite its write intent. */ export class ReviewInvocationGate { private active?: ReviewInvocation; private currentPhase?: ReviewInvocationPhase; begin( parsed: PublishModeParseResult, autoPost: AutoPostResolution, allowStalePublish = true, allowStaleApprovals = false, approveMaxPriorityLevel: ApproveMaxPriorityLevel = "off", ): { accepted: boolean; error?: string } { if (!parsed.matched) return { accepted: false, error: "not a pr-review invocation" }; if (this.active) { return { accepted: false, error: `PR #${this.active.prNumber} review is still active` }; } if (parsed.error || !parsed.mode || !parsed.prNumber) { return { accepted: false, error: parsed.error ?? "missing PR number or publishing mode" }; } const snapshot = Object.freeze({ value: autoPost.value, valid: autoPost.valid, source: autoPost.source, ...(autoPost.error === undefined ? {} : { error: autoPost.error }), }); this.active = Object.freeze({ mode: parsed.mode, prNumber: parsed.prNumber, allowNonOpen: parsed.allowNonOpen === true, allowStalePublish, allowStaleApprovals, autoPost: snapshot, approveMaxPriorityLevel, }); this.currentPhase = "reviewing"; return { accepted: true }; } peek(): ReviewInvocation | undefined { return this.active; } phase(): ReviewInvocationPhase | undefined { return this.currentPhase; } markAwaitingConfirmation(): boolean { if (!this.active || this.currentPhase !== "reviewing") return false; this.currentPhase = "awaiting_confirmation"; return true; } resolveConfirmationInput(text: string): "not_awaiting" | "confirmed" | "cleared" { if (!this.active || this.currentPhase !== "awaiting_confirmation") return "not_awaiting"; if (isAffirmativeReviewConfirmation(text)) { this.currentPhase = "confirmed"; return "confirmed"; } this.clear(); return "cleared"; } consume(): ReviewInvocation | undefined { const value = this.active ? { ...this.active, allowNonOpen: this.active.allowNonOpen || this.currentPhase === "confirmed", } : undefined; this.clear(); return value; } clear(): void { this.active = undefined; this.currentPhase = undefined; } } export function canonicalReviewMarker(headSha: string): string { return ``; } export function githubApiArgs(hostname: string, ...args: string[]): string[] { return ["api", "--hostname", hostname, ...args]; } export const REVIEW_EVENT = "COMMENT" as const; export const APPROVE_EVENT = "APPROVE" as const; export type ReviewEventType = typeof REVIEW_EVENT | typeof APPROVE_EVENT; export const MAX_INLINE_COMMENTS = 50; const MAX_BODY_BYTES = 65_536; const MAX_PAYLOAD_BYTES = 900_000; const RESERVED_MARKER_PREFIX = "/gi; for (const match of body.matchAll(marker)) { if (match[1]?.toLowerCase() === normalizedHeadSha) return true; } return false; } async function hasExistingMarker( cwd: string, hostname: string, repository: string, prNumber: number, identity: string, normalizedHeadSha: string, ): Promise { const reviewPages = await ghJson( githubApiArgs(hostname, "--paginate", "--slurp", `repos/${repository}/pulls/${prNumber}/reviews?per_page=100`), cwd, ); const reviews = normalizeAuthoredBodyPages(reviewPages); if (!reviews) throw new Error("invalid paginated pull review response"); const commentPages = await ghJson( githubApiArgs(hostname, "--paginate", "--slurp", `repos/${repository}/issues/${prNumber}/comments?per_page=100`), cwd, ); const comments = normalizeAuthoredBodyPages(commentPages); if (!comments) throw new Error("invalid paginated issue comment response"); return [...reviews, ...comments].some( (item) => item.user?.login?.toLowerCase() === identity.toLowerCase() && bodyHasHeadMarker(item.body, normalizedHeadSha), ); } interface PullState { state?: string; draft?: boolean; merged_at?: string | null; head?: { sha?: string }; user?: { login?: string }; } export interface HeadPublicationPlan { reviewedHeadSha: string; currentHeadSha: string; stale: boolean; commitId: string; allowInlineComments: boolean; } /** Authorize a reviewed/current head pairing without silently weakening stale protection. */ export function planHeadPublication( reviewedHeadSha: string, currentHeadSha: string | undefined, allowStale: boolean, ): { plan?: HeadPublicationPlan; error?: string } { const reviewed = reviewedHeadSha.toLowerCase(); const current = currentHeadSha?.toLowerCase(); if (!current || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(current)) { return { error: "GitHub returned an invalid current PR head SHA" }; } const stale = current !== reviewed; if (stale && !allowStale) { return { error: `PR head changed after review (${reviewed} -> ${current}); refusing to publish stale results. Use /pr-review-publish with --allow-stale to post the completed review without rerunning it`, }; } return { plan: { reviewedHeadSha: reviewed, currentHeadSha: current, stale, commitId: stale ? current : reviewed, allowInlineComments: !stale, }, }; } export function buildStaleReviewNotice(reviewedHeadSha: string, currentHeadSha: string): string { return [ "> [!WARNING]", `> This review was generated for commit \`${reviewedHeadSha}\`. At publish preflight, the PR pointed to \`${currentHeadSha}\`.`, "> Inline findings were folded into this body because their original diff anchors may be stale.", ].join("\n"); } export type PullLifecycle = "open" | "non_open"; export function authorizePullLifecycle( state: string | undefined, mergedAt: string | null | undefined, allowNonOpen: boolean, ): { lifecycle?: PullLifecycle; error?: string } { const normalized = state?.toLowerCase(); if (normalized === "open" && !mergedAt) return { lifecycle: "open" }; if (normalized === "closed" || !!mergedAt) { return allowNonOpen ? { lifecycle: "non_open" } : { error: "closed or merged PR publication was not authorized by the invocation" }; } return { error: `unknown PR lifecycle state: ${state ?? "missing"}` }; } export type PublishStatus = | "skipped_duplicate" | "posted" | "posted_degraded" | "failed" | "indeterminate"; export interface PublishResult { status: PublishStatus; message: string; event?: ReviewEventType; reviewId?: number; url?: string; reconciled?: boolean; } const publishLocks = new Map>(); async function withPublishLock(key: string, operation: () => Promise): Promise { const previous = publishLocks.get(key) ?? Promise.resolve(); let release = () => {}; const gate = new Promise((resolve) => { release = resolve; }); const chain = previous.then(() => gate); publishLocks.set(key, chain); await previous; try { return await operation(); } finally { release(); if (publishLocks.get(key) === chain) publishLocks.delete(key); } } /** Publish a model-formatted body through the same host-owned GitHub write boundary. */ export async function publishPullReviewBody(input: { cwd: string; prNumber: number; headSha: string; allowNonOpen: boolean; allowStale?: boolean; expectedRepository?: RepositoryBinding; body: string; }): Promise { const { cwd, prNumber, headSha, allowNonOpen, allowStale = false, expectedRepository, body, } = input; if (!Number.isInteger(prNumber) || prNumber <= 0) return { status: "failed", message: "invalid PR number" }; if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i.test(headSha)) return { status: "failed", message: "invalid head SHA" }; const normalizedHeadSha = headSha.toLowerCase(); const bodyError = validateReviewBody(body); if (bodyError) return { status: "failed", message: `publication planning failed: ${bodyError}` }; let repository: string; let hostname: string; let identity: string; try { const binding = await resolveRepositoryBinding(cwd); repository = binding.repository; hostname = binding.hostname; if ( expectedRepository && completedReviewKey(expectedRepository, prNumber) !== completedReviewKey(binding, prNumber) ) { return { status: "failed", message: "current GitHub repository does not match the fallback review repository" }; } identity = await ghText(githubApiArgs(hostname, "user", "--jq", ".login"), cwd); } catch (error) { return { status: "failed", message: `GitHub identity/repository lookup failed: ${String(error)}` }; } if (!identity) return { status: "failed", message: "invalid GitHub identity" }; const lockKey = `${hostname}:${repository}:${prNumber}:${normalizedHeadSha}:${identity.toLowerCase()}`; return withPublishLock(lockKey, async () => { let pull: PullState; let headPlan: HeadPublicationPlan; let isOpen: boolean; try { pull = await ghJson(githubApiArgs(hostname, `repos/${repository}/pulls/${prNumber}`), cwd); const planned = planHeadPublication(normalizedHeadSha, pull.head?.sha, allowStale); if (!planned.plan) return { status: "failed", message: planned.error ?? "invalid PR head" }; headPlan = planned.plan; if (pull.draft) return { status: "failed", message: "draft PR reviews are not automatically published" }; const lifecycle = authorizePullLifecycle(pull.state, pull.merged_at, allowNonOpen); if (!lifecycle.lifecycle) return { status: "failed", message: lifecycle.error ?? "invalid PR lifecycle" }; isOpen = lifecycle.lifecycle === "open"; if (await hasExistingMarker(cwd, hostname, repository, prNumber, identity, normalizedHeadSha)) { return { status: "skipped_duplicate", message: "same head already reviewed by this GitHub identity" }; } } catch (error) { return { status: "failed", message: `GitHub preflight failed: ${String(error)}` }; } const content = headPlan.stale ? `${buildStaleReviewNotice(headPlan.reviewedHeadSha, headPlan.currentHeadSha)}\n\n${body.trim()}` : body.trim(); const finalBody = `${content}\n\n${canonicalReviewMarker(normalizedHeadSha)}`; if (Buffer.byteLength(finalBody, "utf8") > MAX_BODY_BYTES) { return { status: "failed", message: "publication planning failed: final review body exceeds 65536 UTF-8 bytes" }; } const payload = buildPullReviewPayload(headPlan.commitId, finalBody, []); if (Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_PAYLOAD_BYTES) { return { status: "failed", message: "publication planning failed: review payload is too large" }; } try { const refreshed = await ghJson( githubApiArgs(hostname, `repos/${repository}/pulls/${prNumber}`), cwd, ); if (refreshed.head?.sha?.toLowerCase() !== headPlan.currentHeadSha) { return { status: "failed", message: "PR head changed during publish preflight" }; } if (refreshed.draft) return { status: "failed", message: "PR became a draft during publish preflight" }; const lifecycle = authorizePullLifecycle(refreshed.state, refreshed.merged_at, allowNonOpen); if (!lifecycle.lifecycle) return { status: "failed", message: lifecycle.error ?? "invalid refreshed PR lifecycle" }; if ((lifecycle.lifecycle === "open") !== isOpen) { return { status: "failed", message: "PR open/closed state changed during publish preflight" }; } } catch (error) { return { status: "failed", message: `final head check failed: ${String(error)}` }; } const degraded = !isOpen || headPlan.stale; const post = await runGh( githubApiArgs(hostname, "--method", "POST", `repos/${repository}/pulls/${prNumber}/reviews`, "--input", "-"), cwd, JSON.stringify(payload), ); if (post.exitCode === 0) { let response: { id?: number; html_url?: string } = {}; try { response = JSON.parse(post.stdout); } catch { /* GitHub accepted the POST even if response metadata is unavailable. */ } return { status: degraded ? "posted_degraded" : "posted", message: headPlan.stale ? `body-only stale COMMENT review posted (${headPlan.reviewedHeadSha} -> ${headPlan.currentHeadSha})` : isOpen ? "GitHub COMMENT review posted" : "body-only COMMENT review posted for non-open PR", event: REVIEW_EVENT, reviewId: response.id, url: response.html_url, }; } try { if (await hasExistingMarker(cwd, hostname, repository, prNumber, identity, normalizedHeadSha)) { return { status: degraded ? "posted_degraded" : "posted", message: "GitHub COMMENT review found during failure reconciliation", event: REVIEW_EVENT, reconciled: true, }; } } catch { /* reconciliation failure is handled below */ } const detail = post.errorMessage || post.stderr || "gh review request failed"; if (/HTTP\s+4\d\d/i.test(detail) && !post.timedOut) return { status: "failed", message: detail }; return { status: "indeterminate", message: `${detail}; no matching marker found after reconciliation` }; }); } export async function publishPullReview(input: { cwd: string; prNumber: number; headSha: string; allowNonOpen: boolean; allowStale?: boolean; allowStaleApprovals?: boolean; approveMaxPriorityLevel?: ApproveMaxPriorityLevel; expectedRepository?: RepositoryBinding; review: ReviewLike; }): Promise { const { cwd, prNumber, headSha, allowNonOpen, allowStale = false, allowStaleApprovals = false, approveMaxPriorityLevel = "off", expectedRepository, review, } = input; if (!Number.isInteger(prNumber) || prNumber <= 0) return { status: "failed", message: "invalid PR number" }; if (!/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/i.test(headSha)) return { status: "failed", message: "invalid head SHA" }; const normalizedHeadSha = headSha.toLowerCase(); const snapshot = canonicalReviewSnapshot(review); const validatedReview = snapshot.review; if (!validatedReview) { return { status: "failed", message: `publication planning failed: ${snapshot.error ?? "review is not publishable"}`, }; } if (!shouldPublishReview(validatedReview)) { return { status: "failed", message: "only completed reviewed dispositions can be published" }; } if (validatedReview.pr?.number !== prNumber) { return { status: "failed", message: "validated review PR number does not match the publication target" }; } if (validatedReview.pr?.head_sha?.toLowerCase() !== normalizedHeadSha) { return { status: "failed", message: "validated review head does not match the publication target" }; } let repository: string; let hostname: string; let identity: string; try { const binding = await resolveRepositoryBinding(cwd); repository = binding.repository; hostname = binding.hostname; if ( expectedRepository && completedReviewKey(expectedRepository, prNumber) !== completedReviewKey(binding, prNumber) ) { return { status: "failed", message: "current GitHub repository does not match the cached review repository" }; } identity = await ghText(githubApiArgs(hostname, "user", "--jq", ".login"), cwd); } catch (error) { return { status: "failed", message: `GitHub identity/repository lookup failed: ${String(error)}` }; } if (!identity) return { status: "failed", message: "invalid GitHub identity" }; const lockKey = `${hostname}:${repository}:${prNumber}:${normalizedHeadSha}:${identity.toLowerCase()}`; return withPublishLock(lockKey, async () => { let pull: PullState; let headPlan: HeadPublicationPlan; try { pull = await ghJson(githubApiArgs(hostname, `repos/${repository}/pulls/${prNumber}`), cwd); const planned = planHeadPublication(normalizedHeadSha, pull.head?.sha, allowStale); if (!planned.plan) return { status: "failed", message: planned.error ?? "invalid PR head" }; headPlan = planned.plan; if (pull.draft) return { status: "failed", message: "draft PR reviews are not automatically published" }; const lifecycle = authorizePullLifecycle(pull.state, pull.merged_at, allowNonOpen); if (!lifecycle.lifecycle) return { status: "failed", message: lifecycle.error ?? "invalid PR lifecycle" }; if (await hasExistingMarker(cwd, hostname, repository, prNumber, identity, normalizedHeadSha)) { return { status: "skipped_duplicate", message: "same head already reviewed by this GitHub identity" }; } } catch (error) { return { status: "failed", message: `GitHub preflight failed: ${String(error)}` }; } const lifecycle = authorizePullLifecycle(pull.state, pull.merged_at, allowNonOpen); if (!lifecycle.lifecycle) return { status: "failed", message: lifecycle.error ?? "invalid PR lifecycle" }; const isOpen = lifecycle.lifecycle === "open"; let allowInlineComments = isOpen && headPlan.allowInlineComments; let changedFiles: readonly ChangedFileLike[] = []; let changedFileLookupFailed = false; if (allowInlineComments && hasInlineCandidates(validatedReview)) { try { const filePages = await ghJson( githubApiArgs(hostname, "--paginate", "--slurp", `repos/${repository}/pulls/${prNumber}/files?per_page=100`), cwd, ); const normalizedFiles = normalizeChangedFilePages(filePages); if (!normalizedFiles) throw new Error("invalid changed-file JSON response"); changedFiles = normalizedFiles; } catch { allowInlineComments = false; changedFileLookupFailed = true; } } // Stale publication authorization is independent from merge-relevant stale // approval. The latter requires its own explicit frozen config opt-in. // GitHub rejects a formal APPROVE from the PR author. Downgrade before the // single write rather than retrying a rejected review as COMMENT. const isSelfAuthored = pull.user?.login?.toLowerCase() === identity.toLowerCase(); const isApprove = !isSelfAuthored && (!headPlan.stale || allowStaleApprovals) && shouldApproveReview(validatedReview, approveMaxPriorityLevel); const built = buildLosslessReviewPayload({ review: validatedReview, commitId: headPlan.commitId, markerHeadSha: normalizedHeadSha, allowInlineComments, changedFiles, ...(isApprove ? { event: APPROVE_EVENT } : {}), ...(changedFileLookupFailed ? { diagnostics: [CHANGED_FILE_LOOKUP_DIAGNOSTIC] } : {}), ...(headPlan.stale ? { bodyPreamble: buildStaleReviewNotice(headPlan.reviewedHeadSha, headPlan.currentHeadSha) } : {}), }); if (!built.payload) { return { status: "failed", message: `publication planning failed: ${built.errors.join("; ")}` }; } const payload = built.payload; try { const refreshed = await ghJson( githubApiArgs(hostname, `repos/${repository}/pulls/${prNumber}`), cwd, ); if (refreshed.head?.sha?.toLowerCase() !== headPlan.currentHeadSha) { return { status: "failed", message: "PR head changed during publish preflight; run the publish-only command again to acknowledge the new current head", }; } if (refreshed.draft) return { status: "failed", message: "PR became a draft during publish preflight" }; const refreshedLifecycle = authorizePullLifecycle(refreshed.state, refreshed.merged_at, allowNonOpen); if (!refreshedLifecycle.lifecycle) { return { status: "failed", message: refreshedLifecycle.error ?? "invalid refreshed PR lifecycle" }; } if ((refreshedLifecycle.lifecycle === "open") !== isOpen) { return { status: "failed", message: "PR open/closed state changed during publish preflight" }; } } catch (error) { return { status: "failed", message: `final head check failed: ${String(error)}` }; } const inlineWarning = built.diagnostics.length === 0 ? "" : changedFileLookupFailed ? `; ${CHANGED_FILE_LOOKUP_DIAGNOSTIC}` : `; ${built.diagnostics.length} inline finding${built.diagnostics.length === 1 ? "" : "s"} kept in the summary: ${built.diagnostics.join("; ")}`; const degraded = !isOpen || headPlan.stale || built.diagnostics.length > 0; const eventLabel = payload.event === APPROVE_EVENT ? "APPROVE" : "COMMENT"; const post = await runGh( githubApiArgs(hostname, "--method", "POST", `repos/${repository}/pulls/${prNumber}/reviews`, "--input", "-"), cwd, JSON.stringify(payload), ); if (post.exitCode === 0) { let response: { id?: number; html_url?: string } = {}; try { response = JSON.parse(post.stdout); } catch { /* accepted response without parseable metadata */ } return { status: degraded ? "posted_degraded" : "posted", message: headPlan.stale ? `body-only stale ${eventLabel} review posted (${headPlan.reviewedHeadSha} -> ${headPlan.currentHeadSha})` : isOpen ? `GitHub ${eventLabel} review posted${inlineWarning}` : `body-only ${eventLabel} review posted for non-open PR`, event: payload.event, reviewId: response.id, url: response.html_url, }; } try { if (await hasExistingMarker(cwd, hostname, repository, prNumber, identity, normalizedHeadSha)) { return { status: degraded ? "posted_degraded" : "posted", message: `GitHub ${eventLabel} review found during failure reconciliation${inlineWarning}`, event: payload.event, reconciled: true, }; } } catch { /* reconciliation failure is handled below */ } const detail = post.errorMessage || post.stderr || "gh review request failed"; if (/HTTP\s+4\d\d/i.test(detail) && !post.timedOut) { return { status: "failed", message: detail }; } return { status: "indeterminate", message: `${detail}; no matching marker found after reconciliation` }; }); }