Skip to content
Merged
Changes from 8 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
159 changes: 159 additions & 0 deletions .github/workflows/auto-assign-reviewers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: PR Review Assignment Bot

on:
pull_request_target:
types: [opened, reopened, synchronize, labeled]
Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated

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

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

// 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 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);
Comment thread
souvikghosh04 marked this conversation as resolved.
Outdated
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);
// 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}, configured reviewers: [${configuredAssigned.join(", ")}], all reviewers: [${allAssigned.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 reviewers
const candidates = reviewers
.filter((r) => r !== author && !allAssigned.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.");