diff --git a/.github/workflows/request_codeowners_review.yml b/.github/workflows/request_codeowners_review.yml new file mode 100644 index 00000000000..d16f17729dd --- /dev/null +++ b/.github/workflows/request_codeowners_review.yml @@ -0,0 +1,20 @@ +name: 'Request reviews from code owners of a PR' +on: + pull_request: + 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 }} diff --git a/tools/get-codeowners.sh b/tools/get-codeowners.sh new file mode 100755 index 00000000000..53da2be464a --- /dev/null +++ b/tools/get-codeowners.sh @@ -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}" diff --git a/tools/get-components.sh b/tools/get-components.sh new file mode 100755 index 00000000000..c02bda8dff0 --- /dev/null +++ b/tools/get-components.sh @@ -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%' diff --git a/tools/request_codeowners_review.sh b/tools/request_codeowners_review.sh new file mode 100755 index 00000000000..fabbbf2114d --- /dev/null +++ b/tools/request_codeowners_review.sh @@ -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"