@@ -3,6 +3,8 @@ name: Label PR review state
33on :
44 schedule :
55 - cron : " 0 * * * *" # hourly fallback
6+ push :
7+ branches : [main]
68 workflow_dispatch :
79 inputs :
810 pull_request_number :
1517 types : [opened, reopened, ready_for_review, synchronize, review_requested, labeled, unlabeled]
1618 pull_request_review :
1719 types : [submitted, dismissed]
20+ # Fork review events have a read-only token. CodeRabbit's status-comment update
21+ # provides a trusted base-repository event that can reconcile those PRs promptly.
22+ issue_comment :
23+ types : [created, edited]
1824 workflow_run :
1925 workflows : ["Code QA Roo Code", "E2E Tests (Mocked)", "Webview Visual Regression", "CodeQL Advanced"]
2026 types : [completed]
2127
2228permissions :
29+ # This privileged workflow only reads PR/check metadata and writes issue labels,
30+ # comments, and commit statuses. All unspecified permissions, including contents,
31+ # are none; no fork code or configuration is checked out or executed.
2332 pull-requests : write
2433 issues : write
2534 checks : read
@@ -32,6 +41,9 @@ concurrency:
3241jobs :
3342 reconcile :
3443 name : Zoo Code / reconcile PR review state
44+ if : >-
45+ github.event_name != 'issue_comment' ||
46+ (github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]')
3547 runs-on : ubuntu-latest
3648 steps :
3749 - name : Reconcile PR review state labels
@@ -52,17 +64,17 @@ jobs:
5264 {
5365 name: 'awaiting-coderabbit',
5466 color: '5319e7',
55- description: 'Waiting for CodeRabbit to approve the latest commit',
67+ description: 'Waiting for automated review of the latest commit',
5668 },
5769 {
5870 name: 'awaiting-ready',
5971 color: '1d76db',
60- description: 'CodeRabbit approved ; waiting for the draft to be marked ready',
72+ description: 'Automated review complete ; waiting for the draft to be marked ready',
6173 },
6274 {
6375 name: 'awaiting-maintainer',
6476 color: '0e8a16',
65- description: 'CodeRabbit approved; waiting for a human maintainer',
77+ description: 'Waiting for fresh human maintainer or CODEOWNER approval ',
6678 },
6779 {
6880 name: 'coderabbit-review-active',
@@ -73,16 +85,26 @@ jobs:
7385 const guideMarker = '<!-- zoo-code-pr-review-process -->';
7486 const codeRabbitLabelMarkerPrefix = '<!-- coderabbit-review-label:';
7587 const codeRabbitLogin = 'coderabbitai[bot]';
88+ const codeRabbitLogins = new Set([codeRabbitLogin, 'coderabbitai']);
7689 const codeRabbitActiveLabel = 'coderabbit-review-active';
7790 const reviewGateName = 'Zoo Code / PR review gate';
7891 const reconciliationCheckName = 'Zoo Code / reconcile PR review state';
7992
93+ if (context.eventName === 'issue_comment' &&
94+ (!context.payload.issue?.pull_request ||
95+ context.payload.comment?.user?.login?.toLowerCase() !== codeRabbitLogin)) {
96+ core.info('Ignoring untrusted issue comment event');
97+ return;
98+ }
99+
80100 // When triggered by a single PR event, only reconcile that PR.
81- // The hourly schedule and workflow_dispatch reconcile all open PRs.
101+ // Main-branch pushes, the hourly schedule, and workflow_dispatch reconcile all open PRs.
82102 let prs;
83103 let eventPrNumbers = [];
84104 if (context.payload.pull_request?.number) {
85105 eventPrNumbers = [context.payload.pull_request.number];
106+ } else if (context.eventName === 'issue_comment') {
107+ eventPrNumbers = [context.payload.issue.number];
86108 } else if (context.eventName === 'workflow_dispatch') {
87109 eventPrNumbers = [Number(context.payload.inputs.pull_request_number)];
88110 } else if (context.payload.workflow_run?.pull_requests) {
@@ -161,6 +183,19 @@ jobs:
161183 }
162184 const currentLabels = new Set(pr.labels.map(l => l.name));
163185 const labelErrors = [];
186+ if (desiredLabel && !currentLabels.has(desiredLabel)) {
187+ try {
188+ await github.rest.issues.addLabels({
189+ owner, repo, issue_number: pr.number, labels: [desiredLabel],
190+ });
191+ currentLabels.add(desiredLabel);
192+ pr.labels.push({ name: desiredLabel });
193+ } catch (err) {
194+ const error = new Error(`Could not add desired label: ${err.message}`);
195+ error.preserveStateLabels = true;
196+ throw error;
197+ }
198+ }
164199 for (const label of stateLabels) {
165200 if (label !== desiredLabel && currentLabels.has(label)) {
166201 try {
@@ -179,17 +214,6 @@ jobs:
179214 }
180215 }
181216 }
182- if (desiredLabel && !currentLabels.has(desiredLabel)) {
183- try {
184- await github.rest.issues.addLabels({
185- owner, repo, issue_number: pr.number, labels: [desiredLabel],
186- });
187- currentLabels.add(desiredLabel);
188- pr.labels.push({ name: desiredLabel });
189- } catch (err) {
190- labelErrors.push(err);
191- }
192- }
193217 if (desiredLabel !== 'awaiting-author' && currentLabels.has('stale-awaiting-author')) {
194218 try {
195219 await github.rest.issues.removeLabel({
@@ -305,16 +329,17 @@ jobs:
305329
306330 function phaseMessage(phase) {
307331 const messages = {
308- draft: 'Mark the PR ready to start CodeRabbit after required CI passes .',
332+ draft: 'Mark the PR ready. Required CI must pass before CodeRabbit starts .',
309333 conflict: 'Resolve the merge conflicts. The review sequence resumes after the branch is mergeable.',
310- 'ci-pending': 'Wait for the required CI checks to finish.',
311- 'ci-failed': 'Fix the failing required CI checks and push an update.',
334+ 'ci-pending': 'Wait for required CI checks; awaiting-maintainer requires CI and automated review completion.',
335+ 'ci-failed': 'Fix the failing required CI checks; awaiting-maintainer requires CI and automated review completion.',
336+ 'mergeability-pending': 'Wait for GitHub to finish calculating mergeability.',
312337 'configuration-error': 'Repository rules must not require this advisory workflow\'s own gate or reconciliation job.',
313- 'coderabbit-changes': 'Address CodeRabbit findings and push an update. Review restarts after CI passes .',
314- coderabbit: 'Required CI passed. Wait for CodeRabbit to approve the latest commit.',
315- 'draft-approved': 'CodeRabbit approved the latest commit. Mark the draft ready.',
316- 'maintainer-changes': 'Address the maintainer feedback, push an update, and request another review .',
317- maintainer: 'Ready for human maintainer review and approval.',
338+ 'coderabbit-changes': 'Address automated review findings and push fixes .',
339+ coderabbit: 'Required CI passed. Waiting for automated review of the latest commit.',
340+ 'draft-approved': 'Automated review complete for the latest commit. Mark the draft ready.',
341+ 'maintainer-changes': 'Address maintainer or CODEOWNER feedback, then push an update.',
342+ maintainer: 'Awaiting fresh human maintainer or CODEOWNER approval.',
318343 approved: 'The required review sequence passed. Remaining merge requirements apply.',
319344 };
320345 return messages[phase];
@@ -373,12 +398,18 @@ jobs:
373398 ? `\n${codeRabbitLabelMarkerPrefix}${pr.head.sha}${activationPending ? ':pending' : ''} -->`
374399 : '';
375400
376- return `${guideMarker}\n### Review process\n\n${authorNote}\n\n` +
377- '1. Required CI checks pass.\n' +
378- '2. The workflow starts CodeRabbit automatically.\n' +
379- '3. For eligible human-authored PRs, CodeRabbit reviews and approves the latest commit.\n' +
380- '4. A human maintainer reviews and approves after CodeRabbit.\n\n' +
381- `**Current step:** ${phaseMessage(phase)}${labelMarker}`;
401+ const phaseHelp = phase === 'coderabbit-changes'
402+ ? '\n\nAfter fixes are pushed and required CI passes, automated review restarts.'
403+ : phase === 'coderabbit'
404+ ? '\n\nIf automated review does not start, a maintainer must restart it.'
405+ : phase === 'maintainer' && !automatedAuthor
406+ ? '\n\nAutomated review is complete for the latest commit but does not replace human approval.'
407+ : '';
408+
409+ return `${guideMarker}\n### Review status\n\n${authorNote}\n\n` +
410+ `**Current step:** ${phaseMessage(phase)}${phaseHelp}\n\n` +
411+ 'Review-state labels are managed by this workflow; do not edit them manually.' +
412+ labelMarker;
382413 }
383414
384415 async function updateReviewGuide(pr, phase, existingGuide = null, activationPending = false) {
@@ -601,10 +632,11 @@ jobs:
601632 const latest = new Map();
602633 for (const r of reviews) {
603634 const reviewer = r.user.login.toLowerCase();
635+ const reviewerKey = codeRabbitLogins.has(reviewer) ? codeRabbitLogin : reviewer;
604636 if (r.state === 'DISMISSED') {
605- latest.delete(reviewer );
637+ latest.delete(reviewerKey );
606638 } else if (r.state !== 'COMMENTED') {
607- latest.set(reviewer , r);
639+ latest.set(reviewerKey , r);
608640 }
609641 }
610642
@@ -616,6 +648,7 @@ jobs:
616648 for (const review of latest.values()) {
617649 if (review.commit_id !== pr.head.sha ||
618650 review.user?.type === 'Bot' ||
651+ codeRabbitLogins.has(review.user?.login.toLowerCase()) ||
619652 review.user?.login.toLowerCase() === pr.user?.login.toLowerCase()) {
620653 continue;
621654 }
@@ -627,12 +660,12 @@ jobs:
627660 review => review.state === 'CHANGES_REQUESTED'
628661 );
629662 const automatedAuthor = pr.user?.type === 'Bot';
630- const codeRabbitApproved = freshCodeRabbitReview?.state === 'APPROVED';
663+ const codeRabbitReviewComplete = freshCodeRabbitReview?.state === 'APPROVED';
631664 const codeRabbitChangesRequested = freshCodeRabbitReview?.state === 'CHANGES_REQUESTED';
632665 const maintainerApproval = freshMaintainerReviews
633666 .filter(review => review.state === 'APPROVED')
634667 .sort((a, b) => b.id - a.id)[0];
635- const maintainerApprovedAfterCodeRabbit = codeRabbitApproved &&
668+ const maintainerApprovedAfterAutomatedReview = codeRabbitReviewComplete &&
636669 maintainerApproval &&
637670 maintainerApproval.id > freshCodeRabbitReview.id;
638671
@@ -654,7 +687,7 @@ jobs:
654687 desiredLabel = null;
655688 phase = 'approved';
656689 }
657- } else if (!codeRabbitApproved ) {
690+ } else if (!codeRabbitReviewComplete ) {
658691 if (pr.draft) {
659692 desiredLabel = null;
660693 phase = 'draft';
@@ -667,14 +700,32 @@ jobs:
667700 } else if (pr.draft) {
668701 desiredLabel = 'awaiting-ready';
669702 phase = 'draft-approved';
670- } else if (!maintainerApprovedAfterCodeRabbit ) {
703+ } else if (!maintainerApprovedAfterAutomatedReview ) {
671704 desiredLabel = 'awaiting-maintainer';
672705 phase = 'maintainer';
673706 } else {
674707 desiredLabel = null;
675708 phase = 'approved';
676709 }
677710
711+ if (desiredLabel === 'awaiting-maintainer') {
712+ const { data: latestPrDetail } = await github.rest.pulls.get({
713+ owner, repo, pull_number: pr.number,
714+ });
715+ if (latestPrDetail.mergeable === false && latestPrDetail.mergeable_state === 'dirty') {
716+ core.info(`PR #${pr.number}: has merge conflicts — labeling has-conflicts`);
717+ await updateReviewGate(pr, 'conflict', false);
718+ await setCodeRabbitReviewActive(pr, false);
719+ await reconcileLabels(pr, 'has-conflicts');
720+ await updateReviewGuide(pr, 'conflict', existingGuide);
721+ continue;
722+ }
723+ if (latestPrDetail.mergeable === null || latestPrDetail.mergeable_state === 'unknown') {
724+ desiredLabel = null;
725+ phase = 'mergeability-pending';
726+ }
727+ }
728+
678729 core.info(
679730 `PR #${pr.number}: CI passing, reviews=${latest.size}, ` +
680731 `coderabbit=${freshCodeRabbitReview?.state ?? (automatedAuthor ? 'optional' : 'pending')}, ` +
@@ -714,10 +765,12 @@ jobs:
714765 } catch (cleanupError) {
715766 metadataErrors.push(cleanupError);
716767 }
717- try {
718- await reconcileLabels(pr, null);
719- } catch (cleanupError) {
720- metadataErrors.push(cleanupError);
768+ if (!error.preserveStateLabels) {
769+ try {
770+ await reconcileLabels(pr, null);
771+ } catch (cleanupError) {
772+ metadataErrors.push(cleanupError);
773+ }
721774 }
722775 const detail = error.status
723776 ? `${error.message} (HTTP ${error.status}${error.response?.data?.message ? `: ${error.response.data.message}` : ''})`
0 commit comments