Assigning reviewers automatically Jump to heading
Most review delay happens before review starts. A pull request opens, nobody is named, and it waits in a list until either its author asks someone directly or a day passes. Automatic assignment removes that gap β but only if it assigns someone who can actually evaluate the change. A round-robin across the whole team removes the delay and replaces it with reviews from people guessing, which is worse. This recipe routes by ownership and balances within it, as part of pull request automation and bots.
When to use this approach Jump to heading
- Pull requests routinely wait hours before anyone looks at them.
- Your repository has identifiable areas with identifiable owners.
- Review load is unevenly distributed and the same two people do most of it.
- Authors currently chase reviewers over chat.
- If your team is three people who review everything, assignment automation solves nothing; spend the effort on review latency measurement instead.
Step 1 β Make ownership data trustworthy first Jump to heading
Routing is only as good as the map it reads. Check the map before automating anything on top of it.
# Which files have no owner at all?
git ls-files | while read -r f; do
grep -qE "^$(printf '%s' "${f%/*}" | sed 's|[.[\*^$]|\\&|g')" .github/CODEOWNERS || echo "$f"
done | cut -d/ -f1-2 | sort -u | head -20 # Which owners no longer exist?
grep -oE '@[A-Za-z0-9_/-]+' .github/CODEOWNERS | sort -u | while read -r handle; do
gh api "users/${handle#@}" --silent 2>/dev/null || echo "unknown owner: $handle"
done Step 2 β Resolve owners from the diff, longest match first Jump to heading
CODEOWNERS resolution takes the last matching rule, which is easy to get wrong when writing a script that emulates it.
#!/usr/bin/env sh
# scripts/owners-for-diff.sh β print the distinct owners for the current change.
set -eu
base="${1:-origin/main}"
git diff --name-only "$base"...HEAD | while read -r file; do
awk -v f="$file" '
/^[[:space:]]*(#|$)/ { next }
{
pat = $1
gsub(/^\//, "", pat)
if (index(f, pat) == 1 || pat == "*") {
owners = ""
for (i = 2; i <= NF; i++) owners = owners " " $i
last = owners # last match wins, as CODEOWNERS specifies
}
}
END { if (last != "") print last }
' .github/CODEOWNERS
done | tr ' ' '\n' | grep -v '^$' | sort -u # Verification: the script and the forge should agree
sh scripts/owners-for-diff.sh | sort > /tmp/mine
gh pr view --json reviewRequests --jq '.reviewRequests[].login' | sed 's/^/@/' | sort > /tmp/theirs
diff /tmp/mine /tmp/theirs && echo "resolution matches" Step 3 β Balance load inside the owning team Jump to heading
Assigning a whole team notifies everyone and commits nobody. Pick one available member, weighted by current load.
# Open review requests per team member, lowest first
gh api graphql -f query='
query($org:String!, $team:String!) {
organization(login:$org) { team(slug:$team) { members(first:50) { nodes { login } } } }
}' -f org=acme -f team=api --jq '.data.organization.team.members.nodes[].login' |
while read -r user; do
n=$(gh pr list --search "review-requested:$user is:open" --json number --jq 'length')
printf '%s %s\n' "$n" "$user"
done | sort -n | head -3 # Verification: assignment should land on the least-loaded member
gh pr edit "$PR" --add-reviewer "$(sh scripts/pick-reviewer.sh)"
gh pr view "$PR" --json reviewRequests --jq '.reviewRequests[].login' Step 4 β Respect availability Jump to heading
An assignment to someone on leave is a pull request that waits a fortnight and then gets reassigned by hand.
# Skip anyone with no activity in the last five days
active_since=$(date -u -d '5 days ago' +%Y-%m-%d)
gh api "search/issues?q=involves:$user+updated:>=$active_since" --jq '.total_count' \
| awk '{ if ($1 == 0) print "likely away"; else print "active" }' # Or read it from the source of truth if you have one
- name: Filter out anyone marked unavailable
run: |
jq -r '.unavailable[]' .github/availability.json > /tmp/away
grep -vxF -f /tmp/away /tmp/candidates > /tmp/assignable A committed availability file is unglamorous and works better than inference, because it is explicit and anyone can fix it.
Step 5 β Leave an escape hatch and measure the result Jump to heading
Automation must never make it harder to ask a specific person. Keep manual assignment working, and watch whether the automatic choice is being overridden.
# How often is the automatically assigned reviewer replaced?
gh pr list --state merged --limit 100 --json number,reviews,reviewRequests \
--jq '[.[] | select((.reviews[0].author.login // "") != (.reviewRequests[0].login // ""))] | length' A high override rate means the ownership map is wrong, not that the automation is. Fix the map.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What if a change touches areas owned by three different teams? Jump to heading
Assign one reviewer per owning team and say so in the pull request body. If that happens routinely, the change is too broad or the ownership boundaries do not match how the code actually changes β both are worth addressing directly rather than tuning the assignment rules.
Should the author ever be assigned as a reviewer? Jump to heading
No, and the script should filter them out explicitly, because CODEOWNERS will happily return the author for code they own. It is a common cause of a pull request that appears assigned but is waiting on nobody.
Does automatic assignment make review a formality? Jump to heading
It can, if assignment is treated as completion. The counterweight is measuring review depth β comments per pull request, or the proportion approved without a single comment β alongside latency, so a drop in quality is visible rather than inferred.
Related Jump to heading
- Pull Request Automation & Bots β the parent topic and the boundary automation should respect.
- Auto-Labelling Pull Requests by Changed Path β the labels this routing reads.
- Enforcing CODEOWNERS Review on Sensitive Paths β when ownership must be a gate, not a suggestion.