Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically request reviews from codeowners of components #6282

Merged
merged 6 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions .github/workflows/request_codeowners_review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Request reviews from code owners of a PR'
on:
pull_request_target:
types: [opened, synchronize]

jobs:
request_codeowners_review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
if: ${{ github.repository_owner == 'open-telemetry' }}
steps:
- uses: actions/checkout@v4

- name: Run request_codeowners_review.sh
run: ./tools/request_codeowners_review.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR: ${{ github.event.number }}
40 changes: 40 additions & 0 deletions tools/get-codeowners.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
#
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
#
# This script checks the GitHub CODEOWNERS file for any code owners
# of contrib components and returns a string of the code owners if it
# finds them.

set -euo pipefail

get_component_type() {
echo "${COMPONENT}" | cut -f 1 -d '/'
}

get_codeowners() {
# grep arguments explained:
# -m 1: Match the first occurrence
# ^: Match from the beginning of the line
# ${1}: Insert first argument given to this function
# [\/]\?: Match 0 or 1 instances of a forward slash
# \s: Match any whitespace character
(grep -m 1 "^${1}[\/]\?\s" CODEOWNERS || true) | \
sed 's/ */ /g' | \
cut -f3- -d ' '
}

if [[ -z "${COMPONENT:-}" ]]; then
echo "COMPONENT has not been set, please ensure it is set."
exit 1
fi

OWNERS="$(get_codeowners "${COMPONENT}")"

if [[ -z "${OWNERS:-}" ]]; then
COMPONENT_TYPE=$(get_component_type "${COMPONENT}")
OWNERS="$(get_codeowners "${COMPONENT}${COMPONENT_TYPE}")"
fi

echo "${OWNERS}"
11 changes: 11 additions & 0 deletions tools/get-components.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env sh
#
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
#
# Get a list of components within the repository that have some form of ownership
# ascribed to them.

grep -E '^[A-Za-z0-9/]' CODEOWNERS | \
awk '{ print $1 }' | \
sed -E 's%(.+)/$%\1%'
119 changes: 119 additions & 0 deletions tools/request_codeowners_review.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
#
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
#
# Adds code owners without write access as reviewers on a PR. Note that
# the code owners must still be a member of the `open-telemetry`
# organization.
#
# Note that since this script is considered a requirement for PRs,
# it should never fail.

set -euo pipefail

if [[ -z "${REPO:-}" || -z "${PR:-}" ]]; then
echo "One or more of REPO and PR have not been set, please ensure each is set."
exit 0
fi

main () {
CUR_DIRECTORY=$(dirname "$0")

# Reviews may have comments that need to be cleaned up for jq,
# so restrict output to only printable characters and ensure escape
# sequences are removed.
# The latestReviews key only returns the latest review for each reviewer,
# cutting out any other reviews. We use that instead of requestedReviews
# since we need to get the list of users eligible for requesting another
# review. The GitHub CLI does not offer a list of all reviewers, which
# is only available through the API. To cut down on API calls to GitHub,
# we use the latest reviews to determine which users to filter out.
JSON=$(gh pr view "${PR}" --json "files,author,latestReviews" | tr -dc '[:print:]' | sed -E 's/\\[a-z]//g')
AUTHOR=$(echo -n "${JSON}"| jq -r '.author.login')
FILES=$(echo -n "${JSON}"| jq -r '.files[].path')
REVIEW_LOGINS=$(echo -n "${JSON}"| jq -r '.latestReviews[].author.login')
COMPONENTS=$(bash "${CUR_DIRECTORY}/get-components.sh")
REVIEWERS=""
declare -A PROCESSED_COMPONENTS
declare -A REVIEWED

for REVIEWER in ${REVIEW_LOGINS}; do
# GitHub adds "app/" in front of user logins. The API docs don't make
# it clear what this means or whether it will always be present. The
# '/' character isn't a valid character for usernames, so this won't
# replace characters within a username.
REVIEWED["@${REVIEWER//app\//}"]=true
done

if [[ -v REVIEWED[@] ]]; then
echo "Users that have already reviewed this PR and will not have another review requested:" "${!REVIEWED[@]}"
else
echo "This PR has not yet been reviewed, all code owners are eligible for a review request"
fi

for COMPONENT in ${COMPONENTS}; do
# Files will be in alphabetical order and there are many files to
# a component, so loop through files in an inner loop. This allows
# us to remove all files for a component from the list so they
# won't be checked against the remaining components in the components
# list. This provides a meaningful speedup in practice.
for FILE in ${FILES}; do
MATCH=$(echo -n "${FILE}" | grep -E "^${COMPONENT}" || true)

if [[ -z "${MATCH}" ]]; then
continue
fi

# If we match a file with a component we don't need to process the file again.
FILES=$(echo -n "${FILES}" | grep -v "${FILE}")

if [[ -v PROCESSED_COMPONENTS["${COMPONENT}"] ]]; then
continue
fi

PROCESSED_COMPONENTS["${COMPONENT}"]=true

OWNERS=$(COMPONENT="${COMPONENT}" bash "${CUR_DIRECTORY}/get-codeowners.sh")

for OWNER in ${OWNERS}; do
# Users that leave reviews are removed from the "requested reviewers"
# list and are eligible to have another review requested. We only want
# to request a review once, so remove them from the list.
if [[ -v REVIEWED["${OWNER}"] || "${OWNER}" = "@${AUTHOR}" ]]; then
continue
fi

if [[ -n "${REVIEWERS}" ]]; then
REVIEWERS+=","
fi
REVIEWERS+=$(echo -n "${OWNER}" | sed -E 's/@(.+)/"\1"/')
done
done
done

# We have to use the GitHub API directly due to an issue with how the CLI
# handles PR updates that causes it require access to organization teams,
# and the GitHub token doesn't provide that permission.
# For more: https://github.com/cli/cli/issues/4844
#
# The GitHub API validates that authors are not requested to review, but
# accepts duplicate logins and logins that are already reviewers.
if [[ -n "${REVIEWERS}" ]]; then
echo "Requesting review from ${REVIEWERS}"
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
"https://api.github.com/repos/${REPO}/pulls/${PR}/requested_reviewers" \
-d "{\"reviewers\":[${REVIEWERS}]}" \
| jq ".message" \
|| echo "jq was unable to parse GitHub's response"
else
echo "No code owners found"
fi
}

# We don't want this workflow to ever fail and block a PR,
# so ensure all errors are caught.
main || echo "Failed to run $0"
Loading