-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a script that provides GitHub API helpers for automation scripts in the repository. Signed-off-by: Michael Kubacki <[email protected]>
- Loading branch information
Showing
1 changed file
with
160 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
## @file | ||
# GitHub API helper functions. | ||
# | ||
# Copyright (c) Microsoft Corporation. | ||
# SPDX-License-Identifier: BSD-2-Clause-Patent | ||
# | ||
|
||
import logging | ||
import re | ||
import requests | ||
|
||
from collections import OrderedDict | ||
from edk2toollib.utility_functions import RunCmd | ||
from io import StringIO | ||
from typing import List | ||
|
||
"""GitHub API helper functions.""" | ||
|
||
|
||
def leave_pr_comment( | ||
token: str, owner: str, repo: str, pr_number: str, 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. | ||
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 | ||
) -> List[str]: | ||
"""Get the reviewers for the current branch. | ||
Args: | ||
workspace_path (str): The workspace path. | ||
maintainer_file_path (str): The maintainer file path. | ||
Returns: | ||
List[str]: A list of GitHub usernames. | ||
""" | ||
|
||
out_stream_buffer = StringIO() | ||
cmd_ret = RunCmd( | ||
"python", | ||
f"{maintainer_file_path} -g", | ||
workingdir=workspace_path, | ||
outstream=out_stream_buffer, | ||
logging_level=logging.INFO, | ||
) | ||
if cmd_ret != 0: | ||
print( | ||
f"::error title=Reviewer Lookup Error!::Error calling GetMaintainer.py: [{cmd_ret}]: {out_stream_buffer.getvalue()}" | ||
) | ||
return [] | ||
|
||
raw_reviewers = out_stream_buffer.getvalue().strip() | ||
|
||
pattern = r"\[(.*?)\]" | ||
matches = re.findall(pattern, raw_reviewers) | ||
if not matches: | ||
return [] | ||
matches = list(OrderedDict.fromkeys([match.strip() for match in matches])) | ||
return matches | ||
|
||
|
||
def download_gh_file(github_url: str, local_path: str, token=None): | ||
"""Downloads a file from GitHub. | ||
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}" | ||
|
||
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 | ||
|
||
with open(local_path, "w", encoding="utf-8") as file: | ||
file.write(response.text) | ||
|
||
|
||
def add_reviewers_to_pr( | ||
token: str, owner: str, repo: str, pr_number: str, user_names: List[str] | ||
): | ||
"""Adds the set of GitHub usernames as reviewers to the 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. | ||
user_names (List[str]): List of GitHub usernames to add as reviewers. | ||
""" | ||
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) | ||
try: | ||
response.raise_for_status() | ||
print(f"response is {response.json()}") | ||
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')}" | ||
) | ||
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." | ||
) |