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
What an ownership audit usually findsA CODEOWNERS file written two years ago typically covers most of the tree, but a meaningful fraction of paths have no owner and a handful of named owners have left. Routing built on top of that silently sends changes nowhere.share of the tree, by ownership stateowned by an active team71%no owner at all22%owner no longer active7%fix the bottom two bars before automating anything that reads this file

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'
Rota assignment against ownership plus load balancingA rota distributes review work evenly across everyone, including people with no context for the change. Ownership routing picks a team that can judge it, then balances within that team, which spreads load without spreading confusion.Round-robin rotaOwners, then loadreviewer has contextsometimesby constructionload spreadperfectly eveneven within the teamreview depthvaries wildlyconsistentsetup costtrivialneeds CODEOWNERSperfectly even load is the wrong optimisation if half the reviews are guesses

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'
Where the waiting goes after routing is automatedBefore automation, a pull request waits most of a day to be picked up. Automatic assignment removes that gap almost entirely, leaving the remaining time in the parts a bot cannot change: the review itself and the author's response to it.Openedreviewer named immediately0 hFirst lookassignee notified once1.5 hComments postedreview complete4 hAuthor respondsasynchronous by nature9 hMergedgate passed11 hnineteen hours of waiting became ninety minutes; the rest was never the bottleneck

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.