diff --git a/.github/workflows/approved-prs.yml b/.github/workflows/approved-prs.yml new file mode 100644 index 0000000000000..309a9217e42d3 --- /dev/null +++ b/.github/workflows/approved-prs.yml @@ -0,0 +1,39 @@ +name: "Prompt reviewers to merge PRs on behalf of authors" + +permissions: + contents: read + +on: + pull_request_review: + types: + - submitted + +jobs: + merge-on-behalf-information-comment: + runs-on: ubuntu-latest + permissions: + pull-requests: write + if: >- + (github.repository == 'llvm/llvm-project') && + (github.event.review.state == 'APPROVED') + steps: + - name: Checkout Automation Script + uses: actions/checkout@v4 + with: + sparse-checkout: llvm/utils/git/ + ref: main + + - name: Setup Automation Script + working-directory: ./llvm/utils/git/ + run: | + pip install -r requirements.txt + + - name: Add Merge On Behalf Comment + working-directory: ./llvm/utils/git/ + run: | + python3 ./github-automation.py \ + --token '${{ secrets.GITHUB_TOKEN }}' \ + pr-merge-on-behalf-information \ + --issue-number "${{ github.event.pull_request.number }}" \ + --author "${{ github.event.pull_request.user.login }}" \ + --reviewer "${{ github.event.review.user.login }}" diff --git a/llvm/utils/git/github-automation.py b/llvm/utils/git/github-automation.py index 04698cacbff92..cf669a6ee27e5 100755 --- a/llvm/utils/git/github-automation.py +++ b/llvm/utils/git/github-automation.py @@ -298,6 +298,55 @@ def run(self) -> bool: return True +class PRMergeOnBehalfInformation: + COMMENT_TAG = "\n" + + def __init__( + self, token: str, repo: str, pr_number: int, author: str, reviewer: str + ): + self.repo = github.Github(token).get_repo(repo) + self.pr = self.repo.get_issue(pr_number).as_pull_request() + self.author = author + self.reviewer = reviewer + + def can_merge(self, user: str) -> bool: + try: + return self.repo.get_collaborator_permission(user) in ["admin", "write"] + # There is a UnknownObjectException for this scenario, but this method + # does not use it. + except github.GithubException as e: + # 404 means the author was not found in the collaborator list, so we + # know they don't have push permissions. Anything else is a real API + # issue, raise it so it is visible. + if e.status != 404: + raise e + return False + + def run(self) -> bool: + # Check this first because it only costs 1 API point. + if self.can_merge(self.author): + return + + # A review can be approved more than once, only comment the first time. + for comment in self.pr.as_issue().get_comments(): + if self.COMMENT_TAG in comment.body: + return + + # This text is using Markdown formatting. + if self.can_merge(self.reviewer): + comment = f"""\ +{self.COMMENT_TAG} +@{self.reviewer} the PR author does not have permission to merge their own PRs yet. Please merge on their behalf.""" + else: + comment = f"""\ +{self.COMMENT_TAG} +@{self.reviewer} the author of this PR does not have permission to merge and neither do you. +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).""" + + self.pr.as_issue().create_comment(comment) + return True + + def setup_llvmbot_git(git_dir="."): """ Configure the git repo in `git_dir` with the llvmbot account so @@ -647,6 +696,17 @@ def execute_command(self) -> bool: pr_buildbot_information_parser.add_argument("--issue-number", type=int, required=True) pr_buildbot_information_parser.add_argument("--author", type=str, required=True) +pr_merge_on_behalf_information_parser = subparsers.add_parser( + "pr-merge-on-behalf-information" +) +pr_merge_on_behalf_information_parser.add_argument( + "--issue-number", type=int, required=True +) +pr_merge_on_behalf_information_parser.add_argument("--author", type=str, required=True) +pr_merge_on_behalf_information_parser.add_argument( + "--reviewer", type=str, required=True +) + release_workflow_parser = subparsers.add_parser("release-workflow") release_workflow_parser.add_argument( "--llvm-project-dir", @@ -700,6 +760,11 @@ def execute_command(self) -> bool: args.token, args.repo, args.issue_number, args.author ) pr_buildbot_information.run() +elif args.command == "pr-merge-on-behalf-information": + pr_merge_on_behalf_information = PRMergeOnBehalfInformation( + args.token, args.repo, args.issue_number, args.author, args.reviewer + ) + pr_merge_on_behalf_information.run() elif args.command == "release-workflow": release_workflow = ReleaseWorkflow( args.token,