11"""Subcommand to process pending backports."""
22
33import datetime
4+ from typing import Any
45
56from tools .private .release import changelog_news , gh , git
67from tools .private .release .release_issue import (
1112from 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 \n Work towards #{ args . issue } "
87+ new_msg = f"{ current_msg .strip ()} \n \n Work 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