This is an automated email from the ASF dual-hosted git repository. andor pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/master by this push: new 32fb89c9f ZOOKEEPER-4756: Merge script should use GitHub api to merge pull requ… 32fb89c9f is described below commit 32fb89c9f74c6e6d46148a16c422b9440f681970 Author: szucsvillo <81696283+szucsvi...@users.noreply.github.com> AuthorDate: Fri Dec 1 14:05:56 2023 +0100 ZOOKEEPER-4756: Merge script should use GitHub api to merge pull requ… ZOOKEEPER-4756: Merge script should use GitHub api to merge pull requests Change-Id: I22ee835617fd96b540edd65191f6c83aae5365a9 Fix check status handling in merge_pr function Change-Id: I99844bfac98c90e9bb525cb8d3eae5a465a56629 Refactor JIRA ID extraction pattern Change-Id: I85a458eaac03b2a76edbc2ec923d56467503f900 Reviewers: tisonkun Author: szucsvillo Closes #2092 from szucsvillo/ZOOKEEPER-4756 --- zk-merge-pr.py | 216 ++++++++++++++++++++++++++------------------------------- 1 file changed, 98 insertions(+), 118 deletions(-) diff --git a/zk-merge-pr.py b/zk-merge-pr.py old mode 100644 new mode 100755 index 0118cba2c..debc36e31 --- a/zk-merge-pr.py +++ b/zk-merge-pr.py @@ -34,6 +34,7 @@ import subprocess import sys import urllib.request, urllib.error, urllib.parse import getpass +import requests try: import jira.client @@ -123,96 +124,72 @@ def get_current_branch(): return run_cmd("git rev-parse --abbrev-ref HEAD").replace("\n", "") # merge the requested PR and return the merge hash -def merge_pr(pr_num, target_ref, title, body, pr_repo_desc): - pr_branch_name = "%s_MERGE_PR_%s" % (TEMP_BRANCH_PREFIX, pr_num) - target_branch_name = "%s_MERGE_PR_%s_%s" % (TEMP_BRANCH_PREFIX, pr_num, target_ref.upper()) - run_cmd("git fetch %s pull/%s/head:%s" % (PR_REMOTE_NAME, pr_num, pr_branch_name)) - run_cmd("git fetch %s %s:%s" % (PUSH_REMOTE_NAME, target_ref, target_branch_name)) - run_cmd("git checkout %s" % target_branch_name) - - had_conflicts = False - try: - run_cmd(['git', 'merge', pr_branch_name, '--squash']) - except Exception as e: - msg = "Error merging: %s\nWould you like to manually fix-up this merge?" % e - continue_maybe(msg) - msg = "Okay, please fix any conflicts and 'git add' conflicting files... Finished?" - continue_maybe(msg) - had_conflicts = True - - commit_authors = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name, - '--pretty=format:%an <%ae>']).split("\n") - distinct_authors = sorted(set(commit_authors), - key=lambda x: commit_authors.count(x), reverse=True) - primary_author = input( - "Enter primary author in the format of \"name <email>\" [%s]: " % - distinct_authors[0]) - if primary_author == "": - primary_author = distinct_authors[0] - - reviewers = input( - "Enter reviewers in the format of \"name1 <email1>, name2 <email2>\": ").strip() - - commits = run_cmd(['git', 'log', 'HEAD..%s' % pr_branch_name, - '--pretty=format:%h [%an] %s']).split("\n") - - if len(commits) > 1: - result = input("List pull request commits in squashed commit message? (y/n): ") - if result.lower().strip() == "y": - should_list_commits = True - else: - should_list_commits = False +def merge_pr(pr_num, title, pr_repo_desc): + + # Retrieve the commits separately. + json_commits = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}/commits") + merge_message = [] + if json_commits and isinstance(json_commits, list): + for commit in json_commits: + commit_message = commit['commit']['message'] + merge_message += [commit_message] + + # Check for disapproval reviews. + json_reviewers = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}/reviews") + disapproval_reviews = [review['user']['login'] for review in json_reviewers if review['state'] == 'CHANGES_REQUESTED'] + if disapproval_reviews: + continue_maybe("Warning: There are requested changes. Proceed with merging pull request #%s?" % pr_num) + # Verify if there are no approved reviews. + approved_reviewers = [review['user']['login'] for review in json_reviewers if review['state'] == 'APPROVED'] + if not approved_reviewers: + continue_maybe("Warning: Pull Request does not have an approved review. Proceed with merging pull request #%s?" % pr_num) else: - should_list_commits = False - - merge_message_flags = [] - - merge_message_flags += ["-m", title] - if body is not None: - # We remove @ symbols from the body to avoid triggering e-mails - # to people every time someone creates a public fork of the project. - merge_message_flags += ["-m", body.replace("@", "")] - - authors = "\n".join(["Author: %s" % a for a in distinct_authors]) - - merge_message_flags += ["-m", authors] - - if (reviewers != ""): - merge_message_flags += ["-m", "Reviewers: %s" % reviewers] - - if had_conflicts: - committer_name = run_cmd("git config --get user.name").strip() - committer_email = run_cmd("git config --get user.email").strip() - message = "This patch had conflicts when merged, resolved by\nCommitter: %s <%s>" % ( - committer_name, committer_email) - merge_message_flags += ["-m", message] - - # The string "Closes #%s" string is required for GitHub to correctly close the PR + reviewers_string = ', '.join(approved_reviewers) + merge_message += [f"Reviewers: {reviewers_string}"] + # Check the author and the closing line. + json_pr = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}") + primary_author = json_pr["user"]["login"] + if primary_author != "": + merge_message += [f"Author: {primary_author}"] close_line = "Closes #%s from %s" % (pr_num, pr_repo_desc) - if should_list_commits: - close_line += " and squashes the following commits:" - merge_message_flags += ["-m", close_line] - - if should_list_commits: - merge_message_flags += ["-m", "\n".join(commits)] - - run_cmd(['git', 'commit', '--author="%s"' % primary_author] + merge_message_flags) - - continue_maybe("Merge complete (local ref %s). Push to %s?" % ( - target_branch_name, PUSH_REMOTE_NAME)) - - try: - run_cmd('git push %s %s:%s' % (PUSH_REMOTE_NAME, target_branch_name, target_ref)) - except Exception as e: - clean_up() - fail("Exception while pushing: %s" % e) - - merge_hash = run_cmd("git rev-parse %s" % target_branch_name)[:8] - clean_up() - print(("Pull request #%s merged!" % pr_num)) - print(("Merge hash: %s" % merge_hash)) - return merge_hash - + merge_message += [close_line] + merged_string = '\n'.join(merge_message) + + # Get the latest commit SHA. + latest_commit_sha = json_pr["head"]["sha"] + json_status = get_json(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/commits/{latest_commit_sha}/check-runs") + # Check if all checks have passed on GitHub. + all_checks_passed = all(status["conclusion"] == "success" for status in json_status["check_runs"]) + if all_checks_passed: + print("All checks have passed on the github.") + else: + any_in_progress = any(run["status"] == "in_progress" for run in json_status["check_runs"]) + if any_in_progress: + continue_maybe("Warning: There are pending checks. Would you like to continue the merge?") + else: + continue_maybe("Warning: Not all checks have passed on GitHub. Would you like to continue the merge?") + + headers = { + "Authorization": f"token {GITHUB_OAUTH_KEY}", + "Accept": "application/vnd.github.v3+json" + } + data = { + "commit_title": title, + "commit_message": merged_string, + "merge_method": "squash" + } + + response = requests.put(f"https://api.github.com/repos/{PUSH_REMOTE_NAME}/{PROJECT_NAME}/pulls/{pr_num}/merge", headers=headers, json=data) + + if response.status_code == 200: + merge_response_json = response.json() + merge_commit_sha = merge_response_json.get("sha") + print(f"Pull request #{pr_num} merged. Sha: #{merge_commit_sha}") + return merge_commit_sha + else: + print(f"Failed to merge pull request #{pr_num}. Status code: {response.status_code}") + print(response.text) + exit() def cherry_pick(pr_num, merge_hash, default_branch): pick_ref = input("Enter a branch name [%s]: " % default_branch) @@ -221,8 +198,8 @@ def cherry_pick(pr_num, merge_hash, default_branch): pick_branch_name = "%s_PICK_PR_%s_%s" % (TEMP_BRANCH_PREFIX, pr_num, pick_ref.upper()) - run_cmd("git fetch %s %s:%s" % (PUSH_REMOTE_NAME, pick_ref, pick_branch_name)) - run_cmd("git checkout %s" % pick_branch_name) + run_cmd("git fetch %s" % PUSH_REMOTE_NAME) + run_cmd("git checkout -b %s %s/%s" % (pick_branch_name, PUSH_REMOTE_NAME, pick_ref)) try: run_cmd("git cherry-pick -sx %s" % merge_hash) @@ -321,7 +298,7 @@ def resolve_jira_issue(merge_branches, comment, default_jira_id=""): def resolve_jira_issues(title, merge_branches, comment): - jira_ids = re.findall("%s-[0-9]{4,5}" % CAPITALIZED_PROJECT_NAME, title) + jira_ids = re.findall("%s-[0-9]+" % CAPITALIZED_PROJECT_NAME, title) if len(jira_ids) == 0: resolve_jira_issue(merge_branches, comment) @@ -446,7 +423,35 @@ def main(): pr_num = input("Which pull request would you like to merge? (e.g. 34): ") pr = get_json("%s/pulls/%s" % (GITHUB_API_BASE, pr_num)) - pr_events = get_json("%s/issues/%s/events" % (GITHUB_API_BASE, pr_num)) + + # Check if the pull request has already been closed or merged. + pull_request_state = pr.get("state", "") + if pull_request_state == "closed": + merge_hash = pr.get("merge_commit_sha", "") + merged = pr.get("merged") + # Verify if the pull request has been merged by the GitHub API. + if merged is True: + print(f"Pull request #{pr['number']} has already been merged, assuming you want to backport") + cherry_pick(pr_num, merge_hash, latest_branch) + sys.exit(0) + # Some merged pull requests may not appear as merged in the GitHub API, + # for example, those closed by an older version of this script. + else: + pr_events = get_json("%s/issues/%s/events" % (GITHUB_API_BASE, pr_num)) + for event in pr_events: + if event.get("event") == "closed": + commit_id = event.get("commit_id") + if commit_id is not None: + print(f"Pull request #{pr['number']} has already been merged, assuming you want to backport") + cherry_pick(pr_num, merge_hash, latest_branch) + sys.exit(0) + else: + print(f"Pull request #{pr['number']} has already been closed, but not merged, exiting.") + exit() + + if not bool(pr["mergeable"]): + print(f"Pull request %s is not mergeable in its current form.\n" % pr_num) + exit() url = pr["url"] @@ -469,36 +474,11 @@ def main(): print("Using original title:") print(commit_title) - body = pr["body"] target_ref = pr["base"]["ref"] user_login = pr["user"]["login"] base_ref = pr["head"]["ref"] pr_repo_desc = "%s/%s" % (user_login, base_ref) - # Merged pull requests don't appear as merged in the GitHub API; - # Instead, they're closed by asfgit. - merge_commits = \ - [e for e in pr_events if e["actor"]["login"] == "asfgit" and e["event"] == "closed"] - - if merge_commits: - merge_hash = merge_commits[0]["commit_id"] - message = get_json("%s/commits/%s" % (GITHUB_API_BASE, merge_hash))["commit"]["message"] - - print("Pull request %s has already been merged, assuming you want to backport" % pr_num) - commit_is_downloaded = run_cmd(['git', 'rev-parse', '--quiet', '--verify', - "%s^{commit}" % merge_hash]).strip() != "" - if not commit_is_downloaded: - fail("Couldn't find any merge commit for #%s, you may need to update HEAD." % pr_num) - - print("Found commit %s:\n%s" % (merge_hash, message)) - cherry_pick(pr_num, merge_hash, latest_branch) - sys.exit(0) - - if not bool(pr["mergeable"]): - msg = "Pull request %s is not mergeable in its current form.\n" % pr_num + \ - "Continue? (experts only!)" - continue_maybe(msg) - print(("\n=== Pull Request #%s ===" % pr_num)) print(("PR title\t%s\nCommit title\t%s\nSource\t\t%s\nTarget\t\t%s\nURL\t\t%s" % ( pr_title, commit_title, pr_repo_desc, target_ref, url))) @@ -506,7 +486,7 @@ def main(): merged_refs = [target_ref] - merge_hash = merge_pr(pr_num, target_ref, commit_title, body, pr_repo_desc) + merge_hash = merge_pr(pr_num, commit_title, pr_repo_desc) pick_prompt = "Would you like to pick %s into another branch?" % merge_hash while input("\n%s (y/n): " % pick_prompt).lower().strip() == "y":