Skip to content

Commit 38c706e

Browse files
[GitHub][workflows] Ask reviewers to merge PRs when author cannot (#81142)
This uses https://pygithub.readthedocs.io/en/stable/github_objects/Repository.html?highlight=get_collaborator_permission#github.Repository.Repository.get_collaborator_permission. Which does https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#get-repository-permissions-for-a-user and returns the top level "permission" key. This is less detailed than the user/permissions key but should be fine for this use case. When a review is submitted we check: * If it's an approval. * Whether we have already left a merge on behalf comment (by looking for a hidden HTML comment). * Whether the author has permissions to merge their own PR. * Whether the reviewer has permissions to merge. If needed we leave a comment tagging the reviewer. If the reviewer also doesn't have merge permission, then it asks them to find someone else who does.
1 parent 5e5e51e commit 38c706e

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

.github/workflows/approved-prs.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: "Prompt reviewers to merge PRs on behalf of authors"
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
pull_request_review:
8+
types:
9+
- submitted
10+
11+
jobs:
12+
merge-on-behalf-information-comment:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
pull-requests: write
16+
if: >-
17+
(github.repository == 'llvm/llvm-project') &&
18+
(github.event.review.state == 'APPROVED')
19+
steps:
20+
- name: Checkout Automation Script
21+
uses: actions/checkout@v4
22+
with:
23+
sparse-checkout: llvm/utils/git/
24+
ref: main
25+
26+
- name: Setup Automation Script
27+
working-directory: ./llvm/utils/git/
28+
run: |
29+
pip install -r requirements.txt
30+
31+
- name: Add Merge On Behalf Comment
32+
working-directory: ./llvm/utils/git/
33+
run: |
34+
python3 ./github-automation.py \
35+
--token '${{ secrets.GITHUB_TOKEN }}' \
36+
pr-merge-on-behalf-information \
37+
--issue-number "${{ github.event.pull_request.number }}" \
38+
--author "${{ github.event.pull_request.user.login }}" \
39+
--reviewer "${{ github.event.review.user.login }}"

llvm/utils/git/github-automation.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,55 @@ def run(self) -> bool:
298298
return True
299299

300300

301+
class PRMergeOnBehalfInformation:
302+
COMMENT_TAG = "<!--LLVM MERGE ON BEHALF INFORMATION COMMENT-->\n"
303+
304+
def __init__(
305+
self, token: str, repo: str, pr_number: int, author: str, reviewer: str
306+
):
307+
self.repo = github.Github(token).get_repo(repo)
308+
self.pr = self.repo.get_issue(pr_number).as_pull_request()
309+
self.author = author
310+
self.reviewer = reviewer
311+
312+
def can_merge(self, user: str) -> bool:
313+
try:
314+
return self.repo.get_collaborator_permission(user) in ["admin", "write"]
315+
# There is a UnknownObjectException for this scenario, but this method
316+
# does not use it.
317+
except github.GithubException as e:
318+
# 404 means the author was not found in the collaborator list, so we
319+
# know they don't have push permissions. Anything else is a real API
320+
# issue, raise it so it is visible.
321+
if e.status != 404:
322+
raise e
323+
return False
324+
325+
def run(self) -> bool:
326+
# Check this first because it only costs 1 API point.
327+
if self.can_merge(self.author):
328+
return
329+
330+
# A review can be approved more than once, only comment the first time.
331+
for comment in self.pr.as_issue().get_comments():
332+
if self.COMMENT_TAG in comment.body:
333+
return
334+
335+
# This text is using Markdown formatting.
336+
if self.can_merge(self.reviewer):
337+
comment = f"""\
338+
{self.COMMENT_TAG}
339+
@{self.reviewer} the PR author does not have permission to merge their own PRs yet. Please merge on their behalf."""
340+
else:
341+
comment = f"""\
342+
{self.COMMENT_TAG}
343+
@{self.reviewer} the author of this PR does not have permission to merge and neither do you.
344+
Please find someone who has merge permissions who can merge it on the author's behalf. This could be one of the other reviewers or you can ask on [Discord](https://discord.com/invite/xS7Z362)."""
345+
346+
self.pr.as_issue().create_comment(comment)
347+
return True
348+
349+
301350
def setup_llvmbot_git(git_dir="."):
302351
"""
303352
Configure the git repo in `git_dir` with the llvmbot account so
@@ -665,6 +714,17 @@ def execute_command(self) -> bool:
665714
pr_buildbot_information_parser.add_argument("--issue-number", type=int, required=True)
666715
pr_buildbot_information_parser.add_argument("--author", type=str, required=True)
667716

717+
pr_merge_on_behalf_information_parser = subparsers.add_parser(
718+
"pr-merge-on-behalf-information"
719+
)
720+
pr_merge_on_behalf_information_parser.add_argument(
721+
"--issue-number", type=int, required=True
722+
)
723+
pr_merge_on_behalf_information_parser.add_argument("--author", type=str, required=True)
724+
pr_merge_on_behalf_information_parser.add_argument(
725+
"--reviewer", type=str, required=True
726+
)
727+
668728
release_workflow_parser = subparsers.add_parser("release-workflow")
669729
release_workflow_parser.add_argument(
670730
"--llvm-project-dir",
@@ -724,6 +784,11 @@ def execute_command(self) -> bool:
724784
args.token, args.repo, args.issue_number, args.author
725785
)
726786
pr_buildbot_information.run()
787+
elif args.command == "pr-merge-on-behalf-information":
788+
pr_merge_on_behalf_information = PRMergeOnBehalfInformation(
789+
args.token, args.repo, args.issue_number, args.author, args.reviewer
790+
)
791+
pr_merge_on_behalf_information.run()
727792
elif args.command == "release-workflow":
728793
release_workflow = ReleaseWorkflow(
729794
args.token,

0 commit comments

Comments
 (0)