From f673dcc693da741e1662b516bbd8053e39a4fbc2 Mon Sep 17 00:00:00 2001 From: Tom Stellard Date: Tue, 26 Jul 2022 15:18:23 -0700 Subject: [PATCH] github: Automatically assign reviewers for backport requests When there is a backport request, the GitHub Action that handles the backport will now automatically assign the issue to the user(s) who approved the commit in Phabricator and create an issue comment asking them to review the request. Reviewed By: thieta, kwk Differential Revision: https://reviews.llvm.org/D126423 --- .github/workflows/issue-release-workflow.yml | 1 + llvm/utils/git/github-automation.py | 108 ++++++++++++++++++- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/.github/workflows/issue-release-workflow.yml b/.github/workflows/issue-release-workflow.yml index 6a62001f9ecf..927ffb8ae550 100644 --- a/.github/workflows/issue-release-workflow.yml +++ b/.github/workflows/issue-release-workflow.yml @@ -58,6 +58,7 @@ jobs: --token ${{ secrets.RELEASE_WORKFLOW_PUSH_SECRET }} \ release-workflow \ --issue-number ${{ github.event.issue.number }} \ + --phab-token ${{ secrets.RELEASE_WORKFLOW_PHAB_TOKEN }} \ auto create-pull-request: diff --git a/llvm/utils/git/github-automation.py b/llvm/utils/git/github-automation.py index 3862ade65221..8699bda7b18f 100755 --- a/llvm/utils/git/github-automation.py +++ b/llvm/utils/git/github-automation.py @@ -13,6 +13,7 @@ from git import Repo # type: ignore import github import os import re +import requests import sys import time from typing import * @@ -48,6 +49,69 @@ def setup_llvmbot_git(git_dir = '.'): config.set_value('user', 'name', 'llvmbot') config.set_value('user', 'email', 'llvmbot@llvm.org') +def phab_api_call(phab_token:str, url:str, args:dict) -> dict: + """ + Make an API call to the Phabricator web service and return a dictionary + containing the json response. + """ + data = { "api.token" : phab_token } + data.update(args) + response = requests.post(url, data = data) + return response.json() + + +def phab_login_to_github_login(phab_token:str, repo:github.Repository.Repository, phab_login:str) -> str: + """ + Tries to translate a Phabricator login to a github login by + finding a commit made in Phabricator's Differential. + The commit's SHA1 is then looked up in the github repo and + the committer's login associated with that commit is returned. + + :param str phab_token: The Conduit API token to use for communication with Pabricator + :param github.Repository.Repository repo: The github repo to use when looking for the SHA1 found in Differential + :param str phab_login: The Phabricator login to be translated. + """ + + args = { + "constraints[authors][0]" : phab_login, + # PHID for "LLVM Github Monorepo" repository + "constraints[repositories][0]" : "PHID-REPO-f4scjekhnkmh7qilxlcy", + "limit" : 1 + } + # API documentation: https://reviews.llvm.org/conduit/method/diffusion.commit.search/ + r = phab_api_call(phab_token, "https://reviews.llvm.org/api/diffusion.commit.search", args) + data = r['result']['data'] + if len(data) == 0: + # Can't find any commits associated with this user + return None + + commit_sha = data[0]['fields']['identifier'] + return repo.get_commit(commit_sha).committer.login + +def phab_get_commit_approvers(phab_token:str, repo:github.Repository.Repository, commit:github.Commit.Commit) -> list: + args = { "corpus" : commit.commit.message } + # API documentation: https://reviews.llvm.org/conduit/method/differential.parsecommitmessage/ + r = phab_api_call(phab_token, "https://reviews.llvm.org/api/differential.parsecommitmessage", args) + review_id = r['result']['revisionIDFieldInfo']['value'] + + args = { + 'constraints[ids][0]' : review_id, + 'attachments[reviewers]' : True + } + # API documentation: https://reviews.llvm.org/conduit/method/differential.revision.search/ + r = phab_api_call(phab_token, "https://reviews.llvm.org/api/differential.revision.search", args) + reviewers = r['result']['data'][0]['attachments']['reviewers']['reviewers'] + accepted = [] + for reviewer in reviewers: + if reviewer['status'] != 'accepted': + continue + phid = reviewer['reviewerPHID'] + args = { 'constraints[phids][0]' : phid } + # API documentation: https://reviews.llvm.org/conduit/method/user.search/ + r = phab_api_call(phab_token, "https://reviews.llvm.org/api/user.search", args) + accepted.append(r['result']['data'][0]['fields']['username']) + return accepted + class ReleaseWorkflow: CHERRY_PICK_FAILED_LABEL = 'release:cherry-pick-failed' @@ -64,7 +128,7 @@ class ReleaseWorkflow: def __init__(self, token:str, repo:str, issue_number:int, branch_repo_name:str, branch_repo_token:str, - llvm_project_dir:str) -> None: + llvm_project_dir:str, phab_token:str) -> None: self._token = token self._repo_name = repo self._issue_number = issue_number @@ -74,6 +138,7 @@ class ReleaseWorkflow: else: self._branch_repo_token = self.token self._llvm_project_dir = llvm_project_dir + self._phab_token = phab_token @property def token(self) -> str: @@ -100,12 +165,16 @@ class ReleaseWorkflow: return self._llvm_project_dir @property - def __repo(self) -> github.Repository.Repository: + def phab_token(self) -> str: + return self._phab_token + + @property + def repo(self) -> github.Repository.Repository: return github.Github(self.token).get_repo(self.repo_name) @property def issue(self) -> github.Issue.Issue: - return self.__repo.get_issue(self.issue_number) + return self.repo.get_issue(self.issue_number) @property def push_url(self) -> str: @@ -174,6 +243,29 @@ class ReleaseWorkflow: if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]: self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL) + def pr_request_review(self, pr:github.PullRequest.PullRequest): + """ + This function will try to find the best reviewers for `commits` and + then add a comment requesting review of the backport and assign the + pull request to the selected reviewers. + + The reviewers selected are those users who approved the patch in + Phabricator. + """ + reviewers = [] + for commit in pr.get_commits(): + approvers = phab_get_commit_approvers(self.phab_token, self.repo, commit) + for a in approvers: + login = phab_login_to_github_login(self.phab_token, self.repo, a) + if not login: + continue + reviewers.append(login) + if len(reviewers): + message = "{} What do you think about merging this PR to the release branch?".format( + " ".join(["@" + r for r in reviewers])) + pr.create_issue_comment(message) + pr.add_to_assignees(*reviewers) + def create_branch(self, commits:List[str]) -> bool: """ This function attempts to backport `commits` into the branch associated @@ -257,6 +349,13 @@ class ReleaseWorkflow: base=release_branch_for_issue, head=head, maintainer_can_modify=False) + + try: + if self.phab_token: + self.pr_request_review(pull) + except Exception as e: + print("error: Failed while searching for reviewers", e) + except Exception as e: self.issue_notify_pull_request_failure(branch) raise e @@ -313,6 +412,7 @@ 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='.', help='directory containing the llvm-project checout') release_workflow_parser.add_argument('--issue-number', type=int, required=True, help='The issue number to update') +release_workflow_parser.add_argument('--phab-token', type=str, help='Phabricator conduit API token. See https://reviews.llvm.org/settings/user//page/apitokens/') release_workflow_parser.add_argument('--branch-repo-token', type=str, help='GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.') release_workflow_parser.add_argument('--branch-repo', type=str, default='llvm/llvm-project-release-prs', @@ -330,7 +430,7 @@ if args.command == 'issue-subscriber': elif args.command == 'release-workflow': release_workflow = ReleaseWorkflow(args.token, args.repo, args.issue_number, args.branch_repo, args.branch_repo_token, - args.llvm_project_dir) + args.llvm_project_dir, args.phab_token) if not release_workflow.release_branch_for_issue: release_workflow.issue_notify_no_milestone(sys.stdin.readlines()) sys.exit(1) -- 2.34.1