brintos

brintos / llvm-project-archived public Read only

0
0
Text · 35.1 KiB · 6e4417e Raw
945 lines · python
1#!/usr/bin/env python32#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-exception8#9# ==-------------------------------------------------------------------------==#10 11import argparse12from git import Repo  # type: ignore13import html14import json15import github16import os17import re18import requests19import sys20import textwrap21import time22from typing import List, Optional23 24beginner_comment = """25Hi!26 27This 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:28 291. Check that no other contributor is working on this issue. If someone is assigned to the issue or claimed to be working on it, ping the person. After one week without a response, the assignee may be changed.301. Leave a comment indicating that you are working on the issue, 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.311. Fix the issue locally.321. [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.331. Create a Git commit.341. Run [`git clang-format HEAD~1`](https://clang.llvm.org/docs/ClangFormat.html#git-integration) to format your changes.351. 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.36 37If you have any further questions about this issue, don't hesitate to ask via a comment in the thread below.38"""39 40 41def _get_current_team(team_name, teams) -> Optional[github.Team.Team]:42    for team in teams:43        if team_name == team.name.lower():44            return team45    return None46 47 48def escape_description(str):49    # If the description of an issue/pull request is empty, the Github API50    # library returns None instead of an empty string. Handle this here to51    # avoid failures from trying to manipulate None.52    if str is None:53        return ""54    # https://github.com/github/markup/issues/1168#issuecomment-49494616855    str = html.escape(str, False)56    # '@' followed by alphanum is a user name57    str = re.sub("@(?=\w)", "@<!-- -->", str)58    # '#' followed by digits is considered an issue number59    str = re.sub("#(?=\d)", "#<!-- -->", str)60    return str61 62 63class IssueSubscriber:64    @property65    def team_name(self) -> str:66        return self._team_name67 68    def __init__(self, token: str, repo: str, issue_number: int, label_name: str):69        self.repo = github.Github(token).get_repo(repo)70        self.org = github.Github(token).get_organization(self.repo.organization.login)71        self.issue = self.repo.get_issue(issue_number)72        self._team_name = "issue-subscribers-{}".format(label_name).lower()73 74    def run(self) -> bool:75        team = _get_current_team(self.team_name, self.org.get_teams())76        if not team:77            print(f"couldn't find team named {self.team_name}")78            return False79 80        comment = ""81        if team.slug == "issue-subscribers-good-first-issue":82            comment = "{}\n".format(beginner_comment)83            self.issue.create_comment(comment)84 85        body = escape_description(self.issue.body)86        comment = f"""87@llvm/{team.slug}88 89Author: {self.issue.user.name} ({self.issue.user.login})90 91<details>92{body}93</details>94"""95 96        self.issue.create_comment(comment)97        return True98 99 100def human_readable_size(size, decimal_places=2):101    for unit in ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]:102        if size < 1024.0 or unit == "PiB":103            break104        size /= 1024.0105    return f"{size:.{decimal_places}f} {unit}"106 107 108class PRSubscriber:109    @property110    def team_name(self) -> str:111        return self._team_name112 113    def __init__(self, token: str, repo: str, pr_number: int, label_name: str):114        self.repo = github.Github(token).get_repo(repo)115        self.org = github.Github(token).get_organization(self.repo.organization.login)116        self.pr = self.repo.get_issue(pr_number).as_pull_request()117        self._team_name = "pr-subscribers-{}".format(118            label_name.replace("+", "x")119        ).lower()120        self.COMMENT_TAG = "<!--LLVM PR SUMMARY COMMENT-->\n"121 122    def get_summary_comment(self) -> github.IssueComment.IssueComment:123        for comment in self.pr.as_issue().get_comments():124            if self.COMMENT_TAG in comment.body:125                return comment126        return None127 128    def run(self) -> bool:129        patch = None130        team = _get_current_team(self.team_name, self.org.get_teams())131        if not team:132            print(f"couldn't find team named {self.team_name}")133            return False134 135        # GitHub limits comments to 65,536 characters, let's limit the diff136        # and the file list to 20kB each.137        STAT_LIMIT = 20 * 1024138        DIFF_LIMIT = 20 * 1024139 140        # Get statistics for each file141        diff_stats = f"{self.pr.changed_files} Files Affected:\n\n"142        for file in self.pr.get_files():143            diff_stats += f"- ({file.status}) {file.filename} ("144            if file.additions:145                diff_stats += f"+{file.additions}"146            if file.deletions:147                diff_stats += f"-{file.deletions}"148            diff_stats += ") "149            if file.status == "renamed":150                print(f"(from {file.previous_filename})")151            diff_stats += "\n"152            if len(diff_stats) > STAT_LIMIT:153                break154 155        # Get the diff156        try:157            patch = requests.get(self.pr.diff_url).text158        except:159            patch = ""160 161        patch_link = f"Full diff: {self.pr.diff_url}\n"162        if len(patch) > DIFF_LIMIT:163            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"164            patch = patch[0:DIFF_LIMIT] + "...\n[truncated]\n"165        team_mention = "@llvm/{}".format(team.slug)166 167        body = escape_description(self.pr.body)168        # Note: the comment is in markdown and the code below169        # is sensible to line break170        comment = f"""171{self.COMMENT_TAG}172{team_mention}173 174Author: {self.pr.user.name} ({self.pr.user.login})175 176<details>177<summary>Changes</summary>178 179{body}180 181---182{patch_link}183 184{diff_stats}185 186``````````diff187{patch}188``````````189 190</details>191"""192 193        summary_comment = self.get_summary_comment()194        if not summary_comment:195            self.pr.as_issue().create_comment(comment)196        elif team_mention + "\n" in summary_comment.body:197            print("Team {} already mentioned.".format(team.slug))198        else:199            summary_comment.edit(200                summary_comment.body.replace(201                    self.COMMENT_TAG, self.COMMENT_TAG + team_mention + "\n"202                )203            )204        return True205 206    def _get_current_team(self) -> Optional[github.Team.Team]:207        for team in self.org.get_teams():208            if self.team_name == team.name.lower():209                return team210        return None211 212 213def get_top_values(values: dict, top: int = 3) -> list:214    return [v for v in sorted(values.items(), key=lambda x: x[1], reverse=True)][:top]215 216 217def get_user_values_str(values: list) -> str:218    return ", ".join([f"@{v[0]} ({v[1]})" for v in values])219 220 221class PRGreeter:222    COMMENT_TAG = "<!--LLVM NEW CONTRIBUTOR COMMENT-->\n"223 224    def __init__(self, token: str, repo: str, pr_number: int):225        repo = github.Github(token).get_repo(repo)226        self.pr = repo.get_issue(pr_number).as_pull_request()227 228    def run(self) -> bool:229        # We assume that this is only called for a PR that has just been opened230        # by a user new to LLVM and/or GitHub itself.231 232        # This text is using Markdown formatting.233 234        comment = f"""\235{PRGreeter.COMMENT_TAG}236Thank you for submitting a Pull Request (PR) to the LLVM Project!237 238This PR will be automatically labeled and the relevant teams will be notified.239 240If you wish to, you can add reviewers by using the "Reviewers" section on this page.241 242If 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.243 244If 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.245 246If you have further questions, they may be answered by the [LLVM GitHub User Guide](https://llvm.org/docs/GitHub.html).247 248You 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/)."""249        self.pr.as_issue().create_comment(comment)250        return True251 252 253class CommitRequestGreeter:254    def __init__(self, token: str, repo: str, issue_number: int):255        self.repo = github.Github(token).get_repo(repo)256        self.issue = self.repo.get_issue(issue_number)257 258    def run(self) -> bool:259        # Post greeter comment:260        comment = textwrap.dedent(261            f"""262            @{self.issue.user.login} thank you for applying for commit access.  Please  review the project's [code review policy](https://llvm.org/docs/CodeReview.html).263        """264        )265        self.issue.create_comment(comment)266 267        # Post activity summary:268        total_prs = 0269        merged_prs = 0270        merged_by = {}271        reviewed_by = {}272        for i in self.repo.get_issues(creator=self.issue.user.login, state="all"):273            print("Looking at issue:", i)274            issue_reviewed_by = set()275            try:276                pr = i.as_pull_request()277                total_prs += 1278                for c in pr.get_review_comments():279                    if c.user.login == self.issue.user.login:280                        continue281                    issue_reviewed_by.add(c.user.login)282                for r in issue_reviewed_by:283                    if r not in reviewed_by:284                        reviewed_by[r] = 1285                    else:286                        reviewed_by[r] += 1287                if pr.is_merged():288                    merged_prs += 1289                    merger = pr.merged_by.login290                    if merger not in merged_by:291                        merged_by[merger] = 1292                    else:293                        merged_by[merger] += 1294                    continue295 296            except github.GithubException as e:297                print(e)298                continue299 300        total_prs_url = f"https://github.com/llvm/llvm-project/pulls?q=author%3A{self.issue.user.login}+is%3Apr"301        merged_prs_url = total_prs_url + "+is%3Amerged"302        comment = f"""303            ### Activity Summary:304            * [{total_prs} Pull Requests]({total_prs_url})305            * [{merged_prs} Merged Pull Requests]({merged_prs_url})306            * Top 3 Committers: {get_user_values_str(get_top_values(merged_by))}307            * Top 3 Reviewers: {get_user_values_str(get_top_values(reviewed_by))}308 309            Reviewers should clearly state their reasoning for accepting or rejecting this request, and finish with a clear statement such as \"I approve of this request\", \"LGTM\", or \"I do not approve of this request\". Please review the instructions for [obtaining commit access](https://llvm.org/docs/DeveloperPolicy.html#obtaining-commit-access).310        """311        self.issue.create_comment(textwrap.dedent(comment))312 313 314class PRBuildbotInformation:315    COMMENT_TAG = "<!--LLVM BUILDBOT INFORMATION COMMENT-->\n"316 317    def __init__(self, token: str, repo: str, pr_number: int, author: str):318        repo = github.Github(token).get_repo(repo)319        self.pr = repo.get_issue(pr_number).as_pull_request()320        self.author = author321 322    def should_comment(self) -> bool:323        # As soon as a new contributor has a PR merged, they are no longer a new contributor.324        # We can tell that they were a new contributor previously because we would have325        # added a new contributor greeting comment when they opened the PR.326        found_greeting = False327        for comment in self.pr.as_issue().get_comments():328            if PRGreeter.COMMENT_TAG in comment.body:329                found_greeting = True330            elif PRBuildbotInformation.COMMENT_TAG in comment.body:331                # When an issue is reopened, then closed as merged again, we should not332                # add a second comment. This event will be rare in practice as it seems333                # like it's only possible when the main branch is still at the exact334                # revision that the PR was merged on to, beyond that it's closed forever.335                return False336        return found_greeting337 338    def run(self) -> bool:339        if not self.should_comment():340            return341 342        # This text is using Markdown formatting. Some of the lines are longer343        # than others so that the final text is some reasonable looking paragraphs344        # after the long URLs are rendered.345        comment = f"""\346{PRBuildbotInformation.COMMENT_TAG}347@{self.author} Congratulations on having your first Pull Request (PR) merged into the LLVM Project!348 349Your 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.350 351Please 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.352 353How 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).354 355If 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.356 357If you don't get any reports, no action is required from you. Your changes are working as expected, well done!358"""359        self.pr.as_issue().create_comment(comment)360        return True361 362 363def setup_llvmbot_git(git_dir="."):364    """365    Configure the git repo in `git_dir` with the llvmbot account so366    commits are attributed to llvmbot.367    """368    repo = Repo(git_dir)369    with repo.config_writer() as config:370        config.set_value("user", "name", "llvmbot")371        config.set_value("user", "email", "llvmbot@llvm.org")372 373 374def extract_commit_hash(arg: str):375    """376    Extract the commit hash from the argument passed to /action github377    comment actions. We currently only support passing the commit hash378    directly or use the github URL, such as379    https://github.com/llvm/llvm-project/commit/2832d7941f4207f1fcf813b27cf08cecc3086959380    """381    github_prefix = "https://github.com/llvm/llvm-project/commit/"382    if arg.startswith(github_prefix):383        return arg[len(github_prefix) :]384    return arg385 386 387class ReleaseWorkflow:388    CHERRY_PICK_FAILED_LABEL = "release:cherry-pick-failed"389 390    """391    This class implements the sub-commands for the release-workflow command.392    The current sub-commands are:393        * create-branch394        * create-pull-request395 396    The execute_command method will automatically choose the correct sub-command397    based on the text in stdin.398    """399 400    def __init__(401        self,402        token: str,403        repo: str,404        issue_number: int,405        branch_repo_name: str,406        branch_repo_token: str,407        llvm_project_dir: str,408        requested_by: str,409    ) -> None:410        self._token = token411        self._repo_name = repo412        self._issue_number = issue_number413        self._branch_repo_name = branch_repo_name414        if branch_repo_token:415            self._branch_repo_token = branch_repo_token416        else:417            self._branch_repo_token = self.token418        self._llvm_project_dir = llvm_project_dir419        self._requested_by = requested_by420 421    @property422    def token(self) -> str:423        return self._token424 425    @property426    def repo_name(self) -> str:427        return self._repo_name428 429    @property430    def issue_number(self) -> int:431        return self._issue_number432 433    @property434    def branch_repo_owner(self) -> str:435        return self.branch_repo_name.split("/")[0]436 437    @property438    def branch_repo_name(self) -> str:439        return self._branch_repo_name440 441    @property442    def branch_repo_token(self) -> str:443        return self._branch_repo_token444 445    @property446    def llvm_project_dir(self) -> str:447        return self._llvm_project_dir448 449    @property450    def requested_by(self) -> str:451        return self._requested_by452 453    @property454    def repo(self) -> github.Repository.Repository:455        return github.Github(self.token).get_repo(self.repo_name)456 457    @property458    def issue(self) -> github.Issue.Issue:459        return self.repo.get_issue(self.issue_number)460 461    @property462    def push_url(self) -> str:463        return "https://{}@github.com/{}".format(464            self.branch_repo_token, self.branch_repo_name465        )466 467    @property468    def branch_name(self) -> str:469        return "issue{}".format(self.issue_number)470 471    @property472    def release_branch_for_issue(self) -> Optional[str]:473        issue = self.issue474        milestone = issue.milestone475        if milestone is None:476            return None477        m = re.search("branch: (.+)", milestone.description)478        if m:479            return m.group(1)480        return None481 482    def update_issue_project_status(self) -> None:483        """484        A common workflow is to merge a PR and then use the /cherry-pick485        command in a pull request comment to backport the change to the486        release branch.  In this case, once the new PR for the backport has487        been created, we want to mark the project status for the original PR488        as Done.  This is becuase we can track progress now on the new PR489        which is targeting the release branch.490 491        """492 493        pr = self.issue.as_pull_request()494        if not pr:495            return496 497        if pr.state != "closed":498            return499 500        gh = github.Github(login_or_token=self.token)501        query = """502            query($node_id: ID!) {503              node(id: $node_id) {504                ... on PullRequest {505                  url506                  projectItems(first:100){507                    nodes {508                      id509                      project {510                        id511                        number512                        field(name: "Status") {513                          ... on ProjectV2SingleSelectField {514                            id515                            name516                            options {517                              id518                              name519                            }520                          }521                        }522                      }523                    }524                  }525                }526              }527            }528        """529        variables = {"node_id": pr.node_id}530        res_header, res_data = gh._Github__requester.graphql_query(531            query=query, variables=variables532        )533        print(res_header)534 535        llvm_release_status_project_number = 3536        for item in res_data["data"]["node"]["projectItems"]["nodes"]:537            project = item["project"]538            if project["number"] != llvm_release_status_project_number:539                continue540            status_field = project["field"]541            for option in status_field["options"]:542                if option["name"] != "Done":543                    continue544                variables = {545                    "project": project["id"],546                    "item": item["id"],547                    "status_field": status_field["id"],548                    "status_value": option["id"],549                }550 551                query = """552                  mutation($project: ID!, $item: ID!, $status_field: ID!, $status_value: String!) {553                    set_status:554                    updateProjectV2ItemFieldValue(input: {555                      projectId: $project556                      itemId: $item557                      fieldId: $status_field558                      value: {559                        singleSelectOptionId: $status_value560                      }561                    }) {562                      projectV2Item {563                        id564                      }565                    }566                  }567                """568 569                res_header, res_data = gh._Github__requester.graphql_query(570                    query=query, variables=variables571                )572                print(res_header)573 574    def print_release_branch(self) -> None:575        print(self.release_branch_for_issue)576 577    def issue_notify_branch(self) -> None:578        self.issue.create_comment(579            "/branch {}/{}".format(self.branch_repo_name, self.branch_name)580        )581 582    def issue_notify_pull_request(self, pull: github.PullRequest.PullRequest) -> None:583        self.issue.create_comment(584            "/pull-request {}#{}".format(self.repo_name, pull.number)585        )586 587    def make_ignore_comment(self, comment: str) -> str:588        """589        Returns the comment string with a prefix that will cause590        a Github workflow to skip parsing this comment.591 592        :param str comment: The comment to ignore593        """594        return "<!--IGNORE-->\n" + comment595 596    def issue_notify_no_milestone(self, comment: List[str]) -> None:597        message = "{}\n\nError: Command failed due to missing milestone.".format(598            "".join([">" + line for line in comment])599        )600        self.issue.create_comment(self.make_ignore_comment(message))601 602    @property603    def action_url(self) -> str:604        if os.getenv("CI"):605            return "https://github.com/{}/actions/runs/{}".format(606                os.getenv("GITHUB_REPOSITORY"), os.getenv("GITHUB_RUN_ID")607            )608        return ""609 610    def issue_notify_cherry_pick_failure(611        self, commit: str612    ) -> github.IssueComment.IssueComment:613        message = self.make_ignore_comment(614            "Failed to cherry-pick: {}\n\n".format(commit)615        )616        action_url = self.action_url617        if action_url:618            message += action_url + "\n\n"619        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)"620        issue = self.issue621        comment = issue.create_comment(message)622        issue.add_to_labels(self.CHERRY_PICK_FAILED_LABEL)623        return comment624 625    def issue_notify_pull_request_failure(626        self, branch: str627    ) -> github.IssueComment.IssueComment:628        message = "Failed to create pull request for {} ".format(branch)629        message += self.action_url630        return self.issue.create_comment(message)631 632    def issue_remove_cherry_pick_failed_label(self):633        if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]:634            self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL)635 636    def get_main_commit(self, cherry_pick_sha: str) -> github.Commit.Commit:637        commit = self.repo.get_commit(cherry_pick_sha)638        message = commit.commit.message639        m = re.search("\(cherry picked from commit ([0-9a-f]+)\)", message)640        if not m:641            return None642        return self.repo.get_commit(m.group(1))643 644    def pr_request_review(self, pr: github.PullRequest.PullRequest):645        """646        This function will try to find the best reviewers for `commits` and647        then add a comment requesting review of the backport and add them as648        reviewers.649 650        The reviewers selected are those users who approved the pull request651        for the main branch.652        """653        reviewers = []654        for commit in pr.get_commits():655            main_commit = self.get_main_commit(commit.sha)656            if not main_commit:657                continue658            for pull in main_commit.get_pulls():659                for review in pull.get_reviews():660                    if review.state != "APPROVED":661                        continue662                reviewers.append(review.user.login)663        if len(reviewers):664            message = "{} What do you think about merging this PR to the release branch?".format(665                " ".join(["@" + r for r in reviewers])666            )667            pr.create_issue_comment(message)668            pr.create_review_request(reviewers)669 670    def create_branch(self, commits: List[str]) -> bool:671        """672        This function attempts to backport `commits` into the branch associated673        with `self.issue_number`.674 675        If this is successful, then the branch is pushed to `self.branch_repo_name`, if not,676        a comment is added to the issue saying that the cherry-pick failed.677 678        :param list commits: List of commits to cherry-pick.679 680        """681        print("cherry-picking", commits)682        branch_name = self.branch_name683        local_repo = Repo(self.llvm_project_dir)684        local_repo.git.checkout(self.release_branch_for_issue)685 686        for c in commits:687            try:688                local_repo.git.cherry_pick("-x", c)689            except Exception as e:690                self.issue_notify_cherry_pick_failure(c)691                raise e692 693        push_url = self.push_url694        print("Pushing to {} {}".format(push_url, branch_name))695        local_repo.git.push(push_url, "HEAD:{}".format(branch_name), force=True)696 697        self.issue_remove_cherry_pick_failed_label()698        return self.create_pull_request(699            self.branch_repo_owner, self.repo_name, branch_name, commits700        )701 702    def check_if_pull_request_exists(703        self, repo: github.Repository.Repository, head: str704    ) -> bool:705        pulls = repo.get_pulls(head=head)706        return pulls.totalCount != 0707 708    def create_pull_request(709        self, owner: str, repo_name: str, branch: str, commits: List[str]710    ) -> bool:711        """712        Create a pull request in `self.repo_name`.  The base branch of the713        pull request will be chosen based on the the milestone attached to714        the issue represented by `self.issue_number`  For example if the milestone715        is Release 13.0.1, then the base branch will be release/13.x. `branch`716        will be used as the compare branch.717        https://docs.github.com/en/get-started/quickstart/github-glossary#base-branch718        https://docs.github.com/en/get-started/quickstart/github-glossary#compare-branch719        """720        repo = github.Github(self.token).get_repo(self.repo_name)721        issue_ref = "{}#{}".format(self.repo_name, self.issue_number)722        pull = None723        release_branch_for_issue = self.release_branch_for_issue724        if release_branch_for_issue is None:725            return False726 727        head = f"{owner}:{branch}"728        if self.check_if_pull_request_exists(repo, head):729            print("PR already exists...")730            return True731        try:732            commit_message = repo.get_commit(commits[-1]).commit.message733            message_lines = commit_message.splitlines()734            title = "{}: {}".format(release_branch_for_issue, message_lines[0])735            body = "Backport {}\n\nRequested by: @{}".format(736                " ".join(commits), self.requested_by737            )738            pull = repo.create_pull(739                title=title,740                body=body,741                base=release_branch_for_issue,742                head=head,743                maintainer_can_modify=True,744            )745 746            pull.as_issue().edit(milestone=self.issue.milestone)747 748            # Once the pull request has been created, we can close the749            # issue that was used to request the cherry-pick750            self.issue.edit(state="closed", state_reason="completed")751 752            try:753                self.pr_request_review(pull)754            except Exception as e:755                print("error: Failed while searching for reviewers", e)756 757        except Exception as e:758            self.issue_notify_pull_request_failure(branch)759            raise e760 761        if pull is None:762            return False763 764        self.issue_notify_pull_request(pull)765        self.issue_remove_cherry_pick_failed_label()766        self.update_issue_project_status()767 768        # TODO(tstellar): Do you really want to always return True?769        return True770 771    def execute_command(self) -> bool:772        """773        This function reads lines from STDIN and executes the first command774        that it finds.  The supported command is:775        /cherry-pick< ><:> commit0 <commit1> <commit2> <...>776        """777        for line in sys.stdin:778            line.rstrip()779            m = re.search(r"/cherry-pick\s*:? *(.*)", line)780            if not m:781                continue782 783            args = m.group(1)784 785            arg_list = args.split()786            commits = list(map(lambda a: extract_commit_hash(a), arg_list))787            return self.create_branch(commits)788 789        print("Do not understand input:")790        print(sys.stdin.readlines())791        return False792 793 794def request_release_note(token: str, repo_name: str, pr_number: int):795    repo = github.Github(token).get_repo(repo_name)796    pr = repo.get_issue(pr_number).as_pull_request()797    submitter = pr.user.login798    if submitter == "llvmbot":799        m = re.search("Requested by: @(.+)$", pr.body)800        if not m:801            submitter = None802            print("Warning could not determine user who requested backport.")803        submitter = m.group(1)804 805    mention = ""806    if submitter:807        mention = f"@{submitter}"808 809    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. "810    try:811        pr.as_issue().create_comment(comment)812    except:813        # Failed to create comment so emit file instead814        with open("comments", "w") as file:815            data = [{"body": comment}]816            json.dump(data, file)817 818 819parser = argparse.ArgumentParser()820parser.add_argument(821    "--token", type=str, required=True, help="GitHub authentication token"822)823parser.add_argument(824    "--repo",825    type=str,826    default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"),827    help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)",828)829subparsers = parser.add_subparsers(dest="command")830 831issue_subscriber_parser = subparsers.add_parser("issue-subscriber")832issue_subscriber_parser.add_argument("--label-name", type=str, required=True)833issue_subscriber_parser.add_argument("--issue-number", type=int, required=True)834 835pr_subscriber_parser = subparsers.add_parser("pr-subscriber")836pr_subscriber_parser.add_argument("--label-name", type=str, required=True)837pr_subscriber_parser.add_argument("--issue-number", type=int, required=True)838 839pr_greeter_parser = subparsers.add_parser("pr-greeter")840pr_greeter_parser.add_argument("--issue-number", type=int, required=True)841 842commit_request_greeter = subparsers.add_parser("commit-request-greeter")843commit_request_greeter.add_argument("--issue-number", type=int, required=True)844 845pr_buildbot_information_parser = subparsers.add_parser("pr-buildbot-information")846pr_buildbot_information_parser.add_argument("--issue-number", type=int, required=True)847pr_buildbot_information_parser.add_argument("--author", type=str, required=True)848 849release_workflow_parser = subparsers.add_parser("release-workflow")850release_workflow_parser.add_argument(851    "--llvm-project-dir",852    type=str,853    default=".",854    help="directory containing the llvm-project checkout",855)856release_workflow_parser.add_argument(857    "--issue-number", type=int, required=True, help="The issue number to update"858)859release_workflow_parser.add_argument(860    "--branch-repo-token",861    type=str,862    help="GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.",863)864release_workflow_parser.add_argument(865    "--branch-repo",866    type=str,867    default="llvmbot/llvm-project",868    help="The name of the repo where new branches will be pushed (e.g. llvm/llvm-project)",869)870release_workflow_parser.add_argument(871    "sub_command",872    type=str,873    choices=["print-release-branch", "auto"],874    help="Print to stdout the name of the release branch ISSUE_NUMBER should be backported to",875)876 877llvmbot_git_config_parser = subparsers.add_parser(878    "setup-llvmbot-git",879    help="Set the default user and email for the git repo in LLVM_PROJECT_DIR to llvmbot",880)881release_workflow_parser.add_argument(882    "--requested-by",883    type=str,884    required=True,885    help="The user that requested this backport",886)887 888request_release_note_parser = subparsers.add_parser(889    "request-release-note",890    help="Request a release note for a pull request",891)892request_release_note_parser.add_argument(893    "--pr-number",894    type=int,895    required=True,896    help="The pull request to request the release note",897)898 899 900args = parser.parse_args()901 902if args.command == "issue-subscriber":903    issue_subscriber = IssueSubscriber(904        args.token, args.repo, args.issue_number, args.label_name905    )906    issue_subscriber.run()907elif args.command == "pr-subscriber":908    pr_subscriber = PRSubscriber(909        args.token, args.repo, args.issue_number, args.label_name910    )911    pr_subscriber.run()912elif args.command == "pr-greeter":913    pr_greeter = PRGreeter(args.token, args.repo, args.issue_number)914    pr_greeter.run()915elif args.command == "commit-request-greeter":916    commit_greeter = CommitRequestGreeter(args.token, args.repo, args.issue_number)917    commit_greeter.run()918elif args.command == "pr-buildbot-information":919    pr_buildbot_information = PRBuildbotInformation(920        args.token, args.repo, args.issue_number, args.author921    )922    pr_buildbot_information.run()923elif args.command == "release-workflow":924    release_workflow = ReleaseWorkflow(925        args.token,926        args.repo,927        args.issue_number,928        args.branch_repo,929        args.branch_repo_token,930        args.llvm_project_dir,931        args.requested_by,932    )933    if not release_workflow.release_branch_for_issue:934        release_workflow.issue_notify_no_milestone(sys.stdin.readlines())935        sys.exit(1)936    if args.sub_command == "print-release-branch":937        release_workflow.print_release_branch()938    else:939        if not release_workflow.execute_command():940            sys.exit(1)941elif args.command == "setup-llvmbot-git":942    setup_llvmbot_git()943elif args.command == "request-release-note":944    request_release_note(args.token, args.repo, args.pr_number)945