2024-10-03 17:49:44 -06:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
# Get the code owners of the files changed by a PR,
|
|
|
|
# suitable to be consumed by the API endpoint to request reviews:
|
|
|
|
# https://docs.github.com/en/rest/pulls/review-requests?apiVersion=2022-11-28#request-reviewers-for-a-pull-request
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
log() {
|
|
|
|
echo "$@" >&2
|
|
|
|
}
|
|
|
|
|
2024-10-09 14:08:15 -06:00
|
|
|
if (( "$#" < 7 )); then
|
|
|
|
log "Usage: $0 GIT_REPO OWNERS_FILE BASE_REPO BASE_REF HEAD_REF PR_NUMBER PR_AUTHOR"
|
2024-10-03 17:49:44 -06:00
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
gitRepo=$1
|
2024-10-09 14:08:15 -06:00
|
|
|
ownersFile=$2
|
|
|
|
baseRepo=$3
|
|
|
|
baseRef=$4
|
|
|
|
headRef=$5
|
|
|
|
prNumber=$6
|
|
|
|
prAuthor=$7
|
2024-10-03 17:49:44 -06:00
|
|
|
|
|
|
|
tmp=$(mktemp -d)
|
|
|
|
trap 'rm -rf "$tmp"' exit
|
|
|
|
|
|
|
|
git -C "$gitRepo" diff --name-only --merge-base "$baseRef" "$headRef" > "$tmp/touched-files"
|
|
|
|
readarray -t touchedFiles < "$tmp/touched-files"
|
|
|
|
log "This PR touches ${#touchedFiles[@]} files"
|
|
|
|
|
|
|
|
# Get the owners file from the base, because we don't want to allow PRs to
|
|
|
|
# remove code owners to avoid pinging them
|
|
|
|
git -C "$gitRepo" show "$baseRef":"$ownersFile" > "$tmp"/codeowners
|
|
|
|
|
2024-10-09 14:12:12 -06:00
|
|
|
# Associative array with the user as the key for easy de-duplication
|
2024-10-11 21:27:01 -06:00
|
|
|
# Make sure to always lowercase keys to avoid duplicates with different casings
|
2024-10-09 14:12:12 -06:00
|
|
|
declare -A users=()
|
2024-10-03 17:49:44 -06:00
|
|
|
|
|
|
|
for file in "${touchedFiles[@]}"; do
|
|
|
|
result=$(codeowners --file "$tmp"/codeowners "$file")
|
|
|
|
|
|
|
|
read -r file owners <<< "$result"
|
|
|
|
if [[ "$owners" == "(unowned)" ]]; then
|
|
|
|
log "File $file is unowned"
|
|
|
|
continue
|
|
|
|
fi
|
|
|
|
log "File $file is owned by $owners"
|
|
|
|
|
|
|
|
# Split up multiple owners, separated by arbitrary amounts of spaces
|
|
|
|
IFS=" " read -r -a entries <<< "$owners"
|
|
|
|
|
|
|
|
for entry in "${entries[@]}"; do
|
|
|
|
# GitHub technically also supports Emails as code owners,
|
|
|
|
# but we can't easily support that, so let's not
|
|
|
|
if [[ ! "$entry" =~ @(.*) ]]; then
|
|
|
|
warn -e "\e[33mCodeowner \"$entry\" for file $file is not valid: Must start with \"@\"\e[0m" >&2
|
|
|
|
# Don't fail, because the PR for which this script runs can't fix it,
|
|
|
|
# it has to be fixed in the base branch
|
|
|
|
continue
|
|
|
|
fi
|
|
|
|
# The first regex match is everything after the @
|
|
|
|
entry=${BASH_REMATCH[1]}
|
2024-10-09 14:12:12 -06:00
|
|
|
|
|
|
|
if [[ "$entry" =~ (.*)/(.*) ]]; then
|
|
|
|
# Teams look like $org/$team
|
|
|
|
org=${BASH_REMATCH[1]}
|
|
|
|
team=${BASH_REMATCH[2]}
|
|
|
|
|
|
|
|
# Instead of requesting a review from the team itself,
|
|
|
|
# we request reviews from the individual users.
|
|
|
|
# This is because once somebody from a team reviewed the PR,
|
|
|
|
# the API doesn't expose that the team was already requested for a review,
|
|
|
|
# so we wouldn't be able to avoid rerequesting reviews
|
|
|
|
# without saving some some extra state somewhere
|
|
|
|
|
|
|
|
# We could also consider implementing a more advanced heuristic
|
|
|
|
# in the future that e.g. only pings one team member,
|
|
|
|
# but escalates to somebody else if that member doesn't respond in time.
|
|
|
|
gh api \
|
|
|
|
--cache=1h \
|
|
|
|
-H "Accept: application/vnd.github+json" \
|
|
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
|
|
"/orgs/$org/teams/$team/members" \
|
|
|
|
--jq '.[].login' > "$tmp/team-members"
|
|
|
|
readarray -t members < "$tmp/team-members"
|
|
|
|
log "Team $entry has these members: ${members[*]}"
|
|
|
|
|
|
|
|
for user in "${members[@]}"; do
|
2024-10-11 21:27:01 -06:00
|
|
|
users[${user,,}]=
|
2024-10-09 14:12:12 -06:00
|
|
|
done
|
2024-10-03 17:49:44 -06:00
|
|
|
else
|
|
|
|
# Everything else is a user
|
2024-10-11 21:27:01 -06:00
|
|
|
users[${entry,,}]=
|
2024-10-03 17:49:44 -06:00
|
|
|
fi
|
|
|
|
done
|
|
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
# Cannot request a review from the author
|
2024-10-11 21:27:01 -06:00
|
|
|
if [[ -v users[${prAuthor,,}] ]]; then
|
2024-10-03 17:49:44 -06:00
|
|
|
log "One or more files are owned by the PR author, ignoring"
|
2024-10-11 21:27:01 -06:00
|
|
|
unset 'users[${prAuthor,,}]'
|
2024-10-03 17:49:44 -06:00
|
|
|
fi
|
|
|
|
|
2024-10-09 14:08:15 -06:00
|
|
|
gh api \
|
|
|
|
-H "Accept: application/vnd.github+json" \
|
|
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
|
|
"/repos/$baseRepo/pulls/$prNumber/reviews" \
|
|
|
|
--jq '.[].user.login' > "$tmp/already-reviewed-by"
|
|
|
|
|
|
|
|
# And we don't want to rerequest reviews from people who already reviewed
|
|
|
|
while read -r user; do
|
2024-10-11 21:27:01 -06:00
|
|
|
if [[ -v users[${user,,}] ]]; then
|
2024-10-09 14:08:15 -06:00
|
|
|
log "User $user is a code owner but has already left a review, ignoring"
|
2024-10-11 21:27:01 -06:00
|
|
|
unset 'users[${user,,}]'
|
2024-10-09 14:08:15 -06:00
|
|
|
fi
|
|
|
|
done < "$tmp/already-reviewed-by"
|
|
|
|
|
2024-10-03 17:49:44 -06:00
|
|
|
# Turn it into a JSON for the GitHub API call to request PR reviewers
|
|
|
|
jq -n \
|
|
|
|
--arg users "${!users[*]}" \
|
|
|
|
'{
|
|
|
|
reviewers: $users | split(" "),
|
|
|
|
}'
|