Skip to content

Commit f673dcc

Browse files
committed
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
1 parent e3e63f3 commit f673dcc

File tree

2 files changed

+105
-4
lines changed

2 files changed

+105
-4
lines changed

.github/workflows/issue-release-workflow.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
--token ${{ secrets.RELEASE_WORKFLOW_PUSH_SECRET }} \
5959
release-workflow \
6060
--issue-number ${{ github.event.issue.number }} \
61+
--phab-token ${{ secrets.RELEASE_WORKFLOW_PHAB_TOKEN }} \
6162
auto
6263
6364
create-pull-request:

llvm/utils/git/github-automation.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import github
1414
import os
1515
import re
16+
import requests
1617
import sys
1718
import time
1819
from typing import *
@@ -48,6 +49,69 @@ def setup_llvmbot_git(git_dir = '.'):
4849
config.set_value('user', 'name', 'llvmbot')
4950
config.set_value('user', 'email', 'llvmbot@llvm.org')
5051

52+
def phab_api_call(phab_token:str, url:str, args:dict) -> dict:
53+
"""
54+
Make an API call to the Phabricator web service and return a dictionary
55+
containing the json response.
56+
"""
57+
data = { "api.token" : phab_token }
58+
data.update(args)
59+
response = requests.post(url, data = data)
60+
return response.json()
61+
62+
63+
def phab_login_to_github_login(phab_token:str, repo:github.Repository.Repository, phab_login:str) -> str:
64+
"""
65+
Tries to translate a Phabricator login to a github login by
66+
finding a commit made in Phabricator's Differential.
67+
The commit's SHA1 is then looked up in the github repo and
68+
the committer's login associated with that commit is returned.
69+
70+
:param str phab_token: The Conduit API token to use for communication with Pabricator
71+
:param github.Repository.Repository repo: The github repo to use when looking for the SHA1 found in Differential
72+
:param str phab_login: The Phabricator login to be translated.
73+
"""
74+
75+
args = {
76+
"constraints[authors][0]" : phab_login,
77+
# PHID for "LLVM Github Monorepo" repository
78+
"constraints[repositories][0]" : "PHID-REPO-f4scjekhnkmh7qilxlcy",
79+
"limit" : 1
80+
}
81+
# API documentation: https://reviews.llvm.org/conduit/method/diffusion.commit.search/
82+
r = phab_api_call(phab_token, "https://reviews.llvm.org/api/diffusion.commit.search", args)
83+
data = r['result']['data']
84+
if len(data) == 0:
85+
# Can't find any commits associated with this user
86+
return None
87+
88+
commit_sha = data[0]['fields']['identifier']
89+
return repo.get_commit(commit_sha).committer.login
90+
91+
def phab_get_commit_approvers(phab_token:str, repo:github.Repository.Repository, commit:github.Commit.Commit) -> list:
92+
args = { "corpus" : commit.commit.message }
93+
# API documentation: https://reviews.llvm.org/conduit/method/differential.parsecommitmessage/
94+
r = phab_api_call(phab_token, "https://reviews.llvm.org/api/differential.parsecommitmessage", args)
95+
review_id = r['result']['revisionIDFieldInfo']['value']
96+
97+
args = {
98+
'constraints[ids][0]' : review_id,
99+
'attachments[reviewers]' : True
100+
}
101+
# API documentation: https://reviews.llvm.org/conduit/method/differential.revision.search/
102+
r = phab_api_call(phab_token, "https://reviews.llvm.org/api/differential.revision.search", args)
103+
reviewers = r['result']['data'][0]['attachments']['reviewers']['reviewers']
104+
accepted = []
105+
for reviewer in reviewers:
106+
if reviewer['status'] != 'accepted':
107+
continue
108+
phid = reviewer['reviewerPHID']
109+
args = { 'constraints[phids][0]' : phid }
110+
# API documentation: https://reviews.llvm.org/conduit/method/user.search/
111+
r = phab_api_call(phab_token, "https://reviews.llvm.org/api/user.search", args)
112+
accepted.append(r['result']['data'][0]['fields']['username'])
113+
return accepted
114+
51115
class ReleaseWorkflow:
52116

53117
CHERRY_PICK_FAILED_LABEL = 'release:cherry-pick-failed'
@@ -64,7 +128,7 @@ class ReleaseWorkflow:
64128

