fix(pr-review): resolve verdict message contradiction and blocked status limbo (#1151)

* fix(pr-review): resolve verdict message contradiction and blocked status limbo

Backend fixes:
- Fix verdict reasoning to include high/medium findings when branch is behind
- Previously, when branch was behind AND there were medium findings, the
  reasoning said "you can merge" while bottom line said "3 issues require
  attention" - a contradiction
- Now properly combines branch-behind message with findings count

Frontend fixes:
- Add "Post Status" button for BLOCKED/NEEDS_REVISION verdicts with no findings
- Handles edge case where structured output parsing fails but verdict exists
- Users were stuck in limbo with "Pending Post" status but no actionable button
- Button posts the review summary (with blockers) as a comment to GitHub

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(pr-review): address code review findings

- Reset blocked status state variables when switching PRs (prevents stale state)
- Keep blockedStatusMessageFooter in English for French locale (GitHub comments policy)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-01-15 23:23:59 +01:00
committed by GitHub
parent 32811142a2
commit 8b269fea90
5 changed files with 119 additions and 2 deletions
+10 -1
View File
@@ -1097,7 +1097,16 @@ class GitHubOrchestrator:
# Branch behind is a soft blocker - NEEDS_REVISION, not BLOCKED # Branch behind is a soft blocker - NEEDS_REVISION, not BLOCKED
elif is_branch_behind: elif is_branch_behind:
verdict = MergeVerdict.NEEDS_REVISION verdict = MergeVerdict.NEEDS_REVISION
reasoning = BRANCH_BEHIND_REASONING if high or medium:
# Branch behind + code issues that need addressing
total = len(high) + len(medium)
reasoning = (
f"{BRANCH_BEHIND_REASONING} "
f"{total} issue(s) must be addressed ({len(high)} required, {len(medium)} recommended)."
)
else:
# Just branch behind, no code issues
reasoning = BRANCH_BEHIND_REASONING
if low: if low:
reasoning += f" {len(low)} non-blocking suggestion(s) to consider." reasoning += f" {len(low)} non-blocking suggestion(s) to consider."
else: else:
@@ -967,7 +967,16 @@ The SDK will run invoked agents in parallel automatically.
# Branch behind is a soft blocker - NEEDS_REVISION, not BLOCKED # Branch behind is a soft blocker - NEEDS_REVISION, not BLOCKED
elif is_branch_behind: elif is_branch_behind:
verdict = MergeVerdict.NEEDS_REVISION verdict = MergeVerdict.NEEDS_REVISION
reasoning = BRANCH_BEHIND_REASONING if high or medium:
# Branch behind + code issues that need addressing
total = len(high) + len(medium)
reasoning = (
f"{BRANCH_BEHIND_REASONING} "
f"{total} issue(s) must be addressed ({len(high)} required, {len(medium)} recommended)."
)
else:
# Just branch behind, no code issues
reasoning = BRANCH_BEHIND_REASONING
if low: if low:
reasoning += f" {len(low)} non-blocking suggestion(s) to consider." reasoning += f" {len(low)} non-blocking suggestion(s) to consider."
else: else:
@@ -100,6 +100,10 @@ export function PRDetail({
const [cleanReviewPosted, setCleanReviewPosted] = useState(false); const [cleanReviewPosted, setCleanReviewPosted] = useState(false);
const [cleanReviewError, setCleanReviewError] = useState<string | null>(null); const [cleanReviewError, setCleanReviewError] = useState<string | null>(null);
const [showCleanReviewErrorDetails, setShowCleanReviewErrorDetails] = useState(false); const [showCleanReviewErrorDetails, setShowCleanReviewErrorDetails] = useState(false);
// Blocked status posting state (for BLOCKED/NEEDS_REVISION verdicts with no findings)
const [isPostingBlockedStatus, setIsPostingBlockedStatus] = useState(false);
const [blockedStatusPosted, setBlockedStatusPosted] = useState(false);
const [blockedStatusError, setBlockedStatusError] = useState<string | null>(null);
const [isMerging, setIsMerging] = useState(false); const [isMerging, setIsMerging] = useState(false);
// Initialize with store value, then sync and update via local checks // Initialize with store value, then sync and update via local checks
const [newCommitsCheck, setNewCommitsCheck] = useState<NewCommitsCheck | null>(initialNewCommitsCheck ?? null); const [newCommitsCheck, setNewCommitsCheck] = useState<NewCommitsCheck | null>(initialNewCommitsCheck ?? null);
@@ -270,6 +274,10 @@ export function PRDetail({
setCleanReviewError(null); setCleanReviewError(null);
setIsPostingCleanReview(false); setIsPostingCleanReview(false);
setShowCleanReviewErrorDetails(false); setShowCleanReviewErrorDetails(false);
// Reset blocked status state as well
setBlockedStatusPosted(false);
setBlockedStatusError(null);
setIsPostingBlockedStatus(false);
}, [pr.number]); }, [pr.number]);
// Check for workflows awaiting approval (fork PRs) when PR changes or review completes // Check for workflows awaiting approval (fork PRs) when PR changes or review completes
@@ -681,6 +689,46 @@ ${t('prReview.cleanReviewMessageFooter')}`;
} }
}; };
// Post blocked status comment when verdict is BLOCKED/NEEDS_REVISION but no findings
// This handles the edge case where structured output parsing fails but we still have a verdict
const handlePostBlockedStatus = async () => {
if (!reviewResult) return;
// Capture current PR number to prevent state leaks across PR switches
const currentPr = pr.number;
setIsPostingBlockedStatus(true);
setBlockedStatusError(null);
try {
// Format the blocked status comment - post the summary which contains blockers
const blockedStatusMessage = `${t('prReview.blockedStatusMessageTitle')}
${reviewResult.summary}
---
${t('prReview.blockedStatusMessageFooter')}`;
await Promise.resolve(onPostComment(blockedStatusMessage));
// Only mark as posted on success if PR hasn't changed
if (pr.number === currentPr) {
setBlockedStatusPosted(true);
setBlockedStatusError(null);
}
} catch (err) {
console.error('Failed to post blocked status comment:', err);
const fullError = err instanceof Error ? err.message : String(err);
if (pr.number === currentPr) {
setBlockedStatusError(fullError);
}
} finally {
if (pr.number === currentPr) {
setIsPostingBlockedStatus(false);
}
}
};
const handleMerge = async () => { const handleMerge = async () => {
setIsMerging(true); setIsMerging(true);
try { try {
@@ -782,6 +830,29 @@ ${t('prReview.cleanReviewMessageFooter')}`;
</Button> </Button>
)} )}
{/* Post Blocked Status button - shows when verdict is BLOCKED/NEEDS_REVISION but no findings */}
{/* This handles the edge case where structured output parsing fails but we still have a verdict */}
{selectedCount === 0 && !hasPostedFindings && !blockedStatusPosted && reviewResult?.overallStatus === 'request_changes' && (
<Button
onClick={handlePostBlockedStatus}
disabled={isPostingBlockedStatus || isPosting}
variant="secondary"
className="flex-1 sm:flex-none"
>
{isPostingBlockedStatus ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{t('prReview.postingBlockedStatus')}
</>
) : (
<>
<AlertTriangle className="h-4 w-4 mr-2" />
{t('prReview.postBlockedStatus')}
</>
)}
</Button>
)}
{/* Approve button - consolidated logic to avoid duplicate buttons */} {/* Approve button - consolidated logic to avoid duplicate buttons */}
{/* Don't show when overallStatus is 'request_changes' (e.g., workflows blocked, or other issues) */} {/* Don't show when overallStatus is 'request_changes' (e.g., workflows blocked, or other issues) */}
{isCleanReview && !hasPostedFindings && reviewResult?.overallStatus !== 'request_changes' && ( {isCleanReview && !hasPostedFindings && reviewResult?.overallStatus !== 'request_changes' && (
@@ -897,6 +968,22 @@ ${t('prReview.cleanReviewMessageFooter')}`;
{cleanReviewError} {cleanReviewError}
</div> </div>
)} )}
{/* Blocked status posted success message */}
{blockedStatusPosted && !postSuccess && !cleanReviewPosted && (
<div className="ml-auto flex items-center gap-2 text-amber-600 text-sm font-medium animate-pulse">
<CheckCircle className="h-4 w-4" />
{t('prReview.blockedStatusPosted')}
</div>
)}
{/* Blocked status error display */}
{blockedStatusError && (
<div className="ml-auto flex items-center gap-2 text-destructive text-sm font-medium">
<XCircle className="h-4 w-4" />
{t('prReview.failedPostBlockedStatus')}
</div>
)}
</div> </div>
)} )}
@@ -339,6 +339,12 @@
"failedPostCleanReview": "Failed to post clean review", "failedPostCleanReview": "Failed to post clean review",
"viewErrorDetails": "View details", "viewErrorDetails": "View details",
"hideErrorDetails": "Hide details", "hideErrorDetails": "Hide details",
"postBlockedStatus": "Post Status",
"postingBlockedStatus": "Posting...",
"blockedStatusPosted": "Status posted to PR",
"blockedStatusMessageTitle": "## 🤖 Auto Claude PR Review",
"blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Auto Claude.*",
"failedPostBlockedStatus": "Failed to post status",
"branchSynced": "Branch synced ({{count}} commit from base)", "branchSynced": "Branch synced ({{count}} commit from base)",
"branchSynced_plural": "Branch synced ({{count}} commits from base)", "branchSynced_plural": "Branch synced ({{count}} commits from base)",
"newCommitsOverlap": "{{count}} new commit ({{files}} finding file(s) modified)", "newCommitsOverlap": "{{count}} new commit ({{files}} finding file(s) modified)",
@@ -348,6 +348,12 @@
"failedPostCleanReview": "Échec de la publication de la révision", "failedPostCleanReview": "Échec de la publication de la révision",
"viewErrorDetails": "Voir les détails", "viewErrorDetails": "Voir les détails",
"hideErrorDetails": "Masquer les détails", "hideErrorDetails": "Masquer les détails",
"postBlockedStatus": "Publier le statut",
"postingBlockedStatus": "Publication...",
"blockedStatusPosted": "Statut publié sur la PR",
"blockedStatusMessageTitle": "## 🤖 Auto Claude PR Review",
"blockedStatusMessageFooter": "*This review identified blockers that must be resolved before merge. Generated by Auto Claude.*",
"failedPostBlockedStatus": "Échec de la publication du statut",
"logs": { "logs": {
"agentActivity": "Activité des agents", "agentActivity": "Activité des agents",
"showMore": "Afficher {{count}} de plus", "showMore": "Afficher {{count}} de plus",