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