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 def __init__(self, token: str, repo: str, pr_number: int): 212 repo = github.Github(token).get_repo(repo) 213 self.pr = repo.get_issue(pr_number).as_pull_request() 214 215 def run(self) -> bool: 216 # We assume that this is only called for a PR that has just been opened 217 # by a user new to LLVM and/or GitHub itself. 218 219 # This text is using Markdown formatting. 220 comment = f"""\ 221Thank you for submitting a Pull Request (PR) to the LLVM Project! 222 223This PR will be automatically labeled and the relevant teams will be 224notified. 225 226If you wish to, you can add reviewers by using the "Reviewers" section on this page. 227 228If this is not working for you, it is probably because you do not have write 229permissions for the repository. In which case you can instead tag reviewers by 230name in a comment by using `@` followed by their GitHub username. 231 232If you have received no comments on your PR for a week, you can request a review 233by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate 234is once a week. Please remember that you are asking for valuable time from other developers. 235 236If you have further questions, they may be answered by the [LLVM GitHub User Guide](https://llvm.org/docs/GitHub.html). 237 238You 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/).""" 239 self.pr.as_issue().create_comment(comment) 240 return True 241 242 243def setup_llvmbot_git(git_dir="."): 244 """ 245 Configure the git repo in `git_dir` with the llvmbot account so 246 commits are attributed to llvmbot. 247 """ 248 repo = Repo(git_dir) 249 with repo.config_writer() as config: 250 config.set_value("user", "name", "llvmbot") 251 config.set_value("user", "email", "llvmbot@llvm.org") 252 253 254def phab_api_call(phab_token: str, url: str, args: dict) -> dict: 255 """ 256 Make an API call to the Phabricator web service and return a dictionary 257 containing the json response. 258 """ 259 data = {"api.token": phab_token} 260 data.update(args) 261 response = requests.post(url, data=data) 262 return response.json() 263 264 265def phab_login_to_github_login( 266 phab_token: str, repo: github.Repository.Repository, phab_login: str 267) -> Optional[str]: 268 """ 269 Tries to translate a Phabricator login to a github login by 270 finding a commit made in Phabricator's Differential. 271 The commit's SHA1 is then looked up in the github repo and 272 the committer's login associated with that commit is returned. 273 274 :param str phab_token: The Conduit API token to use for communication with Pabricator 275 :param github.Repository.Repository repo: The github repo to use when looking for the SHA1 found in Differential 276 :param str phab_login: The Phabricator login to be translated. 277 """ 278 279 args = { 280 "constraints[authors][0]": phab_login, 281 # PHID for "LLVM Github Monorepo" repository 282 "constraints[repositories][0]": "PHID-REPO-f4scjekhnkmh7qilxlcy", 283 "limit": 1, 284 } 285 # API documentation: https://reviews.llvm.org/conduit/method/diffusion.commit.search/ 286 r = phab_api_call( 287 phab_token, "https://reviews.llvm.org/api/diffusion.commit.search", args 288 ) 289 data = r["result"]["data"] 290 if len(data) == 0: 291 # Can't find any commits associated with this user 292 return None 293 294 commit_sha = data[0]["fields"]["identifier"] 295 committer = repo.get_commit(commit_sha).committer 296 if not committer: 297 # This committer had an email address GitHub could not recognize, so 298 # it can't link the user to a GitHub account. 299 print(f"Warning: Can't find github account for {phab_login}") 300 return None 301 return committer.login 302 303 304def phab_get_commit_approvers(phab_token: str, commit: github.Commit.Commit) -> list: 305 args = {"corpus": commit.commit.message} 306 # API documentation: https://reviews.llvm.org/conduit/method/differential.parsecommitmessage/ 307 r = phab_api_call( 308 phab_token, "https://reviews.llvm.org/api/differential.parsecommitmessage", args 309 ) 310 review_id = r["result"]["revisionIDFieldInfo"]["value"] 311 if not review_id: 312 # No Phabricator revision for this commit 313 return [] 314 315 args = {"constraints[ids][0]": review_id, "attachments[reviewers]": True} 316 # API documentation: https://reviews.llvm.org/conduit/method/differential.revision.search/ 317 r = phab_api_call( 318 phab_token, "https://reviews.llvm.org/api/differential.revision.search", args 319 ) 320 reviewers = r["result"]["data"][0]["attachments"]["reviewers"]["reviewers"] 321 accepted = [] 322 for reviewer in reviewers: 323 if reviewer["status"] != "accepted": 324 continue 325 phid = reviewer["reviewerPHID"] 326 args = {"constraints[phids][0]": phid} 327 # API documentation: https://reviews.llvm.org/conduit/method/user.search/ 328 r = phab_api_call(phab_token, "https://reviews.llvm.org/api/user.search", args) 329 accepted.append(r["result"]["data"][0]["fields"]["username"]) 330 return accepted 331 332 333def extract_commit_hash(arg: str): 334 """ 335 Extract the commit hash from the argument passed to /action github 336 comment actions. We currently only support passing the commit hash 337 directly or use the github URL, such as 338 https://github.com/llvm/llvm-project/commit/2832d7941f4207f1fcf813b27cf08cecc3086959 339 """ 340 github_prefix = "https://github.com/llvm/llvm-project/commit/" 341 if arg.startswith(github_prefix): 342 return arg[len(github_prefix) :] 343 return arg 344 345 346class ReleaseWorkflow: 347 CHERRY_PICK_FAILED_LABEL = "release:cherry-pick-failed" 348 349 """ 350 This class implements the sub-commands for the release-workflow command. 351 The current sub-commands are: 352 * create-branch 353 * create-pull-request 354 355 The execute_command method will automatically choose the correct sub-command 356 based on the text in stdin. 357 """ 358 359 def __init__( 360 self, 361 token: str, 362 repo: str, 363 issue_number: int, 364 branch_repo_name: str, 365 branch_repo_token: str, 366 llvm_project_dir: str, 367 phab_token: str, 368 ) -> None: 369 self._token = token 370 self._repo_name = repo 371 self._issue_number = issue_number 372 self._branch_repo_name = branch_repo_name 373 if branch_repo_token: 374 self._branch_repo_token = branch_repo_token 375 else: 376 self._branch_repo_token = self.token 377 self._llvm_project_dir = llvm_project_dir 378 self._phab_token = phab_token 379 380 @property 381 def token(self) -> str: 382 return self._token 383 384 @property 385 def repo_name(self) -> str: 386 return self._repo_name 387 388 @property 389 def issue_number(self) -> int: 390 return self._issue_number 391 392 @property 393 def branch_repo_name(self) -> str: 394 return self._branch_repo_name 395 396 @property 397 def branch_repo_token(self) -> str: 398 return self._branch_repo_token 399 400 @property 401 def llvm_project_dir(self) -> str: 402 return self._llvm_project_dir 403 404 @property 405 def phab_token(self) -> str: 406 return self._phab_token 407 408 @property 409 def repo(self) -> github.Repository.Repository: 410 return github.Github(self.token).get_repo(self.repo_name) 411 412 @property 413 def issue(self) -> github.Issue.Issue: 414 return self.repo.get_issue(self.issue_number) 415 416 @property 417 def push_url(self) -> str: 418 return "https://{}@github.com/{}".format( 419 self.branch_repo_token, self.branch_repo_name 420 ) 421 422 @property 423 def branch_name(self) -> str: 424 return "issue{}".format(self.issue_number) 425 426 @property 427 def release_branch_for_issue(self) -> Optional[str]: 428 issue = self.issue 429 milestone = issue.milestone 430 if milestone is None: 431 return None 432 m = re.search("branch: (.+)", milestone.description) 433 if m: 434 return m.group(1) 435 return None 436 437 def print_release_branch(self) -> None: 438 print(self.release_branch_for_issue) 439 440 def issue_notify_branch(self) -> None: 441 self.issue.create_comment( 442 "/branch {}/{}".format(self.branch_repo_name, self.branch_name) 443 ) 444 445 def issue_notify_pull_request(self, pull: github.PullRequest.PullRequest) -> None: 446 self.issue.create_comment( 447 "/pull-request {}#{}".format(self.repo_name, pull.number) 448 ) 449 450 def make_ignore_comment(self, comment: str) -> str: 451 """ 452 Returns the comment string with a prefix that will cause 453 a Github workflow to skip parsing this comment. 454 455 :param str comment: The comment to ignore 456 """ 457 return "<!--IGNORE-->\n" + comment 458 459 def issue_notify_no_milestone(self, comment: List[str]) -> None: 460 message = "{}\n\nError: Command failed due to missing milestone.".format( 461 "".join([">" + line for line in comment]) 462 ) 463 self.issue.create_comment(self.make_ignore_comment(message)) 464 465 @property 466 def action_url(self) -> str: 467 if os.getenv("CI"): 468 return "https://github.com/{}/actions/runs/{}".format( 469 os.getenv("GITHUB_REPOSITORY"), os.getenv("GITHUB_RUN_ID") 470 ) 471 return "" 472 473 def issue_notify_cherry_pick_failure( 474 self, commit: str 475 ) -> github.IssueComment.IssueComment: 476 message = self.make_ignore_comment( 477 "Failed to cherry-pick: {}\n\n".format(commit) 478 ) 479 action_url = self.action_url 480 if action_url: 481 message += action_url + "\n\n" 482 message += "Please manually backport the fix and push it to your github fork. Once this is done, please add a comment like this:\n\n`/branch <user>/<repo>/<branch>`" 483 issue = self.issue 484 comment = issue.create_comment(message) 485 issue.add_to_labels(self.CHERRY_PICK_FAILED_LABEL) 486 return comment 487 488 def issue_notify_pull_request_failure( 489 self, branch: str 490 ) -> github.IssueComment.IssueComment: 491 message = "Failed to create pull request for {} ".format(branch) 492 message += self.action_url 493 return self.issue.create_comment(message) 494 495 def issue_remove_cherry_pick_failed_label(self): 496 if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]: 497 self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL) 498 499 def get_main_commit(self, cherry_pick_sha: str) -> github.Commit.Commit: 500 commit = self.repo.get_commit(cherry_pick_sha) 501 message = commit.commit.message 502 m = re.search("\(cherry picked from commit ([0-9a-f]+)\)", message) 503 if not m: 504 return None 505 return self.repo.get_commit(m.group(1)) 506 507 def pr_request_review(self, pr: github.PullRequest.PullRequest): 508 """ 509 This function will try to find the best reviewers for `commits` and 510 then add a comment requesting review of the backport and add them as 511 reviewers. 512 513 The reviewers selected are those users who approved the pull request 514 for the main branch. 515 """ 516 reviewers = [] 517 for commit in pr.get_commits(): 518 main_commit = self.get_main_commit(commit.sha) 519 if not main_commit: 520 continue 521 for pull in main_commit.get_pulls(): 522 for review in pull.get_reviews(): 523 if review.state != "APPROVED": 524 continue 525 reviewers.append(review.user.login) 526 if len(reviewers): 527 message = "{} What do you think about merging this PR to the release branch?".format( 528 " ".join(["@" + r for r in reviewers]) 529 ) 530 pr.create_issue_comment(message) 531 pr.create_review_request(reviewers) 532 533 def create_branch(self, commits: List[str]) -> bool: 534 """ 535 This function attempts to backport `commits` into the branch associated 536 with `self.issue_number`. 537 538 If this is successful, then the branch is pushed to `self.branch_repo_name`, if not, 539 a comment is added to the issue saying that the cherry-pick failed. 540 541 :param list commits: List of commits to cherry-pick. 542 543 """ 544 print("cherry-picking", commits) 545 branch_name = self.branch_name 546 local_repo = Repo(self.llvm_project_dir) 547 local_repo.git.checkout(self.release_branch_for_issue) 548 549 for c in commits: 550 try: 551 local_repo.git.cherry_pick("-x", c) 552 except Exception as e: 553 self.issue_notify_cherry_pick_failure(c) 554 raise e 555 556 push_url = self.push_url 557 print("Pushing to {} {}".format(push_url, branch_name)) 558 local_repo.git.push(push_url, "HEAD:{}".format(branch_name), force=True) 559 560 self.issue_notify_branch() 561 self.issue_remove_cherry_pick_failed_label() 562 return True 563 564 def check_if_pull_request_exists( 565 self, repo: github.Repository.Repository, head: str 566 ) -> bool: 567 pulls = repo.get_pulls(head=head) 568 return pulls.totalCount != 0 569 570 def create_pull_request(self, owner: str, repo_name: str, branch: str) -> bool: 571 """ 572 Create a pull request in `self.repo_name`. The base branch of the 573 pull request will be chosen based on the the milestone attached to 574 the issue represented by `self.issue_number` For example if the milestone 575 is Release 13.0.1, then the base branch will be release/13.x. `branch` 576 will be used as the compare branch. 577 https://docs.github.com/en/get-started/quickstart/github-glossary#base-branch 578 https://docs.github.com/en/get-started/quickstart/github-glossary#compare-branch 579 """ 580 repo = github.Github(self.token).get_repo(self.repo_name) 581 issue_ref = "{}#{}".format(self.repo_name, self.issue_number) 582 pull = None 583 release_branch_for_issue = self.release_branch_for_issue 584 if release_branch_for_issue is None: 585 return False 586 head_branch = branch 587 if not repo.fork: 588 # If the target repo is not a fork of llvm-project, we need to copy 589 # the branch into the target repo. GitHub only supports cross-repo pull 590 # requests on forked repos. 591 head_branch = f"{owner}-{branch}" 592 local_repo = Repo(self.llvm_project_dir) 593 push_done = False 594 for _ in range(0, 5): 595 try: 596 local_repo.git.fetch( 597 f"https://github.com/{owner}/{repo_name}", f"{branch}:{branch}" 598 ) 599 local_repo.git.push( 600 self.push_url, f"{branch}:{head_branch}", force=True 601 ) 602 push_done = True 603 break 604 except Exception as e: 605 print(e) 606 time.sleep(30) 607 continue 608 if not push_done: 609 raise Exception("Failed to mirror branch into {}".format(self.push_url)) 610 owner = repo.owner.login 611 612 head = f"{owner}:{head_branch}" 613 if self.check_if_pull_request_exists(repo, head): 614 print("PR already exists...") 615 return True 616 try: 617 pull = repo.create_pull( 618 title=f"PR for {issue_ref}", 619 body="resolves {}".format(issue_ref), 620 base=release_branch_for_issue, 621 head=head, 622 maintainer_can_modify=False, 623 ) 624 625 pull.as_issue().edit(milestone=self.issue.milestone) 626 627 try: 628 self.pr_request_review(pull) 629 except Exception as e: 630 print("error: Failed while searching for reviewers", e) 631 632 except Exception as e: 633 self.issue_notify_pull_request_failure(branch) 634 raise e 635 636 if pull is None: 637 return False 638 639 self.issue_notify_pull_request(pull) 640 self.issue_remove_cherry_pick_failed_label() 641 642 # TODO(tstellar): Do you really want to always return True? 643 return True 644 645 def execute_command(self) -> bool: 646 """ 647 This function reads lines from STDIN and executes the first command 648 that it finds. The 2 supported commands are: 649 /cherry-pick commit0 <commit1> <commit2> <...> 650 /branch <owner>/<repo>/<branch> 651 """ 652 for line in sys.stdin: 653 line.rstrip() 654 m = re.search(r"/([a-z-]+)\s(.+)", line) 655 if not m: 656 continue 657 command = m.group(1) 658 args = m.group(2) 659 660 if command == "cherry-pick": 661 arg_list = args.split() 662 commits = list(map(lambda a: extract_commit_hash(a), arg_list)) 663 return self.create_branch(commits) 664 665 if command == "branch": 666 m = re.match("([^/]+)/([^/]+)/(.+)", args) 667 if m: 668 owner = m.group(1) 669 repo = m.group(2) 670 branch = m.group(3) 671 return self.create_pull_request(owner, repo, branch) 672 673 print("Do not understand input:") 674 print(sys.stdin.readlines()) 675 return False 676 677 678parser = argparse.ArgumentParser() 679parser.add_argument( 680 "--token", type=str, required=True, help="GitHub authentiation token" 681) 682parser.add_argument( 683 "--repo", 684 type=str, 685 default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"), 686 help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)", 687) 688subparsers = parser.add_subparsers(dest="command") 689 690issue_subscriber_parser = subparsers.add_parser("issue-subscriber") 691issue_subscriber_parser.add_argument("--label-name", type=str, required=True) 692issue_subscriber_parser.add_argument("--issue-number", type=int, required=True) 693 694pr_subscriber_parser = subparsers.add_parser("pr-subscriber") 695pr_subscriber_parser.add_argument("--label-name", type=str, required=True) 696pr_subscriber_parser.add_argument("--issue-number", type=int, required=True) 697 698pr_greeter_parser = subparsers.add_parser("pr-greeter") 699pr_greeter_parser.add_argument("--issue-number", type=int, required=True) 700 701release_workflow_parser = subparsers.add_parser("release-workflow") 702release_workflow_parser.add_argument( 703 "--llvm-project-dir", 704 type=str, 705 default=".", 706 help="directory containing the llvm-project checout", 707) 708release_workflow_parser.add_argument( 709 "--issue-number", type=int, required=True, help="The issue number to update" 710) 711release_workflow_parser.add_argument( 712 "--phab-token", 713 type=str, 714 help="Phabricator conduit API token. See https://reviews.llvm.org/settings/user/<USER>/page/apitokens/", 715) 716release_workflow_parser.add_argument( 717 "--branch-repo-token", 718 type=str, 719 help="GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.", 720) 721release_workflow_parser.add_argument( 722 "--branch-repo", 723 type=str, 724 default="llvmbot/llvm-project", 725 help="The name of the repo where new branches will be pushed (e.g. llvm/llvm-project)", 726) 727release_workflow_parser.add_argument( 728 "sub_command", 729 type=str, 730 choices=["print-release-branch", "auto"], 731 help="Print to stdout the name of the release branch ISSUE_NUMBER should be backported to", 732) 733 734llvmbot_git_config_parser = subparsers.add_parser( 735 "setup-llvmbot-git", 736 help="Set the default user and email for the git repo in LLVM_PROJECT_DIR to llvmbot", 737) 738 739args = parser.parse_args() 740 741if args.command == "issue-subscriber": 742 issue_subscriber = IssueSubscriber( 743 args.token, args.repo, args.issue_number, args.label_name 744 ) 745 issue_subscriber.run() 746elif args.command == "pr-subscriber": 747 pr_subscriber = PRSubscriber( 748 args.token, args.repo, args.issue_number, args.label_name 749 ) 750 pr_subscriber.run() 751elif args.command == "pr-greeter": 752 pr_greeter = PRGreeter(args.token, args.repo, args.issue_number) 753 pr_greeter.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.phab_token, 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() 774