github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/cherrypick-queue.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 "encoding/json" 21 "fmt" 22 "net/http" 23 "sort" 24 "sync" 25 "time" 26 27 "k8s.io/apimachinery/pkg/util/sets" 28 "k8s.io/test-infra/mungegithub/features" 29 "k8s.io/test-infra/mungegithub/github" 30 "k8s.io/test-infra/mungegithub/options" 31 32 "github.com/golang/glog" 33 ) 34 35 const ( 36 cpCandidateLabel = "cherrypick-candidate" 37 cpApprovedLabel = "cherrypick-approved" 38 ) 39 40 var ( 41 _ = fmt.Print 42 maxTime = time.Unix(1<<63-62135596801, 999999999) // http://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go 43 ) 44 45 type rawReadyInfo struct { 46 Number int 47 Title string 48 SHA string 49 } 50 51 type cherrypickStatus struct { 52 statusPullRequest 53 ExtraInfo []string 54 } 55 56 type queueData struct { 57 MergedAndApproved []cherrypickStatus 58 Merged []cherrypickStatus 59 Unmerged []cherrypickStatus 60 } 61 62 // CherrypickQueue will merge PR which meet a set of requirements. 63 type CherrypickQueue struct { 64 sync.Mutex 65 mergedAndApproved map[int]*github.MungeObject 66 merged map[int]*github.MungeObject 67 unmerged map[int]*github.MungeObject 68 } 69 70 func init() { 71 RegisterMungerOrDie(&CherrypickQueue{}) 72 } 73 74 // Name is the name usable in --pr-mungers 75 func (c *CherrypickQueue) Name() string { return "cherrypick-queue" } 76 77 // RequiredFeatures is a slice of 'features' that must be provided 78 func (c *CherrypickQueue) RequiredFeatures() []string { return []string{features.ServerFeatureName} } 79 80 // Initialize will initialize the munger 81 func (c *CherrypickQueue) Initialize(config *github.Config, features *features.Features) error { 82 c.Lock() 83 defer c.Unlock() 84 85 c.mergedAndApproved = map[int]*github.MungeObject{} 86 c.merged = map[int]*github.MungeObject{} 87 c.unmerged = map[int]*github.MungeObject{} 88 89 if features.Server.Enabled { 90 features.Server.HandleFunc("/queue", c.serveQueue) 91 features.Server.HandleFunc("/raw", c.serveRaw) 92 features.Server.HandleFunc("/queue-info", c.serveQueueInfo) 93 } 94 return nil 95 } 96 97 // EachLoop is called at the start of every munge loop 98 func (c *CherrypickQueue) EachLoop() error { return nil } 99 100 // RegisterOptions registers options for this munger; returns any that require a restart when changed. 101 func (c *CherrypickQueue) RegisterOptions(opts *options.Options) sets.String { return nil } 102 103 // Munge is the workhorse the will actually make updates to the PR 104 func (c *CherrypickQueue) Munge(obj *github.MungeObject) { 105 num := *obj.Issue.Number 106 107 if !obj.HasLabel(cpCandidateLabel) { 108 c.Lock() 109 // Make sure we don't track PR that don't have the flag 110 delete(c.mergedAndApproved, num) 111 delete(c.merged, num) 112 delete(c.unmerged, num) 113 c.Unlock() 114 } 115 if !obj.IsPR() { 116 return 117 } 118 // This will cache the PR and events so when we try to view the queue we don't 119 // hit github while trying to load the page 120 obj.GetPR() 121 122 c.Lock() 123 // Delete the PR before we re-add it where it should 124 delete(c.mergedAndApproved, num) 125 delete(c.merged, num) 126 delete(c.unmerged, num) 127 merged, _ := obj.IsMerged() 128 if merged { 129 if obj.HasLabel(cpApprovedLabel) { 130 c.mergedAndApproved[num] = obj 131 } else { 132 c.merged[num] = obj 133 } 134 } else { 135 c.unmerged[num] = obj 136 } 137 c.Unlock() 138 return 139 } 140 141 func mergeTime(obj *github.MungeObject) time.Time { 142 t, ok := obj.MergedAt() 143 if !ok || t == nil { 144 t = &maxTime 145 } 146 return *t 147 } 148 149 type cpQueueSorter []*github.MungeObject 150 151 func (s cpQueueSorter) Len() int { return len(s) } 152 func (s cpQueueSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 153 154 // PLEASE PLEASE PLEASE update serveQueueInfo() if you update this. 155 func (s cpQueueSorter) Less(i, j int) bool { 156 a := s[i] 157 b := s[j] 158 159 // Sort first based on release milestone 160 // ignore errors because ReleaseMilestoneDue returns a time WAY in the future on errors 161 aDue, _ := a.ReleaseMilestoneDue() 162 bDue, _ := b.ReleaseMilestoneDue() 163 if aDue.Before(bDue) { 164 return true 165 } else if aDue.After(bDue) { 166 return false 167 } 168 169 // Then sort by the order in which they were merged 170 aTime := mergeTime(a) 171 bTime := mergeTime(b) 172 if aTime.Before(bTime) { 173 return true 174 } else if aTime.After(bTime) { 175 return false 176 } 177 178 // Then show those which have been approved 179 aApproved := a.HasLabel(cpApprovedLabel) 180 bApproved := b.HasLabel(cpApprovedLabel) 181 if aApproved && !bApproved { 182 return true 183 } else if !aApproved && bApproved { 184 return false 185 } 186 187 // Sort by LGTM as humans are likely to want to approve 188 // those first. After it merges the above check will win 189 // and LGTM won't matter 190 aLGTM := a.HasLabel(lgtmLabel) 191 bLGTM := b.HasLabel(lgtmLabel) 192 if aLGTM && !bLGTM { 193 return true 194 } else if !aLGTM && bLGTM { 195 return false 196 } 197 198 // And finally by issue number, just so there is some consistency 199 return *a.Issue.Number < *b.Issue.Number 200 } 201 202 func orderedQueue(queue map[int]*github.MungeObject) []int { 203 objs := []*github.MungeObject{} 204 for _, obj := range queue { 205 objs = append(objs, obj) 206 } 207 sort.Sort(cpQueueSorter(objs)) 208 209 var ordered []int 210 for _, obj := range objs { 211 ordered = append(ordered, *obj.Issue.Number) 212 } 213 return ordered 214 } 215 216 // copyQueue returns a copy of the queue. 217 func (c *CherrypickQueue) copyQueue(queue map[int]*github.MungeObject) map[int]*github.MungeObject { 218 c.Lock() 219 defer c.Unlock() 220 221 out := map[int]*github.MungeObject{} 222 for i, v := range queue { 223 out[i] = v 224 } 225 return out 226 } 227 228 func (c *CherrypickQueue) serveRaw(res http.ResponseWriter, req *http.Request) { 229 c.Lock() 230 queue := c.copyQueue(c.mergedAndApproved) 231 c.Unlock() 232 keyOrder := orderedQueue(queue) 233 sortedQueue := []rawReadyInfo{} 234 for _, key := range keyOrder { 235 obj := queue[key] 236 sha, ok := obj.MergeCommit() 237 if !ok || sha == nil { 238 empty := "UnknownSHA" 239 sha = &empty 240 } 241 rri := rawReadyInfo{ 242 Number: *obj.Issue.Number, 243 Title: *obj.Issue.Title, 244 SHA: *sha, 245 } 246 sortedQueue = append(sortedQueue, rri) 247 } 248 data, err := json.Marshal(sortedQueue) 249 if err != nil { 250 res.Header().Set("Content-type", "text/plain") 251 res.WriteHeader(http.StatusInternalServerError) 252 glog.Errorf("Unable to Marshal Status: %v: %v", data, err) 253 return 254 } 255 res.Header().Set("Content-type", "application/json") 256 res.WriteHeader(http.StatusOK) 257 res.Write(data) 258 } 259 260 func (c *CherrypickQueue) getQueueData(queue map[int]*github.MungeObject) []cherrypickStatus { 261 out := []cherrypickStatus{} 262 keyOrder := orderedQueue(queue) 263 for _, key := range keyOrder { 264 obj := queue[key] 265 cps := cherrypickStatus{ 266 statusPullRequest: *objToStatusPullRequest(obj), 267 } 268 if obj.HasLabel(cpApprovedLabel) { 269 cps.ExtraInfo = append(cps.ExtraInfo, cpApprovedLabel) 270 } 271 milestone, ok := obj.ReleaseMilestone() 272 if ok && milestone != "" { 273 cps.ExtraInfo = append(cps.ExtraInfo, milestone) 274 } 275 merged, _ := obj.IsMerged() 276 if !merged && obj.HasLabel(lgtmLabel) { 277 // Don't bother showing LGTM for merged things 278 // it's just a distraction at that point 279 cps.ExtraInfo = append(cps.ExtraInfo, lgtmLabel) 280 } 281 out = append(out, cps) 282 } 283 284 return out 285 } 286 287 func (c *CherrypickQueue) serveQueue(res http.ResponseWriter, req *http.Request) { 288 outData := queueData{} 289 290 c.Lock() 291 outData.MergedAndApproved = c.getQueueData(c.mergedAndApproved) 292 outData.Merged = c.getQueueData(c.merged) 293 outData.Unmerged = c.getQueueData(c.unmerged) 294 c.Unlock() 295 296 data, err := json.Marshal(outData) 297 if err != nil { 298 res.Header().Set("Content-type", "text/plain") 299 res.WriteHeader(http.StatusInternalServerError) 300 glog.Errorf("Unable to Marshal Status: %v: %v", data, err) 301 return 302 } 303 res.Header().Set("Content-type", "application/json") 304 res.WriteHeader(http.StatusOK) 305 res.Write(data) 306 } 307 308 func (c *CherrypickQueue) serveQueueInfo(res http.ResponseWriter, req *http.Request) { 309 res.Header().Set("Content-type", "text/plain") 310 res.WriteHeader(http.StatusOK) 311 res.Write([]byte(`The cherrypick queue is sorted by the following. If there is a tie in any test the next test will be used. 312 <ol> 313 <li>Milestone Due Date 314 <ul> 315 <li>Release milestones must be of the form vX.Y</li> 316 <li>PRs without a milestone are considered after PRs with a milestone</li> 317 </ul> 318 </li> 319 <li>Labeld with "` + cpApprovedLabel + `" 320 <ul> 321 <li>PRs with the "` + cpApprovedLabel + `" label come before those without</li> 322 </ul> 323 </li> 324 <li>Merge Time 325 <ul> 326 <li>The earlier a PR was merged the earlier it is in the list</li> 327 <li>PRs which have not merged are considered 'after' any merged PR</li> 328 </ul> 329 </li> 330 <li>Labeld with "` + lgtmLabel + `" 331 <ul> 332 <li>PRs with the "` + lgtmLabel + `" label come before those without</li> 333 </ul> 334 <li>PR number</li> 335 </ol> `)) 336 }