3 # Get the code owners of the files changed by a PR,
4 # suitable to be consumed by the API endpoint to request reviews:
5 # https://docs.github.com/en/rest/pulls/review-requests?apiVersion=2022-11-28#request-reviewers-for-a-pull-request
13 if (( "$#" < 7 )); then
14 log
"Usage: $0 GIT_REPO OWNERS_FILE BASE_REPO BASE_REF HEAD_REF PR_NUMBER PR_AUTHOR"
27 trap 'rm -rf "$tmp"' exit
29 git
-C "$gitRepo" diff --name-only --merge-base "$baseRef" "$headRef" > "$tmp/touched-files"
30 readarray
-t touchedFiles
< "$tmp/touched-files"
31 log
"This PR touches ${#touchedFiles[@]} files"
33 # Get the owners file from the base, because we don't want to allow PRs to
34 # remove code owners to avoid pinging them
35 git
-C "$gitRepo" show
"$baseRef":"$ownersFile" > "$tmp"/codeowners
37 # Associative array with the user as the key for easy de-duplication
38 # Make sure to always lowercase keys to avoid duplicates with different casings
41 for file in "${touchedFiles[@]}"; do
42 result
=$
(codeowners
--file "$tmp"/codeowners
"$file")
44 read -r file owners
<<< "$result"
45 if [[ "$owners" == "(unowned)" ]]; then
46 log
"File $file is unowned"
49 log
"File $file is owned by $owners"
51 # Split up multiple owners, separated by arbitrary amounts of spaces
52 IFS
=" " read -r -a entries
<<< "$owners"
54 for entry
in "${entries[@]}"; do
55 # GitHub technically also supports Emails as code owners,
56 # but we can't easily support that, so let's not
57 if [[ ! "$entry" =~ @
(.
*) ]]; then
58 warn
-e "\e[33mCodeowner \"$entry\" for file $file is not valid: Must start with \"@\"\e[0m" >&2
59 # Don't fail, because the PR for which this script runs can't fix it,
60 # it has to be fixed in the base branch
63 # The first regex match is everything after the @
64 entry
=${BASH_REMATCH[1]}
66 if [[ "$entry" =~
(.
*)/(.
*) ]]; then
67 # Teams look like $org/$team
68 org
=${BASH_REMATCH[1]}
69 team
=${BASH_REMATCH[2]}
71 # Instead of requesting a review from the team itself,
72 # we request reviews from the individual users.
73 # This is because once somebody from a team reviewed the PR,
74 # the API doesn't expose that the team was already requested for a review,
75 # so we wouldn't be able to avoid rerequesting reviews
76 # without saving some some extra state somewhere
78 # We could also consider implementing a more advanced heuristic
79 # in the future that e.g. only pings one team member,
80 # but escalates to somebody else if that member doesn't respond in time.
83 -H "Accept: application/vnd.github+json" \
84 -H "X-GitHub-Api-Version: 2022-11-28" \
85 "/orgs/$org/teams/$team/members" \
86 --jq '.[].login' > "$tmp/team-members"
87 readarray
-t members
< "$tmp/team-members"
88 log
"Team $entry has these members: ${members[*]}"
90 for user
in "${members[@]}"; do
94 # Everything else is a user
101 # Cannot request a review from the author
102 if [[ -v users
[${prAuthor,,}] ]]; then
103 log
"One or more files are owned by the PR author, ignoring"
104 unset 'users[${prAuthor,,}]'
108 -H "Accept: application/vnd.github+json" \
109 -H "X-GitHub-Api-Version: 2022-11-28" \
110 "/repos/$baseRepo/pulls/$prNumber/reviews" \
111 --jq '.[].user.login' > "$tmp/already-reviewed-by"
113 # And we don't want to rerequest reviews from people who already reviewed
114 while read -r user
; do
115 if [[ -v users
[${user,,}] ]]; then
116 log
"User $user is a code owner but has already left a review, ignoring"
117 unset 'users[${user,,}]'
119 done < "$tmp/already-reviewed-by"
121 # Turn it into a JSON for the GitHub API call to request PR reviewers
123 --arg users
"${!users[*]}" \
125 reviewers: $users | split(" "),