@@ -286,6 +286,97 @@ def is_check_failed(check: dict) -> bool:
286286 log_warning (" Mergeable status still UNKNOWN after retries" )
287287 return all_passed , has_failures , "UNKNOWN"
288288
289+ def _handle_no_checks (
290+ self , rollup : list [dict ], attempt : int , check_interval : int
291+ ) -> CheckStatus | None :
292+ """Handle case when no checks are found.
293+
294+ Parameters
295+ ----------
296+ rollup : list[dict]
297+ Raw check rollup from GitHub API.
298+ attempt : int
299+ Current attempt number.
300+ check_interval : int
301+ Seconds to wait between checks.
302+
303+ Returns
304+ -------
305+ CheckStatus | None
306+ "NO_CHECKS" if no checks found after initial attempts, None to continue.
307+
308+ """
309+ if not rollup :
310+ if attempt > 2 : # Give checks time to start
311+ log_warning (" No checks found" )
312+ return "NO_CHECKS"
313+ time .sleep (check_interval )
314+ return None
315+
316+ # Filter out phantom checks
317+ relevant_checks = [c for c in rollup if self ._should_check_be_counted (c )]
318+ if not relevant_checks :
319+ if attempt > 2 :
320+ log_warning (" No relevant checks found" )
321+ return "NO_CHECKS"
322+ time .sleep (check_interval )
323+ return None
324+
325+ # Continue with relevant checks
326+ return None
327+
328+ def _evaluate_check_status (
329+ self ,
330+ relevant_checks : list [dict ],
331+ stable_count_duration : int ,
332+ min_stable_duration : int ,
333+ ) -> CheckStatus | None :
334+ """Evaluate status of relevant checks.
335+
336+ Parameters
337+ ----------
338+ relevant_checks : list[dict]
339+ Filtered checks to evaluate.
340+ stable_count_duration : int
341+ How long check count has been stable (seconds).
342+ min_stable_duration : int
343+ Minimum stability duration before declaring failure (seconds).
344+
345+ Returns
346+ -------
347+ CheckStatus | None
348+ Final status if determined, None to continue waiting.
349+
350+ """
351+ any_running = any (self ._is_check_running (c ) for c in relevant_checks )
352+ any_failed = any (self ._is_check_failed (c ) for c in relevant_checks )
353+ all_passed = all (self ._is_check_passed (c ) for c in relevant_checks )
354+ all_finalized = all (self ._has_finalized_conclusion (c ) for c in relevant_checks )
355+
356+ if not any_running and all_finalized :
357+ if any_failed :
358+ # Wait for check count to stabilize before declaring failure
359+ if stable_count_duration >= min_stable_duration :
360+ log_error (" Checks failed" )
361+ return "FAILED"
362+ log_info (
363+ f" ⏳ Some checks failed, but waiting for check count to "
364+ f"stabilize ({ stable_count_duration } s/{ min_stable_duration } s) "
365+ "to ensure all checks have appeared..."
366+ )
367+ elif all_passed :
368+ log_success (" Checks completed successfully" )
369+ return "COMPLETED"
370+ else :
371+ log_info (" ⏳ Checks finalized but not all passed, waiting..." )
372+ elif not any_running and not all_finalized :
373+ log_info (
374+ " ⏳ Checks appear done but conclusions not finalized yet, "
375+ "waiting..."
376+ )
377+
378+ return None
379+
289380 def wait_for_checks_completion (
290381 self ,
291382 pr : PRQueueItem ,
@@ -296,6 +387,10 @@ def wait_for_checks_completion(
296387 Polls every 30 seconds up to timeout_minutes.
297388 Similar to fix-remote-pr.yml:603-673.
298389
390+ To prevent premature failure detection, we track check count stability.
391+ We only return "FAILED" after the check count has been stable for 60s,
392+ ensuring all checks have appeared in GitHub's API.
393+
299394 Parameters
300395 ----------
301396 pr : PRQueueItem
@@ -311,6 +406,11 @@ def wait_for_checks_completion(
311406 """
312407 check_interval = 30
313408 max_attempts = (timeout_minutes * 60 ) // check_interval
409+ min_stable_duration = 60
410+
411+ # Track check count stability to ensure all checks have appeared
412+ last_check_count = 0
413+ stable_count_duration = 0
314414
315415 log_info (
316416 f" ⏳ Waiting up to { timeout_minutes } minutes for checks to complete..."
@@ -335,48 +435,37 @@ def wait_for_checks_completion(
335435 data = json .loads (status_json )
336436 rollup = data .get ("statusCheckRollup" ) or []
337437
338- if not rollup :
339- if attempt > 2 : # Give checks time to start
340- log_warning ( " No checks found" )
341- return "NO_CHECKS"
342- time . sleep ( check_interval )
438+ # Handle no checks case
439+ result = self . _handle_no_checks ( rollup , attempt , check_interval )
440+ if result == "NO_CHECKS" :
441+ return result
442+ if result is None and not rollup :
343443 continue
344444
345- # Filter out checks that should be ignored (phantom entries )
445+ # Get relevant checks (already filtered by _handle_no_checks )
346446 relevant_checks = [c for c in rollup if self ._should_check_be_counted (c )]
347-
348447 if not relevant_checks :
349- if attempt > 2 : # Give checks time to start
350- log_warning (" No relevant checks found" )
351- return "NO_CHECKS"
352- time .sleep (check_interval )
353448 continue
354449
355- # Check status of relevant checks
356- any_running = any (self ._is_check_running (c ) for c in relevant_checks )
357- any_failed = any (self ._is_check_failed (c ) for c in relevant_checks )
358- all_passed = all (self ._is_check_passed (c ) for c in relevant_checks )
359- all_finalized = all (
360- self ._has_finalized_conclusion (c ) for c in relevant_checks
361- )
362-
363- if not any_running and all_finalized :
364- if any_failed :
365- log_error (" Checks failed" )
366- return "FAILED"
367- if all_passed :
368- log_success (" Checks completed successfully" )
369- return "COMPLETED"
370- # Checks are finalized but not all passed - still waiting
371- log_info (" ⏳ Checks finalized but not all passed, waiting..." )
372-
373- # Debug: Show why we're still waiting
374- if not any_running and not all_finalized :
450+ # Track check count stability
451+ current_check_count = len (relevant_checks )
452+ if current_check_count == last_check_count :
453+ stable_count_duration += check_interval
454+ else :
455+ stable_count_duration = 0
456+ last_check_count = current_check_count
375457 log_info (
376- " ⏳ Checks appear done but conclusions not finalized yet , "
377- "waiting... "
458+ f " Check count changed to { current_check_count } , "
459+ "resetting stability timer "
378460 )
379461
462+ # Evaluate check status
463+ status = self ._evaluate_check_status (
464+ relevant_checks , stable_count_duration , min_stable_duration
465+ )
466+ if status :
467+ return status
468+
380469 if attempt < max_attempts :
381470 time .sleep (check_interval )
382471
0 commit comments