65129
def __init__(self, token:str, repo:str, issue_number:int,
66130
branch_repo_name:str, branch_repo_token:str,
67-
llvm_project_dir:str) -> None:
131+
llvm_project_dir:str, phab_token:str) -> None:
68132
self._token = token
69133
self._repo_name = repo
70134
self._issue_number = issue_number
@@ -74,6 +138,7 @@ def __init__(self, token:str, repo:str, issue_number:int,
74138
else:
75139
self._branch_repo_token = self.token
76140
self._llvm_project_dir = llvm_project_dir
141+
self._phab_token = phab_token
77142

78143
@property
79144
def token(self) -> str:
@@ -100,12 +165,16 @@ def llvm_project_dir(self) -> str:
100165
return self._llvm_project_dir
101166

102167
@property
103-
def __repo(self) -> github.Repository.Repository:
168+
def phab_token(self) -> str:
169+
return self._phab_token
170+
171+
@property
172+
def repo(self) -> github.Repository.Repository:
104173
return github.Github(self.token).get_repo(self.repo_name)
105174

106175
@property
107176
def issue(self) -> github.Issue.Issue:
108-
return self.__repo.get_issue(self.issue_number)
177+
return self.repo.get_issue(self.issue_number)
109178

110179
@property
111180
def push_url(self) -> str:
@@ -174,6 +243,29 @@ def issue_remove_cherry_pick_failed_label(self):
174243
if self.CHERRY_PICK_FAILED_LABEL in [l.name for l in self.issue.labels]:
175244
self.issue.remove_from_labels(self.CHERRY_PICK_FAILED_LABEL)
176245

246+
def pr_request_review(self, pr:github.PullRequest.PullRequest):
247+
"""
248+
This function will try to find the best reviewers for `commits` and
249+
then add a comment requesting review of the backport and assign the
250+
pull request to the selected reviewers.
251+
252+
The reviewers selected are those users who approved the patch in
253+
Phabricator.
254+
"""
255+
reviewers = []
256+
for commit in pr.get_commits():
257+
approvers = phab_get_commit_approvers(self.phab_token, self.repo, commit)
258+
for a in approvers:
259+
login = phab_login_to_github_login(self.phab_token, self.repo, a)
260+
if not login:
261+
continue
262+
reviewers.append(login)
263+
if len(reviewers):
264+
message = "{} What do you think about merging this PR to the release branch?".format(
265+
" ".join(["@" + r for r in reviewers]))
266+
pr.create_issue_comment(message)
267+
pr.add_to_assignees(*reviewers)
268+
177269
def create_branch(self, commits:List[str]) -> bool:
178270
"""
179271
This function attempts to backport `commits` into the branch associated
@@ -257,6 +349,13 @@ def create_pull_request(self, owner:str, branch:str) -> bool:
257349
base=release_branch_for_issue,
258350
head=head,
259351
maintainer_can_modify=False)
352+
353+
try:
354+
if self.phab_token:
355+
self.pr_request_review(pull)
356+
except Exception as e:
357+
print("error: Failed while searching for reviewers", e)
358+
260359
except Exception as e:
261360
self.issue_notify_pull_request_failure(branch)
262361
raise e
@@ -313,6 +412,7 @@ def execute_command(self) -> bool:
313412
release_workflow_parser = subparsers.add_parser('release-workflow')
314413
release_workflow_parser.add_argument('--llvm-project-dir', type=str, default='.', help='directory containing the llvm-project checout')
315414
release_workflow_parser.add_argument('--issue-number', type=int, required=True, help='The issue number to update')
415+
release_workflow_parser.add_argument('--phab-token', type=str, help='Phabricator conduit API token. See https://reviews.llvm.org/settings/user/<USER>/page/apitokens/')
316416
release_workflow_parser.add_argument('--branch-repo-token', type=str,
317417
help='GitHub authentication token to use for the repository where new branches will be pushed. Defaults to TOKEN.')
318418
release_workflow_parser.add_argument('--branch-repo', type=str, default='llvm/llvm-project-release-prs',
@@ -330,7 +430,7 @@ def execute_command(self) -> bool:
330430
elif args.command == 'release-workflow':
331431
release_workflow = ReleaseWorkflow(args.token, args.repo, args.issue_number,
332432
args.branch_repo, args.branch_repo_token,
333-
args.llvm_project_dir)
433+
args.llvm_project_dir, args.phab_token)
334434
if not release_workflow.release_branch_for_issue:
335435
release_workflow.issue_notify_no_milestone(sys.stdin.readlines())
336436
sys.exit(1)

0 commit comments

Comments
 (0)