[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