Skip to content

Commit 25cc295

Browse files
huydhnfacebook-github-bot
authored andcommitted
Add cherry pick script from PT (#2716)
Summary: The copy from third_party/pytorch doesn't work because it points to PyTorch Pull Request resolved: #2716 Reviewed By: guangy10 Differential Revision: D55434703 Pulled By: huydhn fbshipit-source-id: 32ccb466a12d7c602e20e9e0b5fe412641574a4d
1 parent a19a32b commit 25cc295

File tree

7 files changed

+3473
-1
lines changed

7 files changed

+3473
-1
lines changed

.github/scripts/cherry_pick.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import os
5+
import re
6+
from typing import Any, Optional
7+
8+
from urllib.error import HTTPError
9+
10+
from github_utils import gh_fetch_url, gh_post_pr_comment
11+
12+
from gitutils import get_git_remote_name, get_git_repo_dir, GitRepo
13+
from trymerge import get_pr_commit_sha, GitHubPR
14+
15+
16+
# This is only a suggestion for now, not a strict requirement
17+
REQUIRES_ISSUE = {
18+
"regression",
19+
"critical",
20+
"fixnewfeature",
21+
}
22+
23+
24+
def parse_args() -> Any:
25+
from argparse import ArgumentParser
26+
27+
parser = ArgumentParser("cherry pick a landed PR onto a release branch")
28+
parser.add_argument(
29+
"--onto-branch", type=str, required=True, help="the target release branch"
30+
)
31+
parser.add_argument(
32+
"--github-actor", type=str, required=True, help="all the world’s a stage"
33+
)
34+
parser.add_argument(
35+
"--classification",
36+
choices=["regression", "critical", "fixnewfeature", "docs", "release"],
37+
required=True,
38+
help="the cherry pick category",
39+
)
40+
parser.add_argument("pr_num", type=int)
41+
parser.add_argument(
42+
"--fixes",
43+
type=str,
44+
default="",
45+
help="the GitHub issue that the cherry pick fixes",
46+
)
47+
parser.add_argument("--dry-run", action="store_true")
48+
49+
return parser.parse_args()
50+
51+
52+
def get_merge_commit_sha(repo: GitRepo, pr: GitHubPR) -> Optional[str]:
53+
"""
54+
Return the merge commit SHA iff the PR has been merged. For simplicity, we
55+
will only cherry pick PRs that have been merged into main
56+
"""
57+
commit_sha = get_pr_commit_sha(repo, pr)
58+
return commit_sha if pr.is_closed() else None
59+
60+
61+
def cherry_pick(
62+
github_actor: str,
63+
repo: GitRepo,
64+
pr: GitHubPR,
65+
commit_sha: str,
66+
onto_branch: str,
67+
classification: str,
68+
fixes: str,
69+
dry_run: bool = False,
70+
) -> None:
71+
"""
72+
Create a local branch to cherry pick the commit and submit it as a pull request
73+
"""
74+
current_branch = repo.current_branch()
75+
cherry_pick_branch = create_cherry_pick_branch(
76+
github_actor, repo, pr, commit_sha, onto_branch
77+
)
78+
79+
try:
80+
if not dry_run:
81+
org, project = repo.gh_owner_and_name()
82+
cherry_pick_pr = submit_pr(repo, pr, cherry_pick_branch, onto_branch)
83+
84+
msg = f"The cherry pick PR is at {cherry_pick_pr}"
85+
if fixes:
86+
msg += f" and it is linked with issue {fixes}"
87+
elif classification in REQUIRES_ISSUE:
88+
msg += f" and it is recommended to link a {classification} cherry pick PR with an issue"
89+
90+
post_comment(org, project, pr.pr_num, msg)
91+
92+
finally:
93+
if current_branch:
94+
repo.checkout(branch=current_branch)
95+
96+
97+
def create_cherry_pick_branch(
98+
github_actor: str, repo: GitRepo, pr: GitHubPR, commit_sha: str, onto_branch: str
99+
) -> str:
100+
"""
101+
Create a local branch and cherry pick the commit. Return the name of the local
102+
cherry picking branch.
103+
"""
104+
repo.checkout(branch=onto_branch)
105+
repo._run_git("submodule", "update", "--init", "--recursive")
106+
107+
# Remove all special characters if we want to include the actor in the branch name
108+
github_actor = re.sub("[^0-9a-zA-Z]+", "_", github_actor)
109+
110+
cherry_pick_branch = f"cherry-pick-{pr.pr_num}-by-{github_actor}"
111+
repo.create_branch_and_checkout(branch=cherry_pick_branch)
112+
113+
# We might want to support ghstack later
114+
repo._run_git("cherry-pick", "-x", "-X", "theirs", commit_sha)
115+
repo.push(branch=cherry_pick_branch, dry_run=False)
116+
117+
return cherry_pick_branch
118+
119+
120+
def submit_pr(
121+
repo: GitRepo,
122+
pr: GitHubPR,
123+
cherry_pick_branch: str,
124+
onto_branch: str,
125+
) -> str:
126+
"""
127+
Submit the cherry pick PR and return the link to the PR
128+
"""
129+
org, project = repo.gh_owner_and_name()
130+
131+
default_msg = f"Cherry pick #{pr.pr_num} onto {onto_branch} branch"
132+
title = pr.info.get("title", default_msg)
133+
body = pr.info.get("body", default_msg)
134+
135+
try:
136+
response = gh_fetch_url(
137+
f"https://api.github.com/repos/{org}/{project}/pulls",
138+
method="POST",
139+
data={
140+
"title": title,
141+
"body": body,
142+
"head": cherry_pick_branch,
143+
"base": onto_branch,
144+
},
145+
headers={"Accept": "application/vnd.github.v3+json"},
146+
reader=json.load,
147+
)
148+
149+
cherry_pick_pr = response.get("html_url", "")
150+
if not cherry_pick_pr:
151+
raise RuntimeError(
152+
f"Fail to find the cherry pick PR: {json.dumps(response)}"
153+
)
154+
155+
return str(cherry_pick_pr)
156+
157+
except HTTPError as error:
158+
msg = f"Fail to submit the cherry pick PR: {error}"
159+
raise RuntimeError(msg) from error
160+
161+
162+
def post_comment(org: str, project: str, pr_num: int, msg: str) -> None:
163+
"""
164+
Post a comment on the PR itself to point to the cherry picking PR when success
165+
or print the error when failure
166+
"""
167+
internal_debugging = ""
168+
169+
run_url = os.getenv("GH_RUN_URL")
170+
# Post a comment to tell folks that the PR is being cherry picked
171+
if run_url is not None:
172+
internal_debugging = "\n".join(
173+
line
174+
for line in (
175+
"<details><summary>Details for Dev Infra team</summary>",
176+
f'Raised by <a href="{run_url}">workflow job</a>\n',
177+
"</details>",
178+
)
179+
if line
180+
)
181+
182+
comment = "\n".join(
183+
(f"### Cherry picking #{pr_num}", f"{msg}", "", f"{internal_debugging}")
184+
)
185+
gh_post_pr_comment(org, project, pr_num, comment)
186+
187+
188+
def main() -> None:
189+
args = parse_args()
190+
pr_num = args.pr_num
191+
192+
repo = GitRepo(get_git_repo_dir(), get_git_remote_name())
193+
org, project = repo.gh_owner_and_name()
194+
195+
pr = GitHubPR(org, project, pr_num)
196+
197+
try:
198+
commit_sha = get_merge_commit_sha(repo, pr)
199+
if not commit_sha:
200+
raise RuntimeError(
201+
f"Refuse to cherry pick #{pr_num} because it hasn't been merged yet"
202+
)
203+
204+
cherry_pick(
205+
args.github_actor,
206+
repo,
207+
pr,
208+
commit_sha,
209+
args.onto_branch,
210+
args.classification,
211+
args.fixes,
212+
args.dry_run,
213+
)
214+
215+
except RuntimeError as error:
216+
if not args.dry_run:
217+
post_comment(org, project, pr_num, str(error))
218+
else:
219+
raise error
220+
221+
222+
if __name__ == "__main__":
223+
main()

0 commit comments

Comments
 (0)