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

[chore] add automation for codeowner tooling #11739

Merged
merged 13 commits into from
Feb 12, 2025
20 changes: 20 additions & 0 deletions .github/workflows/add-codeowners-to-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Add code owners to a PR'
on:
pull_request_target:
types:
- opened
- synchronize

jobs:
add-owners-to-pr:
runs-on: ubuntu-24.04
if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]' && github.repository_owner == 'open-telemetry' }}
steps:
- uses: actions/checkout@v4

- name: Run add-codeowners-to-pr.sh
run: ./.github/workflows/scripts/add-codeowners-to-pr.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR: ${{ github.event.number }}
20 changes: 20 additions & 0 deletions .github/workflows/add-labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Add Labels'
on:
issue_comment:
types: [created]

jobs:
add-labels:
if: ${{ !github.event.issue.pull_request && startsWith(github.event.comment.body, '/label') && github.repository_owner == 'open-telemetry' }}

runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

- name: Run add-labels.sh
run: ./.github/workflows/scripts/add-labels.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE: ${{ github.event.issue.number }}
COMMENT: ${{ github.event.comment.body }}
SENDER: ${{ github.event.sender.login }}
18 changes: 18 additions & 0 deletions .github/workflows/ping-codeowners-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: 'Ping code owners on issues'
on:
issues:
types: [labeled]

jobs:
ping-owners:
runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'open-telemetry' }}
steps:
- uses: actions/checkout@v4

- name: Run ping-codeowners-issues.sh
run: ./.github/workflows/scripts/ping-codeowners-issues.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE: ${{ github.event.issue.number }}
COMPONENT: ${{ github.event.label.name }}
20 changes: 20 additions & 0 deletions .github/workflows/ping-codeowners-on-new-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Ping code owners on a new issue'
on:
issues:
types: [opened]

jobs:
ping-owners-on-new-issue:
runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'open-telemetry' }}
steps:
- uses: actions/checkout@v4

- name: Run ping-codeowners-on-new-issue.sh
run: ./.github/workflows/scripts/ping-codeowners-on-new-issue.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE: ${{ github.event.issue.number }}
TITLE: ${{ github.event.issue.title }}
BODY: ${{ github.event.issue.body }}
OPENER: ${{ github.event.issue.user.login }}
20 changes: 20 additions & 0 deletions .github/workflows/ping-codeowners-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Ping code owners on PRs'
on:
pull_request_target:
types: [labeled]

jobs:
ping-owners:
runs-on: ubuntu-24.04
if: ${{ github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]' && github.repository_owner == 'open-telemetry' }}
steps:
- uses: actions/checkout@v4

- name: Run ping-codeowners-prs.sh
run: ./.github/workflows/scripts/ping-codeowners-prs.sh
env:
REPO: ${{ github.repository }}
AUTHOR: ${{ github.event.pull_request.user.login }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.number }}
COMPONENT: ${{ github.event.label.name }}
142 changes: 142 additions & 0 deletions .github/workflows/scripts/add-codeowners-to-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/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=""
LABELS=""
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

# Convert the CODEOWNERS entry to a label
COMPONENT_NAME=$(echo -n "${COMPONENT}" | sed -E 's%^(.+)/(.+)\1%\1/\2%')

if (( "${#COMPONENT_NAME}" > 50 )); then
echo "'${COMPONENT_NAME}' exceeds GitHub's 50-character limit on labels, skipping adding label"
continue
fi

if [[ -n "${LABELS}" ]]; then
LABELS+=","
fi
LABELS+="${COMPONENT_NAME}"
done
done

if [[ -n "${LABELS}" ]]; then
echo "Adding labels: ${LABELS}"
gh pr edit "${PR}" --add-label "${LABELS}" || echo "Failed to add labels"
else
echo "No labels found"
fi

# Note that adding the labels above will not trigger any other workflows to
# add code owners, so we have to do it here.
#
# 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"
81 changes: 81 additions & 0 deletions .github/workflows/scripts/add-labels.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
#
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
#
#

set -euo pipefail

if [[ -z "${ISSUE:-}" || -z "${COMMENT:-}" || -z "${SENDER:-}" ]]; then
echo "At least one of ISSUE, COMMENT, or SENDER has not been set, please ensure each is set."
exit 0
fi

CUR_DIRECTORY=$(dirname "$0")

if [[ ${COMMENT:0:6} != "/label" ]]; then
echo "Comment is not a label comment, exiting."
exit 0
fi

# key: label in comment
# value: actual label
declare -A COMMON_LABELS
COMMON_LABELS["arm64"]="arm64"
COMMON_LABELS["good-first-issue"]="good first issue"
COMMON_LABELS["help-wanted"]="help wanted"
COMMON_LABELS["discussion-needed"]="discussion-needed"
COMMON_LABELS["os:macos"]="os:macos"
COMMON_LABELS["os:windows"]="os:windows"
COMMON_LABELS["waiting-for-author"]="waiting-for-author"
COMMON_LABELS["waiting-for-codeowners"]="waiting-for-codeowners"
COMMON_LABELS["bug"]="bug"
COMMON_LABELS["priority:p0"]="priority:p0"
COMMON_LABELS["priority:p1"]="priority:p1"
COMMON_LABELS["priority:p2"]="priority:p2"
COMMON_LABELS["priority:p3"]="priority:p3"
COMMON_LABELS["stale"]="Stale"

LABELS=$(echo "${COMMENT}" | sed -E 's%^/label%%')

for LABEL_REQ in ${LABELS}; do
LABEL=$(echo "${LABEL_REQ}" | sed -E s/^[+-]?//)
# Trim newlines from label that would cause matching to fail
LABEL=$(echo "${LABEL}" | tr -d '\n')

SHOULD_ADD=true
if [[ "${LABEL_REQ:0:1}" = "-" ]]; then
SHOULD_ADD=false
fi

if [[ -v COMMON_LABELS["${LABEL}"] ]]; then
if [[ ${SHOULD_ADD} = true ]]; then
gh issue edit "${ISSUE}" --add-label "${COMMON_LABELS["${LABEL}"]}"
else
gh issue edit "${ISSUE}" --remove-label "${COMMON_LABELS["${LABEL}"]}"
fi
continue
fi

# Grep exits with status code 1 if there are no matches,
# so we manually set RESULT to 0 if nothing is found.
RESULT=$(grep -c "${LABEL}" .github/CODEOWNERS || true)

if [[ ${RESULT} = 0 ]]; then
echo "\"${LABEL}\" doesn't correspond to a component, skipping."
continue
fi

if [[ ${SHOULD_ADD} = true ]]; then
gh issue edit "${ISSUE}" --add-label "${LABEL}"

# Labels added by a GitHub Actions workflow don't trigger other workflows
# by design, so we have to manually ping code owners here.
COMPONENT="${LABEL}" ISSUE=${ISSUE} SENDER="${SENDER}" bash "${CUR_DIRECTORY}/ping-codeowners-issues.sh"
else
gh issue edit "${ISSUE}" --remove-label "${LABEL}"
fi
done


40 changes: 40 additions & 0 deletions .github/workflows/scripts/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" .github/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 .github/workflows/scripts/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/]' .github/CODEOWNERS | \
awk '{ print $1 }' | \
sed -E 's%(.+)/$%\1%'
Loading
Loading