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 # ===------------------------------------------------------------------------===#
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
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.
28 from typing
import List
32 def __init__(self
, 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)
40 raise RuntimeError("Failed to run gh")
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."""
47 return False, f
"state is {state.lower()}, not open"
50 def validate_target_branch(self
, data
):
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
57 baseRefName
: str = data
["baseRefName"]
58 if not baseRefName
.startswith("release/"):
59 return False, f
"target branch is {baseRefName}, not a release branch"
62 def validate_approval(self
, data
):
64 Validate the approval decision. This checks that the PR has been
67 if data
["reviewDecision"] != "APPROVED":
68 return False, "PR is not approved"
71 def validate_status_checks(self
, data
):
73 Check that all the actions / status checks succeeded. Will also
74 fail if we have status checks in progress.
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
:
88 errstr
+= ", ".join([d
["name"] for d
in failures
])
92 errstr
+= " PENDING: "
93 errstr
+= ", ".join([d
["name"] for d
in pending
])
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'])}"
109 def _normalize_pr(self
, parg
: str):
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 :]
116 raise RuntimeError(f
"{i} is not a number, malformatted input.")
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
)
130 "headRepositoryOwner",
137 print(f
"> Loading PR {self.args.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!")
157 "The source owner should never be github.com/llvm, double check the PR!"
161 def validate_pr(self
):
162 print(f
"> Handling PR {self.args.pr} - {self.prdata['title']}")
163 print(f
"> {self.prdata['url']}")
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
,
174 print("> Validations:")
176 for val_name
, val_func
in VALIDATIONS
.items():
178 validation_data
= val_func(self
.prdata
)
180 validation_data
= False
184 if (self
.args
.skip_validation
and val_name
in self
.args
.skip_validation
)
187 if isinstance(validation_data
, bool) and validation_data
:
189 elif isinstance(validation_data
, tuple) and not validation_data
[0]:
190 failstr
= validation_data
[1]
198 ok
= "FAIL! (Unknown)"
199 print(f
" * {val_name}: {ok}")
203 print("> Fetching upstream")
204 subprocess
.run(["git", "fetch", "--all"], check
=True)
205 print("> Rebasing...")
207 ["git", "rebase", self
.args
.upstream
+ "/" + self
.target_branch
], check
=True
209 print("> Publish rebase...")
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
228 # get the branch information so that we can use it for
231 ["git", "config", f
"branch.{self.merge_branch}.merge"],
236 upstream_branch
= p
.stdout
.strip().replace("refs/heads/", "")
237 print(upstream_branch
)
239 def push_upstream(self
):
240 print("> Pushing changes...")
242 ["git", "push", self
.args
.upstream
, "HEAD:" + self
.target_branch
],
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()
256 help="The Pull Request ID that should be merged into a release. Can be number or URL",
262 help="Skip a specific validation, can be passed multiple times. I.e. -s status_checks -s approval",
269 help="The name of the origin that we should push to. (default: upstream)",
274 help="Run validations, rebase and fetch, but don't push.",
277 "--validate-only", action
="store_true", help="Only run the validations."
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()
290 merger
.delete_local_branch()
293 if not merger
.validate_pr():
296 "! Validations failed! Pass --skip-validation/-s <validation name> to pass this, can be passed multiple times"
300 if args
.validate_only
:
302 print("! --validate-only passed, will exit here")
310 print("! --no-push passed, will exit here")
313 merger
.push_upstream()
314 merger
.delete_local_branch()
317 print("> Done! Have a nice day!")