[llvm] f3e950a - [Utils] Add new merge-release-pr.py script. (#101630)

via llvm-commits llvm-commits at lists.llvm.org
Sat Aug 10 12:02:42 PDT 2024


Author: Tobias Hieta
Date: 2024-08-10T21:02:38+02:00
New Revision: f3e950a2fe04dcfcc9202125c99d29451df2be0c

URL: https://github.com/llvm/llvm-project/commit/f3e950a2fe04dcfcc9202125c99d29451df2be0c
DIFF: https://github.com/llvm/llvm-project/commit/f3e950a2fe04dcfcc9202125c99d29451df2be0c.diff

LOG: [Utils] Add new merge-release-pr.py script. (#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.

Added: 
    llvm/utils/release/merge-release-pr.py

Modified: 
    

Removed: 
    


################################################################################
diff  --git a/llvm/utils/release/merge-release-pr.py b/llvm/utils/release/merge-release-pr.py
new file mode 100755
index 00000000000000..b9275a2203f49a
--- /dev/null
+++ b/llvm/utils/release/merge-release-pr.py
@@ -0,0 +1,254 @@
+#!/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.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
+
+    def validate_state(self, data):
+        """Validate the state of the PR, this means making sure that it is OPEN and not already merged or closed."""
+        state = data["state"]
+        if state != "OPEN":
+            return False, f"state is {state.lower()}, not open"
+        return True
+
+    def validate_target_branch(self, data):
+        """
+        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.
+        """
+        baseRefName: str = data["baseRefName"]
+        if not baseRefName.startswith("release/"):
+            return False, f"target branch is {baseRefName}, not a release branch"
+        return True
+
+    def validate_approval(self, data):
+        """
+        Validate the approval decision. This checks that the PR has been
+        approved.
+        """
+        if data["reviewDecision"] != "APPROVED":
+            return False, "PR is not approved"
+        return True
+
+    def validate_status_checks(self, data):
+        """
+        Check that all the actions / status checks succeeded. Will also
+        fail if we have status checks in progress.
+        """
+        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
+
+    def validate_commits(self, data):
+        """
+        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.
+        """
+        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.args.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.args.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.args.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.args.pr,
+                "--force",
+                "--branch",
+                "llvm_merger_" + self.args.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.args.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