From 5ddd48f0c7a319dfef90d073c89ef07c65e061ae Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Mar 2026 22:27:55 +0530 Subject: [PATCH 01/13] PR review bot --- .github/workflows/auto-assign-reviewers.yml | 139 ++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 .github/workflows/auto-assign-reviewers.yml diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml new file mode 100644 index 0000000000..490a4e0559 --- /dev/null +++ b/.github/workflows/auto-assign-reviewers.yml @@ -0,0 +1,139 @@ +name: PR Review Assignment Bot + +on: + pull_request: + types: [opened, reopened, synchronize, labeled] + + workflow_dispatch: + + # schedule: + # - cron: "*/10 * * * *" + +permissions: + pull-requests: write + contents: read + +jobs: + assign-reviewers: + runs-on: ubuntu-latest + steps: + - name: Assign reviewers to eligible PRs + uses: actions/github-script@v7 + with: + script: | + const reviewers = ["souvikghosh04", "Aniruddh25", "reviewerX"]; + const REQUIRED_REVIEWERS = 2; + const DRY_RUN = true; + + const { owner, repo } = context.repo; + + // Fetch all open PRs + const { data: allPRs } = await github.rest.pulls.list({ + owner, + repo, + state: "open", + per_page: 100, + }); + + // Determine if a PR is eligible + function isEligible(pr) { + const labels = pr.labels.map((l) => l.name); + if (!labels.includes("assign-for-review")) return false; + if (pr.draft) return false; + if ((pr.requested_reviewers || []).length >= REQUIRED_REVIEWERS) return false; + return true; + } + + // Calculate PR weight from labels + function getWeight(pr) { + const labels = pr.labels.map((l) => l.name); + let weight = 1; + if (labels.includes("size-medium")) weight = 2; + else if (labels.includes("size-large")) weight = 3; + if (labels.includes("priority-high")) weight += 1; + return weight; + } + + // Build load map from all load-relevant PRs (assigned + unassigned) + const load = {}; + for (const r of reviewers) { + load[r] = 0; + } + + const loadRelevantPRs = allPRs.filter((pr) => { + const labels = pr.labels.map((l) => l.name); + return labels.includes("assign-for-review") && !pr.draft; + }); + + const eligiblePRs = allPRs.filter(isEligible); + core.info(`Total eligible PRs: ${eligiblePRs.length}`); + + for (const pr of loadRelevantPRs) { + const weight = getWeight(pr); + const assigned = (pr.requested_reviewers || []).map((r) => r.login); + for (const reviewer of assigned) { + if (reviewer in load) { + load[reviewer] += weight; + } + } + } + + core.info(`Current load: ${JSON.stringify(load)}`); + + // Sort eligible PRs by weight descending (prioritize large PRs) + eligiblePRs.sort((a, b) => getWeight(b) - getWeight(a)); + + // Assign reviewers to each eligible PR + for (const pr of eligiblePRs) { + const weight = getWeight(pr); + const assigned = (pr.requested_reviewers || []).map((r) => r.login); + const needed = REQUIRED_REVIEWERS - assigned.length; + + core.info(`PR #${pr.number} — weight: ${weight}, existing reviewers: [${assigned.join(", ")}]`); + + if (needed <= 0) { + core.info(`PR #${pr.number} already has ${REQUIRED_REVIEWERS} reviewers, skipping.`); + continue; + } + + const author = pr.user.login; + + // Build candidates: exclude author and already-assigned reviewers + const candidates = reviewers + .filter((r) => r !== author && !assigned.includes(r)); + + if (candidates.length === 0) { + core.info(`PR #${pr.number} — no candidates after filtering.`); + continue; + } + + candidates.sort((a, b) => { + if (load[a] !== load[b]) return load[a] - load[b]; + return a.localeCompare(b); + }); + + core.info(`PR #${pr.number} candidates: ${JSON.stringify(candidates)}`); + + const selected = candidates.slice(0, needed); + + if (DRY_RUN) { + core.info(`[DRY RUN] Would assign [${selected.join(", ")}] to PR #${pr.number}`); + } else { + await github.rest.pulls.requestReviewers({ + owner, + repo, + pull_number: pr.number, + reviewers: selected, + }); + core.info(`Assigned [${selected.join(", ")}] to PR #${pr.number}`); + } + + // Update load in-memory + for (const reviewer of selected) { + load[reviewer] += weight; + } + + core.info(`Updated load: ${JSON.stringify(load)}`); + } + + core.info("Review assignment complete."); From 5ff5538d254af7591b84080ffee3e19f145fd941 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Mar 2026 22:31:01 +0530 Subject: [PATCH 02/13] updated reviewers --- .github/workflows/auto-assign-reviewers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index 490a4e0559..fec171583c 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -21,7 +21,7 @@ jobs: uses: actions/github-script@v7 with: script: | - const reviewers = ["souvikghosh04", "Aniruddh25", "reviewerX"]; + const reviewers = ["souvikghosh04", "Aniruddh25", "aaronburtle", "anushakolan", "RubenCerna2079", "JerryNixon"]; const REQUIRED_REVIEWERS = 2; const DRY_RUN = true; From d64bb4203f01f3a765c26e6552e497bb77d9b8fc Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Mar 2026 22:52:59 +0530 Subject: [PATCH 03/13] draft PR inclusion --- .github/workflows/auto-assign-reviewers.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index fec171583c..16a1a3fe05 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -60,6 +60,13 @@ jobs: load[r] = 0; } + // Debug: log all open PRs for diagnosis + core.info(`Total open PRs fetched: ${allPRs.length}`); + for (const pr of allPRs) { + const labels = pr.labels.map((l) => l.name); + core.info(` PR #${pr.number}: draft=${pr.draft}, labels=[${labels.join(", ")}]`); + } + const loadRelevantPRs = allPRs.filter((pr) => { const labels = pr.labels.map((l) => l.name); return labels.includes("assign-for-review") && !pr.draft; From 12105c5baf57daaad0cd6d6167fa001dd20828a0 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 26 Mar 2026 22:57:47 +0530 Subject: [PATCH 04/13] test draft PR for review bot --- .github/workflows/auto-assign-reviewers.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index 16a1a3fe05..42ce606082 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -39,7 +39,7 @@ jobs: function isEligible(pr) { const labels = pr.labels.map((l) => l.name); if (!labels.includes("assign-for-review")) return false; - if (pr.draft) return false; + // if (pr.draft) return false; if ((pr.requested_reviewers || []).length >= REQUIRED_REVIEWERS) return false; return true; } @@ -69,7 +69,7 @@ jobs: const loadRelevantPRs = allPRs.filter((pr) => { const labels = pr.labels.map((l) => l.name); - return labels.includes("assign-for-review") && !pr.draft; + return labels.includes("assign-for-review"); }); const eligiblePRs = allPRs.filter(isEligible); From 8c118740b4c3579440028fdeffc7fc41e2bb1d84 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 27 Mar 2026 09:25:05 +0530 Subject: [PATCH 05/13] copilot review fixes --- .github/workflows/auto-assign-reviewers.yml | 24 ++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index 42ce606082..eab6e61b84 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -1,10 +1,19 @@ name: PR Review Assignment Bot on: - pull_request: + pull_request_target: types: [opened, reopened, synchronize, labeled] workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no actual assignments)' + required: false + default: 'true' + type: choice + options: + - 'true' + - 'false' # schedule: # - cron: "*/10 * * * *" @@ -13,6 +22,10 @@ permissions: pull-requests: write contents: read +concurrency: + group: pr-review-assignment + cancel-in-progress: false + jobs: assign-reviewers: runs-on: ubuntu-latest @@ -23,12 +36,12 @@ jobs: script: | const reviewers = ["souvikghosh04", "Aniruddh25", "aaronburtle", "anushakolan", "RubenCerna2079", "JerryNixon"]; const REQUIRED_REVIEWERS = 2; - const DRY_RUN = true; + const DRY_RUN = '${{ github.event.inputs.dry_run || 'true' }}' === 'true'; const { owner, repo } = context.repo; - // Fetch all open PRs - const { data: allPRs } = await github.rest.pulls.list({ + // Fetch all open PRs (paginated) + const allPRs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", @@ -40,7 +53,8 @@ jobs: const labels = pr.labels.map((l) => l.name); if (!labels.includes("assign-for-review")) return false; // if (pr.draft) return false; - if ((pr.requested_reviewers || []).length >= REQUIRED_REVIEWERS) return false; + const totalReviewers = (pr.requested_reviewers || []).length + (pr.requested_teams || []).length; + if (totalReviewers >= REQUIRED_REVIEWERS) return false; return true; } From 9cfc7818c4f3d93ca9d0e186daa2280db6502ae7 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 31 Mar 2026 18:04:28 +0530 Subject: [PATCH 06/13] added fixes and PR review suggesstions --- .github/workflows/auto-assign-reviewers.yml | 27 ++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index eab6e61b84..222f60d5e6 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -52,9 +52,11 @@ jobs: function isEligible(pr) { const labels = pr.labels.map((l) => l.name); if (!labels.includes("assign-for-review")) return false; - // if (pr.draft) return false; - const totalReviewers = (pr.requested_reviewers || []).length + (pr.requested_teams || []).length; - if (totalReviewers >= REQUIRED_REVIEWERS) return false; + if (pr.draft) return false; + // Only count reviewers from our configured list (ignore CODEOWNERS auto-assigns) + const configuredAssigned = (pr.requested_reviewers || []) + .filter((r) => reviewers.includes(r.login)).length; + if (configuredAssigned >= REQUIRED_REVIEWERS) return false; return true; } @@ -74,16 +76,11 @@ jobs: load[r] = 0; } - // Debug: log all open PRs for diagnosis core.info(`Total open PRs fetched: ${allPRs.length}`); - for (const pr of allPRs) { - const labels = pr.labels.map((l) => l.name); - core.info(` PR #${pr.number}: draft=${pr.draft}, labels=[${labels.join(", ")}]`); - } const loadRelevantPRs = allPRs.filter((pr) => { const labels = pr.labels.map((l) => l.name); - return labels.includes("assign-for-review"); + return labels.includes("assign-for-review") && !pr.draft; }); const eligiblePRs = allPRs.filter(isEligible); @@ -107,13 +104,15 @@ jobs: // Assign reviewers to each eligible PR for (const pr of eligiblePRs) { const weight = getWeight(pr); - const assigned = (pr.requested_reviewers || []).map((r) => r.login); - const needed = REQUIRED_REVIEWERS - assigned.length; + // Only consider reviewers from our configured list + const allAssigned = (pr.requested_reviewers || []).map((r) => r.login); + const configuredAssigned = allAssigned.filter((r) => reviewers.includes(r)); + const needed = REQUIRED_REVIEWERS - configuredAssigned.length; - core.info(`PR #${pr.number} — weight: ${weight}, existing reviewers: [${assigned.join(", ")}]`); + core.info(`PR #${pr.number} — weight: ${weight}, configured reviewers: [${configuredAssigned.join(", ")}], all reviewers: [${allAssigned.join(", ")}]`); if (needed <= 0) { - core.info(`PR #${pr.number} already has ${REQUIRED_REVIEWERS} reviewers, skipping.`); + core.info(`PR #${pr.number} already has ${REQUIRED_REVIEWERS} configured reviewers, skipping.`); continue; } @@ -121,7 +120,7 @@ jobs: // Build candidates: exclude author and already-assigned reviewers const candidates = reviewers - .filter((r) => r !== author && !assigned.includes(r)); + .filter((r) => r !== author && !allAssigned.includes(r)); if (candidates.length === 0) { core.info(`PR #${pr.number} — no candidates after filtering.`); From 77733d6dd713befb6402724353b13cb7d3e3390b Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 3 Apr 2026 12:36:39 +0530 Subject: [PATCH 07/13] Fixed review comments --- .github/workflows/auto-assign-reviewers.yml | 69 ++++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index 222f60d5e6..eee9827d92 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -2,7 +2,7 @@ name: PR Review Assignment Bot on: pull_request_target: - types: [opened, reopened, synchronize, labeled] + types: [opened, reopened, labeled] workflow_dispatch: inputs: @@ -48,15 +48,31 @@ jobs: per_page: 100, }); - // Determine if a PR is eligible - function isEligible(pr) { + // Fetch the set of configured reviewers who are requested OR have already submitted a review. + // GitHub removes reviewers from requested_reviewers once they submit, so we must check both. + async function getConfiguredReviewerSet(pr) { + const requested = (pr.requested_reviewers || []) + .map((r) => r.login) + .filter((r) => reviewers.includes(r)); + + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr.number, + }); + const submitted = reviews + .map((r) => r.user.login) + .filter((r) => reviewers.includes(r)); + + return [...new Set([...requested, ...submitted])]; + } + + // Determine if a PR is eligible by labels and draft status. + // Reviewer count is checked later after fetching review data. + function isEligibleByLabels(pr) { const labels = pr.labels.map((l) => l.name); if (!labels.includes("assign-for-review")) return false; if (pr.draft) return false; - // Only count reviewers from our configured list (ignore CODEOWNERS auto-assigns) - const configuredAssigned = (pr.requested_reviewers || []) - .filter((r) => reviewers.includes(r.login)).length; - if (configuredAssigned >= REQUIRED_REVIEWERS) return false; return true; } @@ -78,18 +94,21 @@ jobs: core.info(`Total open PRs fetched: ${allPRs.length}`); - const loadRelevantPRs = allPRs.filter((pr) => { - const labels = pr.labels.map((l) => l.name); - return labels.includes("assign-for-review") && !pr.draft; - }); + const labelEligiblePRs = allPRs.filter(isEligibleByLabels); + core.info(`Label-eligible PRs (non-draft, has label): ${labelEligiblePRs.length}`); - const eligiblePRs = allPRs.filter(isEligible); - core.info(`Total eligible PRs: ${eligiblePRs.length}`); + // Fetch configured reviewer sets for all label-eligible PRs (used for load + eligibility) + const prReviewerMap = new Map(); + for (const pr of labelEligiblePRs) { + const configuredSet = await getConfiguredReviewerSet(pr); + prReviewerMap.set(pr.number, configuredSet); + } - for (const pr of loadRelevantPRs) { + // Build load from all label-eligible PRs (includes fully-assigned ones) + for (const pr of labelEligiblePRs) { const weight = getWeight(pr); - const assigned = (pr.requested_reviewers || []).map((r) => r.login); - for (const reviewer of assigned) { + const configuredSet = prReviewerMap.get(pr.number); + for (const reviewer of configuredSet) { if (reviewer in load) { load[reviewer] += weight; } @@ -98,18 +117,22 @@ jobs: core.info(`Current load: ${JSON.stringify(load)}`); + // Filter to PRs that still need reviewers + const eligiblePRs = labelEligiblePRs.filter((pr) => { + return prReviewerMap.get(pr.number).length < REQUIRED_REVIEWERS; + }); + core.info(`Total eligible PRs (need reviewers): ${eligiblePRs.length}`); + // Sort eligible PRs by weight descending (prioritize large PRs) eligiblePRs.sort((a, b) => getWeight(b) - getWeight(a)); // Assign reviewers to each eligible PR for (const pr of eligiblePRs) { const weight = getWeight(pr); - // Only consider reviewers from our configured list - const allAssigned = (pr.requested_reviewers || []).map((r) => r.login); - const configuredAssigned = allAssigned.filter((r) => reviewers.includes(r)); - const needed = REQUIRED_REVIEWERS - configuredAssigned.length; + const configuredSet = prReviewerMap.get(pr.number); + const needed = REQUIRED_REVIEWERS - configuredSet.length; - core.info(`PR #${pr.number} — weight: ${weight}, configured reviewers: [${configuredAssigned.join(", ")}], all reviewers: [${allAssigned.join(", ")}]`); + core.info(`PR #${pr.number} — weight: ${weight}, configured reviewers (requested + submitted): [${configuredSet.join(", ")}]`); if (needed <= 0) { core.info(`PR #${pr.number} already has ${REQUIRED_REVIEWERS} configured reviewers, skipping.`); @@ -118,9 +141,9 @@ jobs: const author = pr.user.login; - // Build candidates: exclude author and already-assigned reviewers + // Build candidates: exclude author and already-assigned/reviewed reviewers const candidates = reviewers - .filter((r) => r !== author && !allAssigned.includes(r)); + .filter((r) => r !== author && !configuredSet.includes(r)); if (candidates.length === 0) { core.info(`PR #${pr.number} — no candidates after filtering.`); From 194e2746d062400e652fc44b04e95006d3c8334a Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 8 Apr 2026 18:15:55 +0530 Subject: [PATCH 08/13] Fixes and improvements --- .github/workflows/auto-assign-reviewers.yml | 219 +++++++++++--------- 1 file changed, 117 insertions(+), 102 deletions(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index eee9827d92..bb8e7ee7dd 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -20,6 +20,7 @@ on: permissions: pull-requests: write + issues: write contents: read concurrency: @@ -34,149 +35,163 @@ jobs: uses: actions/github-script@v7 with: script: | - const reviewers = ["souvikghosh04", "Aniruddh25", "aaronburtle", "anushakolan", "RubenCerna2079", "JerryNixon"]; + // ── Configuration ── + const REVIEWERS = ["souvikghosh04", "Aniruddh25", "aaronburtle", "anushakolan", "RubenCerna2079"]; const REQUIRED_REVIEWERS = 2; - const DRY_RUN = '${{ github.event.inputs.dry_run || 'true' }}' === 'true'; + const STALE_DAYS = 90; + const DRY_RUN = context.eventName === "workflow_dispatch" + ? '${{ github.event.inputs.dry_run }}' === 'true' + : true; const { owner, repo } = context.repo; + const staleCutoff = new Date(); + staleCutoff.setDate(staleCutoff.getDate() - STALE_DAYS); - // Fetch all open PRs (paginated) - const allPRs = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "open", - per_page: 100, - }); - - // Fetch the set of configured reviewers who are requested OR have already submitted a review. - // GitHub removes reviewers from requested_reviewers once they submit, so we must check both. - async function getConfiguredReviewerSet(pr) { - const requested = (pr.requested_reviewers || []) - .map((r) => r.login) - .filter((r) => reviewers.includes(r)); - - const { data: reviews } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: pr.number, - }); - const submitted = reviews - .map((r) => r.user.login) - .filter((r) => reviewers.includes(r)); + // ── Helper functions ── - return [...new Set([...requested, ...submitted])]; + function getPoolAssignees(pr) { + return (pr.assignees || []) + .map((a) => a.login) + .filter((a) => REVIEWERS.includes(a)); } - // Determine if a PR is eligible by labels and draft status. - // Reviewer count is checked later after fetching review data. - function isEligibleByLabels(pr) { - const labels = pr.labels.map((l) => l.name); - if (!labels.includes("assign-for-review")) return false; + function isEligible(pr) { if (pr.draft) return false; + if (!pr.labels.some((l) => l.name === "assign-for-review")) return false; + if (new Date(pr.updated_at) < staleCutoff) return false; return true; } - // Calculate PR weight from labels function getWeight(pr) { const labels = pr.labels.map((l) => l.name); - let weight = 1; - if (labels.includes("size-medium")) weight = 2; - else if (labels.includes("size-large")) weight = 3; - if (labels.includes("priority-high")) weight += 1; - return weight; - } - - // Build load map from all load-relevant PRs (assigned + unassigned) - const load = {}; - for (const r of reviewers) { - load[r] = 0; + let w = 1; + if (labels.includes("size-medium")) w = 2; + else if (labels.includes("size-large")) w = 3; + if (labels.includes("priority-high")) w += 1; + return w; } - core.info(`Total open PRs fetched: ${allPRs.length}`); - - const labelEligiblePRs = allPRs.filter(isEligibleByLabels); - core.info(`Label-eligible PRs (non-draft, has label): ${labelEligiblePRs.length}`); - - // Fetch configured reviewer sets for all label-eligible PRs (used for load + eligibility) - const prReviewerMap = new Map(); - for (const pr of labelEligiblePRs) { - const configuredSet = await getConfiguredReviewerSet(pr); - prReviewerMap.set(pr.number, configuredSet); - } - - // Build load from all label-eligible PRs (includes fully-assigned ones) - for (const pr of labelEligiblePRs) { - const weight = getWeight(pr); - const configuredSet = prReviewerMap.get(pr.number); - for (const reviewer of configuredSet) { - if (reviewer in load) { - load[reviewer] += weight; - } - } + async function getActiveReviewers(pr) { + const assigned = getPoolAssignees(pr); + if (assigned.length === 0) return []; + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, repo, pull_number: pr.number, + }); + const submitted = new Set( + reviews.map((r) => r.user.login).filter((r) => REVIEWERS.includes(r)) + ); + return assigned.filter((r) => !submitted.has(r)); } - core.info(`Current load: ${JSON.stringify(load)}`); - - // Filter to PRs that still need reviewers - const eligiblePRs = labelEligiblePRs.filter((pr) => { - return prReviewerMap.get(pr.number).length < REQUIRED_REVIEWERS; - }); - core.info(`Total eligible PRs (need reviewers): ${eligiblePRs.length}`); - - // Sort eligible PRs by weight descending (prioritize large PRs) - eligiblePRs.sort((a, b) => getWeight(b) - getWeight(a)); - - // Assign reviewers to each eligible PR - for (const pr of eligiblePRs) { + // Assigns reviewers to a single PR, mutating load/activeCount in place. + async function assignReviewers(pr, load, activeCount) { + const assigned = getPoolAssignees(pr); + const needed = REQUIRED_REVIEWERS - assigned.length; const weight = getWeight(pr); - const configuredSet = prReviewerMap.get(pr.number); - const needed = REQUIRED_REVIEWERS - configuredSet.length; - - core.info(`PR #${pr.number} — weight: ${weight}, configured reviewers (requested + submitted): [${configuredSet.join(", ")}]`); if (needed <= 0) { - core.info(`PR #${pr.number} already has ${REQUIRED_REVIEWERS} configured reviewers, skipping.`); - continue; + core.info(`PR #${pr.number} — already has ${assigned.length} pool assignees, skipping.`); + return; } const author = pr.user.login; - - // Build candidates: exclude author and already-assigned/reviewed reviewers - const candidates = reviewers - .filter((r) => r !== author && !configuredSet.includes(r)); - + const candidates = REVIEWERS.filter((r) => r !== author && !assigned.includes(r)); if (candidates.length === 0) { core.info(`PR #${pr.number} — no candidates after filtering.`); - continue; + return; } + // Sort: prefer unblocked → then lowest load → then alphabetical. candidates.sort((a, b) => { + const aBlocked = activeCount[a] > 0 ? 1 : 0; + const bBlocked = activeCount[b] > 0 ? 1 : 0; + if (aBlocked !== bBlocked) return aBlocked - bBlocked; if (load[a] !== load[b]) return load[a] - load[b]; return a.localeCompare(b); }); - core.info(`PR #${pr.number} candidates: ${JSON.stringify(candidates)}`); - const selected = candidates.slice(0, needed); + core.info(`PR #${pr.number} — weight: ${weight}, assigned: [${assigned}], candidates: ${JSON.stringify(candidates.map((c) => `${c}(active:${activeCount[c]},load:${load[c]})`))}, selected: [${selected}]`); if (DRY_RUN) { - core.info(`[DRY RUN] Would assign [${selected.join(", ")}] to PR #${pr.number}`); + core.info(`[DRY RUN] Would assign [${selected}] to PR #${pr.number}`); } else { - await github.rest.pulls.requestReviewers({ - owner, - repo, - pull_number: pr.number, - reviewers: selected, + await github.rest.issues.addAssignees({ + owner, repo, issue_number: pr.number, assignees: selected, }); - core.info(`Assigned [${selected.join(", ")}] to PR #${pr.number}`); + core.info(`Assigned [${selected}] to PR #${pr.number}`); } - // Update load in-memory - for (const reviewer of selected) { - load[reviewer] += weight; + for (const r of selected) { + load[r] += weight; + activeCount[r] += 1; } + } + + // ── Main logic ── + + // For pull_request_target events, early-exit if the triggering PR isn't eligible. + if (context.eventName === "pull_request_target") { + const triggerPR = context.payload.pull_request; + if (!isEligible(triggerPR)) { + core.info(`Triggering PR #${triggerPR.number} is not eligible (draft, missing label, or stale). Exiting.`); + return; + } + const assigned = getPoolAssignees(triggerPR); + if (assigned.length >= REQUIRED_REVIEWERS) { + core.info(`Triggering PR #${triggerPR.number} already has ${assigned.length} pool assignees. Exiting.`); + return; + } + core.info(`Triggering PR #${triggerPR.number} needs reviewers. Proceeding with load calculation.`); + } + + // Fetch all open PRs (sorted by most recently updated, skip stale). + const allPRs = await github.paginate(github.rest.pulls.list, { + owner, repo, state: "open", sort: "updated", direction: "desc", per_page: 100, + }); + const freshPRs = allPRs.filter((pr) => new Date(pr.updated_at) >= staleCutoff); + const eligiblePRs = freshPRs.filter(isEligible); + + core.info(`Open PRs: ${allPRs.length}, fresh (≤${STALE_DAYS}d): ${freshPRs.length}, eligible: ${eligiblePRs.length}`); - core.info(`Updated load: ${JSON.stringify(load)}`); + // Build load map from active reviews across all eligible PRs. + const load = {}; + const activeCount = {}; + for (const r of REVIEWERS) { load[r] = 0; activeCount[r] = 0; } + + for (const pr of eligiblePRs) { + const assigned = getPoolAssignees(pr); + // Only call listReviews if this PR has pool assignees (saves API calls). + if (assigned.length === 0) continue; + const active = await getActiveReviewers(pr); + const weight = getWeight(pr); + for (const r of active) { + load[r] += weight; + activeCount[r] += 1; + } + } + core.info(`Active load: ${JSON.stringify(load)}`); + core.info(`Active counts: ${JSON.stringify(activeCount)}`); + + // Determine which PRs to process. + if (context.eventName === "pull_request_target") { + // Single-PR mode: only assign the triggering PR. + const triggerPR = context.payload.pull_request; + // Re-fetch full PR object to get latest assignees. + const { data: freshPR } = await github.rest.pulls.get({ + owner, repo, pull_number: triggerPR.number, + }); + await assignReviewers(freshPR, load, activeCount); + } else { + // Full-scan mode (workflow_dispatch / schedule): process all eligible PRs that need reviewers. + const needsReviewers = eligiblePRs + .filter((pr) => getPoolAssignees(pr).length < REQUIRED_REVIEWERS) + .sort((a, b) => getWeight(b) - getWeight(a)); + + core.info(`PRs needing reviewers: ${needsReviewers.length}`); + for (const pr of needsReviewers) { + await assignReviewers(pr, load, activeCount); + } } core.info("Review assignment complete."); From b169d890a2d736dcaa72d1d94941d3c50aed45d4 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 8 Apr 2026 18:23:06 +0530 Subject: [PATCH 09/13] Address review comments: paginate listReviews, strip label after assignment, optimizations --- .github/workflows/auto-assign-reviewers.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index bb8e7ee7dd..e9c6410354 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -74,8 +74,8 @@ jobs: async function getActiveReviewers(pr) { const assigned = getPoolAssignees(pr); if (assigned.length === 0) return []; - const { data: reviews } = await github.rest.pulls.listReviews({ - owner, repo, pull_number: pr.number, + const reviews = await github.paginate(github.rest.pulls.listReviews, { + owner, repo, pull_number: pr.number, per_page: 100, }); const submitted = new Set( reviews.map((r) => r.user.login).filter((r) => REVIEWERS.includes(r)) @@ -120,6 +120,16 @@ jobs: owner, repo, issue_number: pr.number, assignees: selected, }); core.info(`Assigned [${selected}] to PR #${pr.number}`); + + // Remove the label once reviewers are fully assigned so the bot + // doesn't re-process this PR on subsequent runs. + const newAssignedCount = assigned.length + selected.length; + if (newAssignedCount >= REQUIRED_REVIEWERS) { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr.number, name: "assign-for-review", + }).catch((e) => core.warning(`Could not remove label from PR #${pr.number}: ${e.message}`)); + core.info(`Removed 'assign-for-review' label from PR #${pr.number}`); + } } for (const r of selected) { From d2fe2f3cfc494cffa1e6d10e8b46f6e53b9c1919 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 8 Apr 2026 18:29:48 +0530 Subject: [PATCH 10/13] Address review comments + add temp push trigger for testing --- .github/pr-review-bot-README.md | 269 ++++++++++++++++++++ .github/workflows/auto-assign-reviewers.yml | 5 + 2 files changed, 274 insertions(+) create mode 100644 .github/pr-review-bot-README.md diff --git a/.github/pr-review-bot-README.md b/.github/pr-review-bot-README.md new file mode 100644 index 0000000000..934c0997fc --- /dev/null +++ b/.github/pr-review-bot-README.md @@ -0,0 +1,269 @@ +# PR Review Assignment Bot + +Automated GitHub Actions workflow that assigns reviewers to pull requests using weighted load balancing. + +**Workflow file:** [workflows/auto-assign-reviewers.yml](workflows/auto-assign-reviewers.yml) +**Branch:** `Usr/sogh/prreviewbot` (PR [#3326](https://github.com/Azure/data-api-builder/pull/3326)) +**Status:** Dry-run mode. Ready for team testing. + +--- + +## How It Works + +### Trigger Events + +| Event | When it fires | +|---|---| +| `pull_request_target: opened` | A new PR is created | +| `pull_request_target: reopened` | A closed PR is reopened | +| `pull_request_target: labeled` | A label is added to a PR | +| `workflow_dispatch` | Manual run from the Actions tab (with dry-run toggle) | + +> A cron schedule (`*/10 * * * *`) is available but currently commented out. + +### Opt-In Mechanism + +A PR must have the **`assign-for-review`** label to be considered. Draft PRs are automatically skipped. + +The label is added **manually** by the PR author when they consider the PR ready for review. Future improvements may automate this (e.g., auto-add on publish, or remove the label requirement entirely and trigger on all non-draft PRs). + +### Reviewer Pool + +| GitHub Handle | Name | Status | +|---|---|---| +| `souvikghosh04` | Souvik | Active | +| `Aniruddh25` | Aniruddh | Active (see note) | +| `aaronburtle` | Aaron | Active | +| `anushakolan` | Anusha | Active | +| `RubenCerna2079` | Ruben | Active | + +> Jerry (`JerryNixon`) has been removed from the pool — PM role, should not review code PRs. + +> **Decision needed:** Ruben raised that Aniruddh already reviews a disproportionate number of PRs. Options: (a) remove Ani from the bot pool so he's not auto-assigned, (b) keep him as equal, or (c) add a reduced-weight mechanism. Team leaning toward removing both Jerry and Ani to keep the pool to 4 engineers (Souvik, Aaron, Anusha, Ruben). + +Each PR gets **2 reviewers** assigned (configurable via `REQUIRED_REVIEWERS`). + +### Weighted Load Balancing + +The bot calculates a *weight* for each PR based on its labels, then distributes reviewers so the total weighted load is balanced across the pool. + +| Label | Weight | +|---|---| +| *(no size label)* | 1 | +| `size-medium` | 2 | +| `size-large` | 3 | +| `priority-high` | +1 (additive) | + +**Examples:** +- A default PR = weight 1 +- A `size-large` + `priority-high` PR = weight 4 + +Size labels are **manual and optional**. If no size label is set, the PR gets a default weight of 1 and the bot still assigns reviewers — it just won't do weighted load balancing. Auto-detection of PR size is a possible future enhancement. + +### Reviewer Lifecycle & Availability + +A reviewer goes through a defined lifecycle per PR assignment. The bot must respect this when deciding who is available. + +#### Active Review State (Blocked) + +A reviewer is considered **actively reviewing** a PR — and therefore **blocked** — from the moment they are assigned until they complete one of the following actions: +- **Approve** the PR +- **Request changes** on the PR +- **Comment** (submit a review with comments) +- **The PR is closed or merged** + +While a reviewer is in the active-review state for a PR, that PR counts toward their load. The bot **prefers unblocked reviewers** (those with no pending active reviews) when selecting candidates. If all reviewers in the pool have active reviews, the bot falls back to round-robin by least total load (see below). + +#### Round-Robin Fallback + +When **every reviewer** in the pool has active assignments, the bot does not stop — it continues assigning using the existing weighted load-balancing logic: +- Candidates are sorted by current weighted load (ascending). +- The reviewer(s) with the **fewest active assignments** are picked first. +- This naturally round-robins through the pool as load accumulates. + +This ensures PRs are never left without reviewers even when the team is busy. + +#### Freeing a Reviewer + +A reviewer's assignment on a PR is **released** (no longer counts toward their load) when any of these occur: +1. The reviewer **submits a review** (approve, request changes, or comment). +2. The PR is **merged**. +3. The PR is **closed**. + +Once freed, the reviewer becomes available for new assignments with a reduced load count. + +#### Re-Review Requests + +If a PR author **requests a re-review** (e.g., after addressing feedback and pushing new changes), the assignment lifecycle **starts from scratch**: +- The bot treats the PR as needing reviewers again. +- The same rules apply — load balancing, author exclusion, active-review checks. +- Previously assigned reviewers may or may not be re-assigned depending on current load. +- This can be triggered by removing and re-adding the `assign-for-review` label, or by a `reopened` event. + +### Assignment Algorithm + +The bot operates in two modes depending on the trigger: + +**Single-PR mode** (`pull_request_target`): Only processes the triggering PR. Early-exits if the PR is ineligible (draft, missing label, stale, or already fully assigned). Still builds the global load map for fair selection. + +**Full-scan mode** (`workflow_dispatch` / schedule): Scans all open, non-stale, eligible PRs and assigns reviewers to those that need them. Larger/higher-priority PRs are processed first. + +In both modes, the core assignment logic per PR is: +1. Exclude the PR author and already-assigned reviewers from candidates. +2. **Prefer unblocked reviewers** (no active reviews) over blocked ones. +3. Within the same availability tier, sort by weighted load (ascending), then alphabetical. +4. Pick the top N candidates needed. +5. Assign (or log in dry-run mode) and update the in-memory load map. + +### Optimizations + +| Optimization | Effect | +|---|---| +| **Single-PR fast path** | `pull_request_target` only assigns the triggering PR instead of scanning all PRs | +| **Early exit** | Skips immediately if trigger PR is ineligible or already fully assigned | +| **Stale PR filter** | PRs not updated in 90 days are excluded from eligibility and load calculation | +| **Sorted fetch** | PRs fetched sorted by `updated_at` desc for cache-friendly access | +| **Conditional `listReviews`** | Only calls `listReviews` for PRs that have pool assignees (skips zero-assignee PRs) | + +### Reviewer Counting + +The bot uses the **Assignees** field (`pr.assignees`) to track who has been bot-assigned to a PR. This avoids the CODEOWNERS problem where `requested_reviewers` is pre-populated with all code owners. + +To determine active vs. freed reviewers, the bot calls `pulls.listReviews` for each eligible PR: +- **Active reviewer:** Assigned to the PR but has **not** submitted a review yet → counts toward load. +- **Freed reviewer:** Has submitted a review (approve, request changes, or comment) → does **not** count toward load. + +Only reviewers from the configured pool are counted. CODEOWNERS or external reviewers are ignored. + +### Dry-Run Mode + +The bot is currently in **dry-run mode** (`DRY_RUN = true`). In this mode it logs `[DRY RUN] Would assign [...]` but does not actually add reviewers. + +To go live, set `DRY_RUN` to `false` in the workflow (or select `false` when triggering manually via `workflow_dispatch`). + +### Concurrency + +The workflow uses a concurrency group (`pr-review-assignment`) with `cancel-in-progress: false` to prevent overlapping runs from producing inconsistent assignments. + +--- + +## Configuration Reference + +| Setting | Location | Default | Description | +|---|---|---|---| +| `REVIEWERS` | Script constant | 5-person pool | GitHub usernames eligible to be assigned | +| `REQUIRED_REVIEWERS` | Script constant | `2` | Number of reviewers per PR | +| `STALE_DAYS` | Script constant | `90` | PRs not updated in this many days are skipped | +| `DRY_RUN` | Script constant / workflow input | `true` | When true, logs only — no actual assignments | +| `assign-for-review` | PR label | — | Label that opts a PR into auto-assignment (removed after assignment when live) | +| Size labels | PR labels | `size-medium`, `size-large` | Increase PR weight for load balancing | +| `priority-high` | PR label | — | Adds +1 to PR weight | + +--- + +## How to Validate (Dry-Run Testing) + +1. Open (or reopen) a PR and add the **`assign-for-review`** label. +2. Go to the **Actions** tab in the repo. +3. Find the latest **"PR Review Assignment Bot"** workflow run. +4. Open the **"Assign reviewers to eligible PRs"** step log. +5. Look for `[DRY RUN] Would assign [...]` lines — these show who would be assigned and to which PR. +6. Verify the assignments look correct (author excluded, load balanced, right reviewer count). + +You can also trigger a manual run from the **Actions** tab → **PR Review Assignment Bot** → **Run workflow** and choose dry-run `true` or `false`. + +--- + +## Changelog + +| Date | Change | +|---|---| +| March 26 | Initial workflow created with `pulls.requestReviewers`, dry-run mode | +| April 8 | **BUG FIX:** CODEOWNERS inflated `requested_reviewers` → switched to `pr.assignees` | +| April 8 | **BUG FIX:** `pulls.requestReviewers` wrote to Reviewers field → switched to `issues.addAssignees` | +| April 8 | **BUG FIX:** Dry-run toggle `\|\|` expression always true → fixed with `context.eventName` check | +| April 8 | Removed Jerry from pool (PM role) | +| April 8 | Added active-review blocking, reviewer-freed logic, round-robin fallback | +| April 8 | Added optimizations: single-PR fast path, early exit, stale PR filter (90 days), conditional `listReviews` | +| April 8 | Paginated `listReviews` to handle PRs with 30+ reviews (review comment fix) | +| April 8 | Auto-remove `assign-for-review` label after successful assignment (review comment fix) | + +--- + +## Open Items + +| # | Item | Owner | Status | +|---|---|---|---| +| 1 | **Decide on Aniruddh's inclusion** — remove to reduce his load, or keep as equal | Team | Pending | +| 2 | **Re-enable workflow & test dry-run for 1 week** | Souvik + Team | Next step | +| 3 | **Flip to live mode** after dry-run validation | Souvik | Blocked on #2 | +| 4 | Consider auto-detecting PR size from line count | Souvik | Future | +| 5 | Consider removing label requirement (auto-assign all non-draft PRs) | Team | Future | + +--- + +## Testing Plan + +1. **Re-enable the workflow** in dry-run mode. +2. **Team adds `assign-for-review` labels** to published PRs for 1 week. +3. **Check logs** — verify `[DRY RUN] Would assign [...]` output: + - Author excluded, 2 reviewers selected, load balanced. + - Stale PRs (>90 days) are skipped. + - Unblocked reviewers preferred over blocked ones. + - Round-robin fallback when everyone has active reviews. + - Re-review (label re-add) triggers fresh assignment. +4. **Flip to live** — set `DRY_RUN` to `false`. +5. **Monitor** for 1-2 weeks, refine as needed. + +--- + +## Team Communication + +### Original Communication (March 26) + +> Hi Team, +> +> We now have a GitHub Actions workflow that automatically assigns reviewers to pull requests. +> +> **How it works:** +> - Scans all open PRs for the `assign-for-review` label — this is the opt-in mechanism. +> - Assigns 2 reviewers per PR from the pool: Souvik, Aniruddh, Aaron, Anusha, Ruben, Jerry. +> - Uses weighted load balancing so reviewers are distributed fairly: +> - `size-medium` = 2x weight, `size-large` = 3x, default = 1x. +> - `priority-high` adds +1 to the weight. +> - Reviewers with the lightest current load are picked first. +> - Larger/higher-priority PRs are assigned first for best reviewer availability. +> - The PR author is automatically excluded from being assigned on their own PR. +> +> **Current state:** The bot is in dry-run mode and has been tested against draft PRs. It logs who would be assigned but doesn't actually add reviewers yet. Once the team is comfortable, we flip DRY_RUN to false to go live. +> +> Thanks, +> Souvik + +### Updated Communication (Draft) + +> **Subject: PR Review Assignment Bot — Ready for Testing** +> +> Hi Team, +> +> The PR review assignment bot has been updated with all the fixes from our demo discussion. Here's the summary: +> +> **What it does:** +> - Scans PRs for the `assign-for-review` label and assigns **2 reviewers** from the pool: Souvik, Aniruddh, Aaron, Anusha, Ruben. +> - Uses **weighted load balancing** — reviewers with the lightest active load are picked first. +> - **Prefers available reviewers** — those not currently reviewing any PR are prioritized. If everyone is busy, it round-robins by least load. +> - Reviewers are **freed** once they submit a review (approve, request changes, or comment). +> - PR author is automatically excluded. Stale PRs (>90 days) are skipped. +> +> **What changed:** +> - Fixed CODEOWNERS bug — assignments now use the **Assignees** field (not Reviewers), so CODEOWNERS no longer interferes. +> - Fixed API — bot now uses `issues.addAssignees` to write to the correct field. +> - Fixed dry-run toggle logic. +> - Added reviewer lifecycle: active-review blocking, freed-on-submit, round-robin fallback. +> - Optimized: single-PR mode for faster `pull_request_target` runs, stale PR filter. +> - Removed Jerry from pool (PM role). +> +> **Next step:** Add the `assign-for-review` label to your published PRs. Check the Actions tab logs to verify the `[DRY RUN]` output looks correct. Once we're confident, we flip to live. +> +> Thanks, +> Souvik diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index e9c6410354..1f653865ee 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -4,6 +4,11 @@ on: pull_request_target: types: [opened, reopened, labeled] + # TODO: Remove before merging — temporary trigger for testing from PR branch. + push: + branches: + - 'Usr/sogh/prreviewbot' + workflow_dispatch: inputs: dry_run: From f9d9723af57196c68eccc4b01464870d6c60799f Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 8 Apr 2026 18:45:14 +0530 Subject: [PATCH 11/13] =?UTF-8?q?Fix:=20exclude=20PR=20author=20from=20ass?= =?UTF-8?q?igned=20count=20=E2=80=94=20author=20cannot=20review=20their=20?= =?UTF-8?q?own=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-assign-reviewers.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index 1f653865ee..6e97257138 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -90,7 +90,9 @@ jobs: // Assigns reviewers to a single PR, mutating load/activeCount in place. async function assignReviewers(pr, load, activeCount) { - const assigned = getPoolAssignees(pr); + const author = pr.user.login; + // Exclude the author from assigned count — they can't review their own PR. + const assigned = getPoolAssignees(pr).filter((a) => a !== author); const needed = REQUIRED_REVIEWERS - assigned.length; const weight = getWeight(pr); @@ -153,8 +155,9 @@ jobs: return; } const assigned = getPoolAssignees(triggerPR); - if (assigned.length >= REQUIRED_REVIEWERS) { - core.info(`Triggering PR #${triggerPR.number} already has ${assigned.length} pool assignees. Exiting.`); + const author = triggerPR.user.login; + if (assigned.filter((a) => a !== author).length >= REQUIRED_REVIEWERS) { + core.info(`Triggering PR #${triggerPR.number} already has ${assigned.length} pool assignees (excl. author). Exiting.`); return; } core.info(`Triggering PR #${triggerPR.number} needs reviewers. Proceeding with load calculation.`); @@ -200,7 +203,10 @@ jobs: } else { // Full-scan mode (workflow_dispatch / schedule): process all eligible PRs that need reviewers. const needsReviewers = eligiblePRs - .filter((pr) => getPoolAssignees(pr).length < REQUIRED_REVIEWERS) + .filter((pr) => { + const assigned = getPoolAssignees(pr).filter((a) => a !== pr.user.login); + return assigned.length < REQUIRED_REVIEWERS; + }) .sort((a, b) => getWeight(b) - getWeight(a)); core.info(`PRs needing reviewers: ${needsReviewers.length}`); From f700c9317f943c03ab61b7ea03d8cf6b205aa543 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 8 Apr 2026 18:49:41 +0530 Subject: [PATCH 12/13] Fix: remove duplicate author declaration --- .github/workflows/auto-assign-reviewers.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index 6e97257138..cbf455ff5f 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -101,7 +101,6 @@ jobs: return; } - const author = pr.user.login; const candidates = REVIEWERS.filter((r) => r !== author && !assigned.includes(r)); if (candidates.length === 0) { core.info(`PR #${pr.number} — no candidates after filtering.`); From ac432e682ddc33f1fe931e4f7f00748c36d6a73f Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 8 Apr 2026 18:58:02 +0530 Subject: [PATCH 13/13] Turn off dry-run for live testing --- .github/workflows/auto-assign-reviewers.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index cbf455ff5f..970f8587c8 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -46,7 +46,7 @@ jobs: const STALE_DAYS = 90; const DRY_RUN = context.eventName === "workflow_dispatch" ? '${{ github.event.inputs.dry_run }}' === 'true' - : true; + : false; const { owner, repo } = context.repo; const staleCutoff = new Date();