1#!/usr/bin/env python3 2# 3# ======- github-automation - LLVM GitHub Automation Routines--*- python -*--==# 4# 5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6# See https://llvm.org/LICENSE.txt for license information. 7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8# 9# ==-------------------------------------------------------------------------==# 10 11import argparse 12from git import Repo # type: ignore 13import html 14import github 15import os 16import re 17import requests 18import sys 19import time 20from typing import List, Optional 21 22beginner_comment = """ 23Hi! 24 25This 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: 26 271. 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. 281. 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. 291. Fix the issue locally. 301. [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. 311. Create a Git commit. 321. Run [`git clang-format HEAD~1`](https://clang.llvm.org/docs/ClangFormat.html#git-integration) to format your changes. 331. 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. 34 35If you have any further questions about this issue, don't hesitate to ask via a comment in the thread below. 36""" 37 38 39def _get_current_team(team_name, teams) -> Optional[github.Team.Team]: 40 for team in teams: 41 if team_name == team.name.lower(): 42 return team 43 return None 44 45 46def escape_description(str): 47 # If the description of an issue/pull request is empty, the Github API 48 # library returns None instead of an empty string. Handle this here to 49 # avoid failures from trying to manipulate None. 50 if str is None: 51 return "" 52 # https://github.com/github/markup/issues/1168#issuecomment-494946168 53 str = html.escape(str, False) 54 # '@' followed by alphanum is a user name 55 str = re.sub("@(?=\w)", "@<!-- -->", str) 56 # '#' followed by digits is considered an issue number 57 str = re.sub("#(?=\d)", "#<!-- -->", str) 58 return str 59 60 61class IssueSubscriber: 62 @property 63 def team_name(self) -> str: 64 return self._team_name 65 66 def __init__(self, token: str, repo: str, issue_number: int, label_name: str): 67 self.repo = github.Github(token).get_repo(repo) 68 self.org = github.Github(token).get_organization(self.repo.organization.login) 69 self.issue = self.repo.get_issue(issue_number) 70 self._team_name = "issue-subscribers-{}".format(label_name).lower() 71 72 def run(self) -> bool: 73 team = _get_current_team(self.team_name, self.org.get_teams()) 74 if not team: 75 print(f"couldn't find team named {self.team_name}") 76 return False 77 78 comment = "" 79 if team.slug == "issue-subscribers-good-first-issue": 80 comment = "{}\n".format(beginner_comment) 81 self.issue.create_comment(comment) 82 83 body = escape_description(self.issue.body) 84 comment = f""" 85@llvm/{team.slug} 86 87Author: {self.issue.user.name} ({self.issue.user.login}) 88 89<details> 90{body} 91</details> 92""" 93 94 self.issue.create_comment(comment) 95 return True 96 97 98def human_readable_size(size, decimal_places=2): 99 for unit in ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]: 100 if size < 1024.0 or unit == "PiB": 101 break 102 size /= 1024.0 103 return f"{size:.{decimal_places}f} {unit}" 104 105 106class PRSubscriber: 107 @property 108 def team_name(self) -> str: 109 return self._team_name 110 111 def __init__(self, token: str, repo: str, pr_number: int, label_name: str): 112 self.repo = github.Github(token).get_repo(repo) 113 self.org = github.Github(token).get_organization(self.repo.organization.login) 114 self.pr = self.repo.get_issue(pr_number).as_pull_request() 115 self._team_name = "pr-subscribers-{}".format( 116 label_name.replace("+", "x") 117 ).lower() 118 self.COMMENT_TAG = "<!--LLVM PR SUMMARY COMMENT-->\n" 119 120 def get_summary_comment(self) -> github.IssueComment.IssueComment: 121 for comment in self.pr.as_issue().get_comments(): 122 if self.COMMENT_TAG in comment.body: 123 return comment 124 return None 125 126 def run(self) -> bool: 127 patch = None 128 team = _get_current_team(self.team_name, self.org.get_teams()) 129 if not team: 130 print(f"couldn't find team named {self.team_name}") 131 return False 132 133 # GitHub limits comments to 65,536 characters, let's limit the diff 134 # and the file list to 20kB each. 135 STAT_LIMIT = 20 * 1024 136 DIFF_LIMIT = 20 * 1024 137 138 # Get statistics for each file 139 diff_stats = f"{self.pr.changed_files} Files Affected:\n\n" 140 for file in self.pr.get_files(): 141 diff_stats += f"- ({file.status}) {file.filename} (" 142 if file.additions: 143 diff_stats += f"+{file.additions}" 144 if file.deletions: 145 diff_stats += f"-{file.deletions}" 146 diff_stats += ") " 147 if file.status == "renamed": 148 print(f"(from {file.previous_filename})") 149 diff_stats += "\n" 150 if len(diff_stats) > STAT_LIMIT: 151 break 152 153 # Get the diff 154 try: 155 patch = requests.get(self.pr.diff_url).text 156 except: 157 patch = "" 158 159 patch_link = f"Full diff: {self.pr.diff_url}\n" 160 if len(patch) > DIFF_LIMIT: 161 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" 162 patch = patch[0:DIFF_LIMIT] + "...\n[truncated]\n" 163 team_mention = "@llvm/{}".format(team.slug) 164 165 body = escape_description(self.pr.body) 166 # Note: the comment is in markdown and the code below 167 # is sensible to line break 168 comment = f""" 169{self.COMMENT_TAG} 170{team_mention} 171 172Author: {self.pr.user.name} ({self.pr.user.login}) 173 174<details> 175<summary>Changes</summary> 176 177{body} 178 179--- 180{patch_link} 181 182{diff_stats} 183 184``````````diff 185{patch} 186`````````` 187 188</details> 189""" 190 191 summary_comment = self.get_summary_comment() 192 if not summary_comment: 193 self.pr.as_issue().create_comment(comment) 194 elif team_mention + "\n" in summary_comment.body: 195 print("Team {} already mentioned.".format(team.slug)) 196 else: 197 summary_comment.edit( 198 summary_comment.body.replace( 199 self.COMMENT_TAG, self.COMMENT_TAG + team_mention + "\n" 200 ) 201 ) 202 return True 203 204 def _get_current_team(self) -> Optional[github.Team.Team]: 205 for team in self.org.get_teams(): 206 if self.team_name == team.name.lower(): 207 return team 208 return None 209 210 211class PRGreeter: 212 COMMENT_TAG = "<!--LLVM NEW CONTRIBUTOR COMMENT-->\n" 213 214 def __init__(self, token: str, repo: str, pr_number: int): 215 repo = github.Github(token).get_repo(repo) 216 self.pr = repo.get_issue(pr_number).as_pull_request() 217 218 def run(self) -> bool: 219 # We assume that this is only called for a PR that has just been opened 220 # by a user new to LLVM and/or GitHub itself. 221 222 # This text is using Markdown formatting. 223 224 comment = f"""\ 225{PRGreeter.COMMENT_TAG} 226Thank you for submitting a Pull Request (PR) to the LLVM Project! 227 228This PR will be automatically labeled and the relevant teams will be 229notified. 230 231If you wish to, you can add reviewers by using the "Reviewers" section on this page. 232 233If this is not working for you, it is probably because you do not have write 234permissions for the repository. In which case you can instead tag reviewers by 235name in a comment by using `@` followed by their GitHub username. 236 237If you have received no comments on your PR for a week, you can request a review 238by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate 239is once a week. Please remember that you are asking for valuable time from other developers. 240 241If you have further questions, they may be answered by the [LLVM GitHub User Guide](https://llvm.org/docs/GitHub.html). 242 243You 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/).""" 244 self.pr.as_issue().create_comment(comment) 245 return True 246 247 248class PRBuildbotInformation: 249 COMMENT_TAG = "<!--LLVM BUILDBOT INFORMATION COMMENT-->\n" 250 251 def __init__(self, token: str, repo: str, pr_number: int, author: str): 252 repo = github.Github(token).get_repo(repo) 253 self.pr = repo.get_issue(pr_number).as_pull_request() 254 self.author = author 255 256 def should_comment(self) -> bool: 257 # As soon as a new contributor has a PR merged, they are no longer a new contributor. 258 # We can tell that they were a new contributor previously because we would have 259 # added a new contributor greeting comment when they opened the PR. 260 found_greeting = False 261 for comment in self.pr.as_issue().get_comments(): 262 if PRGreeter.COMMENT_TAG in comment.body: 263 found_greeting = True 264 elif PRBuildbotInformation.COMMENT_TAG in comment.body: 265 # When an issue is reopened, then closed as merged again, we should not 266 # add a second comment. This event will be rare in practice as it seems 267 # like it's only possible when the main branch is still at the exact 268 # revision that the PR was merged on to, beyond that it's closed forever. 269 return False 270 return found_greeting 271 272 def run(self) -> bool: 273 if not self.should_comment(): 274 return 275 276 # This text is using Markdown formatting. Some of the lines are longer 277 # than others so that the final text is some reasonable looking paragraphs 278 # after the long URLs are rendered. 279 comment = f"""\ 280{PRBuildbotInformation.COMMENT_TAG} 281@{self.author} Congratulations on having your first Pull Request (PR) merged into the LLVM Project! 282 283Your changes will be combined with recent changes from other authors, then tested 284by 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. 285 286Please check whether problems have been caused by your change specifically, as 287the builds can include changes from many authors. It is not uncommon for your 288change to be included in a build that fails due to someone else's changes, or 289infrastructure issues. 290 291How 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). 292 293If your change does cause a problem, it may be reverted, or you can revert it yourself. 294This 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. 295 296If you don't get any reports, no action is required from you. Your changes are working as expected, well done! 297""" 298 self.pr.as_issue().create_comment(comment) 299 return True 300 301 302def setup_llvmbot_git(git_dir="."): 303 """ 304 Configure the git repo in `git_dir` with the llvmbot account so 305 commits are attributed to llvmbot. 306 """ 307 repo = Repo(git_dir) 308 with repo.config_writer() as config: 309 config.set_value("user", "name", "llvmbot") 310 config.set_value("user", "email", "llvmbot@llvm.org") 311 312 313def extract_commit_hash(arg: str): 314 """ 315 Extract the commit hash from the argument passed to /action github 316 comment actions. We currently only support passing the commit hash 317 directly or use the github URL, such as 318 https://github.com/llvm/llvm-project/commit/2832d7941f4207f1fcf813b27cf08cecc3086959 319 """ 320 github_prefix = "https://github.com/llvm/llvm-project/commit/" 321 if arg.startswith(github_prefix): 322 return arg[len(github_prefix) :] 323 return arg 324 325 326class ReleaseWorkflow: 327 CHERRY_PICK_FAILED_LABEL = "release:cherry-pick-failed" 328 329 """ 330 This class implements the sub-commands for the release-workflow command. 331 The current sub-commands are: 332 * create-branch 333 * create-pull-request 334 335 The execute_command method will automatically choose the correct sub-command 336 based on the text in stdin. 337 """ 338 339 def __init__( 340 self, 341 token: str, 342 repo: str, 343 issue_number: int, 344 branch_repo_name: str, 345 branch_repo_token: str, 346 llvm_project_dir: str, 347 requested_by: str, 348 ) -> None: 349 self._token = token 350 self._repo_name = repo 351 self._issue_number = issue_number 352 self._branch_repo_name = branch_repo_name 353 if branch_repo_token: 354 self._branch_repo_token = branch_repo_token 355 else: 356 self._branch_repo_token = self.token 357 self._llvm_project_dir = llvm_project_dir 358 self._requested_by = requested_by 359 360 @property 361 def token(self) -> str: 362 return self._token 363 364 @property 365 def repo_name(self) -> str: 366 return self._repo_name 367 368 @property 369 def issue_number(self) -> int: 370 return self._issue_number 371 372 @property 373 def branch_repo_owner(self) -> str: 374 return self.branch_repo_name.split("/")[0] 375 376 @property 377 def branch_repo_name(self) -> str: 378 return self._branch_repo_name 379 380 @property 381 def branch_repo_token(self) -> str: 382 return self._branch_repo_token 383 384 @property 385 def llvm_project_dir(self) -> str: 386 return self._llvm_project_dir 387 388 @property 389 def requested_by(self) -> str: 390 return self._requested_by 391 392 @property 393 def repo(self) -> github.Repository.Repository: 394 return github.Github(self.token).get_repo(self.repo_name) 395 396 @property 397 def issue(self) -> github.Issue.Issue: 398 return self.repo.get_issue(self.issue_number) 399 400 @property 401 def push_url(self) -> str: 402 return "https://{}@github.com/{}".format( 403 self.branch_repo_token, self.branch_repo_name 404 ) 405 406 @property 407 def branch_name(self) -> str: 408 return "issue{}".format(self.issue_number) 409 410 @property 411 def release_branch_for_issue(self) -> Optional[str]: 412 issue = self.issue 413 milestone = issue.milestone 414 if milestone is None: 415 return None 416 m = re.search("branch: (.+)", milestone.description) 417 if m: 418 return m.group(1) 419 return None 420 421 def print_release_branch(self) -> None: 422 print(self.release_branch_for_issue) 423 424 def issue_notify_branch(self) -> None: 425 self.issue.create_comment( 426 "/branch {}/{}".format(self.branch_repo_name, self.branch_name) 427 ) 428 429 def issue_notify_pull_request(self, pull: github.PullRequest.PullRequest) -> None: 430 self.issue.create_comment( 431 "/pull-request {}#{}".format(self.repo_name, pull.number) 432 ) 433 434 def make_ignore_comment(self, comment: str) -> str: 435 """ 436 Returns the comment string with a prefix that will cause 437 a Github workflow to skip parsing this comment. 438 439 :param str comment: The comment to ignore 440 """ 441 return "<!--IGNORE-->\n" + comment 442 443 def issue_notify_no_milestone(self, comment: List[str]) -> None: 444 message = "{}\n\nError: Command failed due to missing milestone.".format( 445 "".join([">" + line for line in comment]) 446 ) 447 self.issue.create_comment(self.make_ignore_comment(message)) 448 449 @property 450 def action_url(self) -> str: 451 if os.getenv("CI"): 452 return "https://github.com/{}/actions/runs/{}".format( 453 os.getenv("GITHUB_REPOSITORY"), os.getenv("GITHUB_RUN_ID") 454 ) 455 return "" 456 457 def issue_notify_cherry_pick_failure( 458 self, commit: str 459 ) -> github.IssueComment.IssueComment: 460 message = self.make_ignore_comment( 461 "Failed to cherry-pick: {}\n\n".format(commit) 462 ) 463 action_url = self.action_url 464 if action_url: 465 message += action_url + "\n\n" 466 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)" 467 issue = self.issue 468 comment = issue.create_comment(message) 469 issue.add_to_labels(self.CHERRY_PICK_FAILED_LABEL) 470 return comment 471 472 def issue_notify_pull_request_failure( 473 self, branch: str 474 ) -> github.IssueComment.IssueComment: 475 message = "Failed to create pull request for {} ".format(branch) 476 message += self.action_url 477 return self.issue.create_comment(message) 478 479 def issue_remove_cherry_pick_failed_label(self): 480 if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]: 481 self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL) 482 483 def get_main_commit(self, cherry_pick_sha: str) -> github.Commit.Commit: 484 commit = self.repo.get_commit(cherry_pick_sha) 485 message = commit.commit.message 486 m = re.search("\(cherry picked from commit ([0-9a-f]+)\)", message) 487 if not m: 488 return None 489 return self.repo.get_commit(m.group(1)) 490 491 def pr_request_review(self, pr: github.PullRequest.PullRequest): 492 """ 493 This function will try to find the best reviewers for `commits` and 494 then add a comment requesting review of the backport and add them as 495 reviewers. 496 497 The reviewers selected are those users who approved the pull request 498 for the main branch. 499 """ 500 reviewers = [] 501 for commit in pr.get_commits(): 502 main_commit = self.get_main_commit(commit.sha) 503 if not main_commit: 504 continue 505 for pull in main_commit.get_pulls(): 506 for review in pull.get_reviews(): 507 if review.state != "APPROVED": 508 continue 509 reviewers.append(review.user.login) 510 if len(reviewers): 511 message = "{} What do you think about merging this PR to the release branch?".format( 512 " ".join(["@" + r for r in reviewers]) 513 ) 514 pr.create_issue_comment(message) 515 pr.create_review_request(reviewers) 516 517 def create_branch(self, commits: List[str]) -> bool: 518 """ 519 This function attempts to backport `commits` into the branch associated 520 with `self.issue_number`. 521 522 If this is successful, then the branch is pushed to `self.branch_repo_name`, if not, 523 a comment is added to the issue saying that the cherry-pick failed. 524 525 :param list commits: List of commits to cherry-pick. 526 527 """ 528 print("cherry-picking", commits) 529 branch_name = self.branch_name 530 local_repo = Repo(self.llvm_project_dir) 531 local_repo.git.checkout(self.release_branch_for_issue) 532 533 for c in commits: 534 try: 535 local_repo.git.cherry_pick("-x", c) 536 except Exception as e: 537 self.issue_notify_cherry_pick_failure(c) 538 raise e 539 540 push_url = self.push_url 541 print("Pushing to {} {}".format(push_url, branch_name)) 542 local_repo.git.push(push_url, "HEAD:{}".format(branch_name), force=True) 543 544 self.issue_remove_cherry_pick_failed_label() 545 return self.create_pull_request( 546 self.branch_repo_owner, self.repo_name, branch_name, commits 547 ) 548 549 def check_if_pull_request_exists( 550 self, repo: github.Repository.Repository, head: str 551 ) -> bool: 552 pulls = repo.get_pulls(head=head) 553 return pulls.totalCount != 0 554 555 def create_pull_request( 556 self, owner: str, repo_name: str, branch: str, commits: List[str] 557 ) -> bool: 558 """ 559 Create a pull request in `self.repo_name`. The base branch of the 560 pull request will be chosen based on the the milestone attached to 561 the issue represented by `self.issue_number` For example if the milestone 562 is Release 13.0.1, then the base branch will be release/13.x. `branch` 563 will be used as the compare branch. 564 https://docs.github.com/en/get-started/quickstart/github-glossary#base-branch 565 https://docs.github.com/en/get-started/quickstart/github-glossary#compare-branch 566 """ 567 repo = github.Github(self.token).get_repo(self.repo_name) 568 issue_ref = "{}#{}".format(self.repo_name, self.issue_number) 569 pull = None 570 release_branch_for_issue = self.release_branch_for_issue 571 if release_branch_for_issue is None: 572 return False 573 574 head = f"{owner}:{branch}" 575 if self.check_if_pull_request_exists(repo, head): 576 print("PR already exists...") 577 return True 578 try: 579 commit_message = repo.get_commit(commits[-1]).commit.message 580 message_lines = commit_message.splitlines() 581 title = "{}: {}".format(release_branch_for_issue, message_lines[0]) 582 body = "Backport {}\n\nRequested by: @{}".format( 583 " ".join(commits), self.requested_by 584 ) 585 pull = repo.create_pull( 586 title=title, 587 body=body, 588 base=release_branch_for_issue, 589 head=head, 590 maintainer_can_modify=True, 591 ) 592 593 pull.as_issue().edit(milestone=self.issue.milestone) 594 595 # Once the pull request has been created, we can close the 596 # issue that was used to request the cherry-pick 597 self.issue.edit(state="closed", state_reason="completed") 598 599 try: 600 self.pr_request_review(pull) 601 except Exception as e: 602 print("error: Failed while searching for reviewers", e) 603 604 except Exception as e: 605 self.issue_notify_pull_request_failure(branch) 606 raise e 607 608 if pull is None: 609 return False 610 611 self.issue_notify_pull_request(pull) 612 self.issue_remove_cherry_pick_failed_label() 613 614 # TODO(tstellar): Do you really want to always return True? 615 return True 616 617 def execute_command(self) -> bool: 618 """ 619 This function reads lines from STDIN and executes the first command 620 that it finds. The supported command is: 621 /cherry-pick< ><:> commit0 <commit1> <commit2> <...> 622 """ 623 for line in sys.stdin: 624 line.rstrip() 625 m = re.search(r"/cherry-pick\s*:? *(.*)", line) 626 if not m: 627 continue 628 629 args = m.group(1) 630 631 arg_list = args.split() 632 commits = list(map(lambda a: extract_commit_hash(a), arg_list)) 633 return self.create_branch(commits) 634 635 print("Do not understand input:") 636 print(sys.stdin.readlines()) 637 return False 638 639 640parser = argparse.ArgumentParser() 641parser.add_argument( 642 "--token", type=str, required=True, help="GitHub authentication token" 643) 644parser.add_argument( 645 "--repo", 646 type=str, 647 default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"), 648 help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)", 649) 650subparsers = parser.add_subparsers(dest="command") 651 652issue_subscriber_parser = subparsers.add_parser("issue-subscriber") 653issue_subscriber_parser.add_argument("--label-name", type=str, required=True) 654issue_subscriber_parser.add_argument("--issue-number", type=int, required=True) 655 656pr_subscriber_parser = subparsers.add_parser("pr-subscriber") 657pr_subscriber_parser.add_argument("--label-name", type=str, required=True) 658pr_subscriber_parser.add_argument("--issue-number", type=int, required=True) 659 660pr_greeter_parser = subparsers.add_parser("pr-greeter") 661pr_greeter_parser.add_argument("--issue-number", type=int, required=True) 662 663pr_buildbot_information_parser = subparsers.add_parser("pr-buildbot-information") 664pr_buildbot_information_parser.add_argument("--issue-number", type=int, required=True) 665pr_buildbot_information_parser.add_argument("--author", type=str, required=True) 666 667release_workflow_parser = subparsers.add_parser("release-workflow") 668release_workflow_parser.add_argument( 669 "--llvm-project-dir", 670 type=str, 671 default=".", 672 help="directory containing the llvm-project checkout", 673) 674release_workflow_parser.add_argument( 675 "--issue-number", type=int, required=True, help="The issue number to update" 676) 677release_workflow_parser.add_argument( 678 "--branch-repo-token", 679 type=str, 680 help="GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.", 681) 682release_workflow_parser.add_argument( 683 "--branch-repo", 684 type=str, 685 default="llvmbot/llvm-project", 686 help="The name of the repo where new branches will be pushed (e.g. llvm/llvm-project)", 687) 688release_workflow_parser.add_argument( 689 "sub_command", 690 type=str, 691 choices=["print-release-branch", "auto"], 692 help="Print to stdout the name of the release branch ISSUE_NUMBER should be backported to", 693) 694 695llvmbot_git_config_parser = subparsers.add_parser( 696 "setup-llvmbot-git", 697 help="Set the default user and email for the git repo in LLVM_PROJECT_DIR to llvmbot", 698) 699release_workflow_parser.add_argument( 700 "--requested-by", 701 type=str, 702 required=True, 703 help="The user that requested this backport", 704) 705 706args = parser.parse_args() 707 708if args.command == "issue-subscriber": 709 issue_subscriber = IssueSubscriber( 710 args.token, args.repo, args.issue_number, args.label_name 711 ) 712 issue_subscriber.run() 713elif args.command == "pr-subscriber": 714 pr_subscriber = PRSubscriber( 715 args.token, args.repo, args.issue_number, args.label_name 716 ) 717 pr_subscriber.run() 718elif args.command == "pr-greeter": 719 pr_greeter = PRGreeter(args.token, args.repo, args.issue_number) 720 pr_greeter.run() 721elif args.command == "pr-buildbot-information": 722 pr_buildbot_information = PRBuildbotInformation( 723 args.token, args.repo, args.issue_number, args.author 724 ) 725 pr_buildbot_information.run() 726elif args.command == "release-workflow": 727 release_workflow = ReleaseWorkflow( 728 args.token, 729 args.repo, 730 args.issue_number, 731 args.branch_repo, 732 args.branch_repo_token, 733 args.llvm_project_dir, 734 args.requested_by, 735 ) 736 if not release_workflow.release_branch_for_issue: 737 release_workflow.issue_notify_no_milestone(sys.stdin.readlines()) 738 sys.exit(1) 739 if args.sub_command == "print-release-branch": 740 release_workflow.print_release_branch() 741 else: 742 if not release_workflow.execute_command(): 743 sys.exit(1) 744elif args.command == "setup-llvmbot-git": 745 setup_llvmbot_git() 746