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