Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bcf287e
TEMP: Modify Maintainers.txt for testing
makubacki Jul 22, 2024
c32dca1
Allow reviewer addition in this org
makubacki Jul 25, 2024
d4e8ba1
Use pull_request_target
makubacki Jul 25, 2024
c83ab47
Changes for getting PR branch SHA
makubacki Jul 25, 2024
cc5e115
GitHub.py: Use merge commit SHA
makubacki Jul 25, 2024
c5cad6c
Merge commit updates
makubacki Jul 25, 2024
cd771fa
Update workspace path
makubacki Jul 25, 2024
92bf582
Add remote to fetch
makubacki Jul 25, 2024
539f1ae
Add consolidated commit dbg msg
makubacki Jul 25, 2024
3eeca19
Add another test reviewer
makubacki Jul 25, 2024
995c4ad
Remove non-collaborator user
makubacki Jul 28, 2024
8e98241
Switch to PyGithub
makubacki Jul 28, 2024
50c86e4
PIP caching and other minor updates
makubacki Jul 28, 2024
a0218a4
Update GH repo object
makubacki Jul 28, 2024
3b703b0
Convert PaginatedList to normal list
makubacki Jul 28, 2024
c6242d3
Replace merge commit SHA fetch with PyGithub
makubacki Jul 28, 2024
a0500f3
Get login from more lists
makubacki Jul 28, 2024
8eacfc5
Keep notice messages on a single line
makubacki Jul 28, 2024
20a5d94
Test minimal repo checkout
makubacki Jul 28, 2024
db8e599
Also checkout BaseTools/Scripts
makubacki Jul 28, 2024
c87b48b
Adjust no new reviewers notice message
makubacki Jul 28, 2024
0bbf7f3
Use GitPython
makubacki Jul 28, 2024
55e0d05
Use GitPython in GitHub.py
makubacki Jul 28, 2024
56fbe25
Add another test reviewer
makubacki Jul 25, 2024
ee1e98d
Update admin search
makubacki Jul 28, 2024
1a26127
Adjust GitHub.py formatting slightly
makubacki Jul 28, 2024
2a13fc3
Prevent non-collab dup comments
makubacki Jul 28, 2024
ee12191
Add another non-collab to UefiCpuPkg
makubacki Jul 28, 2024
0e5eddf
Show workflow UI message even on no comment
makubacki Jul 28, 2024
4542d7d
Add exclusion
makubacki Jul 28, 2024
58a8fad
Remove requests from requirements.txt
makubacki Jul 28, 2024
e0c3d29
Move workflow implementation to standalone py file
makubacki Aug 3, 2024
5c5a418
add more info to non-collab comment
makubacki Aug 5, 2024
12820f6
update comment
makubacki Aug 5, 2024
7c24853
update comment
makubacki Aug 5, 2024
ab014fc
update comment
makubacki Aug 5, 2024
dafe447
update comment
makubacki Aug 5, 2024
1011497
pip: bump lcov-cobertura from 2.0.2 to 2.1.1
dependabot[bot] Jul 21, 2025
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
280 changes: 187 additions & 93 deletions .github/scripts/GitHub.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,113 @@
# SPDX-License-Identifier: BSD-2-Clause-Patent
#

import git
import logging
import re
import requests

from collections import OrderedDict
from edk2toollib.utility_functions import RunCmd, RunPythonScript
from edk2toollib.utility_functions import RunPythonScript
from github import Auth, Github, GithubException
from io import StringIO
from typing import List


"""GitHub API helper functions."""


def _authenticate(token: str):
"""Authenticate to GitHub using a token.

Args:
token (str): The GitHub token to use for authentication.

Returns:
Github: A GitHub instance.
"""
auth = Auth.Token(token)
return Github(auth=auth)


def _get_pr(token: str, owner: str, repo: str, pr_number: int):
"""Get the PR object from GitHub.

Args:
token (str): The GitHub token to use for authentication.
owner (str): The GitHub owner (organization) name.
repo (str): The GitHub repository name (e.g. 'edk2').
pr_number (int): The pull request number.

Returns:
PullRequest: The PullRequest object.
"""
try:
g = _authenticate(token)
return g.get_repo(f"{owner}/{repo}").get_pull(pr_number)
except GithubException as ge:
print(
f"::error title=Error Getting PR {pr_number} Info!::"
f"{ge.data['message']}"
)
return None


def leave_pr_comment(
token: str, owner: str, repo: str, pr_number: str, comment_body: str
token: str, owner: str, repo: str, pr_number: int, comment_body: str
):
"""Leaves a comment on a PR.

Args:
token (str): The GitHub token to use for authentication.
owner (str): The GitHub owner (organization) name.
repo (str): The GitHub repository name (e.g. 'edk2').
pr_number (str): The pull request number.
pr_number (int): The pull request number.
comment_body (str): The comment text. Markdown is supported.
"""
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_number}/comments"
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
}
data = {"body": comment_body}
response = requests.post(url, json=data, headers=headers)
response.raise_for_status()


