Skip to content

Commit 0eaa599

Browse files
guangy10facebook-github-bot
authored andcommitted
Fix cherry-pick workflow (#2727)
Summary: Pull Request resolved: #2727 imported-using-ghimport Test Plan: Imported from OSS Reviewed By: mcr229 Differential Revision: D55450296 Pulled By: guangy10 fbshipit-source-id: 8037d1e06f14f1c7ace1fbe7aa150f1c8b4c697f
1 parent e7a429a commit 0eaa599

File tree

8 files changed

+3507
-1
lines changed

8 files changed

+3507
-1
lines changed

.github/scripts/cherry_pick.py

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

0 commit comments

Comments
 (0)