@@ -406,6 +406,7 @@ func NewController(
406406 statusUpdate := & statusUpdate {
407407 dontUpdateStatus : & threadSafePRSet {},
408408 newPoolPending : make (chan bool ),
409+ contextHistory : newContextHistory (),
409410 }
410411
411412 sc , err := newStatusController (ctx , logger , ghcStatus , mgr , gc , cfg , opener , statusURI , mergeChecker , usesGitHubAppsAuth , statusUpdate )
@@ -675,7 +676,11 @@ func (c *syncController) filterSubpools(mergeAllowed func(*CodeReviewCommon) (st
675676 return
676677 }
677678 key := poolKey (sp .org , sp .repo , sp .branch )
678- if spFiltered := filterSubpool (c .provider , mergeAllowed , sp ); spFiltered != nil {
679+ var ch * contextHistory
680+ if c .statusUpdate != nil {
681+ ch = c .statusUpdate .contextHistory
682+ }
683+ if spFiltered := filterSubpool (c .provider , mergeAllowed , sp , ch ); spFiltered != nil {
679684 sp .log .WithField ("key" , key ).WithField ("pool" , spFiltered ).Debug ("filtered sub-pool" )
680685
681686 lock .Lock ()
@@ -727,10 +732,10 @@ func (c *syncController) initSubpoolData(sp *subpool) error {
727732// should be deleted.
728733//
729734// This function works for any source code provider.
730- func filterSubpool (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool ) * subpool {
735+ func filterSubpool (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool , ch * contextHistory ) * subpool {
731736 var toKeep []CodeReviewCommon
732737 for _ , pr := range sp .prs {
733- if ! filterPR (provider , mergeAllowed , sp , & pr ) {
738+ if ! filterPR (provider , mergeAllowed , sp , & pr , ch ) {
734739 toKeep = append (toKeep , pr )
735740 }
736741 }
@@ -752,7 +757,7 @@ func filterSubpool(provider provider, mergeAllowed func(*CodeReviewCommon) (stri
752757// retesting them.)
753758//
754759// This function works for any source code provider.
755- func filterPR (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool , pr * CodeReviewCommon ) bool {
760+ func filterPR (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool , pr * CodeReviewCommon , ch * contextHistory ) bool {
756761 log := sp .log .WithFields (pr .logFields ())
757762 // Skip PRs that are known to be unmergeable.
758763 if reason , err := mergeAllowed (pr ); err != nil {
@@ -778,7 +783,7 @@ func filterPR(provider provider, mergeAllowed func(*CodeReviewCommon) (string, e
778783 }
779784 return false
780785 }
781- for _ , ctx := range unsuccessfulContexts (contexts , sp .cc [pr .Number ], log ) {
786+ for _ , ctx := range unsuccessfulContexts (contexts , sp .cc [pr .Number ], pr , ch , log ) {
782787 if ctx .State != githubql .StatusStatePending {
783788 log .WithField ("context" , ctx .Context ).Debug ("filtering out PR as unsuccessful context is not pending" )
784789 return true
@@ -853,7 +858,11 @@ func (c *syncController) isPassingTests(log *logrus.Entry, pr *CodeReviewCommon,
853858 // If we can't get the status of the commit, assume that it is failing.
854859 return false
855860 }
856- unsuccessful := unsuccessfulContexts (contexts , cc , log )
861+ var ch * contextHistory
862+ if c .statusUpdate != nil {
863+ ch = c .statusUpdate .contextHistory
864+ }
865+ unsuccessful := unsuccessfulContexts (contexts , cc , pr , ch , log )
857866 return len (unsuccessful ) == 0
858867}
859868
@@ -862,8 +871,12 @@ func (c *syncController) isPassingTests(log *logrus.Entry, pr *CodeReviewCommon,
862871// If the branchProtection is set to only check for required checks, we will skip
863872// all non-required tests. If required tests are missing from the list, they will be
864873// added to the list of failed contexts.
865- func unsuccessfulContexts (contexts []Context , cc contextChecker , log * logrus.Entry ) []Context {
874+ // It also detects when required contexts disappear (e.g., when GitHub Actions are re-triggered)
875+ // and treats them as PENDING to prevent premature merging (addresses issue #337).
876+ func unsuccessfulContexts (contexts []Context , cc contextChecker , pr * CodeReviewCommon , ch * contextHistory , log * logrus.Entry ) []Context {
866877 var failed []Context
878+ contextNames := contextsToStrings (contexts )
879+
867880 for _ , ctx := range contexts {
868881 if string (ctx .Context ) == statusContext {
869882 continue
@@ -875,13 +888,31 @@ func unsuccessfulContexts(contexts []Context, cc contextChecker, log *logrus.Ent
875888 failed = append (failed , ctx )
876889 }
877890 }
878- for _ , c := range cc .MissingRequiredContexts (contextsToStrings (contexts )) {
891+
892+ // Add missing required contexts
893+ for _ , c := range cc .MissingRequiredContexts (contextNames ) {
879894 failed = append (failed , newExpectedContext (c ))
880895 }
881896
897+ // Check for disappeared contexts (race condition fix for issue #337)
898+ // When a GitHub Action is re-triggered, the check may temporarily disappear
899+ // from the status before the new run starts. Detect this and treat disappeared
900+ // required contexts as non-passing to prevent premature merging.
901+ if ch != nil && pr != nil {
902+ for _ , c := range ch .checkAndUpdate (pr , contextNames ) {
903+ if ! cc .IsOptional (c ) {
904+ failed = append (failed , Context {
905+ Context : githubql .String (c ),
906+ State : githubql .StatusStatePending ,
907+ Description : githubql .String ("Context disappeared - likely re-triggered" ),
908+ })
909+ }
910+ }
911+ }
912+
882913 log .WithFields (logrus.Fields {
883914 "total_context_count" : len (contexts ),
884- "context_names" : contextsToStrings ( contexts ) ,
915+ "context_names" : contextNames ,
885916 "failed_context_count" : len (failed ),
886917 "failed_context_names" : contextsToStrings (failed ),
887918 }).Debug ("Filtered out failed contexts" )
0 commit comments