@@ -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 )
@@ -582,6 +583,10 @@ func (c *syncController) Sync() error {
582583 c .statusUpdate .poolPRs = poolPRMap (filteredPools )
583584 c .statusUpdate .baseSHAs = baseSHAMap (filteredPools )
584585 c .statusUpdate .requiredContexts = requiredContextsMap (filteredPools )
586+ // Prune old PR entries from context history to prevent memory leak
587+ if c .statusUpdate .contextHistory != nil {
588+ c .statusUpdate .contextHistory .prune (c .statusUpdate .poolPRs )
589+ }
585590 select {
586591 case c .statusUpdate .newPoolPending <- true :
587592 c .statusUpdate .dontUpdateStatus .reset ()
@@ -675,7 +680,11 @@ func (c *syncController) filterSubpools(mergeAllowed func(*CodeReviewCommon) (st
675680 return
676681 }
677682 key := poolKey (sp .org , sp .repo , sp .branch )
678- if spFiltered := filterSubpool (c .provider , mergeAllowed , sp ); spFiltered != nil {
683+ var ch * contextHistory
684+ if c .statusUpdate != nil {
685+ ch = c .statusUpdate .contextHistory
686+ }
687+ if spFiltered := filterSubpool (c .provider , mergeAllowed , sp , ch ); spFiltered != nil {
679688 sp .log .WithField ("key" , key ).WithField ("pool" , spFiltered ).Debug ("filtered sub-pool" )
680689
681690 lock .Lock ()
@@ -727,10 +736,10 @@ func (c *syncController) initSubpoolData(sp *subpool) error {
727736// should be deleted.
728737//
729738// This function works for any source code provider.
730- func filterSubpool (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool ) * subpool {
739+ func filterSubpool (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool , ch * contextHistory ) * subpool {
731740 var toKeep []CodeReviewCommon
732741 for _ , pr := range sp .prs {
733- if ! filterPR (provider , mergeAllowed , sp , & pr ) {
742+ if ! filterPR (provider , mergeAllowed , sp , & pr , ch ) {
734743 toKeep = append (toKeep , pr )
735744 }
736745 }
@@ -752,7 +761,7 @@ func filterSubpool(provider provider, mergeAllowed func(*CodeReviewCommon) (stri
752761// retesting them.)
753762//
754763// This function works for any source code provider.
755- func filterPR (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool , pr * CodeReviewCommon ) bool {
764+ func filterPR (provider provider , mergeAllowed func (* CodeReviewCommon ) (string , error ), sp * subpool , pr * CodeReviewCommon , ch * contextHistory ) bool {
756765 log := sp .log .WithFields (pr .logFields ())
757766 // Skip PRs that are known to be unmergeable.
758767 if reason , err := mergeAllowed (pr ); err != nil {
@@ -778,7 +787,7 @@ func filterPR(provider provider, mergeAllowed func(*CodeReviewCommon) (string, e
778787 }
779788 return false
780789 }
781- for _ , ctx := range unsuccessfulContexts (contexts , sp .cc [pr .Number ], log ) {
790+ for _ , ctx := range unsuccessfulContexts (contexts , sp .cc [pr .Number ], pr , ch , log ) {
782791 if ctx .State != githubql .StatusStatePending {
783792 log .WithField ("context" , ctx .Context ).Debug ("filtering out PR as unsuccessful context is not pending" )
784793 return true
@@ -853,7 +862,11 @@ func (c *syncController) isPassingTests(log *logrus.Entry, pr *CodeReviewCommon,
853862 // If we can't get the status of the commit, assume that it is failing.
854863 return false
855864 }
856- unsuccessful := unsuccessfulContexts (contexts , cc , log )
865+ var ch * contextHistory
866+ if c .statusUpdate != nil {
867+ ch = c .statusUpdate .contextHistory
868+ }
869+ unsuccessful := unsuccessfulContexts (contexts , cc , pr , ch , log )
857870 return len (unsuccessful ) == 0
858871}
859872
@@ -862,8 +875,12 @@ func (c *syncController) isPassingTests(log *logrus.Entry, pr *CodeReviewCommon,
862875// If the branchProtection is set to only check for required checks, we will skip
863876// all non-required tests. If required tests are missing from the list, they will be
864877// added to the list of failed contexts.
865- func unsuccessfulContexts (contexts []Context , cc contextChecker , log * logrus.Entry ) []Context {
878+ // It also detects when required contexts disappear (e.g., when GitHub Actions are re-triggered)
879+ // and treats them as PENDING to prevent premature merging (addresses issue #337).
880+ func unsuccessfulContexts (contexts []Context , cc contextChecker , pr * CodeReviewCommon , ch * contextHistory , log * logrus.Entry ) []Context {
866881 var failed []Context
882+ contextNames := contextsToStrings (contexts )
883+
867884 for _ , ctx := range contexts {
868885 if string (ctx .Context ) == statusContext {
869886 continue
@@ -875,13 +892,40 @@ func unsuccessfulContexts(contexts []Context, cc contextChecker, log *logrus.Ent
875892 failed = append (failed , ctx )
876893 }
877894 }
878- for _ , c := range cc .MissingRequiredContexts (contextsToStrings (contexts )) {
879- failed = append (failed , newExpectedContext (c ))
895+
896+ // Track which contexts are added as failed to avoid duplicates
897+ failedSet := sets .New [string ]()
898+ for _ , ctx := range failed {
899+ failedSet .Insert (string (ctx .Context ))
900+ }
901+
902+ // Add missing required contexts
903+ for _ , c := range cc .MissingRequiredContexts (contextNames ) {
904+ if ! failedSet .Has (c ) {
905+ failed = append (failed , newExpectedContext (c ))
906+ failedSet .Insert (c )
907+ }
908+ }
909+
910+ // Check for disappeared contexts (race condition fix for issue #337)
911+ // When a GitHub Action is re-triggered, the check may temporarily disappear
912+ // from the status before the new run starts. Detect this and treat disappeared
913+ // required contexts as non-passing to prevent premature merging.
914+ if ch != nil && pr != nil {
915+ for _ , c := range ch .checkAndUpdate (pr , contextNames ) {
916+ if ! cc .IsOptional (c ) && ! failedSet .Has (c ) {
917+ failed = append (failed , Context {
918+ Context : githubql .String (c ),
919+ State : githubql .StatusStatePending ,
920+ Description : githubql .String ("Context disappeared - likely re-triggered" ),
921+ })
922+ }
923+ }
880924 }
881925
882926 log .WithFields (logrus.Fields {
883927 "total_context_count" : len (contexts ),
884- "context_names" : contextsToStrings ( contexts ) ,
928+ "context_names" : contextNames ,
885929 "failed_context_count" : len (failed ),
886930 "failed_context_names" : contextsToStrings (failed ),
887931 }).Debug ("Filtered out failed contexts" )
0 commit comments