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