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