Skip to content

Commit 8f4463e

Browse files
Merge branch 'main' into test/lmstudio-reasoning-e2e
2 parents 5679472 + 0dbd584 commit 8f4463e

168 files changed

Lines changed: 6578 additions & 529 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coderabbit.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ tone_instructions: >-
55
Prioritize correctness, security, data loss, lifecycle, and regressions; avoid speculative style
66
comments and unrelated refactors.
77
8+
chat:
9+
allow_non_org_members: false
10+
811
knowledge_base:
912
web_search:
1013
enabled: true
@@ -24,6 +27,7 @@ reviews:
2427
enabled: false
2528
drafts: false
2629
auto_incremental_review: true
30+
auto_pause_after_reviewed_commits: 0
2731
labels:
2832
- "coderabbit-review-active"
2933

@@ -125,6 +129,7 @@ reviews:
125129
and deprioritize prose-only nits that do not affect correctness or usability.
126130
127131
pre_merge_checks:
132+
override_requested_reviewers_only: true
128133
custom_checks:
129134
- name: Regression evidence
130135
mode: warning

.github/workflows/label-pr-review-state.yml

Lines changed: 92 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ name: Label PR review state
33
on:
44
schedule:
55
- cron: "0 * * * *" # hourly fallback
6+
push:
7+
branches: [main]
68
workflow_dispatch:
79
inputs:
810
pull_request_number:
@@ -15,11 +17,18 @@ on:
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

2228
permissions:
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:
3241
jobs:
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}` : ''})`
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Changed-code mutation testing
2+
3+
on:
4+
pull_request:
5+
types: [edited, opened, reopened, ready_for_review, synchronize]
6+
merge_group:
7+
types: [checks_requested]
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
mutation-diff:
18+
name: mutation-diff
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 30
21+
steps:
22+
- name: Record merge-queue enforcement
23+
if: github.event_name == 'merge_group'
24+
run: |
25+
echo "## Changed-code mutation testing" >> "$GITHUB_STEP_SUMMARY"
26+
echo "Mutation testing was enforced on each pull request before it entered the merge queue." >> "$GITHUB_STEP_SUMMARY"
27+
28+
- name: Checkout pull request merge result
29+
if: github.event_name == 'pull_request'
30+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
31+
with:
32+
ref: ${{ github.sha }}
33+
fetch-depth: 0
34+
persist-credentials: false
35+
36+
- name: Fetch pull request base
37+
if: github.event_name == 'pull_request'
38+
env:
39+
BASE_REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository }}.git
40+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
41+
run: git fetch --no-tags "$BASE_REPOSITORY_URL" "$BASE_SHA"
42+
43+
- name: Setup Node.js and pnpm
44+
if: github.event_name == 'pull_request'
45+
uses: ./.github/actions/setup-node-pnpm
46+
with:
47+
install-args: "--frozen-lockfile"
48+
49+
- name: Test mutation gate logic
50+
if: github.event_name == 'pull_request'
51+
run: pnpm test:mutation-ci
52+
53+
- name: Mutate changed executable lines
54+
if: github.event_name == 'pull_request'
55+
env:
56+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
57+
HEAD_SHA: ${{ github.sha }}
58+
run: node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
59+
60+
- name: Upload mutation reports
61+
id: mutation_report
62+
if: always() && github.event_name == 'pull_request'
63+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
64+
with:
65+
name: changed-code-mutation-report
66+
path: reports/mutation/
67+
if-no-files-found: ignore
68+
retention-days: 7
69+
70+
- name: Link mutation report artifact
71+
if: always() && github.event_name == 'pull_request' && steps.mutation_report.outputs.artifact-url != ''
72+
env:
73+
ARTIFACT_URL: ${{ steps.mutation_report.outputs.artifact-url }}
74+
run: |
75+
{
76+
echo ""
77+
echo "### Download mutation reports"
78+
echo "[Open the changed-code-mutation-report artifact]($ARTIFACT_URL), then open the package's mutation.html file."
79+
} >> "$GITHUB_STEP_SUMMARY"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ out-*
55
node_modules
66
package-lock.json
77
coverage/
8+
reports/mutation/
9+
.stryker-tmp/
810
mock/
911

1012
.DS_Store

0 commit comments

Comments
 (0)