VirtualBox

source: vbox/trunk/src/VBox/Devices/EFI/FirmwareNew/.github/scripts/GitHub.py@ 109019

Last change on this file since 109019 was 108794, checked in by vboxsync, 4 weeks ago

Devices/EFI/FirmwareNew: Merge edk2-stable202502 from the vendor branch and make it build for the important platforms, bugref:4643

  • Property svn:eol-style set to native
File size: 9.6 KB
Line 
1## @file
2# GitHub API helper functions.
3#
4# Copyright (c) Microsoft Corporation.
5# SPDX-License-Identifier: BSD-2-Clause-Patent
6#
7
8import git
9import logging
10import re
11
12from collections import OrderedDict
13from edk2toollib.utility_functions import RunPythonScript
14from github import Auth, Github, GithubException
15from io import StringIO
16from typing import List
17
18
19"""GitHub API helper functions."""
20
21
22def _authenticate(token: str):
23 """Authenticate to GitHub using a token.
24
25 Returns a GitHub instance that is authenticated using the provided
26 token.
27
28 Args:
29 token (str): The GitHub token to use for authentication.
30
31 Returns:
32 Github: A GitHub instance.
33 """
34 auth = Auth.Token(token)
35 return Github(auth=auth)
36
37
38def _get_pr(token: str, owner: str, repo: str, pr_number: int):
39 """Get the PR object from GitHub.
40
41 Args:
42 token (str): The GitHub token to use for authentication.
43 owner (str): The GitHub owner (organization) name.
44 repo (str): The GitHub repository name (e.g. 'edk2').
45 pr_number (int): The pull request number.
46
47 Returns:
48 PullRequest: A PyGithub PullRequest object for the given pull request
49 or None if the attempt to get the PR fails.
50 """
51 try:
52 g = _authenticate(token)
53 return g.get_repo(f"{owner}/{repo}").get_pull(pr_number)
54 except GithubException as ge:
55 print(
56 f"::error title=Error Getting PR {pr_number} Info!::"
57 f"{ge.data['message']}"
58 )
59 return None
60
61
62def leave_pr_comment(
63 token: str, owner: str, repo: str, pr_number: int, comment_body: str
64):
65 """Leaves a comment on a PR.
66
67 Args:
68 token (str): The GitHub token to use for authentication.
69 owner (str): The GitHub owner (organization) name.
70 repo (str): The GitHub repository name (e.g. 'edk2').
71 pr_number (int): The pull request number.
72 comment_body (str): The comment text. Markdown is supported.
73 """
74 if pr := _get_pr(token, owner, repo, pr_number):
75 try:
76 pr.create_issue_comment(comment_body)
77 except GithubException as ge:
78 print(
79 f"::error title=Error Commenting on PR {pr_number}!::"
80 f"{ge.data['message']}"
81 )
82
83
84def get_reviewers_for_range(
85 workspace_path: str,
86 maintainer_file_path: str,
87 range_start: str = "master",
88 range_end: str = "HEAD",
89) -> List[str]:
90 """Get the reviewers for the current branch.
91
92 !!! note
93 This function accepts a range of commits and returns the reviewers
94 for that set of commits as a single list of GitHub usernames. To get
95 the reviewers for a single commit, set `range_start` and `range_end`
96 to the commit SHA.
97
98 Args:
99 workspace_path (str): The workspace path.
100 maintainer_file_path (str): The maintainer file path.
101 range_start (str, optional): The range start ref. Defaults to "master".
102 range_end (str, optional): The range end ref. Defaults to "HEAD".
103
104 Returns:
105 List[str]: A list of GitHub usernames.
106 """
107 if range_start == range_end:
108 commits = [range_start]
109 else:
110 commits = [
111 c.hexsha
112 for c in git.Repo(workspace_path).iter_commits(
113 f"{range_start}..{range_end}"
114 )
115 ]
116
117 raw_reviewers = []
118 for commit_sha in commits:
119 reviewer_stream_buffer = StringIO()
120 cmd_ret = RunPythonScript(
121 maintainer_file_path,
122 f"-g {commit_sha}",
123 workingdir=workspace_path,
124 outstream=reviewer_stream_buffer,
125 logging_level=logging.INFO,
126 )
127 if cmd_ret != 0:
128 print(
129 f"::error title=Reviewer Lookup Error!::Error calling "
130 f"GetMaintainer.py: [{cmd_ret}]: "
131 f"{reviewer_stream_buffer.getvalue()}"
132 )
133 return []
134
135 commit_reviewers = reviewer_stream_buffer.getvalue()
136
137 pattern = r"\[(.*?)\]"
138 matches = re.findall(pattern, commit_reviewers)
139 if not matches:
140 return []
141
142 print(
143 f"::debug title=Commit {commit_sha[:7]} "
144 f"Reviewer(s)::{', '.join(matches)}"
145 )
146
147 raw_reviewers.extend(matches)
148
149 reviewers = list(OrderedDict.fromkeys([r.strip() for r in raw_reviewers]))
150
151 print(f"::debug title=Total Reviewer Set::{', '.join(reviewers)}")
152
153 return reviewers
154
155
156def get_pr_sha(token: str, owner: str, repo: str, pr_number: int) -> str:
157 """Returns the commit SHA of given PR branch.
158
159 This returns the SHA of the merge commit that GitHub creates from a
160 PR branch. This commit contains all of the files in the PR branch in
161 a single commit.
162
163 Args:
164 token (str): The GitHub token to use for authentication.
165 owner (str): The GitHub owner (organization) name.
166 repo (str): The GitHub repository name (e.g. 'edk2').
167 pr_number (int): The pull request number.
168
169 Returns:
170 str: The commit SHA of the PR branch. An empty string is returned
171 if the request fails.
172 """
173 if pr := _get_pr(token, owner, repo, pr_number):
174 merge_commit_sha = pr.merge_commit_sha
175 print(f"::debug title=PR {pr_number} Merge Commit SHA::{merge_commit_sha}")
176 return merge_commit_sha
177
178 return ""
179
180
181def add_reviewers_to_pr(
182 token: str, owner: str, repo: str, pr_number: int, user_names: List[str]
183) -> List[str]:
184 """Adds the set of GitHub usernames as reviewers to the PR.
185
186 Args:
187 token (str): The GitHub token to use for authentication.
188 owner (str): The GitHub owner (organization) name.
189 repo (str): The GitHub repository name (e.g. 'edk2').
190 pr_number (int): The pull request number.
191 user_names (List[str]): List of GitHub usernames to add as reviewers.
192
193 Returns:
194 List[str]: A list of GitHub usernames that were successfully added as
195 reviewers to the PR. This list will exclude any reviewers
196 from the list provided if they are not relevant to the PR.
197 """
198 if not user_names:
199 print(
200 "::debug title=No PR Reviewers Requested!::"
201 "The list of PR reviewers is empty so not adding any reviewers."
202 )
203 return []
204
205 try:
206 g = _authenticate(token)
207 repo_gh = g.get_repo(f"{owner}/{repo}")
208 pr = repo_gh.get_pull(pr_number)
209 except GithubException as ge:
210 print(
211 f"::error title=Error Getting PR {pr_number} Info!::"
212 f"{ge.data['message']}"
213 )
214 return None
215
216 # The pull request author cannot be a reviewer.
217 pr_author = pr.user.login.strip()
218
219 # The current PR reviewers do not need to be requested again.
220 current_pr_requested_reviewers = [
221 r.login.strip() for r in pr.get_review_requests()[0] if r
222 ]
223 current_pr_reviewed_reviewers = [
224 r.user.login.strip() for r in pr.get_reviews() if r and r.user
225 ]
226 current_pr_reviewers = list(
227 set(current_pr_requested_reviewers + current_pr_reviewed_reviewers)
228 )
229
230 # A user can only be added if they are a collaborator of the repository.
231 repo_collaborators = [c.login.strip() for c in repo_gh.get_collaborators() if c]
232 non_collaborators = [u for u in user_names if u not in repo_collaborators]
233
234 excluded_pr_reviewers = [pr_author] + current_pr_reviewers + non_collaborators
235 new_pr_reviewers = [u for u in user_names if u not in excluded_pr_reviewers]
236
237 # Notify the admins of the repository if non-collaborators are requested.
238 if non_collaborators:
239 print(
240 f"::warning title=Non-Collaborator Reviewers Found!::"
241 f"{', '.join(non_collaborators)}"
242 )
243
244 for comment in pr.get_issue_comments():
245 # If a comment has already been made for these non-collaborators,
246 # do not make another comment.
247 if (
248 comment.user
249 and comment.user.login == "tianocore-assign-reviewers[bot]"
250 and "WARNING: Cannot add some reviewers" in comment.body
251 and all(u in comment.body for u in non_collaborators)
252 ):
253 break
254 else:
255 repo_admins = [
256 a.login for a in repo_gh.get_collaborators(permission="admin") if a
257 ]
258
259 leave_pr_comment(
260 token,
261 owner,
262 repo,
263 pr_number,
264 f"⚠ **WARNING: Cannot add some reviewers**: A user "
265 f"specified as a reviewer for this PR is not a collaborator "
266 f"of the repository. Please add them as a collaborator to "
267 f"the repository so they can be requested in the future.\n\n"
268 f"Non-collaborators requested:\n"
269 f"{'\n'.join([f'- @{c}' for c in non_collaborators])}"
270 f"\n\nAttn Admins:\n"
271 f"{'\n'.join([f'- @{a}' for a in repo_admins])}\n---\n"
272 f"**Admin Instructions:**\n"
273 f"- Add the non-collaborators as collaborators to the "
274 f"appropriate team(s) listed in "
275 f"[teams](https://github.com/orgs/tianocore/teams)\n"
276 f"- If they are no longer needed as reviewers, remove them "
277 f"from [`Maintainers.txt`](https://github.com/tianocore/edk2/blob/HEAD/Maintainers.txt)",
278 )
279
280 # Add any new reviewers to the PR if needed.
281 if new_pr_reviewers:
282 print(
283 f"::debug title=Adding New PR Reviewers::" f"{', '.join(new_pr_reviewers)}"
284 )
285
286 pr.create_review_request(reviewers=new_pr_reviewers)
287
288 return new_pr_reviewers
Note: See TracBrowser for help on using the repository browser.

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette