1*d415bd75Srobert#!/usr/bin/env python3 2*d415bd75Srobert# 3*d415bd75Srobert# ======- github-automation - LLVM GitHub Automation Routines--*- python -*--==# 4*d415bd75Srobert# 5*d415bd75Srobert# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6*d415bd75Srobert# See https://llvm.org/LICENSE.txt for license information. 7*d415bd75Srobert# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8*d415bd75Srobert# 9*d415bd75Srobert# ==-------------------------------------------------------------------------==# 10*d415bd75Srobert 11*d415bd75Srobertimport argparse 12*d415bd75Srobertfrom git import Repo # type: ignore 13*d415bd75Srobertimport github 14*d415bd75Srobertimport os 15*d415bd75Srobertimport re 16*d415bd75Srobertimport requests 17*d415bd75Srobertimport sys 18*d415bd75Srobertimport time 19*d415bd75Srobertfrom typing import List, Optional 20*d415bd75Srobert 21*d415bd75Srobertclass IssueSubscriber: 22*d415bd75Srobert 23*d415bd75Srobert @property 24*d415bd75Srobert def team_name(self) -> str: 25*d415bd75Srobert return self._team_name 26*d415bd75Srobert 27*d415bd75Srobert def __init__(self, token:str, repo:str, issue_number:int, label_name:str): 28*d415bd75Srobert self.repo = github.Github(token).get_repo(repo) 29*d415bd75Srobert self.org = github.Github(token).get_organization(self.repo.organization.login) 30*d415bd75Srobert self.issue = self.repo.get_issue(issue_number) 31*d415bd75Srobert self._team_name = 'issue-subscribers-{}'.format(label_name).lower() 32*d415bd75Srobert 33*d415bd75Srobert def run(self) -> bool: 34*d415bd75Srobert for team in self.org.get_teams(): 35*d415bd75Srobert if self.team_name != team.name.lower(): 36*d415bd75Srobert continue 37*d415bd75Srobert comment = '@llvm/{}'.format(team.slug) 38*d415bd75Srobert self.issue.create_comment(comment) 39*d415bd75Srobert return True 40*d415bd75Srobert return False 41*d415bd75Srobert 42*d415bd75Srobertdef setup_llvmbot_git(git_dir = '.'): 43*d415bd75Srobert """ 44*d415bd75Srobert Configure the git repo in `git_dir` with the llvmbot account so 45*d415bd75Srobert commits are attributed to llvmbot. 46*d415bd75Srobert """ 47*d415bd75Srobert repo = Repo(git_dir) 48*d415bd75Srobert with repo.config_writer() as config: 49*d415bd75Srobert config.set_value('user', 'name', 'llvmbot') 50*d415bd75Srobert config.set_value('user', 'email', 'llvmbot@llvm.org') 51*d415bd75Srobert 52*d415bd75Srobertdef phab_api_call(phab_token:str, url:str, args:dict) -> dict: 53*d415bd75Srobert """ 54*d415bd75Srobert Make an API call to the Phabricator web service and return a dictionary 55*d415bd75Srobert containing the json response. 56*d415bd75Srobert """ 57*d415bd75Srobert data = { "api.token" : phab_token } 58*d415bd75Srobert data.update(args) 59*d415bd75Srobert response = requests.post(url, data = data) 60*d415bd75Srobert return response.json() 61*d415bd75Srobert 62*d415bd75Srobert 63*d415bd75Srobertdef phab_login_to_github_login(phab_token:str, repo:github.Repository.Repository, phab_login:str) -> Optional[str]: 64*d415bd75Srobert """ 65*d415bd75Srobert Tries to translate a Phabricator login to a github login by 66*d415bd75Srobert finding a commit made in Phabricator's Differential. 67*d415bd75Srobert The commit's SHA1 is then looked up in the github repo and 68*d415bd75Srobert the committer's login associated with that commit is returned. 69*d415bd75Srobert 70*d415bd75Srobert :param str phab_token: The Conduit API token to use for communication with Pabricator 71*d415bd75Srobert :param github.Repository.Repository repo: The github repo to use when looking for the SHA1 found in Differential 72*d415bd75Srobert :param str phab_login: The Phabricator login to be translated. 73*d415bd75Srobert """ 74*d415bd75Srobert 75*d415bd75Srobert args = { 76*d415bd75Srobert "constraints[authors][0]" : phab_login, 77*d415bd75Srobert # PHID for "LLVM Github Monorepo" repository 78*d415bd75Srobert "constraints[repositories][0]" : "PHID-REPO-f4scjekhnkmh7qilxlcy", 79*d415bd75Srobert "limit" : 1 80*d415bd75Srobert } 81*d415bd75Srobert # API documentation: https://reviews.llvm.org/conduit/method/diffusion.commit.search/ 82*d415bd75Srobert r = phab_api_call(phab_token, "https://reviews.llvm.org/api/diffusion.commit.search", args) 83*d415bd75Srobert data = r['result']['data'] 84*d415bd75Srobert if len(data) == 0: 85*d415bd75Srobert # Can't find any commits associated with this user 86*d415bd75Srobert return None 87*d415bd75Srobert 88*d415bd75Srobert commit_sha = data[0]['fields']['identifier'] 89*d415bd75Srobert committer = repo.get_commit(commit_sha).committer 90*d415bd75Srobert if not committer: 91*d415bd75Srobert # This committer had an email address GitHub could not recognize, so 92*d415bd75Srobert # it can't link the user to a GitHub account. 93*d415bd75Srobert print(f"Warning: Can't find github account for {phab_login}") 94*d415bd75Srobert return None 95*d415bd75Srobert return committer.login 96*d415bd75Srobert 97*d415bd75Srobertdef phab_get_commit_approvers(phab_token:str, commit:github.Commit.Commit) -> list: 98*d415bd75Srobert args = { "corpus" : commit.commit.message } 99*d415bd75Srobert # API documentation: https://reviews.llvm.org/conduit/method/differential.parsecommitmessage/ 100*d415bd75Srobert r = phab_api_call(phab_token, "https://reviews.llvm.org/api/differential.parsecommitmessage", args) 101*d415bd75Srobert review_id = r['result']['revisionIDFieldInfo']['value'] 102*d415bd75Srobert if not review_id: 103*d415bd75Srobert # No Phabricator revision for this commit 104*d415bd75Srobert return [] 105*d415bd75Srobert 106*d415bd75Srobert args = { 107*d415bd75Srobert 'constraints[ids][0]' : review_id, 108*d415bd75Srobert 'attachments[reviewers]' : True 109*d415bd75Srobert } 110*d415bd75Srobert # API documentation: https://reviews.llvm.org/conduit/method/differential.revision.search/ 111*d415bd75Srobert r = phab_api_call(phab_token, "https://reviews.llvm.org/api/differential.revision.search", args) 112*d415bd75Srobert reviewers = r['result']['data'][0]['attachments']['reviewers']['reviewers'] 113*d415bd75Srobert accepted = [] 114*d415bd75Srobert for reviewer in reviewers: 115*d415bd75Srobert if reviewer['status'] != 'accepted': 116*d415bd75Srobert continue 117*d415bd75Srobert phid = reviewer['reviewerPHID'] 118*d415bd75Srobert args = { 'constraints[phids][0]' : phid } 119*d415bd75Srobert # API documentation: https://reviews.llvm.org/conduit/method/user.search/ 120*d415bd75Srobert r = phab_api_call(phab_token, "https://reviews.llvm.org/api/user.search", args) 121*d415bd75Srobert accepted.append(r['result']['data'][0]['fields']['username']) 122*d415bd75Srobert return accepted 123*d415bd75Srobert 124*d415bd75Srobertclass ReleaseWorkflow: 125*d415bd75Srobert 126*d415bd75Srobert CHERRY_PICK_FAILED_LABEL = 'release:cherry-pick-failed' 127*d415bd75Srobert 128*d415bd75Srobert """ 129*d415bd75Srobert This class implements the sub-commands for the release-workflow command. 130*d415bd75Srobert The current sub-commands are: 131*d415bd75Srobert * create-branch 132*d415bd75Srobert * create-pull-request 133*d415bd75Srobert 134*d415bd75Srobert The execute_command method will automatically choose the correct sub-command 135*d415bd75Srobert based on the text in stdin. 136*d415bd75Srobert """ 137*d415bd75Srobert 138*d415bd75Srobert def __init__(self, token:str, repo:str, issue_number:int, 139*d415bd75Srobert branch_repo_name:str, branch_repo_token:str, 140*d415bd75Srobert llvm_project_dir:str, phab_token:str) -> None: 141*d415bd75Srobert self._token = token 142*d415bd75Srobert self._repo_name = repo 143*d415bd75Srobert self._issue_number = issue_number 144*d415bd75Srobert self._branch_repo_name = branch_repo_name 145*d415bd75Srobert if branch_repo_token: 146*d415bd75Srobert self._branch_repo_token = branch_repo_token 147*d415bd75Srobert else: 148*d415bd75Srobert self._branch_repo_token = self.token 149*d415bd75Srobert self._llvm_project_dir = llvm_project_dir 150*d415bd75Srobert self._phab_token = phab_token 151*d415bd75Srobert 152*d415bd75Srobert @property 153*d415bd75Srobert def token(self) -> str: 154*d415bd75Srobert return self._token 155*d415bd75Srobert 156*d415bd75Srobert @property 157*d415bd75Srobert def repo_name(self) -> str: 158*d415bd75Srobert return self._repo_name 159*d415bd75Srobert 160*d415bd75Srobert @property 161*d415bd75Srobert def issue_number(self) -> int: 162*d415bd75Srobert return self._issue_number 163*d415bd75Srobert 164*d415bd75Srobert @property 165*d415bd75Srobert def branch_repo_name(self) -> str: 166*d415bd75Srobert return self._branch_repo_name 167*d415bd75Srobert 168*d415bd75Srobert @property 169*d415bd75Srobert def branch_repo_token(self) -> str: 170*d415bd75Srobert return self._branch_repo_token 171*d415bd75Srobert 172*d415bd75Srobert @property 173*d415bd75Srobert def llvm_project_dir(self) -> str: 174*d415bd75Srobert return self._llvm_project_dir 175*d415bd75Srobert 176*d415bd75Srobert @property 177*d415bd75Srobert def phab_token(self) -> str: 178*d415bd75Srobert return self._phab_token 179*d415bd75Srobert 180*d415bd75Srobert @property 181*d415bd75Srobert def repo(self) -> github.Repository.Repository: 182*d415bd75Srobert return github.Github(self.token).get_repo(self.repo_name) 183*d415bd75Srobert 184*d415bd75Srobert @property 185*d415bd75Srobert def issue(self) -> github.Issue.Issue: 186*d415bd75Srobert return self.repo.get_issue(self.issue_number) 187*d415bd75Srobert 188*d415bd75Srobert @property 189*d415bd75Srobert def push_url(self) -> str: 190*d415bd75Srobert return 'https://{}@github.com/{}'.format(self.branch_repo_token, self.branch_repo_name) 191*d415bd75Srobert 192*d415bd75Srobert @property 193*d415bd75Srobert def branch_name(self) -> str: 194*d415bd75Srobert return 'issue{}'.format(self.issue_number) 195*d415bd75Srobert 196*d415bd75Srobert @property 197*d415bd75Srobert def release_branch_for_issue(self) -> Optional[str]: 198*d415bd75Srobert issue = self.issue 199*d415bd75Srobert milestone = issue.milestone 200*d415bd75Srobert if milestone is None: 201*d415bd75Srobert return None 202*d415bd75Srobert m = re.search('branch: (.+)',milestone.description) 203*d415bd75Srobert if m: 204*d415bd75Srobert return m.group(1) 205*d415bd75Srobert return None 206*d415bd75Srobert 207*d415bd75Srobert def print_release_branch(self) -> None: 208*d415bd75Srobert print(self.release_branch_for_issue) 209*d415bd75Srobert 210*d415bd75Srobert def issue_notify_branch(self) -> None: 211*d415bd75Srobert self.issue.create_comment('/branch {}/{}'.format(self.branch_repo_name, self.branch_name)) 212*d415bd75Srobert 213*d415bd75Srobert def issue_notify_pull_request(self, pull:github.PullRequest.PullRequest) -> None: 214*d415bd75Srobert self.issue.create_comment('/pull-request {}#{}'.format(self.branch_repo_name, pull.number)) 215*d415bd75Srobert 216*d415bd75Srobert def make_ignore_comment(self, comment: str) -> str: 217*d415bd75Srobert """ 218*d415bd75Srobert Returns the comment string with a prefix that will cause 219*d415bd75Srobert a Github workflow to skip parsing this comment. 220*d415bd75Srobert 221*d415bd75Srobert :param str comment: The comment to ignore 222*d415bd75Srobert """ 223*d415bd75Srobert return "<!--IGNORE-->\n"+comment 224*d415bd75Srobert 225*d415bd75Srobert def issue_notify_no_milestone(self, comment:List[str]) -> None: 226*d415bd75Srobert message = "{}\n\nError: Command failed due to missing milestone.".format(''.join(['>' + line for line in comment])) 227*d415bd75Srobert self.issue.create_comment(self.make_ignore_comment(message)) 228*d415bd75Srobert 229*d415bd75Srobert @property 230*d415bd75Srobert def action_url(self) -> str: 231*d415bd75Srobert if os.getenv('CI'): 232*d415bd75Srobert return 'https://github.com/{}/actions/runs/{}'.format(os.getenv('GITHUB_REPOSITORY'), os.getenv('GITHUB_RUN_ID')) 233*d415bd75Srobert return "" 234*d415bd75Srobert 235*d415bd75Srobert def issue_notify_cherry_pick_failure(self, commit:str) -> github.IssueComment.IssueComment: 236*d415bd75Srobert message = self.make_ignore_comment("Failed to cherry-pick: {}\n\n".format(commit)) 237*d415bd75Srobert action_url = self.action_url 238*d415bd75Srobert if action_url: 239*d415bd75Srobert message += action_url + "\n\n" 240*d415bd75Srobert 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>`" 241*d415bd75Srobert issue = self.issue 242*d415bd75Srobert comment = issue.create_comment(message) 243*d415bd75Srobert issue.add_to_labels(self.CHERRY_PICK_FAILED_LABEL) 244*d415bd75Srobert return comment 245*d415bd75Srobert 246*d415bd75Srobert def issue_notify_pull_request_failure(self, branch:str) -> github.IssueComment.IssueComment: 247*d415bd75Srobert message = "Failed to create pull request for {} ".format(branch) 248*d415bd75Srobert message += self.action_url 249*d415bd75Srobert return self.issue.create_comment(message) 250*d415bd75Srobert 251*d415bd75Srobert def issue_remove_cherry_pick_failed_label(self): 252*d415bd75Srobert if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]: 253*d415bd75Srobert self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL) 254*d415bd75Srobert 255*d415bd75Srobert def pr_request_review(self, pr:github.PullRequest.PullRequest): 256*d415bd75Srobert """ 257*d415bd75Srobert This function will try to find the best reviewers for `commits` and 258*d415bd75Srobert then add a comment requesting review of the backport and assign the 259*d415bd75Srobert pull request to the selected reviewers. 260*d415bd75Srobert 261*d415bd75Srobert The reviewers selected are those users who approved the patch in 262*d415bd75Srobert Phabricator. 263*d415bd75Srobert """ 264*d415bd75Srobert reviewers = [] 265*d415bd75Srobert for commit in pr.get_commits(): 266*d415bd75Srobert approvers = phab_get_commit_approvers(self.phab_token, commit) 267*d415bd75Srobert for a in approvers: 268*d415bd75Srobert login = phab_login_to_github_login(self.phab_token, self.repo, a) 269*d415bd75Srobert if not login: 270*d415bd75Srobert continue 271*d415bd75Srobert reviewers.append(login) 272*d415bd75Srobert if len(reviewers): 273*d415bd75Srobert message = "{} What do you think about merging this PR to the release branch?".format( 274*d415bd75Srobert " ".join(["@" + r for r in reviewers])) 275*d415bd75Srobert pr.create_issue_comment(message) 276*d415bd75Srobert pr.add_to_assignees(*reviewers) 277*d415bd75Srobert 278*d415bd75Srobert def create_branch(self, commits:List[str]) -> bool: 279*d415bd75Srobert """ 280*d415bd75Srobert This function attempts to backport `commits` into the branch associated 281*d415bd75Srobert with `self.issue_number`. 282*d415bd75Srobert 283*d415bd75Srobert If this is successful, then the branch is pushed to `self.branch_repo_name`, if not, 284*d415bd75Srobert a comment is added to the issue saying that the cherry-pick failed. 285*d415bd75Srobert 286*d415bd75Srobert :param list commits: List of commits to cherry-pick. 287*d415bd75Srobert 288*d415bd75Srobert """ 289*d415bd75Srobert print('cherry-picking', commits) 290*d415bd75Srobert branch_name = self.branch_name 291*d415bd75Srobert local_repo = Repo(self.llvm_project_dir) 292*d415bd75Srobert local_repo.git.checkout(self.release_branch_for_issue) 293*d415bd75Srobert 294*d415bd75Srobert for c in commits: 295*d415bd75Srobert try: 296*d415bd75Srobert local_repo.git.cherry_pick('-x', c) 297*d415bd75Srobert except Exception as e: 298*d415bd75Srobert self.issue_notify_cherry_pick_failure(c) 299*d415bd75Srobert raise e 300*d415bd75Srobert 301*d415bd75Srobert push_url = self.push_url 302*d415bd75Srobert print('Pushing to {} {}'.format(push_url, branch_name)) 303*d415bd75Srobert local_repo.git.push(push_url, 'HEAD:{}'.format(branch_name), force=True) 304*d415bd75Srobert 305*d415bd75Srobert self.issue_notify_branch() 306*d415bd75Srobert self.issue_remove_cherry_pick_failed_label() 307*d415bd75Srobert return True 308*d415bd75Srobert 309*d415bd75Srobert def check_if_pull_request_exists(self, repo:github.Repository.Repository, head:str) -> bool: 310*d415bd75Srobert pulls = repo.get_pulls(head=head) 311*d415bd75Srobert return pulls.totalCount != 0 312*d415bd75Srobert 313*d415bd75Srobert def create_pull_request(self, owner:str, repo_name:str, branch:str) -> bool: 314*d415bd75Srobert """ 315*d415bd75Srobert reate a pull request in `self.branch_repo_name`. The base branch of the 316*d415bd75Srobert pull request will be chosen based on the the milestone attached to 317*d415bd75Srobert the issue represented by `self.issue_number` For example if the milestone 318*d415bd75Srobert is Release 13.0.1, then the base branch will be release/13.x. `branch` 319*d415bd75Srobert will be used as the compare branch. 320*d415bd75Srobert https://docs.github.com/en/get-started/quickstart/github-glossary#base-branch 321*d415bd75Srobert https://docs.github.com/en/get-started/quickstart/github-glossary#compare-branch 322*d415bd75Srobert """ 323*d415bd75Srobert repo = github.Github(self.token).get_repo(self.branch_repo_name) 324*d415bd75Srobert issue_ref = '{}#{}'.format(self.repo_name, self.issue_number) 325*d415bd75Srobert pull = None 326*d415bd75Srobert release_branch_for_issue = self.release_branch_for_issue 327*d415bd75Srobert if release_branch_for_issue is None: 328*d415bd75Srobert return False 329*d415bd75Srobert head_branch = branch 330*d415bd75Srobert if not repo.fork: 331*d415bd75Srobert # If the target repo is not a fork of llvm-project, we need to copy 332*d415bd75Srobert # the branch into the target repo. GitHub only supports cross-repo pull 333*d415bd75Srobert # requests on forked repos. 334*d415bd75Srobert head_branch = f'{owner}-{branch}' 335*d415bd75Srobert local_repo = Repo(self.llvm_project_dir) 336*d415bd75Srobert push_done = False 337*d415bd75Srobert for _ in range(0,5): 338*d415bd75Srobert try: 339*d415bd75Srobert local_repo.git.fetch(f'https://github.com/{owner}/{repo_name}', f'{branch}:{branch}') 340*d415bd75Srobert local_repo.git.push(self.push_url, f'{branch}:{head_branch}', force=True) 341*d415bd75Srobert push_done = True 342*d415bd75Srobert break 343*d415bd75Srobert except Exception as e: 344*d415bd75Srobert print(e) 345*d415bd75Srobert time.sleep(30) 346*d415bd75Srobert continue 347*d415bd75Srobert if not push_done: 348*d415bd75Srobert raise Exception("Failed to mirror branch into {}".format(self.push_url)) 349*d415bd75Srobert owner = repo.owner.login 350*d415bd75Srobert 351*d415bd75Srobert head = f"{owner}:{head_branch}" 352*d415bd75Srobert if self.check_if_pull_request_exists(repo, head): 353*d415bd75Srobert print("PR already exists...") 354*d415bd75Srobert return True 355*d415bd75Srobert try: 356*d415bd75Srobert pull = repo.create_pull(title=f"PR for {issue_ref}", 357*d415bd75Srobert body='resolves {}'.format(issue_ref), 358*d415bd75Srobert base=release_branch_for_issue, 359*d415bd75Srobert head=head, 360*d415bd75Srobert maintainer_can_modify=False) 361*d415bd75Srobert 362*d415bd75Srobert try: 363*d415bd75Srobert if self.phab_token: 364*d415bd75Srobert self.pr_request_review(pull) 365*d415bd75Srobert except Exception as e: 366*d415bd75Srobert print("error: Failed while searching for reviewers", e) 367*d415bd75Srobert 368*d415bd75Srobert except Exception as e: 369*d415bd75Srobert self.issue_notify_pull_request_failure(branch) 370*d415bd75Srobert raise e 371*d415bd75Srobert 372*d415bd75Srobert if pull is None: 373*d415bd75Srobert return False 374*d415bd75Srobert 375*d415bd75Srobert self.issue_notify_pull_request(pull) 376*d415bd75Srobert self.issue_remove_cherry_pick_failed_label() 377*d415bd75Srobert 378*d415bd75Srobert # TODO(tstellar): Do you really want to always return True? 379*d415bd75Srobert return True 380*d415bd75Srobert 381*d415bd75Srobert 382*d415bd75Srobert def execute_command(self) -> bool: 383*d415bd75Srobert """ 384*d415bd75Srobert This function reads lines from STDIN and executes the first command 385*d415bd75Srobert that it finds. The 2 supported commands are: 386*d415bd75Srobert /cherry-pick commit0 <commit1> <commit2> <...> 387*d415bd75Srobert /branch <owner>/<repo>/<branch> 388*d415bd75Srobert """ 389*d415bd75Srobert for line in sys.stdin: 390*d415bd75Srobert line.rstrip() 391*d415bd75Srobert m = re.search(r"/([a-z-]+)\s(.+)", line) 392*d415bd75Srobert if not m: 393*d415bd75Srobert continue 394*d415bd75Srobert command = m.group(1) 395*d415bd75Srobert args = m.group(2) 396*d415bd75Srobert 397*d415bd75Srobert if command == 'cherry-pick': 398*d415bd75Srobert return self.create_branch(args.split()) 399*d415bd75Srobert 400*d415bd75Srobert if command == 'branch': 401*d415bd75Srobert m = re.match('([^/]+)/([^/]+)/(.+)', args) 402*d415bd75Srobert if m: 403*d415bd75Srobert owner = m.group(1) 404*d415bd75Srobert repo = m.group(2) 405*d415bd75Srobert branch = m.group(3) 406*d415bd75Srobert return self.create_pull_request(owner, repo, branch) 407*d415bd75Srobert 408*d415bd75Srobert print("Do not understand input:") 409*d415bd75Srobert print(sys.stdin.readlines()) 410*d415bd75Srobert return False 411*d415bd75Srobert 412*d415bd75Srobertparser = argparse.ArgumentParser() 413*d415bd75Srobertparser.add_argument('--token', type=str, required=True, help='GitHub authentiation token') 414*d415bd75Srobertparser.add_argument('--repo', type=str, default=os.getenv('GITHUB_REPOSITORY', 'llvm/llvm-project'), 415*d415bd75Srobert help='The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)') 416*d415bd75Srobertsubparsers = parser.add_subparsers(dest='command') 417*d415bd75Srobert 418*d415bd75Srobertissue_subscriber_parser = subparsers.add_parser('issue-subscriber') 419*d415bd75Srobertissue_subscriber_parser.add_argument('--label-name', type=str, required=True) 420*d415bd75Srobertissue_subscriber_parser.add_argument('--issue-number', type=int, required=True) 421*d415bd75Srobert 422*d415bd75Srobertrelease_workflow_parser = subparsers.add_parser('release-workflow') 423*d415bd75Srobertrelease_workflow_parser.add_argument('--llvm-project-dir', type=str, default='.', help='directory containing the llvm-project checout') 424*d415bd75Srobertrelease_workflow_parser.add_argument('--issue-number', type=int, required=True, help='The issue number to update') 425*d415bd75Srobertrelease_workflow_parser.add_argument('--phab-token', type=str, help='Phabricator conduit API token. See https://reviews.llvm.org/settings/user/<USER>/page/apitokens/') 426*d415bd75Srobertrelease_workflow_parser.add_argument('--branch-repo-token', type=str, 427*d415bd75Srobert help='GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.') 428*d415bd75Srobertrelease_workflow_parser.add_argument('--branch-repo', type=str, default='llvm/llvm-project-release-prs', 429*d415bd75Srobert help='The name of the repo where new branches will be pushed (e.g. llvm/llvm-project)') 430*d415bd75Srobertrelease_workflow_parser.add_argument('sub_command', type=str, choices=['print-release-branch', 'auto'], 431*d415bd75Srobert help='Print to stdout the name of the release branch ISSUE_NUMBER should be backported to') 432*d415bd75Srobert 433*d415bd75Srobertllvmbot_git_config_parser = subparsers.add_parser('setup-llvmbot-git', help='Set the default user and email for the git repo in LLVM_PROJECT_DIR to llvmbot') 434*d415bd75Srobert 435*d415bd75Srobertargs = parser.parse_args() 436*d415bd75Srobert 437*d415bd75Srobertif args.command == 'issue-subscriber': 438*d415bd75Srobert issue_subscriber = IssueSubscriber(args.token, args.repo, args.issue_number, args.label_name) 439*d415bd75Srobert issue_subscriber.run() 440*d415bd75Srobertelif args.command == 'release-workflow': 441*d415bd75Srobert release_workflow = ReleaseWorkflow(args.token, args.repo, args.issue_number, 442*d415bd75Srobert args.branch_repo, args.branch_repo_token, 443*d415bd75Srobert args.llvm_project_dir, args.phab_token) 444*d415bd75Srobert if not release_workflow.release_branch_for_issue: 445*d415bd75Srobert release_workflow.issue_notify_no_milestone(sys.stdin.readlines()) 446*d415bd75Srobert sys.exit(1) 447*d415bd75Srobert if args.sub_command == 'print-release-branch': 448*d415bd75Srobert release_workflow.print_release_branch() 449*d415bd75Srobert else: 450*d415bd75Srobert if not release_workflow.execute_command(): 451*d415bd75Srobert sys.exit(1) 452*d415bd75Srobertelif args.command == 'setup-llvmbot-git': 453*d415bd75Srobert setup_llvmbot_git() 454