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