This is an automated email from the ASF dual-hosted git repository.
andor pushed a commit to branch branch-3.9
in repository https://gitbox.apache.org/repos/asf/zookeeper.git
The following commit(s) were added to refs/heads/branch-3.9 by this push:
new 40cd4a86a ZOOKEEPER-4756: Merge script should use GitHub api to merge
pull requ…
40cd4a86a is described below
commit 40cd4a86ac3c1a128b96d0890c5cb0801bcd015e
Author: szucsvillo <[email protected]>
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
(cherry picked from commit 32fb89c9f74c6e6d46148a16c422b9440f681970)
Signed-off-by: Andor Molnar <[email protected]>
---
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":