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