diff options
author | Michael Kubacki <michael.kubacki@microsoft.com> | 2024-07-22 17:15:52 -0400 |
---|---|---|
committer | mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> | 2024-07-25 02:28:49 +0000 |
commit | 89a06a245bd27c6a83fa2b2ed80cfe186dab0029 (patch) | |
tree | c342baacc76de0d42a17e71eeb9b3db55d61d6e6 /.github | |
parent | 3f0c4cee940d550cf7543eef188b3b068df8b818 (diff) | |
download | edk2-89a06a245bd27c6a83fa2b2ed80cfe186dab0029.tar.gz |
.github: Add GitHub helper python script
Adds a script that provides GitHub API helpers for workflows and
other GitHub automation in the repository.
Signed-off-by: Michael Kubacki <michael.kubacki@microsoft.com>
Diffstat (limited to '.github')
-rw-r--r-- | .github/scripts/GitHub.py | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/.github/scripts/GitHub.py b/.github/scripts/GitHub.py new file mode 100644 index 0000000000..cd6bea5205 --- /dev/null +++ b/.github/scripts/GitHub.py @@ -0,0 +1,187 @@ +## @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, RunPythonScript
+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, target_branch: str = "master"
+) -> List[str]:
+ """Get the reviewers for the current branch.
+
+ 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".
+
+ 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 []
+
+ raw_reviewers = []
+ for commit_sha in commit_stream_buffer.getvalue().splitlines():
+ reviewer_stream_buffer = StringIO()
+ cmd_ret = RunPythonScript(
+ maintainer_file_path,
+ f"-g {commit_sha}",
+ workingdir=workspace_path,
+ outstream=reviewer_stream_buffer,
+ logging_level=logging.INFO,
+ )
+ if cmd_ret != 0:
+ print(
+ f"::error title=Reviewer Lookup Error!::Error calling GetMaintainer.py: [{cmd_ret}]: {reviewer_stream_buffer.getvalue()}"
+ )
+ return []
+
+ commit_reviewers = reviewer_stream_buffer.getvalue()
+
+ pattern = r"\[(.*?)\]"
+ matches = re.findall(pattern, commit_reviewers)
+ if not matches:
+ return []
+
+ print(
+ f"::debug title=Commit {commit_sha[:7]} Reviewer(s)::{', '.join(matches)}"
+ )
+
+ raw_reviewers.extend(matches)
+
+ reviewers = list(OrderedDict.fromkeys([r.strip() for r in raw_reviewers]))
+
+ print(f"::debug title=Total Reviewer Set::{', '.join(reviewers)}")
+
+ return reviewers
+
+
+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()
+ 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."
+ )
|