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