1a2adebf4STom Stellard#!/usr/bin/env python3 2a2adebf4STom Stellard# 3a2adebf4STom Stellard# ======- github-automation - LLVM GitHub Automation Routines--*- python -*--==# 4a2adebf4STom Stellard# 5a2adebf4STom Stellard# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6a2adebf4STom Stellard# See https://llvm.org/LICENSE.txt for license information. 7a2adebf4STom Stellard# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8a2adebf4STom Stellard# 9a2adebf4STom Stellard# ==-------------------------------------------------------------------------==# 10a2adebf4STom Stellard 11a2adebf4STom Stellardimport argparse 12daf82a51STom Stellardfrom git import Repo # type: ignore 13a2adebf4STom Stellardimport github 14a2adebf4STom Stellardimport os 15daf82a51STom Stellardimport re 16daf82a51STom Stellardimport sys 17daf82a51STom Stellardfrom typing import * 18a2adebf4STom Stellard 19a2adebf4STom Stellardclass IssueSubscriber: 20a2adebf4STom Stellard 21a2adebf4STom Stellard @property 22a2adebf4STom Stellard def team_name(self) -> str: 23a2adebf4STom Stellard return self._team_name 24a2adebf4STom Stellard 25a2adebf4STom Stellard def __init__(self, token:str, repo:str, issue_number:int, label_name:str): 26a2adebf4STom Stellard self.repo = github.Github(token).get_repo(repo) 27a2adebf4STom Stellard self.org = github.Github(token).get_organization(self.repo.organization.login) 28a2adebf4STom Stellard self.issue = self.repo.get_issue(issue_number) 29a2adebf4STom Stellard self._team_name = 'issue-subscribers-{}'.format(label_name).lower() 30a2adebf4STom Stellard 31a2adebf4STom Stellard def run(self) -> bool: 32a2adebf4STom Stellard for team in self.org.get_teams(): 33a2adebf4STom Stellard if self.team_name != team.name.lower(): 34a2adebf4STom Stellard continue 35a2adebf4STom Stellard comment = '@llvm/{}'.format(team.slug) 36a2adebf4STom Stellard self.issue.create_comment(comment) 37a2adebf4STom Stellard return True 38a2adebf4STom Stellard return False 39a2adebf4STom Stellard 40daf82a51STom Stellarddef setup_llvmbot_git(git_dir = '.'): 41daf82a51STom Stellard """ 42daf82a51STom Stellard Configure the git repo in `git_dir` with the llvmbot account so 43daf82a51STom Stellard commits are attributed to llvmbot. 44daf82a51STom Stellard """ 45daf82a51STom Stellard repo = Repo(git_dir) 46daf82a51STom Stellard with repo.config_writer() as config: 47daf82a51STom Stellard config.set_value('user', 'name', 'llvmbot') 48daf82a51STom Stellard config.set_value('user', 'email', '[email protected]') 49daf82a51STom Stellard 50daf82a51STom Stellardclass ReleaseWorkflow: 51daf82a51STom Stellard 52*3929f913STom Stellard CHERRY_PICK_FAILED_LABEL = 'release:cherry-pick-failed' 53*3929f913STom Stellard 54daf82a51STom Stellard """ 55daf82a51STom Stellard This class implements the sub-commands for the release-workflow command. 56daf82a51STom Stellard The current sub-commands are: 57daf82a51STom Stellard * create-branch 58daf82a51STom Stellard * create-pull-request 59daf82a51STom Stellard 60daf82a51STom Stellard The execute_command method will automatically choose the correct sub-command 61daf82a51STom Stellard based on the text in stdin. 62daf82a51STom Stellard """ 63daf82a51STom Stellard 64daf82a51STom Stellard def __init__(self, token:str, repo:str, issue_number:int, 65daf82a51STom Stellard branch_repo_name:str, branch_repo_token:str, 66daf82a51STom Stellard llvm_project_dir:str) -> None: 67daf82a51STom Stellard self._token = token 68daf82a51STom Stellard self._repo_name = repo 69daf82a51STom Stellard self._issue_number = issue_number 70daf82a51STom Stellard self._branch_repo_name = branch_repo_name 71daf82a51STom Stellard if branch_repo_token: 72daf82a51STom Stellard self._branch_repo_token = branch_repo_token 73daf82a51STom Stellard else: 74daf82a51STom Stellard self._branch_repo_token = self.token 75daf82a51STom Stellard self._llvm_project_dir = llvm_project_dir 76daf82a51STom Stellard 77daf82a51STom Stellard @property 78daf82a51STom Stellard def token(self) -> str: 79daf82a51STom Stellard return self._token 80daf82a51STom Stellard 81daf82a51STom Stellard @property 82daf82a51STom Stellard def repo_name(self) -> str: 83daf82a51STom Stellard return self._repo_name 84daf82a51STom Stellard 85daf82a51STom Stellard @property 86daf82a51STom Stellard def issue_number(self) -> int: 87daf82a51STom Stellard return self._issue_number 88daf82a51STom Stellard 89daf82a51STom Stellard @property 90daf82a51STom Stellard def branch_repo_name(self) -> str: 91daf82a51STom Stellard return self._branch_repo_name 92daf82a51STom Stellard 93daf82a51STom Stellard @property 94daf82a51STom Stellard def branch_repo_token(self) -> str: 95daf82a51STom Stellard return self._branch_repo_token 96daf82a51STom Stellard 97daf82a51STom Stellard @property 98daf82a51STom Stellard def llvm_project_dir(self) -> str: 99daf82a51STom Stellard return self._llvm_project_dir 100daf82a51STom Stellard 101daf82a51STom Stellard @property 102daf82a51STom Stellard def __repo(self) -> github.Repository.Repository: 103daf82a51STom Stellard return github.Github(self.token).get_repo(self.repo_name) 104daf82a51STom Stellard 105daf82a51STom Stellard @property 106daf82a51STom Stellard def issue(self) -> github.Issue.Issue: 107daf82a51STom Stellard return self.__repo.get_issue(self.issue_number) 108daf82a51STom Stellard 109daf82a51STom Stellard @property 110daf82a51STom Stellard def push_url(self) -> str: 111daf82a51STom Stellard return 'https://{}@github.com/{}'.format(self.branch_repo_token, self.branch_repo_name) 112daf82a51STom Stellard 113daf82a51STom Stellard @property 114daf82a51STom Stellard def branch_name(self) -> str: 115daf82a51STom Stellard return 'issue{}'.format(self.issue_number) 116daf82a51STom Stellard 117daf82a51STom Stellard @property 118daf82a51STom Stellard def release_branch_for_issue(self) -> Optional[str]: 119daf82a51STom Stellard issue = self.issue 120daf82a51STom Stellard milestone = issue.milestone 121daf82a51STom Stellard if milestone is None: 122daf82a51STom Stellard return None 123daf82a51STom Stellard m = re.search('branch: (.+)',milestone.description) 124daf82a51STom Stellard if m: 125daf82a51STom Stellard return m.group(1) 126daf82a51STom Stellard return None 127daf82a51STom Stellard 128daf82a51STom Stellard def print_release_branch(self) -> None: 129daf82a51STom Stellard print(self.release_branch_for_issue) 130daf82a51STom Stellard 131daf82a51STom Stellard def issue_notify_branch(self) -> None: 132daf82a51STom Stellard self.issue.create_comment('/branch {}/{}'.format(self.branch_repo_name, self.branch_name)) 133daf82a51STom Stellard 134daf82a51STom Stellard def issue_notify_pull_request(self, pull:github.PullRequest.PullRequest) -> None: 135daf82a51STom Stellard self.issue.create_comment('/pull-request {}#{}'.format(self.branch_repo_name, pull.number)) 136daf82a51STom Stellard 137daf82a51STom Stellard @property 138daf82a51STom Stellard def action_url(self) -> str: 139daf82a51STom Stellard if os.getenv('CI'): 140daf82a51STom Stellard return 'https://github.com/{}/actions/runs/{}'.format(os.getenv('GITHUB_REPOSITORY'), os.getenv('GITHUB_RUN_ID')) 141daf82a51STom Stellard return "" 142daf82a51STom Stellard 143daf82a51STom Stellard def issue_notify_cherry_pick_failure(self, commit:str) -> github.IssueComment.IssueComment: 144daf82a51STom Stellard message = "<!--IGNORE-->\nFailed to cherry-pick: {}\n\n".format(commit) 145daf82a51STom Stellard action_url = self.action_url 146daf82a51STom Stellard if action_url: 147daf82a51STom Stellard message += action_url + "\n\n" 148daf82a51STom Stellard message += "Please manually backport the fix and push it to your github fork. Once this is done, please add a comment like this:\n\n`/branch <user>/<repo>/<branch>`" 149daf82a51STom Stellard issue = self.issue 150daf82a51STom Stellard comment = issue.create_comment(message) 151*3929f913STom Stellard issue.add_to_labels(self.CHERRY_PICK_FAILED_LABEL) 152daf82a51STom Stellard return comment 153daf82a51STom Stellard 154daf82a51STom Stellard def issue_notify_pull_request_failure(self, branch:str) -> github.IssueComment.IssueComment: 155daf82a51STom Stellard message = "Failed to create pull request for {} ".format(branch) 156daf82a51STom Stellard message += self.action_url 157daf82a51STom Stellard return self.issue.create_comment(message) 158daf82a51STom Stellard 159*3929f913STom Stellard def issue_remove_cherry_pick_failed_label(self): 160*3929f913STom Stellard if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]: 161*3929f913STom Stellard self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL) 162daf82a51STom Stellard 163daf82a51STom Stellard def create_branch(self, commits:List[str]) -> bool: 164daf82a51STom Stellard """ 165daf82a51STom Stellard This function attempts to backport `commits` into the branch associated 166daf82a51STom Stellard with `self.issue_number`. 167daf82a51STom Stellard 168daf82a51STom Stellard If this is successful, then the branch is pushed to `self.branch_repo_name`, if not, 169daf82a51STom Stellard a comment is added to the issue saying that the cherry-pick failed. 170daf82a51STom Stellard 171daf82a51STom Stellard :param list commits: List of commits to cherry-pick. 172daf82a51STom Stellard 173daf82a51STom Stellard """ 174daf82a51STom Stellard print('cherry-picking', commits) 175daf82a51STom Stellard branch_name = self.branch_name 176daf82a51STom Stellard local_repo = Repo(self.llvm_project_dir) 177daf82a51STom Stellard local_repo.git.checkout(self.release_branch_for_issue) 178daf82a51STom Stellard 179daf82a51STom Stellard for c in commits: 180daf82a51STom Stellard try: 181daf82a51STom Stellard local_repo.git.cherry_pick('-x', c) 182daf82a51STom Stellard except Exception as e: 183daf82a51STom Stellard self.issue_notify_cherry_pick_failure(c) 184daf82a51STom Stellard raise e 185daf82a51STom Stellard 186daf82a51STom Stellard push_url = self.push_url 187daf82a51STom Stellard print('Pushing to {} {}'.format(push_url, branch_name)) 188daf82a51STom Stellard local_repo.git.push(push_url, 'HEAD:{}'.format(branch_name)) 189daf82a51STom Stellard 190daf82a51STom Stellard self.issue_notify_branch() 191*3929f913STom Stellard self.issue_remove_cherry_pick_failed_label() 192daf82a51STom Stellard return True 193daf82a51STom Stellard 194daf82a51STom Stellard 195daf82a51STom Stellard def create_pull_request(self, owner:str, branch:str) -> bool: 196daf82a51STom Stellard """ 197daf82a51STom Stellard reate a pull request in `self.branch_repo_name`. The base branch of the 198daf82a51STom Stellard pull request will be choosen based on the the milestone attached to 199daf82a51STom Stellard the issue represented by `self.issue_number` For example if the milestone 200daf82a51STom Stellard is Release 13.0.1, then the base branch will be release/13.x. `branch` 201daf82a51STom Stellard will be used as the compare branch. 202daf82a51STom Stellard https://docs.github.com/en/get-started/quickstart/github-glossary#base-branch 203daf82a51STom Stellard https://docs.github.com/en/get-started/quickstart/github-glossary#compare-branch 204daf82a51STom Stellard """ 205daf82a51STom Stellard repo = github.Github(self.token).get_repo(self.branch_repo_name) 206daf82a51STom Stellard issue_ref = '{}#{}'.format(self.repo_name, self.issue_number) 207daf82a51STom Stellard pull = None 208daf82a51STom Stellard release_branch_for_issue = self.release_branch_for_issue 209daf82a51STom Stellard if release_branch_for_issue is None: 210daf82a51STom Stellard return False 211daf82a51STom Stellard try: 212daf82a51STom Stellard pull = repo.create_pull(title='PR for {}'.format(issue_ref), 213daf82a51STom Stellard body='resolves {}'.format(issue_ref), 214daf82a51STom Stellard base=release_branch_for_issue, 215daf82a51STom Stellard head='{}:{}'.format(owner, branch), 216daf82a51STom Stellard maintainer_can_modify=False) 217daf82a51STom Stellard except Exception as e: 218daf82a51STom Stellard self.issue_notify_pull_request_failure(branch) 219daf82a51STom Stellard raise e 220daf82a51STom Stellard 221daf82a51STom Stellard if pull is None: 222daf82a51STom Stellard return False 223daf82a51STom Stellard 224daf82a51STom Stellard self.issue_notify_pull_request(pull) 225*3929f913STom Stellard self.issue_remove_cherry_pick_failed_label() 226daf82a51STom Stellard 227daf82a51STom Stellard # TODO(tstellar): Do you really want to always return True? 228daf82a51STom Stellard return True 229daf82a51STom Stellard 230daf82a51STom Stellard 231daf82a51STom Stellard def execute_command(self) -> bool: 232daf82a51STom Stellard """ 233daf82a51STom Stellard This function reads lines from STDIN and executes the first command 234daf82a51STom Stellard that it finds. The 2 supported commands are: 235daf82a51STom Stellard /cherry-pick commit0 <commit1> <commit2> <...> 236daf82a51STom Stellard /branch <owner>/<repo>/<branch> 237daf82a51STom Stellard """ 238daf82a51STom Stellard for line in sys.stdin: 239daf82a51STom Stellard line.rstrip() 240daf82a51STom Stellard m = re.search("/([a-z-]+)\s(.+)", line) 241daf82a51STom Stellard if not m: 242daf82a51STom Stellard continue 243daf82a51STom Stellard command = m.group(1) 244daf82a51STom Stellard args = m.group(2) 245daf82a51STom Stellard 246daf82a51STom Stellard if command == 'cherry-pick': 247daf82a51STom Stellard return self.create_branch(args.split()) 248daf82a51STom Stellard 249daf82a51STom Stellard if command == 'branch': 250daf82a51STom Stellard m = re.match('([^/]+)/([^/]+)/(.+)', args) 251daf82a51STom Stellard if m: 252daf82a51STom Stellard owner = m.group(1) 253daf82a51STom Stellard branch = m.group(3) 254daf82a51STom Stellard return self.create_pull_request(owner, branch) 255daf82a51STom Stellard 256daf82a51STom Stellard print("Do not understand input:") 257daf82a51STom Stellard print(sys.stdin.readlines()) 258daf82a51STom Stellard return False 259a2adebf4STom Stellard 260a2adebf4STom Stellardparser = argparse.ArgumentParser() 261daf82a51STom Stellardparser.add_argument('--token', type=str, required=True, help='GitHub authentiation token') 262daf82a51STom Stellardparser.add_argument('--repo', type=str, default=os.getenv('GITHUB_REPOSITORY', 'llvm/llvm-project'), 263daf82a51STom Stellard help='The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)') 264a2adebf4STom Stellardsubparsers = parser.add_subparsers(dest='command') 265a2adebf4STom Stellard 266a2adebf4STom Stellardissue_subscriber_parser = subparsers.add_parser('issue-subscriber') 267a2adebf4STom Stellardissue_subscriber_parser.add_argument('--label-name', type=str, required=True) 268a2adebf4STom Stellardissue_subscriber_parser.add_argument('--issue-number', type=int, required=True) 269a2adebf4STom Stellard 270daf82a51STom Stellardrelease_workflow_parser = subparsers.add_parser('release-workflow') 271daf82a51STom Stellardrelease_workflow_parser.add_argument('--llvm-project-dir', type=str, default='.', help='directory containing the llvm-project checout') 272daf82a51STom Stellardrelease_workflow_parser.add_argument('--issue-number', type=int, required=True, help='The issue number to update') 273daf82a51STom Stellardrelease_workflow_parser.add_argument('--branch-repo-token', type=str, 274daf82a51STom Stellard help='GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.') 275daf82a51STom Stellardrelease_workflow_parser.add_argument('--branch-repo', type=str, default='llvmbot/llvm-project', 276daf82a51STom Stellard help='The name of the repo where new branches will be pushed (e.g. llvm/llvm-project)') 277daf82a51STom Stellardrelease_workflow_parser.add_argument('sub_command', type=str, choices=['print-release-branch', 'auto'], 278daf82a51STom Stellard help='Print to stdout the name of the release branch ISSUE_NUMBER should be backported to') 279daf82a51STom Stellard 280daf82a51STom Stellardllvmbot_git_config_parser = subparsers.add_parser('setup-llvmbot-git', help='Set the default user and email for the git repo in LLVM_PROJECT_DIR to llvmbot') 281daf82a51STom Stellard 282a2adebf4STom Stellardargs = parser.parse_args() 283a2adebf4STom Stellard 284a2adebf4STom Stellardif args.command == 'issue-subscriber': 285a2adebf4STom Stellard issue_subscriber = IssueSubscriber(args.token, args.repo, args.issue_number, args.label_name) 286a2adebf4STom Stellard issue_subscriber.run() 287daf82a51STom Stellardelif args.command == 'release-workflow': 288daf82a51STom Stellard release_workflow = ReleaseWorkflow(args.token, args.repo, args.issue_number, 289daf82a51STom Stellard args.branch_repo, args.branch_repo_token, 290daf82a51STom Stellard args.llvm_project_dir) 291daf82a51STom Stellard if args.sub_command == 'print-release-branch': 292daf82a51STom Stellard release_workflow.print_release_branch() 293daf82a51STom Stellard else: 294daf82a51STom Stellard if not release_workflow.execute_command(): 295daf82a51STom Stellard sys.exit(1) 296daf82a51STom Stellardelif args.command == 'setup-llvmbot-git': 297daf82a51STom Stellard setup_llvmbot_git() 298