@@ -584,22 +584,28 @@ def mark_test_failed(db, test, repository, message: str) -> bool:
584584 # Continue to try GitHub update even if DB fails
585585
586586 # Step 2: Try to update GitHub status (CRITICAL - must not be skipped)
587+ # Use retry logic since this is critical to prevent stuck "pending" status
587588 try :
588- gh_commit = repository .get_commit (test .commit )
589- # Include target_url so the status links to the test page
589+ # Build target_url first (doesn't need retry)
590590 from flask import url_for
591591 try :
592592 target_url = url_for ('test.by_id' , test_id = test .id , _external = True )
593593 except RuntimeError :
594594 # Outside of request context
595595 target_url = f"https://sampleplatform.ccextractor.org/test/{ test .id } "
596- update_status_on_github (gh_commit , Status .ERROR , message , f"CI - { test .platform .value } " , target_url )
596+
597+ # Use retry_with_backoff for GitHub API calls
598+ def update_github_status ():
599+ gh_commit = repository .get_commit (test .commit )
600+ update_status_on_github (gh_commit , Status .ERROR , message , f"CI - { test .platform .value } " , target_url )
601+
602+ retry_with_backoff (update_github_status , max_retries = 3 , initial_backoff = 2.0 )
597603 github_success = True
598604 log .info (f"Test { test .id } : GitHub status updated to ERROR: { message } " )
599605 except GithubException as e :
600- log .error (f"Test { test .id } : GitHub API error while updating status: { e .status } - { e .data } " )
606+ log .error (f"Test { test .id } : GitHub API error while updating status (after retries) : { e .status } - { e .data } " )
601607 except Exception as e :
602- log .error (f"Test { test .id } : Failed to update GitHub status: { type (e ).__name__ } : { e } " )
608+ log .error (f"Test { test .id } : Failed to update GitHub status (after retries) : { type (e ).__name__ } : { e } " )
603609
604610 # Log final status
605611 if db_success and github_success :
@@ -663,8 +669,33 @@ def _diagnose_missing_artifact(repository, commit_sha: str, platform, log) -> tu
663669 f"'{ workflow_run .conclusion } '. Check the GitHub Actions logs for details." )
664670 return (message , False ) # Not retryable
665671 else :
666- # Build succeeded but artifact not found - may have expired
667- # This is a PERMANENT failure
672+ # Build succeeded but artifact not found
673+ # Check if the build completed very recently - if so, this might be
674+ # GitHub API propagation delay (artifact exists but not visible yet)
675+ # In that case, treat as retryable
676+ ARTIFACT_PROPAGATION_GRACE_PERIOD = 300 # 5 minutes in seconds
677+ try :
678+ from datetime import datetime , timezone
679+ now = datetime .now (timezone .utc )
680+ # workflow_run.updated_at is when the run completed
681+ if workflow_run .updated_at :
682+ completed_at = workflow_run .updated_at
683+ if completed_at .tzinfo is None :
684+ completed_at = completed_at .replace (tzinfo = timezone .utc )
685+ seconds_since_completion = (now - completed_at ).total_seconds ()
686+ if seconds_since_completion < ARTIFACT_PROPAGATION_GRACE_PERIOD :
687+ message = (f"Build completed recently ({ int (seconds_since_completion )} s ago): "
688+ f"'{ expected_workflow } ' succeeded but artifact not yet visible. "
689+ f"Will retry (GitHub API propagation delay)." )
690+ log .info (f"Artifact not found but build completed { int (seconds_since_completion )} s ago - "
691+ f"treating as retryable (possible API propagation delay)" )
692+ return (message , True ) # Retryable - API propagation delay
693+ except Exception as e :
694+ log .warning (f"Could not check workflow completion time: { e } " )
695+ # Fall through to permanent failure
696+
697+ # Build completed more than 5 minutes ago - artifact should be visible
698+ # This is a PERMANENT failure (artifact expired or not uploaded)
668699 message = (f"Artifact not found: '{ expected_workflow } ' completed successfully, "
669700 f"but no artifact was found. The artifact may have expired (GitHub deletes "
670701 f"artifacts after a retention period) or was not uploaded properly." )
@@ -1959,8 +1990,16 @@ def progress_type_request(log, test, test_id, request) -> bool:
19591990 else :
19601991 message = progress .message
19611992
1962- gh_commit = repository .get_commit (test .commit )
1963- update_status_on_github (gh_commit , state , message , context , target_url = target_url )
1993+ # Use retry logic for final GitHub status update to prevent stuck "pending" states
1994+ # This is critical - if this fails, the PR will show "Tests queued" forever
1995+ try :
1996+ def update_final_status ():
1997+ gh_commit = repository .get_commit (test .commit )
1998+ update_status_on_github (gh_commit , state , message , context , target_url = target_url )
1999+
2000+ retry_with_backoff (update_final_status , max_retries = 3 , initial_backoff = 2.0 )
2001+ except Exception as e :
2002+ log .error (f"Test { test_id } : Failed to update final GitHub status after retries: { e } " )
19642003
19652004 if status in [TestStatus .completed , TestStatus .canceled ]:
19662005 # Delete the current instance
0 commit comments