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