Skip to content
Merged
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5ddd48f
PR review bot
souvikghosh04 Mar 26, 2026
5ff5538
updated reviewers
souvikghosh04 Mar 26, 2026
d64bb42
draft PR inclusion
souvikghosh04 Mar 26, 2026
12105c5
test draft PR for review bot
souvikghosh04 Mar 26, 2026
8c11874
copilot review fixes
souvikghosh04 Mar 27, 2026
7e2463c
Merge branch 'main' into Usr/sogh/prreviewbot
souvikghosh04 Mar 27, 2026
d085f1f
Merge branch 'main' into Usr/sogh/prreviewbot
Aniruddh25 Mar 30, 2026
9cfc781
added fixes and PR review suggesstions
souvikghosh04 Mar 31, 2026
0ec4dfc
Merge branch 'main' of https://github.com/Azure/data-api-builder into…
souvikghosh04 Apr 3, 2026
77733d6
Fixed review comments
souvikghosh04 Apr 3, 2026
194e274
Fixes and improvements
souvikghosh04 Apr 8, 2026
b169d89
Address review comments: paginate listReviews, strip label after assi…
souvikghosh04 Apr 8, 2026
d2fe2f3
Address review comments + add temp push trigger for testing
souvikghosh04 Apr 8, 2026
d74bcae
Merge branch 'main' into Usr/sogh/prreviewbot
souvikghosh04 Apr 8, 2026
9fb3069
Merge branch 'Usr/sogh/prreviewbot' of https://github.com/Azure/data-…
souvikghosh04 Apr 8, 2026
f9d9723
Fix: exclude PR author from assigned count — author cannot review the…
souvikghosh04 Apr 8, 2026
f700c93
Fix: remove duplicate author declaration
souvikghosh04 Apr 8, 2026
ac432e6
Turn off dry-run for live testing
souvikghosh04 Apr 8, 2026
97ec29a
Remove GHCP inst file and TODO section
souvikghosh04 Apr 13, 2026
1ca8743
remove GHCP inst file
souvikghosh04 Apr 13, 2026
7b206d6
Merge branch 'main' into Usr/sogh/prreviewbot
souvikghosh04 Apr 13, 2026
a43a208
Merge branch 'main' into Usr/sogh/prreviewbot
Aniruddh25 Apr 13, 2026
7973eef
Set DRY_RUN to false for pull_request_target events
souvikghosh04 Apr 14, 2026
2972316
Randomize assignees instead of fixed/alphabetical ordering
souvikghosh04 Apr 14, 2026
ca3d956
Merge branch 'main' into Usr/sogh/prreviewbot
aaronburtle Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions .github/workflows/auto-assign-reviewers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: PR Review Assignment Bot

on:
pull_request_target:
types: [opened, reopened, labeled]

Comment thread
souvikghosh04 marked this conversation as resolved.
workflow_dispatch:
inputs:
dry_run:
description: 'Run in dry-run mode (no actual assignments)'
Comment thread
souvikghosh04 marked this conversation as resolved.
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'

# schedule:
# - cron: "*/10 * * * *"

permissions:
pull-requests: write
contents: read

concurrency:
group: pr-review-assignment
cancel-in-progress: false

jobs:
assign-reviewers:
runs-on: ubuntu-latest
steps:
Comment thread
souvikghosh04 marked this conversation as resolved.
- name: Assign reviewers to eligible PRs
uses: actions/github-script@v7
with:
script: |
const reviewers = ["souvikghosh04", "Aniruddh25", "aaronburtle", "anushakolan", "RubenCerna2079", "JerryNixon"];
const REQUIRED_REVIEWERS = 2;
const DRY_RUN = '${{ github.event.inputs.dry_run || 'true' }}' === 'true';
Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated

const { owner, repo } = context.repo;

// Fetch all open PRs (paginated)
Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated
const allPRs = await github.paginate(github.rest.pulls.list, {
Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated
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));

Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated
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));

Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated
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;
return true;
}

// Calculate PR weight from labels
function getWeight(pr) {
const labels = pr.labels.map((l) => l.name);
Comment thread
souvikghosh04 marked this conversation as resolved.
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;
}

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;
}
}
}

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);
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;
Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated
}

const author = pr.user.login;

// Build candidates: exclude author and already-assigned/reviewed reviewers
const candidates = reviewers
.filter((r) => r !== author && !configuredSet.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,
Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated
});
core.info(`Assigned [${selected.join(", ")}] to PR #${pr.number}`);
}

Comment thread
souvikghosh04 marked this conversation as resolved.
// Update load in-memory
for (const reviewer of selected) {
load[reviewer] += weight;
}

core.info(`Updated load: ${JSON.stringify(load)}`);
}

core.info("Review assignment complete.");