github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/stale-green-ci.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package mungers 18 19 import ( 20 "fmt" 21 "sync" 22 "time" 23 24 "k8s.io/apimachinery/pkg/util/sets" 25 "k8s.io/test-infra/mungegithub/features" 26 "k8s.io/test-infra/mungegithub/github" 27 "k8s.io/test-infra/mungegithub/mungeopts" 28 "k8s.io/test-infra/mungegithub/options" 29 30 "github.com/golang/glog" 31 githubapi "github.com/google/go-github/github" 32 ) 33 34 const ( 35 staleGreenCIHours = 96 36 greenMsgFormat = `/test all 37 38 Tests are more than %d hours old. Re-running tests.` 39 ) 40 41 var greenMsgBody = fmt.Sprintf(greenMsgFormat, staleGreenCIHours) 42 43 // StaleGreenCI will re-run passed tests for LGTM PRs if they are more than 44 // 96 hours old. 45 type StaleGreenCI struct { 46 getRetestContexts func() []string 47 features *features.Features 48 opts *options.Options 49 50 waitingForPending map[int]struct{} 51 sync.Mutex 52 } 53 54 func init() { 55 s := &StaleGreenCI{} 56 RegisterMungerOrDie(s) 57 RegisterStaleIssueComments(s) 58 } 59 60 // Name is the name usable in --pr-mungers 61 func (s *StaleGreenCI) Name() string { return "stale-green-ci" } 62 63 // RequiredFeatures is a slice of 'features' that must be provided 64 func (s *StaleGreenCI) RequiredFeatures() []string { return []string{} } 65 66 // Initialize will initialize the munger 67 func (s *StaleGreenCI) Initialize(config *github.Config, features *features.Features) error { 68 s.features = features 69 s.waitingForPending = map[int]struct{}{} 70 return nil 71 } 72 73 // EachLoop is called at the start of every munge loop 74 func (s *StaleGreenCI) EachLoop() error { return nil } 75 76 // RegisterOptions registers options for this munger; returns any that require a restart when changed. 77 func (s *StaleGreenCI) RegisterOptions(opts *options.Options) sets.String { 78 s.opts = opts 79 return nil 80 } 81 82 // Munge is the workhorse the will actually make updates to the PR 83 func (s *StaleGreenCI) Munge(obj *github.MungeObject) { 84 if !obj.IsPR() { 85 return 86 } 87 88 // Avoid leaving multiple comments before the retest job is triggered. 89 s.Lock() 90 _, ok := s.waitingForPending[*obj.Issue.Number] 91 s.Unlock() 92 if ok { 93 return // Already commented with trigger command. Still waiting for pending state. 94 } 95 96 if !obj.HasLabel(lgtmLabel) { 97 return 98 } 99 100 if obj.HasLabel(retestNotRequiredLabel) || obj.HasLabel(retestNotRequiredDocsOnlyLabel) { 101 return 102 } 103 104 if mergeable, ok := obj.IsMergeable(); !mergeable || !ok { 105 return 106 } 107 108 s.opts.Lock() 109 requiredContexts := mungeopts.RequiredContexts.Retest 110 prMaxWaitTime := mungeopts.PRMaxWaitTime 111 s.opts.Unlock() 112 if success, ok := obj.IsStatusSuccess(requiredContexts); !success || !ok { 113 return 114 } 115 116 for _, context := range requiredContexts { 117 statusTime, ok := obj.GetStatusTime(context) 118 if statusTime == nil || !ok { 119 glog.Errorf("%d: unable to determine time %q context was set", *obj.Issue.Number, context) 120 return 121 } 122 if time.Since(*statusTime) > staleGreenCIHours*time.Hour { 123 err := obj.WriteComment(greenMsgBody) 124 if err != nil { 125 glog.Errorf("Failed to write retrigger old test comment") 126 return 127 } 128 s.Lock() 129 s.waitingForPending[*obj.Issue.Number] = struct{}{} 130 s.Unlock() 131 go s.waitForPending(requiredContexts, obj, prMaxWaitTime) 132 return 133 } 134 } 135 } 136 137 // waitForPending is an asynchronous wrapper for obj.WaitForPending that marks the obj as handled 138 // when the status changes to pending or the timeout expires. 139 func (s *StaleGreenCI) waitForPending(requiredContexts []string, obj *github.MungeObject, maxWait time.Duration) { 140 if !obj.WaitForPending(requiredContexts, maxWait) { 141 glog.Errorf("Failed waiting for PR #%d to start testing", *obj.Issue.Number) 142 } 143 s.Lock() 144 defer s.Unlock() 145 delete(s.waitingForPending, *obj.Issue.Number) 146 } 147 148 func (s *StaleGreenCI) isStaleIssueComment(obj *github.MungeObject, comment *githubapi.IssueComment) bool { 149 if !obj.IsRobot(comment.User) { 150 return false 151 } 152 if *comment.Body != greenMsgBody { 153 return false 154 } 155 stale := commentBeforeLastCI(obj, comment, mungeopts.RequiredContexts.Retest) 156 if stale { 157 glog.V(6).Infof("Found stale StaleGreenCI comment") 158 } 159 return stale 160 } 161 162 // StaleIssueComments returns a slice of stale issue comments. 163 func (s *StaleGreenCI) StaleIssueComments(obj *github.MungeObject, comments []*githubapi.IssueComment) []*githubapi.IssueComment { 164 return forEachCommentTest(obj, comments, s.isStaleIssueComment) 165 } 166 167 func commentBeforeLastCI(obj *github.MungeObject, comment *githubapi.IssueComment, requiredContexts []string) bool { 168 if success, ok := obj.IsStatusSuccess(requiredContexts); !success || !ok { 169 return false 170 } 171 if comment.CreatedAt == nil { 172 return false 173 } 174 commentTime := *comment.CreatedAt 175 176 for _, context := range requiredContexts { 177 statusTimeP, ok := obj.GetStatusTime(context) 178 if statusTimeP == nil || !ok { 179 return false 180 } 181 statusTime := statusTimeP.Add(30 * time.Minute) 182 if commentTime.After(statusTime) { 183 return false 184 } 185 } 186 return true 187 }