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