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