diff --git a/.github/workflows/issue-release-workflow.yml b/.github/workflows/issue-release-workflow.yml --- a/.github/workflows/issue-release-workflow.yml +++ b/.github/workflows/issue-release-workflow.yml @@ -58,6 +58,7 @@ --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 --- a/llvm/utils/git/github-automation.py +++ b/llvm/utils/git/github-automation.py @@ -12,6 +12,7 @@ from git import Repo # type: ignore import github import os +import requests import re import sys import time @@ -48,6 +49,69 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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)