[llvm] [Utils] Add new merge-release-pr.py script. (PR #101630)
Tobias Hieta via llvm-commits
llvm-commits at lists.llvm.org
Fri Aug 2 01:04:42 PDT 2024
https://github.com/tru created https://github.com/llvm/llvm-project/pull/101630
This script helps the release managers merge backport PR's.
It does the following things:
* Validate the PR, checks approval, target branch and many other things.
* Rebases the PR
* Checkout the PR locally
* Pushes the PR to the release branch
* Deletes the local branch
I have found the script very helpful to merge the PR's.
>From 22fd991bcff56818a682238dd4398def83f665b7 Mon Sep 17 00:00:00 2001
From: Tobias Hieta <tobias at hieta.se>
Date: Fri, 2 Aug 2024 09:55:24 +0200
Subject: [PATCH] [Utils] Add new merge-release-pr.py script.
This script helps the release managers merge backport PR's.
It does the following things:
* Validate the PR, checks approval, target branch and many other things.
* Rebases the PR
* Checkout the PR locally
* Pushes the PR to the release branch
* Deletes the local branch
I have found the script very helpful to merge the PR's.
---
llvm/utils/release/merge-release-pr.py | 239 +++++++++++++++++++++++++
1 file changed, 239 insertions(+)
create mode 100755 llvm/utils/release/merge-release-pr.py
diff --git a/llvm/utils/release/merge-release-pr.py b/llvm/utils/release/merge-release-pr.py
new file mode 100755
index 0000000000000..f9ee8aa7da54a
--- /dev/null
+++ b/llvm/utils/release/merge-release-pr.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+# ===-- merge-release-pr.py ------------------------------------------------===#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ===------------------------------------------------------------------------===#
+#
+# Helper script that will merge a Pull Request into a release branch. It will first
+# do some validations of the PR then rebase and finally push the changes to the
+# release branch.
+#
+# Usage: merge-release-pr.py <PR id>
+# By default it will push to the 'upstream' origin, but you can pass
+# --upstream-origin/-o <origin> if you want to change it.
+#
+# If you want to skip a specific validation, like the status checks you can
+# pass -s status_checks, this argument can be passed multiple times.
+#
+import argparse
+import json
+import subprocess
+import sys
+import time
+from typing import List
+
+
+class PRMerger:
+
+ def __init__(self, args):
+ self.pr = args.pr
+ self.args = args
+
+ def run_gh(self, gh_cmd: str, args: List[str]) -> str:
+ cmd = ["gh", gh_cmd, "-Rllvm/llvm-project"] + args
+ p = subprocess.run(cmd, capture_output=True)
+ if p.returncode != 0:
+ print(p.stderr)
+ raise RuntimeError("Failed to run gh")
+ return p.stdout
+
+ # Validate the state of the PR, this means making sure that it is
+ # OPEN and not already merged or closed.
+ def validate_state(self, data):
+ state = data["state"]
+ if state != "OPEN":
+ return False, f"state is {state.lower()}, not open"
+ return True
+
+ # Validate that the PR is targetting a release/ branch. We could
+ # validate the exact branch here, but I am not sure how to figure
+ # out what we want except an argument and that might be a bit to
+ # to much overhead.
+ def validate_target_branch(self, data):
+ baseRefName: str = data["baseRefName"]
+ if not baseRefName.startswith("release/"):
+ return False, f"target branch is {baseRefName}, not a release branch"
+ return True
+
+ # Validate the approval decision. This checks that the PR has been
+ # approved.
+ def validate_approval(self, data):
+ if data["reviewDecision"] != "APPROVED":
+ return False, "PR is not approved"
+ return True
+
+ # Check that all the actions / status checks succeeded. Will also
+ # fail if we have status checks in progress.
+ def validate_status_checks(self, data):
+ failures = []
+ pending = []
+ for status in data["statusCheckRollup"]:
+ if "conclusion" in status and status["conclusion"] == "FAILURE":
+ failures.append(status)
+ if "status" in status and status["status"] == "IN_PROGRESS":
+ pending.append(status)
+
+ if failures or pending:
+ errstr = "\n"
+ if failures:
+ errstr += " FAILED: "
+ errstr += ", ".join([d["name"] for d in failures])
+ if pending:
+ if failures:
+ errstr += "\n"
+ errstr += " PENDING: "
+ errstr += ", ".join([d["name"] for d in pending])
+
+ return False, errstr
+
+ return True
+
+ # Validate that the PR contains just one commit. If it has more
+ # we might want to squash. Which is something we could add to
+ # this script in the future.
+ def validate_commits(self, data):
+ if len(data["commits"]) > 1:
+ return False, f"More than 1 commit! {len(data['commits'])}"
+ return True
+
+ def validate_pr(self):
+ fields_to_fetch = [
+ "baseRefName",
+ "reviewDecision",
+ "title",
+ "statusCheckRollup",
+ "url",
+ "state",
+ "commits",
+ ]
+ o = self.run_gh(
+ "pr",
+ ["view", self.pr, "--json", ",".join(fields_to_fetch)],
+ )
+ prdata = json.loads(o)
+
+ # save the baseRefName (target branch) so that we know where to push
+ self.target_branch = prdata["baseRefName"]
+
+ print(f"> Handling PR {self.pr} - {prdata['title']}")
+ print(f"> {prdata['url']}")
+
+ VALIDATIONS = {
+ "state": self.validate_state,
+ "target_branch": self.validate_target_branch,
+ "approval": self.validate_approval,
+ "commits": self.validate_commits,
+ "status_checks": self.validate_status_checks,
+ }
+
+ print()
+ print("> Validations:")
+ total_ok = True
+ for val_name, val_func in VALIDATIONS.items():
+ try:
+ validation_data = val_func(prdata)
+ except:
+ validation_data = False
+ ok = None
+ skipped = (
+ True
+ if (self.args.skip_validation and val_name in self.args.skip_validation)
+ else False
+ )
+ if isinstance(validation_data, bool) and validation_data:
+ ok = "OK"
+ elif isinstance(validation_data, tuple) and not validation_data[0]:
+ failstr = validation_data[1]
+ if skipped:
+ ok = "SKIPPED: "
+ else:
+ total_ok = False
+ ok = "FAIL: "
+ ok += failstr
+ else:
+ ok = "FAIL! (Unknown)"
+ print(f" * {val_name}: {ok}")
+ return total_ok
+
+ def rebase_pr(self):
+ print("> Rebasing")
+ self.run_gh("pr", ["update-branch", "--rebase", self.pr])
+ print("> Waiting for GitHub to update PR")
+ time.sleep(4)
+
+ def checkout_pr(self):
+ print("> Fetching PR changes...")
+ self.run_gh(
+ "pr", ["checkout", self.pr, "--force", "--branch", "llvm_merger_" + self.pr]
+ )
+
+ def push_upstream(self):
+ print("> Pushing changes...")
+ subprocess.run(
+ ["git", "push", self.args.upstream, "HEAD:" + self.target_branch], check=True
+ )
+
+ def delete_local_branch(self):
+ print("> Deleting the old branch...")
+ subprocess.run(["git", "switch", "main"])
+ subprocess.run(["git", "branch", "-D", f"llvm_merger_{self.pr}"])
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "pr",
+ help="The Pull Request ID that should be merged into a release.",
+ )
+ parser.add_argument(
+ "--skip-validation",
+ "-s",
+ action="append",
+ help="Skip a specific validation, can be passed multiple times. I.e. -s status_checks -s approval",
+ )
+ parser.add_argument(
+ "--upstream-origin",
+ "-o",
+ default="upstream",
+ dest="upstream",
+ help="The name of the origin that we should push to. (default: upstream)",
+ )
+ parser.add_argument(
+ "--no-push",
+ action="store_true",
+ help="Run validations, rebase and fetch, but don't push.",
+ )
+ parser.add_argument(
+ "--validate-only", action="store_true", help="Only run the validations."
+ )
+ args = parser.parse_args()
+
+ merger = PRMerger(args)
+ if not merger.validate_pr():
+ print()
+ print(
+ "! Validations failed! Pass --skip-validation/-s <validation name> to pass this, can be passed multiple times"
+ )
+ sys.exit(1)
+
+ if args.validate_only:
+ print()
+ print("! --validate-only passed, will exit here")
+ sys.exit(0)
+
+ merger.rebase_pr()
+ merger.checkout_pr()
+
+ if args.no_push:
+ print()
+ print("! --no-push passed, will exit here")
+ sys.exit(0)
+
+ merger.push_upstream()
+ merger.delete_local_branch()
+
+ print()
+ print("> Done! Have a nice day!")
More information about the llvm-commits
mailing list