[AMDGPU] prevent shrinking udiv/urem if either operand is in (SignedMax,UnsignedMax...
[llvm-project.git] / llvm / utils / release / merge-release-pr.py
blobee908e00d06bab09abb1c81e4cba43f1ee6f2cf2
1 #!/usr/bin/env python3
2 # ===-- merge-release-pr.py ------------------------------------------------===#
4 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5 # See https://llvm.org/LICENSE.txt for license information.
6 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8 # ===------------------------------------------------------------------------===#
10 """
11 Helper script that will merge a Pull Request into a release branch. It will first
12 do some validations of the PR then rebase and finally push the changes to the
13 release branch.
15 Usage: merge-release-pr.py <PR id>
16 By default it will push to the 'upstream' origin, but you can pass
17 --upstream-origin/-o <origin> if you want to change it.
19 If you want to skip a specific validation, like the status checks you can
20 pass -s status_checks, this argument can be passed multiple times.
21 """
23 import argparse
24 import json
25 import subprocess
26 import sys
27 import time
28 from typing import List
31 class PRMerger:
32 def __init__(self, args):
33 self.args = args
35 def run_gh(self, gh_cmd: str, args: List[str]) -> str:
36 cmd = ["gh", gh_cmd, "-Rllvm/llvm-project"] + args
37 p = subprocess.run(cmd, capture_output=True)
38 if p.returncode != 0:
39 print(p.stderr)
40 raise RuntimeError("Failed to run gh")
41 return p.stdout
43 def validate_state(self, data):
44 """Validate the state of the PR, this means making sure that it is OPEN and not already merged or closed."""
45 state = data["state"]
46 if state != "OPEN":
47 return False, f"state is {state.lower()}, not open"
48 return True
50 def validate_target_branch(self, data):
51 """
52 Validate that the PR is targetting a release/ branch. We could
53 validate the exact branch here, but I am not sure how to figure
54 out what we want except an argument and that might be a bit to
55 to much overhead.
56 """
57 baseRefName: str = data["baseRefName"]
58 if not baseRefName.startswith("release/"):
59 return False, f"target branch is {baseRefName}, not a release branch"
60 return True
62 def validate_approval(self, data):
63 """
64 Validate the approval decision. This checks that the PR has been
65 approved.
66 """
67 if data["reviewDecision"] != "APPROVED":
68 return False, "PR is not approved"
69 return True
71 def validate_status_checks(self, data):
72 """
73 Check that all the actions / status checks succeeded. Will also
74 fail if we have status checks in progress.
75 """
76 failures = []
77 pending = []
78 for status in data["statusCheckRollup"]:
79 if "conclusion" in status and status["conclusion"] == "FAILURE":
80 failures.append(status)
81 if "status" in status and status["status"] == "IN_PROGRESS":
82 pending.append(status)
84 if failures or pending:
85 errstr = "\n"
86 if failures:
87 errstr += " FAILED: "
88 errstr += ", ".join([d["name"] for d in failures])
89 if pending:
90 if failures:
91 errstr += "\n"
92 errstr += " PENDING: "
93 errstr += ", ".join([d["name"] for d in pending])
95 return False, errstr
97 return True
99 def validate_commits(self, data):
101 Validate that the PR contains just one commit. If it has more
102 we might want to squash. Which is something we could add to
103 this script in the future.
105 if len(data["commits"]) > 1:
106 return False, f"More than 1 commit! {len(data['commits'])}"
107 return True
109 def _normalize_pr(self, parg: str):
110 if parg.isdigit():
111 return parg
112 elif parg.startswith("https://github.com/llvm/llvm-project/pull"):
113 # try to parse the following url https://github.com/llvm/llvm-project/pull/114089
114 i = parg[parg.rfind("/") + 1 :]
115 if not i.isdigit():
116 raise RuntimeError(f"{i} is not a number, malformatted input.")
117 return i
118 else:
119 raise RuntimeError(
120 f"PR argument must be PR ID or pull request URL - {parg} is wrong."
123 def load_pr_data(self):
124 self.args.pr = self._normalize_pr(self.args.pr)
125 fields_to_fetch = [
126 "baseRefName",
127 "commits",
128 "headRefName",
129 "headRepository",
130 "headRepositoryOwner",
131 "reviewDecision",
132 "state",
133 "statusCheckRollup",
134 "title",
135 "url",
137 print(f"> Loading PR {self.args.pr}...")
138 o = self.run_gh(
139 "pr",
140 ["view", self.args.pr, "--json", ",".join(fields_to_fetch)],
142 self.prdata = json.loads(o)
144 # save the baseRefName (target branch) so that we know where to push
145 self.target_branch = self.prdata["baseRefName"]
146 srepo = self.prdata["headRepository"]["name"]
147 sowner = self.prdata["headRepositoryOwner"]["login"]
148 self.source_url = f"https://github.com/{sowner}/{srepo}"
149 self.source_branch = self.prdata["headRefName"]
151 if srepo != "llvm-project":
152 print("The target repo is NOT llvm-project, check the PR!")
153 sys.exit(1)
155 if sowner == "llvm":
156 print(
157 "The source owner should never be github.com/llvm, double check the PR!"
159 sys.exit(1)
161 def validate_pr(self):
162 print(f"> Handling PR {self.args.pr} - {self.prdata['title']}")
163 print(f"> {self.prdata['url']}")
165 VALIDATIONS = {
166 "state": self.validate_state,
167 "target_branch": self.validate_target_branch,
168 "approval": self.validate_approval,
169 "commits": self.validate_commits,
170 "status_checks": self.validate_status_checks,
173 print()
174 print("> Validations:")
175 total_ok = True
176 for val_name, val_func in VALIDATIONS.items():
177 try:
178 validation_data = val_func(self.prdata)
179 except:
180 validation_data = False
181 ok = None
182 skipped = (
183 True
184 if (self.args.skip_validation and val_name in self.args.skip_validation)
185 else False
187 if isinstance(validation_data, bool) and validation_data:
188 ok = "OK"
189 elif isinstance(validation_data, tuple) and not validation_data[0]:
190 failstr = validation_data[1]
191 if skipped:
192 ok = "SKIPPED: "
193 else:
194 total_ok = False
195 ok = "FAIL: "
196 ok += failstr
197 else:
198 ok = "FAIL! (Unknown)"
199 print(f" * {val_name}: {ok}")
200 return total_ok
202 def rebase_pr(self):
203 print("> Fetching upstream")
204 subprocess.run(["git", "fetch", "--all"], check=True)
205 print("> Rebasing...")
206 subprocess.run(
207 ["git", "rebase", self.args.upstream + "/" + self.target_branch], check=True
209 print("> Publish rebase...")
210 subprocess.run(
211 ["git", "push", "--force", self.source_url, f"HEAD:{self.source_branch}"]
214 def checkout_pr(self):
215 print("> Fetching PR changes...")
216 self.merge_branch = "llvm_merger_" + self.args.pr
217 self.run_gh(
218 "pr",
220 "checkout",
221 self.args.pr,
222 "--force",
223 "--branch",
224 self.merge_branch,
228 # get the branch information so that we can use it for
229 # pushing later.
230 p = subprocess.run(
231 ["git", "config", f"branch.{self.merge_branch}.merge"],
232 check=True,
233 capture_output=True,
234 text=True,
236 upstream_branch = p.stdout.strip().replace("refs/heads/", "")
237 print(upstream_branch)
239 def push_upstream(self):
240 print("> Pushing changes...")
241 subprocess.run(
242 ["git", "push", self.args.upstream, "HEAD:" + self.target_branch],
243 check=True,
246 def delete_local_branch(self):
247 print("> Deleting the old branch...")
248 subprocess.run(["git", "switch", "main"])
249 subprocess.run(["git", "branch", "-D", f"llvm_merger_{self.args.pr}"])
252 if __name__ == "__main__":
253 parser = argparse.ArgumentParser()
254 parser.add_argument(
255 "pr",
256 help="The Pull Request ID that should be merged into a release. Can be number or URL",
258 parser.add_argument(
259 "--skip-validation",
260 "-s",
261 action="append",
262 help="Skip a specific validation, can be passed multiple times. I.e. -s status_checks -s approval",
264 parser.add_argument(
265 "--upstream-origin",
266 "-o",
267 default="upstream",
268 dest="upstream",
269 help="The name of the origin that we should push to. (default: upstream)",
271 parser.add_argument(
272 "--no-push",
273 action="store_true",
274 help="Run validations, rebase and fetch, but don't push.",
276 parser.add_argument(
277 "--validate-only", action="store_true", help="Only run the validations."
279 parser.add_argument(
280 "--rebase-only", action="store_true", help="Only rebase and exit"
282 args = parser.parse_args()
284 merger = PRMerger(args)
285 merger.load_pr_data()
287 if args.rebase_only:
288 merger.checkout_pr()
289 merger.rebase_pr()
290 merger.delete_local_branch()
291 sys.exit(0)
293 if not merger.validate_pr():
294 print()
295 print(
296 "! Validations failed! Pass --skip-validation/-s <validation name> to pass this, can be passed multiple times"
298 sys.exit(1)
300 if args.validate_only:
301 print()
302 print("! --validate-only passed, will exit here")
303 sys.exit(0)
305 merger.checkout_pr()
306 merger.rebase_pr()
308 if args.no_push:
309 print()
310 print("! --no-push passed, will exit here")
311 sys.exit(0)
313 merger.push_upstream()
314 merger.delete_local_branch()
316 print()
317 print("> Done! Have a nice day!")