Skip to content

Commit 027e35e

Browse files
committed
refactor(release): factor out loops in process-backports into helpers
1 parent 2b92a0d commit 027e35e

1 file changed

Lines changed: 110 additions & 75 deletions

File tree

tools/private/release/process_backports.py

Lines changed: 110 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Subcommand to process pending backports."""
22

33
import datetime
4+
from typing import Any
45

56
from tools.private.release import changelog_news, gh, git
67
from tools.private.release.release_issue import (
@@ -11,46 +12,9 @@
1112
from tools.private.release.utils import get_latest_rc_tag
1213

1314

14-
def cmd_process_backports(args):
15-
"""Executes the process-backports subcommand."""
16-
body = gh.get_issue_body(args.issue)
17-
items = parse_backports(body)
18-
19-
pending_items = [
20-
item
21-
for item in items
22-
if not item.checked and not item.status.startswith("error-")
23-
]
24-
25-
if not pending_items:
26-
print("No pending backports found.")
27-
return 0
28-
29-
print(f"Found {len(pending_items)} pending backports to process.")
30-
31-
# Determine branch name from issue title
32-
issue_title = gh.get_issue_title(args.issue)
33-
version_match = RELEASE_TITLE_RE.search(issue_title)
34-
if not version_match:
35-
print(f"Error: Could not parse version from issue title: {issue_title}")
36-
return 1
37-
38-
version = version_match.group(1)
39-
branch_version = ".".join(version.split(".")[:2])
40-
branch_name = f"release/{branch_version}"
41-
42-
# Determine next RC tag to write to backport metadata
43-
git.fetch(args.remote, tags=True, force=True)
44-
latest_rc = get_latest_rc_tag(version, remote=args.remote)
45-
if not latest_rc:
46-
next_rc_suffix = "rc0"
47-
else:
48-
rc_num = int(latest_rc.split("-rc")[-1])
49-
next_rc_suffix = f"rc{rc_num + 1}"
50-
51-
# Resolve PRs to merge commits using gh helper.
52-
pr_commit_infos = gh.get_merge_commits_for_prs(pending_items)
53-
15+
def _process_pr_commit_infos(
16+
pr_commit_infos, body, issue, dry_run
17+
) -> tuple[list[str], dict[str, Any], list[str], list[str], str]:
5418
shas = []
5519
sha_to_item = {}
5620
failed_prs = []
@@ -64,11 +28,9 @@ def cmd_process_backports(args):
6428
print(f"PR {item.pr_ref} is open or draft. Ignoring.")
6529
ignored_prs.append(item.pr_ref)
6630
else:
67-
# Handle case where PR could not be resolved to a merge commit.
68-
# We immediately mark it as failed in the tracking issue.
6931
failed_prs.append(item.pr_ref)
7032
status_to_set = item.status or "error-unmerged-pr"
71-
if args.dry_run:
33+
if dry_run:
7234
print(
7335
f"[DRY RUN] Would update tracking issue checklist for unresolved PR {item.pr_ref} to status={status_to_set}"
7436
)
@@ -83,58 +45,50 @@ def cmd_process_backports(args):
8345
checked=False,
8446
metadata={"status": status_to_set},
8547
)
86-
gh.update_issue_body(args.issue, body)
48+
gh.update_issue_body(issue, body)
8749
except Exception as e:
8850
print(
8951
f"ERROR: Failed to update tracking issue for unresolved PR {item.pr_ref}: {e}"
9052
)
91-
92-
if not shas:
93-
print("No valid merge commits to process.")
94-
if failed_prs:
95-
print("Failed PRs:")
96-
for pr in failed_prs:
97-
print(f"- {pr}")
98-
return 1
99-
return 0
100-
101-
# Verify workspace is clean before proceeding
102-
if git.status():
103-
print(
104-
"ERROR: Git workspace is dirty. Please commit or stash changes before running backports."
105-
)
106-
return 1
107-
108-
# Sort chronologically using git helper
109-
sorted_shas = git.sort_commits_chronologically(shas)
110-
111-
git.fetch(args.remote)
112-
git.checkout(branch_name, track_remote=args.remote)
113-
53+
return shas, sha_to_item, failed_prs, ignored_prs, body
54+
55+
56+
def _cherry_pick_and_update_prs(
57+
sorted_shas,
58+
sha_to_item,
59+
body,
60+
issue,
61+
remote,
62+
dry_run,
63+
version,
64+
branch_name,
65+
next_rc_suffix,
66+
) -> tuple[list[str], str]:
67+
failed_prs = []
11468
for sha in sorted_shas:
11569
item = sha_to_item[sha]
11670
print(f"Cherry-picking {item.pr_ref} / {sha}...")
11771
try:
118-
git.cherry_pick(sha, no_commit=args.dry_run)
72+
git.cherry_pick(sha, no_commit=dry_run)
11973

12074
# Perform news processing (merging news/ files into the changelog)
12175
print(f"Merging news fragments into changelog for PR {item.pr_ref}...")
12276
release_date = datetime.date.today().strftime("%Y-%m-%d")
12377
changelog_news.update_changelog(version, release_date)
12478

125-
if not args.dry_run:
79+
if not dry_run:
12680
# Stage changelog changes and news/ deletions
12781
git.add("CHANGELOG.md", "news/")
12882

12983
# Amend cherry-pick commit to include news merging and deletions,
13084
# and reference the release tracking issue.
13185
print(f"Amending cherry-pick commit for PR {item.pr_ref}...")
13286
current_msg = git.get_commit_message("HEAD")
133-
new_msg = f"{current_msg.strip()}\n\nWork towards #{args.issue}"
87+
new_msg = f"{current_msg.strip()}\n\nWork towards #{issue}"
13488
git.commit(new_msg, amend=True)
13589

13690
# Push amended commit
137-
git.push(args.remote, branch_name)
91+
git.push(remote, branch_name)
13892

13993
new_sha = git.get_commit_sha("HEAD", short=True)
14094
metadata = {"status": "done", "rc": next_rc_suffix, "commit": new_sha}
@@ -143,7 +97,7 @@ def cmd_process_backports(args):
14397
body = update_task_in_body(
14498
body, item.pr_ref, checked=True, metadata=metadata
14599
)
146-
gh.update_issue_body(args.issue, body)
100+
gh.update_issue_body(issue, body)
147101
except Exception as e:
148102
print(
149103
f"ERROR: Failed to update tracking issue for PR {item.pr_ref}: {e}"
@@ -164,7 +118,7 @@ def cmd_process_backports(args):
164118
pass
165119
failed_prs.append(item.pr_ref)
166120

167-
if args.dry_run:
121+
if dry_run:
168122
print(
169123
f"[DRY RUN] Would update tracking issue checklist for failed PR {item.pr_ref} to status=error-merge-conflict"
170124
)
@@ -179,7 +133,7 @@ def cmd_process_backports(args):
179133
checked=False,
180134
metadata={"status": "error-merge-conflict"},
181135
)
182-
gh.update_issue_body(args.issue, body)
136+
gh.update_issue_body(issue, body)
183137
print(
184138
f"Updated back port of {item.pr_ref} to status=error-merge-conflict (unchecked)"
185139
)
@@ -188,8 +142,89 @@ def cmd_process_backports(args):
188142
f"ERROR: Failed to update tracking issue for failed PR {item.pr_ref}: {e}"
189143
)
190144
finally:
191-
if args.dry_run:
145+
if dry_run:
192146
git.reset_hard("HEAD")
147+
return failed_prs, body
148+
149+
150+
def cmd_process_backports(args):
151+
"""Executes the process-backports subcommand."""
152+
body = gh.get_issue_body(args.issue)
153+
items = parse_backports(body)
154+
155+
pending_items = [
156+
item
157+
for item in items
158+
if not item.checked and not item.status.startswith("error-")
159+
]
160+
161+
if not pending_items:
162+
print("No pending backports found.")
163+
return 0
164+
165+
print(f"Found {len(pending_items)} pending backports to process.")
166+
167+
# Determine branch name from issue title
168+
issue_title = gh.get_issue_title(args.issue)
169+
version_match = RELEASE_TITLE_RE.search(issue_title)
170+
if not version_match:
171+
print(f"Error: Could not parse version from issue title: {issue_title}")
172+
return 1
173+
174+
version = version_match.group(1)
175+
branch_version = ".".join(version.split(".")[:2])
176+
branch_name = f"release/{branch_version}"
177+
178+
# Determine next RC tag to write to backport metadata
179+
git.fetch(args.remote, tags=True, force=True)
180+
latest_rc = get_latest_rc_tag(version, remote=args.remote)
181+
if not latest_rc:
182+
next_rc_suffix = "rc0"
183+
else:
184+
rc_num = int(latest_rc.split("-rc")[-1])
185+
next_rc_suffix = f"rc{rc_num + 1}"
186+
187+
# Resolve PRs to merge commits using gh helper.
188+
pr_commit_infos = gh.get_merge_commits_for_prs(pending_items)
189+
190+
shas, sha_to_item, failed_prs, ignored_prs, body = _process_pr_commit_infos(
191+
pr_commit_infos, body, args.issue, args.dry_run
192+
)
193+
194+
if not shas:
195+
print("No valid merge commits to process.")
196+
if failed_prs:
197+
print("Failed PRs:")
198+
for pr in failed_prs:
199+
print(f"- {pr}")
200+
return 1
201+
return 0
202+
203+
# Verify workspace is clean before proceeding
204+
if git.status():
205+
print(
206+
"ERROR: Git workspace is dirty. Please commit or stash changes before running backports."
207+
)
208+
return 1
209+
210+
# Sort chronologically using git helper
211+
sorted_shas = git.sort_commits_chronologically(shas)
212+
213+
git.fetch(args.remote)
214+
git.checkout(branch_name, track_remote=args.remote)
215+
216+
new_failed_prs, body = _cherry_pick_and_update_prs(
217+
sorted_shas,
218+
sha_to_item,
219+
body,
220+
args.issue,
221+
args.remote,
222+
args.dry_run,
223+
version,
224+
branch_name,
225+
next_rc_suffix,
226+
)
227+
failed_prs.extend(new_failed_prs)
193228

194229
if failed_prs:
195230
print("ERROR: One or more cherry-picks/resolutions failed:")

0 commit comments

Comments
 (0)