1a2adebf4STom Stellard#!/usr/bin/env python3 2a2adebf4STom Stellard# 3a2adebf4STom Stellard# ======- github-automation - LLVM GitHub Automation Routines--*- python -*--==# 4a2adebf4STom Stellard# 5a2adebf4STom Stellard# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6a2adebf4STom Stellard# See https://llvm.org/LICENSE.txt for license information. 7a2adebf4STom Stellard# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8a2adebf4STom Stellard# 9a2adebf4STom Stellard# ==-------------------------------------------------------------------------==# 10a2adebf4STom Stellard 11a2adebf4STom Stellardimport argparse 12daf82a51STom Stellardfrom git import Repo # type: ignore 1364751ea2STom Stellardimport html 142879a036STom Stellardimport json 15a2adebf4STom Stellardimport github 16a2adebf4STom Stellardimport os 17daf82a51STom Stellardimport re 18f673dcc6STom Stellardimport requests 19daf82a51STom Stellardimport sys 2017d4796cSTom Stellardimport time 21c116bd9fSKeith Smileyfrom typing import List, Optional 22a2adebf4STom Stellard 23b71edfaaSTobias Hietabeginner_comment = """ 2409effa70STimm BäderHi! 2509effa70STimm Bäder 2609effa70STimm BäderThis issue may be a good introductory issue for people new to working on LLVM. If you would like to work on this issue, your first steps are: 2709effa70STimm Bäder 28cd8286a6SDanny Mösch1. Check that no other contributor has already been assigned to this issue. If you believe that no one is actually working on it despite an assignment, ping the person. After one week without a response, the assignee may be changed. 29cd8286a6SDanny Mösch1. In the comments of this issue, request for it to be assigned to you, or just create a [pull request](https://github.com/llvm/llvm-project/pulls) after following the steps below. [Mention](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) this issue in the description of the pull request. 30cd8286a6SDanny Mösch1. Fix the issue locally. 31cd8286a6SDanny Mösch1. [Run the test suite](https://llvm.org/docs/TestingGuide.html#unit-and-regression-tests) locally. Remember that the subdirectories under `test/` create fine-grained testing targets, so you can e.g. use `make check-clang-ast` to only run Clang's AST tests. 32cd8286a6SDanny Mösch1. Create a Git commit. 33cd8286a6SDanny Mösch1. Run [`git clang-format HEAD~1`](https://clang.llvm.org/docs/ClangFormat.html#git-integration) to format your changes. 34cd8286a6SDanny Mösch1. Open a [pull request](https://github.com/llvm/llvm-project/pulls) to the [upstream repository](https://github.com/llvm/llvm-project) on GitHub. Detailed instructions can be found [in GitHub's documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). [Mention](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) this issue in the description of the pull request. 3509effa70STimm Bäder 366a4489a7SDanny MöschIf you have any further questions about this issue, don't hesitate to ask via a comment in the thread below. 3709effa70STimm Bäder""" 3809effa70STimm Bäder 39a2adebf4STom Stellard 408a7f021fSJ. Ryan Stinnettdef _get_current_team(team_name, teams) -> Optional[github.Team.Team]: 4138e90068SMehdi Amini for team in teams: 4238e90068SMehdi Amini if team_name == team.name.lower(): 4338e90068SMehdi Amini return team 4438e90068SMehdi Amini return None 4538e90068SMehdi Amini 4638e90068SMehdi Amini 471b18e986Scor3ntindef escape_description(str): 485d39f3f6SAiden Grossman # If the description of an issue/pull request is empty, the Github API 495d39f3f6SAiden Grossman # library returns None instead of an empty string. Handle this here to 505d39f3f6SAiden Grossman # avoid failures from trying to manipulate None. 515d39f3f6SAiden Grossman if str is None: 525d39f3f6SAiden Grossman return "" 531b18e986Scor3ntin # https://github.com/github/markup/issues/1168#issuecomment-494946168 541b18e986Scor3ntin str = html.escape(str, False) 551b18e986Scor3ntin # '@' followed by alphanum is a user name 563058d290SCorentin Jabot str = re.sub("@(?=\w)", "@<!-- -->", str) 571b18e986Scor3ntin # '#' followed by digits is considered an issue number 583058d290SCorentin Jabot str = re.sub("#(?=\d)", "#<!-- -->", str) 591b18e986Scor3ntin return str 601b18e986Scor3ntin 611b18e986Scor3ntin 62b71edfaaSTobias Hietaclass IssueSubscriber: 63a2adebf4STom Stellard @property 64a2adebf4STom Stellard def team_name(self) -> str: 65a2adebf4STom Stellard return self._team_name 66a2adebf4STom Stellard 67a2adebf4STom Stellard def __init__(self, token: str, repo: str, issue_number: int, label_name: str): 68a2adebf4STom Stellard self.repo = github.Github(token).get_repo(repo) 69a2adebf4STom Stellard self.org = github.Github(token).get_organization(self.repo.organization.login) 70a2adebf4STom Stellard self.issue = self.repo.get_issue(issue_number) 71b71edfaaSTobias Hieta self._team_name = "issue-subscribers-{}".format(label_name).lower() 72a2adebf4STom Stellard 73a2adebf4STom Stellard def run(self) -> bool: 748a7f021fSJ. Ryan Stinnett team = _get_current_team(self.team_name, self.org.get_teams()) 7538e90068SMehdi Amini if not team: 7638e90068SMehdi Amini print(f"couldn't find team named {self.team_name}") 7738e90068SMehdi Amini return False 7881761bd0STimm Baeder 79b71edfaaSTobias Hieta comment = "" 80b71edfaaSTobias Hieta if team.slug == "issue-subscribers-good-first-issue": 81b71edfaaSTobias Hieta comment = "{}\n".format(beginner_comment) 8281761bd0STimm Baeder self.issue.create_comment(comment) 8309effa70STimm Bäder 841b18e986Scor3ntin body = escape_description(self.issue.body) 851b18e986Scor3ntin comment = f""" 861b18e986Scor3ntin@llvm/{team.slug} 871b18e986Scor3ntin 88f8148e48SAiden GrossmanAuthor: {self.issue.user.name} ({self.issue.user.login}) 89f8148e48SAiden Grossman 901b18e986Scor3ntin<details> 911b18e986Scor3ntin{body} 921b18e986Scor3ntin</details> 931b18e986Scor3ntin""" 9438e90068SMehdi Amini 95a2adebf4STom Stellard self.issue.create_comment(comment) 96a2adebf4STom Stellard return True 97a2adebf4STom Stellard 98b71edfaaSTobias Hieta 994085cb38SMehdi Aminidef human_readable_size(size, decimal_places=2): 1004085cb38SMehdi Amini for unit in ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]: 1014085cb38SMehdi Amini if size < 1024.0 or unit == "PiB": 1024085cb38SMehdi Amini break 1034085cb38SMehdi Amini size /= 1024.0 1044085cb38SMehdi Amini return f"{size:.{decimal_places}f} {unit}" 1054085cb38SMehdi Amini 1064085cb38SMehdi Amini 1075f16a3a4STom Stellardclass PRSubscriber: 1085f16a3a4STom Stellard @property 1095f16a3a4STom Stellard def team_name(self) -> str: 1105f16a3a4STom Stellard return self._team_name 1115f16a3a4STom Stellard 1125f16a3a4STom Stellard def __init__(self, token: str, repo: str, pr_number: int, label_name: str): 1135f16a3a4STom Stellard self.repo = github.Github(token).get_repo(repo) 1145f16a3a4STom Stellard self.org = github.Github(token).get_organization(self.repo.organization.login) 1155f16a3a4STom Stellard self.pr = self.repo.get_issue(pr_number).as_pull_request() 116cff72d70STom Stellard self._team_name = "pr-subscribers-{}".format( 117cff72d70STom Stellard label_name.replace("+", "x") 118cff72d70STom Stellard ).lower() 11964751ea2STom Stellard self.COMMENT_TAG = "<!--LLVM PR SUMMARY COMMENT-->\n" 12064751ea2STom Stellard 12164751ea2STom Stellard def get_summary_comment(self) -> github.IssueComment.IssueComment: 12264751ea2STom Stellard for comment in self.pr.as_issue().get_comments(): 12364751ea2STom Stellard if self.COMMENT_TAG in comment.body: 12464751ea2STom Stellard return comment 12564751ea2STom Stellard return None 1265f16a3a4STom Stellard 1275f16a3a4STom Stellard def run(self) -> bool: 1284085cb38SMehdi Amini patch = None 1298a7f021fSJ. Ryan Stinnett team = _get_current_team(self.team_name, self.org.get_teams()) 1304085cb38SMehdi Amini if not team: 1314085cb38SMehdi Amini print(f"couldn't find team named {self.team_name}") 1324085cb38SMehdi Amini return False 1334085cb38SMehdi Amini 1341b18e986Scor3ntin # GitHub limits comments to 65,536 characters, let's limit the diff 1351b18e986Scor3ntin # and the file list to 20kB each. 1361b18e986Scor3ntin STAT_LIMIT = 20 * 1024 1371b18e986Scor3ntin DIFF_LIMIT = 20 * 1024 1381b18e986Scor3ntin 1394085cb38SMehdi Amini # Get statistics for each file 1404085cb38SMehdi Amini diff_stats = f"{self.pr.changed_files} Files Affected:\n\n" 1414085cb38SMehdi Amini for file in self.pr.get_files(): 1424085cb38SMehdi Amini diff_stats += f"- ({file.status}) {file.filename} (" 1434085cb38SMehdi Amini if file.additions: 1444085cb38SMehdi Amini diff_stats += f"+{file.additions}" 1454085cb38SMehdi Amini if file.deletions: 1464085cb38SMehdi Amini diff_stats += f"-{file.deletions}" 1474085cb38SMehdi Amini diff_stats += ") " 1484085cb38SMehdi Amini if file.status == "renamed": 1494085cb38SMehdi Amini print(f"(from {file.previous_filename})") 1504085cb38SMehdi Amini diff_stats += "\n" 1511b18e986Scor3ntin if len(diff_stats) > STAT_LIMIT: 1521b18e986Scor3ntin break 1534085cb38SMehdi Amini 1544085cb38SMehdi Amini # Get the diff 1555f16a3a4STom Stellard try: 1561b18e986Scor3ntin patch = requests.get(self.pr.diff_url).text 1575f16a3a4STom Stellard except: 1585f16a3a4STom Stellard patch = "" 1594085cb38SMehdi Amini 1604085cb38SMehdi Amini patch_link = f"Full diff: {self.pr.diff_url}\n" 1614085cb38SMehdi Amini if len(patch) > DIFF_LIMIT: 1624085cb38SMehdi Amini patch_link = f"\nPatch is {human_readable_size(len(patch))}, truncated to {human_readable_size(DIFF_LIMIT)} below, full version: {self.pr.diff_url}\n" 1631b18e986Scor3ntin patch = patch[0:DIFF_LIMIT] + "...\n[truncated]\n" 16464751ea2STom Stellard team_mention = "@llvm/{}".format(team.slug) 1654085cb38SMehdi Amini 1661b18e986Scor3ntin body = escape_description(self.pr.body) 1671b18e986Scor3ntin # Note: the comment is in markdown and the code below 1681b18e986Scor3ntin # is sensible to line break 16964751ea2STom Stellard comment = f""" 17064751ea2STom Stellard{self.COMMENT_TAG} 17164751ea2STom Stellard{team_mention} 1724085cb38SMehdi Amini 173f8148e48SAiden GrossmanAuthor: {self.pr.user.name} ({self.pr.user.login}) 174f8148e48SAiden Grossman 17564751ea2STom Stellard<details> 17664751ea2STom Stellard<summary>Changes</summary> 1771b18e986Scor3ntin 17864751ea2STom Stellard{body} 1793ce8eda5SCorentin Jabot 1801b18e986Scor3ntin--- 18164751ea2STom Stellard{patch_link} 1821b18e986Scor3ntin 18364751ea2STom Stellard{diff_stats} 1841b18e986Scor3ntin 1851b18e986Scor3ntin``````````diff 1861b18e986Scor3ntin{patch} 1871b18e986Scor3ntin`````````` 1881b18e986Scor3ntin 18964751ea2STom Stellard</details> 19064751ea2STom Stellard""" 19164751ea2STom Stellard 19264751ea2STom Stellard summary_comment = self.get_summary_comment() 19364751ea2STom Stellard if not summary_comment: 1945f16a3a4STom Stellard self.pr.as_issue().create_comment(comment) 19564751ea2STom Stellard elif team_mention + "\n" in summary_comment.body: 19664751ea2STom Stellard print("Team {} already mentioned.".format(team.slug)) 19764751ea2STom Stellard else: 19864751ea2STom Stellard summary_comment.edit( 19964751ea2STom Stellard summary_comment.body.replace( 20064751ea2STom Stellard self.COMMENT_TAG, self.COMMENT_TAG + team_mention + "\n" 20164751ea2STom Stellard ) 20264751ea2STom Stellard ) 2035f16a3a4STom Stellard return True 2045f16a3a4STom Stellard 2058a7f021fSJ. Ryan Stinnett def _get_current_team(self) -> Optional[github.Team.Team]: 2064085cb38SMehdi Amini for team in self.org.get_teams(): 2074085cb38SMehdi Amini if self.team_name == team.name.lower(): 2084085cb38SMehdi Amini return team 2094085cb38SMehdi Amini return None 2104085cb38SMehdi Amini 2115f16a3a4STom Stellard 21277249546SDavid Spickettclass PRGreeter: 21344ba4c73SDavid Spickett COMMENT_TAG = "<!--LLVM NEW CONTRIBUTOR COMMENT-->\n" 21444ba4c73SDavid Spickett 21577249546SDavid Spickett def __init__(self, token: str, repo: str, pr_number: int): 21677249546SDavid Spickett repo = github.Github(token).get_repo(repo) 21777249546SDavid Spickett self.pr = repo.get_issue(pr_number).as_pull_request() 21877249546SDavid Spickett 21977249546SDavid Spickett def run(self) -> bool: 22077249546SDavid Spickett # We assume that this is only called for a PR that has just been opened 22177249546SDavid Spickett # by a user new to LLVM and/or GitHub itself. 22277249546SDavid Spickett 22377249546SDavid Spickett # This text is using Markdown formatting. 22444ba4c73SDavid Spickett 22577249546SDavid Spickett comment = f"""\ 22644ba4c73SDavid Spickett{PRGreeter.COMMENT_TAG} 22777249546SDavid SpickettThank you for submitting a Pull Request (PR) to the LLVM Project! 22877249546SDavid Spickett 229*5adbce07SDavid SpickettThis PR will be automatically labeled and the relevant teams will be notified. 23077249546SDavid Spickett 23177249546SDavid SpickettIf you wish to, you can add reviewers by using the "Reviewers" section on this page. 23277249546SDavid Spickett 233*5adbce07SDavid SpickettIf this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using `@` followed by their GitHub username. 23477249546SDavid Spickett 235*5adbce07SDavid SpickettIf you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers. 23677249546SDavid Spickett 23777249546SDavid SpickettIf you have further questions, they may be answered by the [LLVM GitHub User Guide](https://llvm.org/docs/GitHub.html). 23877249546SDavid Spickett 23977249546SDavid SpickettYou can also ask questions in a comment on this PR, on the [LLVM Discord](https://discord.com/invite/xS7Z362) or on the [forums](https://discourse.llvm.org/).""" 24077249546SDavid Spickett self.pr.as_issue().create_comment(comment) 24177249546SDavid Spickett return True 24277249546SDavid Spickett 24377249546SDavid Spickett 24444ba4c73SDavid Spickettclass PRBuildbotInformation: 24544ba4c73SDavid Spickett COMMENT_TAG = "<!--LLVM BUILDBOT INFORMATION COMMENT-->\n" 24644ba4c73SDavid Spickett 24744ba4c73SDavid Spickett def __init__(self, token: str, repo: str, pr_number: int, author: str): 24844ba4c73SDavid Spickett repo = github.Github(token).get_repo(repo) 24944ba4c73SDavid Spickett self.pr = repo.get_issue(pr_number).as_pull_request() 25044ba4c73SDavid Spickett self.author = author 25144ba4c73SDavid Spickett 25244ba4c73SDavid Spickett def should_comment(self) -> bool: 25344ba4c73SDavid Spickett # As soon as a new contributor has a PR merged, they are no longer a new contributor. 25444ba4c73SDavid Spickett # We can tell that they were a new contributor previously because we would have 25544ba4c73SDavid Spickett # added a new contributor greeting comment when they opened the PR. 25644ba4c73SDavid Spickett found_greeting = False 25744ba4c73SDavid Spickett for comment in self.pr.as_issue().get_comments(): 25844ba4c73SDavid Spickett if PRGreeter.COMMENT_TAG in comment.body: 25944ba4c73SDavid Spickett found_greeting = True 26044ba4c73SDavid Spickett elif PRBuildbotInformation.COMMENT_TAG in comment.body: 26144ba4c73SDavid Spickett # When an issue is reopened, then closed as merged again, we should not 26244ba4c73SDavid Spickett # add a second comment. This event will be rare in practice as it seems 26344ba4c73SDavid Spickett # like it's only possible when the main branch is still at the exact 26444ba4c73SDavid Spickett # revision that the PR was merged on to, beyond that it's closed forever. 26544ba4c73SDavid Spickett return False 26644ba4c73SDavid Spickett return found_greeting 26744ba4c73SDavid Spickett 26844ba4c73SDavid Spickett def run(self) -> bool: 26944ba4c73SDavid Spickett if not self.should_comment(): 27044ba4c73SDavid Spickett return 27144ba4c73SDavid Spickett 27244ba4c73SDavid Spickett # This text is using Markdown formatting. Some of the lines are longer 27344ba4c73SDavid Spickett # than others so that the final text is some reasonable looking paragraphs 27444ba4c73SDavid Spickett # after the long URLs are rendered. 27544ba4c73SDavid Spickett comment = f"""\ 27644ba4c73SDavid Spickett{PRBuildbotInformation.COMMENT_TAG} 27744ba4c73SDavid Spickett@{self.author} Congratulations on having your first Pull Request (PR) merged into the LLVM Project! 27844ba4c73SDavid Spickett 279*5adbce07SDavid SpickettYour changes will be combined with recent changes from other authors, then tested by our [build bots](https://lab.llvm.org/buildbot/). If there is a problem with a build, you may receive a report in an email or a comment on this PR. 28044ba4c73SDavid Spickett 281*5adbce07SDavid SpickettPlease check whether problems have been caused by your change specifically, as the builds can include changes from many authors. It is not uncommon for your change to be included in a build that fails due to someone else's changes, or infrastructure issues. 28244ba4c73SDavid Spickett 28344ba4c73SDavid SpickettHow to do this, and the rest of the post-merge process, is covered in detail [here](https://llvm.org/docs/MyFirstTypoFix.html#myfirsttypofix-issues-after-landing-your-pr). 28444ba4c73SDavid Spickett 285*5adbce07SDavid SpickettIf your change does cause a problem, it may be reverted, or you can revert it yourself. This is a normal part of [LLVM development](https://llvm.org/docs/DeveloperPolicy.html#patch-reversion-policy). You can fix your changes and open a new PR to merge them again. 28644ba4c73SDavid Spickett 28744ba4c73SDavid SpickettIf you don't get any reports, no action is required from you. Your changes are working as expected, well done! 28844ba4c73SDavid Spickett""" 28944ba4c73SDavid Spickett self.pr.as_issue().create_comment(comment) 29044ba4c73SDavid Spickett return True 29144ba4c73SDavid Spickett 29244ba4c73SDavid Spickett 293b71edfaaSTobias Hietadef setup_llvmbot_git(git_dir="."): 294daf82a51STom Stellard """ 295daf82a51STom Stellard Configure the git repo in `git_dir` with the llvmbot account so 296daf82a51STom Stellard commits are attributed to llvmbot. 297daf82a51STom Stellard """ 298daf82a51STom Stellard repo = Repo(git_dir) 299daf82a51STom Stellard with repo.config_writer() as config: 300b71edfaaSTobias Hieta config.set_value("user", "name", "llvmbot") 301b71edfaaSTobias Hieta config.set_value("user", "email", "llvmbot@llvm.org") 302b71edfaaSTobias Hieta 303daf82a51STom Stellard 304405932afSTimm Bäderdef extract_commit_hash(arg: str): 305405932afSTimm Bäder """ 306405932afSTimm Bäder Extract the commit hash from the argument passed to /action github 307405932afSTimm Bäder comment actions. We currently only support passing the commit hash 308405932afSTimm Bäder directly or use the github URL, such as 309405932afSTimm Bäder https://github.com/llvm/llvm-project/commit/2832d7941f4207f1fcf813b27cf08cecc3086959 310405932afSTimm Bäder """ 311405932afSTimm Bäder github_prefix = "https://github.com/llvm/llvm-project/commit/" 312405932afSTimm Bäder if arg.startswith(github_prefix): 313405932afSTimm Bäder return arg[len(github_prefix) :] 314405932afSTimm Bäder return arg 315405932afSTimm Bäder 316405932afSTimm Bäder 317daf82a51STom Stellardclass ReleaseWorkflow: 318b71edfaaSTobias Hieta CHERRY_PICK_FAILED_LABEL = "release:cherry-pick-failed" 3193929f913STom Stellard 320daf82a51STom Stellard """ 321daf82a51STom Stellard This class implements the sub-commands for the release-workflow command. 322daf82a51STom Stellard The current sub-commands are: 323daf82a51STom Stellard * create-branch 324daf82a51STom Stellard * create-pull-request 325daf82a51STom Stellard 326daf82a51STom Stellard The execute_command method will automatically choose the correct sub-command 327daf82a51STom Stellard based on the text in stdin. 328daf82a51STom Stellard """ 329daf82a51STom Stellard 330b71edfaaSTobias Hieta def __init__( 331b71edfaaSTobias Hieta self, 332b71edfaaSTobias Hieta token: str, 333b71edfaaSTobias Hieta repo: str, 334b71edfaaSTobias Hieta issue_number: int, 335b71edfaaSTobias Hieta branch_repo_name: str, 336b71edfaaSTobias Hieta branch_repo_token: str, 337b71edfaaSTobias Hieta llvm_project_dir: str, 338bd655478STom Stellard requested_by: str, 339b71edfaaSTobias Hieta ) -> None: 340daf82a51STom Stellard self._token = token 341daf82a51STom Stellard self._repo_name = repo 342daf82a51STom Stellard self._issue_number = issue_number 343daf82a51STom Stellard self._branch_repo_name = branch_repo_name 344daf82a51STom Stellard if branch_repo_token: 345daf82a51STom Stellard self._branch_repo_token = branch_repo_token 346daf82a51STom Stellard else: 347daf82a51STom Stellard self._branch_repo_token = self.token 348daf82a51STom Stellard self._llvm_project_dir = llvm_project_dir 349bd655478STom Stellard self._requested_by = requested_by 350daf82a51STom Stellard 351daf82a51STom Stellard @property 352daf82a51STom Stellard def token(self) -> str: 353daf82a51STom Stellard return self._token 354daf82a51STom Stellard 355daf82a51STom Stellard @property 356daf82a51STom Stellard def repo_name(self) -> str: 357daf82a51STom Stellard return self._repo_name 358daf82a51STom Stellard 359daf82a51STom Stellard @property 360daf82a51STom Stellard def issue_number(self) -> int: 361daf82a51STom Stellard return self._issue_number 362daf82a51STom Stellard 363daf82a51STom Stellard @property 364e99edf6bSTom Stellard def branch_repo_owner(self) -> str: 365e99edf6bSTom Stellard return self.branch_repo_name.split("/")[0] 366e99edf6bSTom Stellard 367e99edf6bSTom Stellard @property 368daf82a51STom Stellard def branch_repo_name(self) -> str: 369daf82a51STom Stellard return self._branch_repo_name 370daf82a51STom Stellard 371daf82a51STom Stellard @property 372daf82a51STom Stellard def branch_repo_token(self) -> str: 373daf82a51STom Stellard return self._branch_repo_token 374daf82a51STom Stellard 375daf82a51STom Stellard @property 376daf82a51STom Stellard def llvm_project_dir(self) -> str: 377daf82a51STom Stellard return self._llvm_project_dir 378daf82a51STom Stellard 379daf82a51STom Stellard @property 380bd655478STom Stellard def requested_by(self) -> str: 381bd655478STom Stellard return self._requested_by 382bd655478STom Stellard 383bd655478STom Stellard @property 384f673dcc6STom Stellard def repo(self) -> github.Repository.Repository: 385daf82a51STom Stellard return github.Github(self.token).get_repo(self.repo_name) 386daf82a51STom Stellard 387daf82a51STom Stellard @property 388daf82a51STom Stellard def issue(self) -> github.Issue.Issue: 389f673dcc6STom Stellard return self.repo.get_issue(self.issue_number) 390daf82a51STom Stellard 391daf82a51STom Stellard @property 392daf82a51STom Stellard def push_url(self) -> str: 393b71edfaaSTobias Hieta return "https://{}@github.com/{}".format( 394b71edfaaSTobias Hieta self.branch_repo_token, self.branch_repo_name 395b71edfaaSTobias Hieta ) 396daf82a51STom Stellard 397daf82a51STom Stellard @property 398daf82a51STom Stellard def branch_name(self) -> str: 399b71edfaaSTobias Hieta return "issue{}".format(self.issue_number) 400daf82a51STom Stellard 401daf82a51STom Stellard @property 402daf82a51STom Stellard def release_branch_for_issue(self) -> Optional[str]: 403daf82a51STom Stellard issue = self.issue 404daf82a51STom Stellard milestone = issue.milestone 405daf82a51STom Stellard if milestone is None: 406daf82a51STom Stellard return None 407b71edfaaSTobias Hieta m = re.search("branch: (.+)", milestone.description) 408daf82a51STom Stellard if m: 409daf82a51STom Stellard return m.group(1) 410daf82a51STom Stellard return None 411daf82a51STom Stellard 412daf82a51STom Stellard def print_release_branch(self) -> None: 413daf82a51STom Stellard print(self.release_branch_for_issue) 414daf82a51STom Stellard 415daf82a51STom Stellard def issue_notify_branch(self) -> None: 416b71edfaaSTobias Hieta self.issue.create_comment( 417b71edfaaSTobias Hieta "/branch {}/{}".format(self.branch_repo_name, self.branch_name) 418b71edfaaSTobias Hieta ) 419daf82a51STom Stellard 420daf82a51STom Stellard def issue_notify_pull_request(self, pull: github.PullRequest.PullRequest) -> None: 421b71edfaaSTobias Hieta self.issue.create_comment( 422f33e9276STom Stellard "/pull-request {}#{}".format(self.repo_name, pull.number) 423b71edfaaSTobias Hieta ) 424daf82a51STom Stellard 42551eaaa30STom Stellard def make_ignore_comment(self, comment: str) -> str: 42651eaaa30STom Stellard """ 42751eaaa30STom Stellard Returns the comment string with a prefix that will cause 42851eaaa30STom Stellard a Github workflow to skip parsing this comment. 42951eaaa30STom Stellard 43051eaaa30STom Stellard :param str comment: The comment to ignore 43151eaaa30STom Stellard """ 43251eaaa30STom Stellard return "<!--IGNORE-->\n" + comment 43351eaaa30STom Stellard 43451eaaa30STom Stellard def issue_notify_no_milestone(self, comment: List[str]) -> None: 435b71edfaaSTobias Hieta message = "{}\n\nError: Command failed due to missing milestone.".format( 436b71edfaaSTobias Hieta "".join([">" + line for line in comment]) 437b71edfaaSTobias Hieta ) 43851eaaa30STom Stellard self.issue.create_comment(self.make_ignore_comment(message)) 43951eaaa30STom Stellard 440daf82a51STom Stellard @property 441daf82a51STom Stellard def action_url(self) -> str: 442b71edfaaSTobias Hieta if os.getenv("CI"): 443b71edfaaSTobias Hieta return "https://github.com/{}/actions/runs/{}".format( 444b71edfaaSTobias Hieta os.getenv("GITHUB_REPOSITORY"), os.getenv("GITHUB_RUN_ID") 445b71edfaaSTobias Hieta ) 446daf82a51STom Stellard return "" 447daf82a51STom Stellard 448b71edfaaSTobias Hieta def issue_notify_cherry_pick_failure( 449b71edfaaSTobias Hieta self, commit: str 450b71edfaaSTobias Hieta ) -> github.IssueComment.IssueComment: 451b71edfaaSTobias Hieta message = self.make_ignore_comment( 452b71edfaaSTobias Hieta "Failed to cherry-pick: {}\n\n".format(commit) 453b71edfaaSTobias Hieta ) 454daf82a51STom Stellard action_url = self.action_url 455daf82a51STom Stellard if action_url: 456daf82a51STom Stellard message += action_url + "\n\n" 457e99edf6bSTom Stellard message += "Please manually backport the fix and push it to your github fork. Once this is done, please create a [pull request](https://github.com/llvm/llvm-project/compare)" 458daf82a51STom Stellard issue = self.issue 459daf82a51STom Stellard comment = issue.create_comment(message) 4603929f913STom Stellard issue.add_to_labels(self.CHERRY_PICK_FAILED_LABEL) 461daf82a51STom Stellard return comment 462daf82a51STom Stellard 463b71edfaaSTobias Hieta def issue_notify_pull_request_failure( 464b71edfaaSTobias Hieta self, branch: str 465b71edfaaSTobias Hieta ) -> github.IssueComment.IssueComment: 466daf82a51STom Stellard message = "Failed to create pull request for {} ".format(branch) 467daf82a51STom Stellard message += self.action_url 468daf82a51STom Stellard return self.issue.create_comment(message) 469daf82a51STom Stellard 4703929f913STom Stellard def issue_remove_cherry_pick_failed_label(self): 4713929f913STom Stellard if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]: 4723929f913STom Stellard self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL) 473daf82a51STom Stellard 474f33e9276STom Stellard def get_main_commit(self, cherry_pick_sha: str) -> github.Commit.Commit: 475f33e9276STom Stellard commit = self.repo.get_commit(cherry_pick_sha) 476f33e9276STom Stellard message = commit.commit.message 477f33e9276STom Stellard m = re.search("\(cherry picked from commit ([0-9a-f]+)\)", message) 478f33e9276STom Stellard if not m: 479f33e9276STom Stellard return None 480f33e9276STom Stellard return self.repo.get_commit(m.group(1)) 481f33e9276STom Stellard 482f673dcc6STom Stellard def pr_request_review(self, pr: github.PullRequest.PullRequest): 483f673dcc6STom Stellard """ 484f673dcc6STom Stellard This function will try to find the best reviewers for `commits` and 485f33e9276STom Stellard then add a comment requesting review of the backport and add them as 486f33e9276STom Stellard reviewers. 487f673dcc6STom Stellard 488f33e9276STom Stellard The reviewers selected are those users who approved the pull request 489f33e9276STom Stellard for the main branch. 490f673dcc6STom Stellard """ 491f673dcc6STom Stellard reviewers = [] 492f673dcc6STom Stellard for commit in pr.get_commits(): 493f33e9276STom Stellard main_commit = self.get_main_commit(commit.sha) 494f33e9276STom Stellard if not main_commit: 495f673dcc6STom Stellard continue 496f33e9276STom Stellard for pull in main_commit.get_pulls(): 497f33e9276STom Stellard for review in pull.get_reviews(): 498f33e9276STom Stellard if review.state != "APPROVED": 499f33e9276STom Stellard continue 500f33e9276STom Stellard reviewers.append(review.user.login) 501f673dcc6STom Stellard if len(reviewers): 502f673dcc6STom Stellard message = "{} What do you think about merging this PR to the release branch?".format( 503b71edfaaSTobias Hieta " ".join(["@" + r for r in reviewers]) 504b71edfaaSTobias Hieta ) 505f673dcc6STom Stellard pr.create_issue_comment(message) 506f33e9276STom Stellard pr.create_review_request(reviewers) 507f673dcc6STom Stellard 508daf82a51STom Stellard def create_branch(self, commits: List[str]) -> bool: 509daf82a51STom Stellard """ 510daf82a51STom Stellard This function attempts to backport `commits` into the branch associated 511daf82a51STom Stellard with `self.issue_number`. 512daf82a51STom Stellard 513daf82a51STom Stellard If this is successful, then the branch is pushed to `self.branch_repo_name`, if not, 514daf82a51STom Stellard a comment is added to the issue saying that the cherry-pick failed. 515daf82a51STom Stellard 516daf82a51STom Stellard :param list commits: List of commits to cherry-pick. 517daf82a51STom Stellard 518daf82a51STom Stellard """ 519b71edfaaSTobias Hieta print("cherry-picking", commits) 520daf82a51STom Stellard branch_name = self.branch_name 521daf82a51STom Stellard local_repo = Repo(self.llvm_project_dir) 522daf82a51STom Stellard local_repo.git.checkout(self.release_branch_for_issue) 523daf82a51STom Stellard 524daf82a51STom Stellard for c in commits: 525daf82a51STom Stellard try: 526b71edfaaSTobias Hieta local_repo.git.cherry_pick("-x", c) 527daf82a51STom Stellard except Exception as e: 528daf82a51STom Stellard self.issue_notify_cherry_pick_failure(c) 529daf82a51STom Stellard raise e 530daf82a51STom Stellard 531daf82a51STom Stellard push_url = self.push_url 532b71edfaaSTobias Hieta print("Pushing to {} {}".format(push_url, branch_name)) 533b71edfaaSTobias Hieta local_repo.git.push(push_url, "HEAD:{}".format(branch_name), force=True) 534daf82a51STom Stellard 5353929f913STom Stellard self.issue_remove_cherry_pick_failed_label() 536e99edf6bSTom Stellard return self.create_pull_request( 537bd655478STom Stellard self.branch_repo_owner, self.repo_name, branch_name, commits 538e99edf6bSTom Stellard ) 539daf82a51STom Stellard 540b71edfaaSTobias Hieta def check_if_pull_request_exists( 541b71edfaaSTobias Hieta self, repo: github.Repository.Repository, head: str 542b71edfaaSTobias Hieta ) -> bool: 54349ad577cSTobias Hieta pulls = repo.get_pulls(head=head) 544fde9ef52STobias Hieta return pulls.totalCount != 0 545daf82a51STom Stellard 546bd655478STom Stellard def create_pull_request( 547bd655478STom Stellard self, owner: str, repo_name: str, branch: str, commits: List[str] 548bd655478STom Stellard ) -> bool: 549daf82a51STom Stellard """ 550f33e9276STom Stellard Create a pull request in `self.repo_name`. The base branch of the 551a2d45017SKazu Hirata pull request will be chosen based on the the milestone attached to 552daf82a51STom Stellard the issue represented by `self.issue_number` For example if the milestone 553daf82a51STom Stellard is Release 13.0.1, then the base branch will be release/13.x. `branch` 554daf82a51STom Stellard will be used as the compare branch. 555daf82a51STom Stellard https://docs.github.com/en/get-started/quickstart/github-glossary#base-branch 556daf82a51STom Stellard https://docs.github.com/en/get-started/quickstart/github-glossary#compare-branch 557daf82a51STom Stellard """ 558f33e9276STom Stellard repo = github.Github(self.token).get_repo(self.repo_name) 559b71edfaaSTobias Hieta issue_ref = "{}#{}".format(self.repo_name, self.issue_number) 560daf82a51STom Stellard pull = None 561daf82a51STom Stellard release_branch_for_issue = self.release_branch_for_issue 562daf82a51STom Stellard if release_branch_for_issue is None: 563daf82a51STom Stellard return False 56417d4796cSTom Stellard 56556444d56SNikita Popov head = f"{owner}:{branch}" 56649ad577cSTobias Hieta if self.check_if_pull_request_exists(repo, head): 56749ad577cSTobias Hieta print("PR already exists...") 56849ad577cSTobias Hieta return True 569daf82a51STom Stellard try: 570bd655478STom Stellard commit_message = repo.get_commit(commits[-1]).commit.message 571bd655478STom Stellard message_lines = commit_message.splitlines() 572bd655478STom Stellard title = "{}: {}".format(release_branch_for_issue, message_lines[0]) 573bd655478STom Stellard body = "Backport {}\n\nRequested by: @{}".format( 574bd655478STom Stellard " ".join(commits), self.requested_by 575bd655478STom Stellard ) 576b71edfaaSTobias Hieta pull = repo.create_pull( 577bd655478STom Stellard title=title, 578bd655478STom Stellard body=body, 579daf82a51STom Stellard base=release_branch_for_issue, 58049ad577cSTobias Hieta head=head, 581d125d557STom Stellard maintainer_can_modify=True, 582b71edfaaSTobias Hieta ) 583f673dcc6STom Stellard 584f33e9276STom Stellard pull.as_issue().edit(milestone=self.issue.milestone) 585f33e9276STom Stellard 5869805c051STom Stellard # Once the pull request has been created, we can close the 5879805c051STom Stellard # issue that was used to request the cherry-pick 5889805c051STom Stellard self.issue.edit(state="closed", state_reason="completed") 5899805c051STom Stellard 590f673dcc6STom Stellard try: 591f673dcc6STom Stellard self.pr_request_review(pull) 592f673dcc6STom Stellard except Exception as e: 593f673dcc6STom Stellard print("error: Failed while searching for reviewers", e) 594f673dcc6STom Stellard 595daf82a51STom Stellard except Exception as e: 596daf82a51STom Stellard self.issue_notify_pull_request_failure(branch) 597daf82a51STom Stellard raise e 598daf82a51STom Stellard 599daf82a51STom Stellard if pull is None: 600daf82a51STom Stellard return False 601daf82a51STom Stellard 602daf82a51STom Stellard self.issue_notify_pull_request(pull) 6033929f913STom Stellard self.issue_remove_cherry_pick_failed_label() 604daf82a51STom Stellard 605daf82a51STom Stellard # TODO(tstellar): Do you really want to always return True? 606daf82a51STom Stellard return True 607daf82a51STom Stellard 608daf82a51STom Stellard def execute_command(self) -> bool: 609daf82a51STom Stellard """ 610daf82a51STom Stellard This function reads lines from STDIN and executes the first command 6119a894e7dSShourya Goel that it finds. The supported command is: 6129a894e7dSShourya Goel /cherry-pick< ><:> commit0 <commit1> <commit2> <...> 613daf82a51STom Stellard """ 614daf82a51STom Stellard for line in sys.stdin: 615daf82a51STom Stellard line.rstrip() 6169a894e7dSShourya Goel m = re.search(r"/cherry-pick\s*:? *(.*)", line) 617daf82a51STom Stellard if not m: 618daf82a51STom Stellard continue 619daf82a51STom Stellard 6209a894e7dSShourya Goel args = m.group(1) 6219a894e7dSShourya Goel 622405932afSTimm Bäder arg_list = args.split() 623405932afSTimm Bäder commits = list(map(lambda a: extract_commit_hash(a), arg_list)) 624405932afSTimm Bäder return self.create_branch(commits) 625daf82a51STom Stellard 626daf82a51STom Stellard print("Do not understand input:") 627daf82a51STom Stellard print(sys.stdin.readlines()) 628daf82a51STom Stellard return False 629a2adebf4STom Stellard 630b71edfaaSTobias Hieta 631c99d1156STom Stellarddef request_release_note(token: str, repo_name: str, pr_number: int): 632c99d1156STom Stellard repo = github.Github(token).get_repo(repo_name) 633c99d1156STom Stellard pr = repo.get_issue(pr_number).as_pull_request() 634c99d1156STom Stellard submitter = pr.user.login 635c99d1156STom Stellard if submitter == "llvmbot": 636c99d1156STom Stellard m = re.search("Requested by: @(.+)$", pr.body) 637c99d1156STom Stellard if not m: 638c99d1156STom Stellard submitter = None 639c99d1156STom Stellard print("Warning could not determine user who requested backport.") 640c99d1156STom Stellard submitter = m.group(1) 641c99d1156STom Stellard 642c99d1156STom Stellard mention = "" 643c99d1156STom Stellard if submitter: 644c99d1156STom Stellard mention = f"@{submitter}" 645c99d1156STom Stellard 646c99d1156STom Stellard comment = f"{mention} (or anyone else). If you would like to add a note about this fix in the release notes (completely optional). Please reply to this comment with a one or two sentence description of the fix. When you are done, please add the release:note label to this PR. " 6472879a036STom Stellard try: 648c99d1156STom Stellard pr.as_issue().create_comment(comment) 6492879a036STom Stellard except: 6502879a036STom Stellard # Failed to create comment so emit file instead 6512879a036STom Stellard with open("comments", "w") as file: 6522879a036STom Stellard data = [{"body": comment}] 6532879a036STom Stellard json.dump(data, file) 654c99d1156STom Stellard 655c99d1156STom Stellard 656a2adebf4STom Stellardparser = argparse.ArgumentParser() 657b71edfaaSTobias Hietaparser.add_argument( 6588a7f021fSJ. Ryan Stinnett "--token", type=str, required=True, help="GitHub authentication token" 659b71edfaaSTobias Hieta) 660b71edfaaSTobias Hietaparser.add_argument( 661b71edfaaSTobias Hieta "--repo", 662b71edfaaSTobias Hieta type=str, 663b71edfaaSTobias Hieta default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"), 664b71edfaaSTobias Hieta help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)", 665b71edfaaSTobias Hieta) 666b71edfaaSTobias Hietasubparsers = parser.add_subparsers(dest="command") 667a2adebf4STom Stellard 668b71edfaaSTobias Hietaissue_subscriber_parser = subparsers.add_parser("issue-subscriber") 669b71edfaaSTobias Hietaissue_subscriber_parser.add_argument("--label-name", type=str, required=True) 670b71edfaaSTobias Hietaissue_subscriber_parser.add_argument("--issue-number", type=int, required=True) 671a2adebf4STom Stellard 6725f16a3a4STom Stellardpr_subscriber_parser = subparsers.add_parser("pr-subscriber") 6735f16a3a4STom Stellardpr_subscriber_parser.add_argument("--label-name", type=str, required=True) 6745f16a3a4STom Stellardpr_subscriber_parser.add_argument("--issue-number", type=int, required=True) 6755f16a3a4STom Stellard 67677249546SDavid Spickettpr_greeter_parser = subparsers.add_parser("pr-greeter") 67777249546SDavid Spickettpr_greeter_parser.add_argument("--issue-number", type=int, required=True) 67877249546SDavid Spickett 67944ba4c73SDavid Spickettpr_buildbot_information_parser = subparsers.add_parser("pr-buildbot-information") 68044ba4c73SDavid Spickettpr_buildbot_information_parser.add_argument("--issue-number", type=int, required=True) 68144ba4c73SDavid Spickettpr_buildbot_information_parser.add_argument("--author", type=str, required=True) 68244ba4c73SDavid Spickett 683b71edfaaSTobias Hietarelease_workflow_parser = subparsers.add_parser("release-workflow") 684b71edfaaSTobias Hietarelease_workflow_parser.add_argument( 685b71edfaaSTobias Hieta "--llvm-project-dir", 686b71edfaaSTobias Hieta type=str, 687b71edfaaSTobias Hieta default=".", 6888a7f021fSJ. Ryan Stinnett help="directory containing the llvm-project checkout", 689b71edfaaSTobias Hieta) 690b71edfaaSTobias Hietarelease_workflow_parser.add_argument( 691b71edfaaSTobias Hieta "--issue-number", type=int, required=True, help="The issue number to update" 692b71edfaaSTobias Hieta) 693b71edfaaSTobias Hietarelease_workflow_parser.add_argument( 694b71edfaaSTobias Hieta "--branch-repo-token", 695b71edfaaSTobias Hieta type=str, 696b71edfaaSTobias Hieta help="GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.", 697b71edfaaSTobias Hieta) 698b71edfaaSTobias Hietarelease_workflow_parser.add_argument( 699b71edfaaSTobias Hieta "--branch-repo", 700b71edfaaSTobias Hieta type=str, 701f33e9276STom Stellard default="llvmbot/llvm-project", 702b71edfaaSTobias Hieta help="The name of the repo where new branches will be pushed (e.g. llvm/llvm-project)", 703b71edfaaSTobias Hieta) 704b71edfaaSTobias Hietarelease_workflow_parser.add_argument( 705b71edfaaSTobias Hieta "sub_command", 706b71edfaaSTobias Hieta type=str, 707b71edfaaSTobias Hieta choices=["print-release-branch", "auto"], 708b71edfaaSTobias Hieta help="Print to stdout the name of the release branch ISSUE_NUMBER should be backported to", 709b71edfaaSTobias Hieta) 710daf82a51STom Stellard 711b71edfaaSTobias Hietallvmbot_git_config_parser = subparsers.add_parser( 712b71edfaaSTobias Hieta "setup-llvmbot-git", 713b71edfaaSTobias Hieta help="Set the default user and email for the git repo in LLVM_PROJECT_DIR to llvmbot", 714b71edfaaSTobias Hieta) 715bd655478STom Stellardrelease_workflow_parser.add_argument( 716bd655478STom Stellard "--requested-by", 717bd655478STom Stellard type=str, 718bd655478STom Stellard required=True, 719bd655478STom Stellard help="The user that requested this backport", 720bd655478STom Stellard) 721daf82a51STom Stellard 722c99d1156STom Stellardrequest_release_note_parser = subparsers.add_parser( 723c99d1156STom Stellard "request-release-note", 724c99d1156STom Stellard help="Request a release note for a pull request", 725c99d1156STom Stellard) 726c99d1156STom Stellardrequest_release_note_parser.add_argument( 727c99d1156STom Stellard "--pr-number", 728c99d1156STom Stellard type=int, 729c99d1156STom Stellard required=True, 730c99d1156STom Stellard help="The pull request to request the release note", 731c99d1156STom Stellard) 732c99d1156STom Stellard 733c99d1156STom Stellard 734a2adebf4STom Stellardargs = parser.parse_args() 735a2adebf4STom Stellard 736b71edfaaSTobias Hietaif args.command == "issue-subscriber": 737b71edfaaSTobias Hieta issue_subscriber = IssueSubscriber( 738b71edfaaSTobias Hieta args.token, args.repo, args.issue_number, args.label_name 739b71edfaaSTobias Hieta ) 740a2adebf4STom Stellard issue_subscriber.run() 7415f16a3a4STom Stellardelif args.command == "pr-subscriber": 7425f16a3a4STom Stellard pr_subscriber = PRSubscriber( 7435f16a3a4STom Stellard args.token, args.repo, args.issue_number, args.label_name 7445f16a3a4STom Stellard ) 7455f16a3a4STom Stellard pr_subscriber.run() 74677249546SDavid Spickettelif args.command == "pr-greeter": 74777249546SDavid Spickett pr_greeter = PRGreeter(args.token, args.repo, args.issue_number) 74877249546SDavid Spickett pr_greeter.run() 74944ba4c73SDavid Spickettelif args.command == "pr-buildbot-information": 75044ba4c73SDavid Spickett pr_buildbot_information = PRBuildbotInformation( 75144ba4c73SDavid Spickett args.token, args.repo, args.issue_number, args.author 75244ba4c73SDavid Spickett ) 75344ba4c73SDavid Spickett pr_buildbot_information.run() 754b71edfaaSTobias Hietaelif args.command == "release-workflow": 755b71edfaaSTobias Hieta release_workflow = ReleaseWorkflow( 756b71edfaaSTobias Hieta args.token, 757b71edfaaSTobias Hieta args.repo, 758b71edfaaSTobias Hieta args.issue_number, 759b71edfaaSTobias Hieta args.branch_repo, 760b71edfaaSTobias Hieta args.branch_repo_token, 761b71edfaaSTobias Hieta args.llvm_project_dir, 762bd655478STom Stellard args.requested_by, 763b71edfaaSTobias Hieta ) 76451eaaa30STom Stellard if not release_workflow.release_branch_for_issue: 76551eaaa30STom Stellard release_workflow.issue_notify_no_milestone(sys.stdin.readlines()) 76651eaaa30STom Stellard sys.exit(1) 767b71edfaaSTobias Hieta if args.sub_command == "print-release-branch": 768daf82a51STom Stellard release_workflow.print_release_branch() 769daf82a51STom Stellard else: 770daf82a51STom Stellard if not release_workflow.execute_command(): 771daf82a51STom Stellard sys.exit(1) 772b71edfaaSTobias Hietaelif args.command == "setup-llvmbot-git": 773daf82a51STom Stellard setup_llvmbot_git() 774c99d1156STom Stellardelif args.command == "request-release-note": 775c99d1156STom Stellard request_release_note(args.token, args.repo, args.pr_number) 776