2121from pw import Patchwork
2222
2323
24+ class PatchworkSeries :
25+ """Represents a Patchwork series with its patches"""
26+
27+ def __init__ (self , patchwork : Patchwork , series_id : int , check_name : str ):
28+ """Initialize series
29+
30+ Args:
31+ patchwork: Patchwork client
32+ series_id: Series ID
33+ check_name: Check name to look for
34+ """
35+ self .patchwork = patchwork
36+ self .series_id = series_id
37+ self .check_name = check_name
38+ self .series_data = None
39+ self .patches = []
40+ self .patches_ready = []
41+
42+ # Fetch series and patch data
43+ self ._fetch ()
44+
45+ def _fetch (self ):
46+ """Fetch series and check which patches are ready"""
47+ self .series_data = self .patchwork .get ('series' , self .series_id )
48+ self .patches = self .series_data .get ('patches' , [])
49+
50+ # Check each patch for existing check
51+ self .patches_ready = []
52+ for i , patch in enumerate (self .patches ):
53+ patch_id = patch ['id' ]
54+ try :
55+ # Fetch checks for this patch
56+ existing_checks = self .patchwork .get_all (f'patches/{ patch_id } /checks' )
57+ check_exists = any (c .get ('context' ) == self .check_name for c in existing_checks )
58+ self .patches_ready .append (check_exists )
59+ except Exception as e :
60+ print (f" Warning: Error fetching checks for patch { i + 1 } (id={ patch_id } ): { e } " )
61+ self .patches_ready .append (False )
62+
63+ def all_patches_ready (self ) -> bool :
64+ """Check if all patches have the check entry
65+
66+ Returns:
67+ True if all patches have the check entry
68+ """
69+ return all (self .patches_ready )
70+
71+ def ready_count (self ) -> int :
72+ """Get count of patches that are ready
73+
74+ Returns:
75+ Number of patches with check entry
76+ """
77+ return sum (self .patches_ready )
78+
79+
2480class AirPatchworkSync :
2581 """Synchronize AIR reviews to Patchwork checks"""
2682
@@ -132,12 +188,12 @@ def get_review_details(self, review_id: str) -> Optional[Dict]:
132188 print (f"Error fetching review { review_id } : { e } " )
133189 return None
134190
135- def post_patchwork_check (self , series_id : int , review_id : str ,
191+ def post_patchwork_check (self , pw_series : PatchworkSeries , review_id : str ,
136192 review_data : Dict ) -> bool :
137193 """Post check result to Patchwork
138194
139195 Args:
140- series_id: Patchwork series ID
196+ pw_series: PatchworkSeries object with patches
141197 review_id: AIR review ID
142198 review_data: Review data with 'review' field containing per-patch results
143199
@@ -147,22 +203,14 @@ def post_patchwork_check(self, series_id: int, review_id: str,
147203 # Build check URL
148204 check_url = f"{ self .air_server } /ai-review.html?id={ review_id } "
149205
150- try :
151- # Fetch series to get individual patches
152- series = self .patchwork .get ('series' , series_id )
153- patches = series .get ('patches' , [])
154-
155- if not patches :
156- print (f" Warning: Series { series_id } has no patches" )
157- return False
206+ # Get review results (one per patch)
207+ reviews = review_data .get ('review' , [])
158208
159- # Get review results (one per patch)
160- reviews = review_data .get ('review' , [])
161-
162- print (f" Posting checks to { len (patches )} patches in series { series_id } " )
209+ print (f" Posting checks to { len (pw_series .patches )} patches in series { pw_series .series_id } " )
163210
211+ try :
164212 # Post check to each patch in the series
165- for i , patch in enumerate (patches ):
213+ for i , patch in enumerate (pw_series . patches ):
166214 patch_id = patch ['id' ]
167215
168216 # Check if this patch has review comments
@@ -174,7 +222,7 @@ def post_patchwork_check(self, series_id: int, review_id: str,
174222 state = 'warning' if patch_has_comments else 'success'
175223 desc = 'AI review found issues' if patch_has_comments else 'AI review completed'
176224
177- print (f" Patch { i + 1 } /{ len (patches )} (id={ patch_id } ): { state } " )
225+ print (f" Patch { i + 1 } /{ len (pw_series . patches )} (id={ patch_id } ): { state } " )
178226
179227 self .patchwork .post_check (patch = patch_id , name = self .check_name ,
180228 state = state , url = check_url , desc = desc )
@@ -191,7 +239,7 @@ def process_review(self, review: Dict) -> bool:
191239 review: Review summary from AIR
192240
193241 Returns:
194- True if processed (whether matched or not )
242+ True if processed successfully (checks posted to all patches )
195243 """
196244 review_id = review .get ('review_id' )
197245 status = review .get ('status' )
@@ -217,13 +265,31 @@ def process_review(self, review: Dict) -> bool:
217265
218266 print (f" Patchwork series ID: { pw_series_id } " )
219267
220- # Post check to Patchwork (will check each patch individually)
221- success = self .post_patchwork_check (pw_series_id , review_id , review_data )
268+ # Fetch series and check if patches are ready
269+ try :
270+ pw_series = PatchworkSeries (self .patchwork , pw_series_id , self .check_name )
271+ except Exception as e :
272+ print (f" Error fetching series: { e } " )
273+ return False
274+
275+ if not pw_series .patches :
276+ print (f" Warning: Series has no patches" )
277+ return True
278+
279+ # Check if all patches have the check entry (prevents race with initial scan)
280+ if not pw_series .all_patches_ready ():
281+ ready = pw_series .ready_count ()
282+ total = len (pw_series .patches )
283+ print (f" Not ready: only { ready } /{ total } patches have check '{ self .check_name } ' (will retry later)" )
284+ return False
285+
286+ # Post check to Patchwork
287+ success = self .post_patchwork_check (pw_series , review_id , review_data )
222288
223289 if success :
224- print (f" Successfully posted check to Patchwork " )
290+ print (f" Successfully posted checks to all patches " )
225291
226- return True
292+ return success
227293
228294 def run_once (self ):
229295 """Run one sync iteration"""
0 commit comments