def get_reviewers_for_current_branch(
workspace_path: str, maintainer_file_path: str, target_branch: str = "master"
if pr := _get_pr(token, owner, repo, pr_number):
try:
pr.create_issue_comment(comment_body)
except GithubException as ge:
print(
f"::error title=Error Commenting on PR {pr_number}!::"
f"{ge.data['message']}"
)


def get_reviewers_for_range(
workspace_path: str,
maintainer_file_path: str,
range_start: str = "master",
range_end: str = "HEAD",
) -> List[str]:
"""Get the reviewers for the current branch.

!!! note
This function accepts a range of commits and returns the reviewers
for that set of commits as a single list of GitHub usernames. To get
the reviewers for a single commit, set `range_start` and `range_end`
to the commit SHA.

Args:
workspace_path (str): The workspace path.
maintainer_file_path (str): The maintainer file path.
target_branch (str, optional): The name of the target branch that the
current HEAD will merge to. Defaults to "master".
range_start (str, optional): The range start ref. Defaults to "master".
range_end (str, optional): The range end ref. Defaults to "HEAD".

Returns:
List[str]: A list of GitHub usernames.
"""

commit_stream_buffer = StringIO()
cmd_ret = RunCmd(
"git",
f"log --format=format:%H {target_branch}..HEAD",
workingdir=workspace_path,
outstream=commit_stream_buffer,
logging_level=logging.INFO,
)
if cmd_ret != 0:
print(
f"::error title=Commit Lookup Error!::Error getting branch commits: [{cmd_ret}]: {commit_stream_buffer.getvalue()}"
)
return []
if range_start == range_end:
commits = [range_start]
else:
commits = [
c.hexsha
for c in git.Repo(workspace_path).iter_commits(
f"{range_start}..{range_end}"
)
]

raw_reviewers = []
for commit_sha in commit_stream_buffer.getvalue().splitlines():
for commit_sha in commits:
reviewer_stream_buffer = StringIO()
cmd_ret = RunPythonScript(
maintainer_file_path,
Expand All @@ -80,7 +122,9 @@ def get_reviewers_for_current_branch(
)
if cmd_ret != 0:
print(
f"::error title=Reviewer Lookup Error!::Error calling GetMaintainer.py: [{cmd_ret}]: {reviewer_stream_buffer.getvalue()}"
f"::error title=Reviewer Lookup Error!::Error calling "
f"GetMaintainer.py: [{cmd_ret}]: "
f"{reviewer_stream_buffer.getvalue()}"
)
return []

Expand All @@ -92,7 +136,8 @@ def get_reviewers_for_current_branch(
return []

print(
f"::debug title=Commit {commit_sha[:7]} Reviewer(s)::{', '.join(matches)}"
f"::debug title=Commit {commit_sha[:7]} "
f"Reviewer(s)::{', '.join(matches)}"
)

raw_reviewers.extend(matches)
Expand All @@ -104,35 +149,34 @@ def get_reviewers_for_current_branch(
return reviewers


def download_gh_file(github_url: str, local_path: str, token=None):
"""Downloads a file from GitHub.
def get_pr_sha(token: str, owner: str, repo: str, pr_number: int) -> str:
"""Returns the commit SHA of given PR branch.

This returns the SHA of the merge commit that GitHub creates from a
PR branch. This commit contains all of the files in the PR branch in
a single commit.

Args:
github_url (str): The GitHub raw file URL.
local_path (str): A local path to write the file contents to.
token (_type_, optional): A GitHub authentication token.
Only needed for a private repo. Defaults to None.
"""
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
token (str): The GitHub token to use for authentication.
owner (str): The GitHub owner (organization) name.
repo (str): The GitHub repository name (e.g. 'edk2').
pr_number (int): The pull request number.

try:
response = requests.get(github_url, headers=headers)
response.raise_for_status()
except requests.exceptions.HTTPError:
print(
f"::error title=HTTP Error!::Error downloading {github_url}: {response.reason}"
)
return
Returns:
str: The commit SHA of the PR branch. An empty string is returned
if the request fails.
"""
if pr := _get_pr(token, owner, repo, pr_number):
merge_commit_sha = pr.merge_commit_sha
print(f"::debug title=PR {pr_number} Merge Commit SHA::{merge_commit_sha}")
return merge_commit_sha

with open(local_path, "w", encoding="utf-8") as file:
file.write(response.text)
return ""


def add_reviewers_to_pr(
token: str, owner: str, repo: str, pr_number: str, user_names: List[str]
):
token: str, owner: str, repo: str, pr_number: int, user_names: List[str]
) -> List[str]:
"""Adds the set of GitHub usernames as reviewers to the PR.

Args:
Expand All @@ -141,47 +185,97 @@ def add_reviewers_to_pr(
repo (str): The GitHub repository name (e.g. 'edk2').
pr_number (str): The pull request number.
user_names (List[str]): List of GitHub usernames to add as reviewers.

Returns:
List[str]: A list of GitHub usernames that were successfully added as
reviewers to the PR. This list will exclude any reviewers
from the list provided if they are not relevant to the PR.
"""
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.v3+json",
}
pr_author_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/requested_reviewers"

response = requests.get(pr_author_url, headers=headers)
if response.status_code != 200:
print(f"::error title=HTTP Error!::Error getting PR author: {response.reason}")
return
pr_author = response.json().get("user").get("login").strip()
while pr_author in user_names:
user_names.remove(pr_author)
data = {"reviewers": user_names}
response = requests.post(url, json=data, headers=headers)
if not user_names:
print(
"::debug title=No PR Reviewers Requested!::"
"The list of PR reviewers is empty so not adding any reviewers."
)
return []

try:
response.raise_for_status()
except requests.exceptions.HTTPError:
if (
response.status_code == 422
and "Reviews may only be requested from collaborators"
in response.json().get("message")
):
print(
f"::error title=User is not a Collaborator!::{response.json().get('message')}"
)
g = _authenticate(token)
repo_gh = g.get_repo(f"{owner}/{repo}")
pr = repo_gh.get_pull(pr_number)
except GithubException as ge:
print(
f"::error title=Error Getting PR {pr_number} Info!::"
f"{ge.data['message']}"
)
return None

# The pull request author cannot be a reviewer.
pr_author = pr.user.login.strip()

# The current PR reviewers do not need to be requested again.
current_pr_requested_reviewers = [
r.login.strip() for r in pr.get_review_requests()[0]
]
current_pr_reviewed_reviewers = [r.user.login.strip() for r in pr.get_reviews()]
current_pr_reviewers = list(
set(current_pr_requested_reviewers + current_pr_reviewed_reviewers)
)

# A user can only be added if they are a collaborator of the repository.
repo_collaborators = [c.login.strip() for c in repo_gh.get_collaborators()]
non_collaborators = [u for u in user_names if u not in repo_collaborators]

excluded_pr_reviewers = [pr_author] + current_pr_reviewers + non_collaborators
new_pr_reviewers = [u for u in user_names if u not in excluded_pr_reviewers]

# Notify the admins of the repository if non-collaborators are requested.
if non_collaborators:
print(
f"::warning title=Non-Collaborator Reviewers Found!::"
f"{', '.join(non_collaborators)}"
)

for comment in pr.get_issue_comments():
# If a comment has already been made for these non-collaborators,
# do not make another comment.
if (
comment.user.login == "github-actions[bot]"
and "WARNING: Cannot add some reviewers" in comment.body
and all(u in comment.body for u in non_collaborators)
):
break
else:
repo_admins = [
a.login for a in repo_gh.get_collaborators(permission="admin")
]

leave_pr_comment(
token,
owner,
repo,
pr_number,
f"⚠ **WARNING: Cannot add reviewers**: A user specified as a "
f"reviewer for this PR is not a collaborator "
f"of the edk2 repository. Please add them as a collaborator to the "
f"repository and re-request the review.\n\n"
f"Users requested:\n{', '.join(user_names)}",
)
elif response.status_code == 422:
print(
"::error title=Invalid Request!::The request is invalid. "
"Verify the API request string."
f"⚠ **WARNING: Cannot add some reviewers**: A user "
f"specified as a reviewer for this PR is not a collaborator "
f"of the repository. Please add them as a collaborator to "
f"the repository so they can be requested in the future.\n\n"
f"Non-collaborators requested:\n"
f"{'\n'.join([f'- @{c}' for c in non_collaborators])}"
f"\n\nAttn Admins:\n"
f"{'\n'.join([f'- @{a}' for a in repo_admins])}\n---\n"
f"Admin Instructions:\n"
f"- Add the non-collaborators as collaborators to the "
f"appropriate team(s) listed in "
f"[teams](https://github.com/orgs/tianocore/teams)\n"
f"- If they are no longer needed as reviewers, remove them "
f"from [`Maintainers.txt`](https://github.com/tianocore/edk2/blob/HEAD/Maintainers.txt)",
)

# Add any new reviewers to the PR if needed.
if new_pr_reviewers:
print(
f"::debug title=Adding New PR Reviewers::" f"{', '.join(new_pr_reviewers)}"
)

pr.create_review_request(reviewers=new_pr_reviewers)

return new_pr_reviewers
Loading