diff --git a/.github/workflows/issue-release-workflow.yml b/.github/workflows/issue-release-workflow.yml new file mode 100644 --- /dev/null +++ b/.github/workflows/issue-release-workflow.yml @@ -0,0 +1,58 @@ +name: Issue Release Workflow + +on: + issue_comment: + types: + - created + - edited + +jobs: + backport-commits: + name: Backport Commits + runs-on: ubuntu-latest + if: (github.repository == 'llvm/llvm-project') && startswith(github.event.comment.body, '/cherry-pick') + + steps: + - name: Fetch LLVM sources + uses: actions/checkout@v2 + with: + repository: llvm/llvm-project + fetch-depth: 0 + + - name: Setup Environment + run: | + pip install PyGithub GitPython + branch=`./llvm/utils/git/github-automation.py --token ${{ github.token }} release-workflow --issue-number ${{ github.event.issue.number }} print-release-branch` + git checkout $branch + ./llvm/utils/git/github-automation.py --token ${{ github.token }} setup-llvmbot-git + + - name: Backport Commits + run: | + printf "${{ github.event.comment.body }}" | + ./llvm/utils/git/github-automation.py \ + --token ${{ secrets.RELEASE_WORKFLOW_PUSH_SECRET }} \ + release-workflow \ + --issue-number ${{ github.event.issue.number }} \ + auto + +jobs: + create-pull-request: + name: Create Pull Request + runs-on: ubuntu-latest + if: if: (github.repository == 'llvm/llvm-project') && startswith(github.event.comment.body, '/branch') + + steps: + - name: Setup Environment + run: | + curl -O -L https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$GITHUB_SHA/llvm/utils/git/github-automation.py + chmod a+x github-automation.py + pip install PyGithub GitPython + + - name: Create Pull Request + run: | + printf "${{ github.event.comment.body }}" | + ./github-automation.py \ + --token ${{ secrets.RELEASE_WORKFLOW_PUSH_SECRET }} \ + release-workflow \ + --issue-number ${{ github.event.issue.number }} \ + auto diff --git a/llvm/utils/git/github-automation.py b/llvm/utils/git/github-automation.py --- a/llvm/utils/git/github-automation.py +++ b/llvm/utils/git/github-automation.py @@ -9,8 +9,11 @@ # ==-------------------------------------------------------------------------==# import argparse +from git import Repo import github import os +import re +import sys class IssueSubscriber: @@ -33,6 +36,132 @@ return True return False +def setup_llvmbot_git(git_dir = '.'): + repo = Repo(git_dir) + with repo.config_writer() as config: + config.set_value('user', 'name', 'llvmbot') + config.set_value('user', 'email', 'llvmbot@llvm.org') + +class ReleaseWorkflow: + + def __init__(self, args): + self.args = args + self.token = args.token + if args.branch_repo_token: + self.branch_repo_token = args.branch_repo_token + else: + self.branch_repo_token = self.token + + self.repo_name = args.repo + self.branch_repo_name = args.branch_repo + + def get_issue_number(self): + return self.args.issue_number + + def get_issue(self): + repo = github.Github(self.token).get_repo(self.repo_name) + issue = repo.get_issue(self.get_issue_number()) + return issue + + def get_push_url(self): + return 'https://{}@github.com/{}'.format(self.branch_repo_token, self.branch_repo_name) + + def get_branch_name(self): + return 'issue{}'.format(self.get_issue_number()) + + def get_release_branch_for_issue(self): + issue = self.get_issue() + m = re.search('branch: (.+)',issue.milestone.description) + if m: + return m.group(1) + return None + + def print_release_branch(self): + print(self.get_release_branch_for_issue()) + + def issue_notify_branch(self): + issue = self.get_issue() + issue.create_comment('/branch {}/{}'.format(self.branch_repo_name, self.get_branch_name())) + + def issue_notify_pull_request(self, pull): + issue = self.get_issue() + issue.create_comment('/pull-request {}#{}'.format(self.branch_repo_name, pull.number)) + + def get_action_url(self): + if os.getenv('CI'): + return 'https://github.com/{}/actions/runs/{}'.format(os.getenv('GITHUB_REPOSITORY'), os.getenv('GITHUB_RUN_ID')) + return "" + + def issue_notify_cherry_pick_failure(self, commit): + message = "Failed to cherry-pick: {} ".format(commit) + message += self.get_action_url() + issue = self.get_issue() + issue.create_comment(message) + issue.add_to_labels('release:cherry-pick-failed') + + def issue_notify_pull_request_failure(self, branch): + message = "Failed to create pull request for {} ".format(branch) + message += self.get_action_url() + issue = self.get_issue() + issue.create_comment(message) + + + def create_branch(self, commits): + print('cherry-picking', commits) + local_repo = Repo(self.args.llvm_project_dir) + for c in commits: + if not local_repo.git.cherry_pick('-x', c): + self.issue_notify_cherry_pick_failure(c) + return False + + branch_name = self.get_branch_name() + push_url = self.get_push_url() + print('Pushing to {} {}'.format(push_url, branch_name)) + local_repo.git.push(push_url, 'HEAD:{}'.format(branch_name)) + + self.issue_notify_branch() + + + def create_pull_request(self, owner, branch): + repo = github.Github(self.token).get_repo(self.branch_repo_name) + issue_ref = '{}#{}'.format(self.repo_name, self.get_issue_number()) + pull = None + try: + pull = repo.create_pull(title='PR for {}'.format(issue_ref), + body='resolves {}'.format(issue_ref), + base=self.get_release_branch_for_issue(), + head='{}:{}'.format(owner, branch), + maintainer_can_modify=False) + except Exception as e: + self.issue_notify_pull_request_failure(branch) + raise e + self.issue_notify_pull_request(pull) + + + def execute_command(self): + for line in sys.stdin: + line.rstrip() + m = re.search("/([a-z-]+)\s(.+)", line) + if not m: + continue + command = m.group(1) + args = m.group(2) + + if command == 'cherry-pick': + self.create_branch(args.split()) + return True + + if command == 'branch': + m = re.match('([^/]+)/([^/]+)/(.+)', args) + if m: + owner = m.group(1) + branch = m.group(3) + self.create_pull_request(owner, branch) + return True + + print("Do not understand input:") + print(sys.stdin.readlines()) + return False parser = argparse.ArgumentParser() parser.add_argument('--token', type=str, required=True) @@ -43,8 +172,26 @@ issue_subscriber_parser.add_argument('--label-name', type=str, required=True) issue_subscriber_parser.add_argument('--issue-number', type=int, required=True) +release_workflow_parser = subparsers.add_parser('release-workflow') +release_workflow_parser.add_argument('--llvm-project-dir', type=str, default='.') +release_workflow_parser.add_argument('--issue-number', type=int, required=True) +release_workflow_parser.add_argument('--branch-repo-token', type=str) +release_workflow_parser.add_argument('--branch-repo', type=str, default='llvmbot/llvm-project') +release_workflow_parser.add_argument('sub_command', type=str, choices=['print-release-branch', 'auto']) + +llvmbot_git_config_parser = subparsers.add_parser('setup-llvmbot-git') + args = parser.parse_args() if args.command == 'issue-subscriber': issue_subscriber = IssueSubscriber(args.token, args.repo, args.issue_number, args.label_name) issue_subscriber.run() +elif args.command == 'release-workflow': + release_workflow = ReleaseWorkflow(args) + if args.sub_command == 'print-release-branch': + release_workflow.print_release_branch() + else: + if not release_workflow.execute_command(): + sys.exit(1) +elif args.command == 'setup-llvmbot-git': + setup_llvmbot_git()