Commit58c39f5unknown_key
fix(pr-pipeline): re-review on new commits, draft ready trigger, ai-approved gate fix
fix(pr-pipeline): re-review on new commits, draft ready trigger, ai-approved gate fix
Bug 1: alreadyReviewed() now extracts and compares the HEAD SHA embedded in
the AI summary comment (<!-- gluecron-ai-review:sha:<sha> -->), so each new
push to a PR triggers a fresh review instead of being silently skipped after
the first review. Legacy comments without the SHA embed are treated as stale
and also re-review.
Bug 2: SHA-based idempotency (Bug 1) enables the existing /ready handler's
triggerAiReview() call to correctly re-trigger when the PR gains new commits
between draft→ready cycles.
Bug 3: aiApproved check in the merge handler now matches the actual review
comment format ("no blocking issues found" / "severity: blocking") instead
of the incorrect "**Approved**" / "approved: true" / "lgtm" strings that
were never emitted, which had been silently bypassing the merge gate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1Wr1gducAJ51tSHJuJ9tY2 files changed+36−1358c39f5988532bb945c0d383d4dff27878737528
2 changed files+36−13
Modifiedsrc/lib/ai-review.ts+24−10View fileUnifiedSplit
@@ -232,15 +232,18 @@ async function resolveHeadSha(
232232}
233233
234234/**
235 * Has this PR already been reviewed by the AI? Detected by an existing
236 * PR comment carrying our summary marker. Cheap LIKE query — if it
237 * fails (DB hiccup) we fall back to "not yet" and re-review, which is
238 * idempotent at worst (a duplicate summary), never destructive.
235 * Has this PR already been reviewed by the AI for the given head SHA?
236 * Detected by an existing PR comment carrying our summary marker that
237 * also embeds a SHA marker matching the current head. If the stored SHA
238 * differs (new commits pushed) we allow a re-review. If no review
239 * exists yet we also allow it. Cheap LIKE query — if it fails (DB
240 * hiccup) we fall back to "not yet" and re-review, which is idempotent
241 * at worst (a duplicate summary), never destructive.
239242 */
240async function alreadyReviewed(prId: string): Promise<boolean> {
243async function alreadyReviewed(prId: string, headSha: string): Promise<boolean> {
241244 try {
242245 const [row] = await db
243 .select({ id: prComments.id })
246 .select({ body: prComments.body })
244247 .from(prComments)
245248 .where(
246249 and(
@@ -250,7 +253,13 @@ async function alreadyReviewed(prId: string): Promise<boolean> {
250253 )
251254 )
252255 .limit(1);
253 return !!row;
256 if (!row) return false;
257 // Extract the SHA embedded in the comment. If it matches the current
258 // head we skip (same commit reviewed already). If it differs or is
259 // missing we allow re-review (new commits pushed).
260 const shaMatch = row.body.match(/<!-- gluecron-ai-review:sha:([0-9a-f]+) -->/);
261 if (!shaMatch) return false; // Legacy comment without SHA — re-review.
262 return shaMatch[1] === headSha;
254263 } catch {
255264 return false;
256265 }
@@ -286,12 +295,17 @@ export async function triggerAiReview(
286295): Promise<void> {
287296 try {
288297 if (!isAiReviewEnabled()) return;
298
299 // Resolve the current head SHA early — needed for SHA-based idempotency
300 // on both the single-Claude and trio paths.
301 const headSha = await resolveHeadSha(ownerName, repoName, headBranch);
302
289303 const useTrio = isTrioReviewEnabled();
290304 if (
291305 !options.force &&
292306 (useTrio
293307 ? await alreadyTrioReviewed(prId)
294 : await alreadyReviewed(prId))
308 : await alreadyReviewed(prId, headSha))
295309 )
296310 return;
297311
@@ -344,7 +358,6 @@ export async function triggerAiReview(
344358 // trio helper owns its own persistence + audit; we bail after it.
345359 if (useTrio) {
346360 try {
347 const headSha = await resolveHeadSha(ownerName, repoName, headBranch);
348361 await runTrioReview({
349362 pullRequestId: prId,
350363 headSha,
@@ -400,7 +413,8 @@ export async function triggerAiReview(
400413 const verdict = result.approved
401414 ? "**AI review:** no blocking issues found."
402415 : `**AI review:** flagged ${result.comments.length} item(s) for human attention.`;
403 const summaryBody = `${AI_REVIEW_MARKER}\n## AI Code Review\n\n${verdict}\n\n${result.summary}`;
416 const shaMarker = headSha ? `<!-- gluecron-ai-review:sha:${headSha} -->` : "";
417 const summaryBody = `${AI_REVIEW_MARKER}${shaMarker ? `\n${shaMarker}` : ""}\n## AI Code Review\n\n${verdict}\n\n${result.summary}`;
404418 await db
405419 .insert(prComments)
406420 .values({
Modifiedsrc/routes/pulls.tsx+12−3View fileUnifiedSplit
@@ -5931,7 +5931,12 @@ pulls.post(
59315931 );
59325932 }
59335933
5934 // Check if AI review approved this PR
5934 // Check if AI review approved this PR.
5935 // The AI summary comment body uses:
5936 // "**AI review:** no blocking issues found." → approved
5937 // "**AI review:** flagged N item(s)..." → not approved
5938 // "severity: blocking" → explicit blocking (future)
5939 // If no AI comments exist yet, treat as approved (gate hasn't run).
59355940 const aiComments = await db
59365941 .select()
59375942 .from(prComments)
@@ -5941,9 +5946,13 @@ pulls.post(
59415946 eq(prComments.isAiReview, true)
59425947 )
59435948 );
5944 const aiApproved = aiComments.length === 0 || aiComments.some(
5945 (c) => c.body.includes("**Approved**") || c.body.includes("approved: true") || c.body.toLowerCase().includes("lgtm")
5949 const aiSummaryComment = aiComments.find((c) =>
5950 c.body.includes("<!-- gluecron-ai-review:summary -->")
59465951 );
5952 const aiApproved =
5953 !aiSummaryComment ||
5954 (aiSummaryComment.body.includes("no blocking issues found") &&
5955 !aiSummaryComment.body.includes("severity: blocking"));
59475956
59485957 // Run all green gate checks (GateTest + mergeability + AI review)
59495958 const gateResult = await runAllGateChecks(
59505959