github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/submit-queue-batch.go (about) 1 /* 2 Copyright 2016 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 "errors" 21 "fmt" 22 "strconv" 23 "strings" 24 "sync/atomic" 25 "time" 26 27 "github.com/golang/glog" 28 githubapi "github.com/google/go-github/github" 29 "k8s.io/test-infra/mungegithub/github" 30 "k8s.io/test-infra/mungegithub/mungeopts" 31 ) 32 33 type batchPull struct { 34 Number int 35 Sha string 36 } 37 38 // Batch represents a specific merge state: 39 // a base branch and SHA, and the SHAs of each PR merged into it. 40 type Batch struct { 41 BaseName string 42 BaseSha string 43 Pulls []batchPull 44 } 45 46 func (b *Batch) String() string { 47 out := b.BaseName + ":" + b.BaseSha 48 for _, pull := range b.Pulls { 49 out += "," + strconv.Itoa(pull.Number) + ":" + pull.Sha 50 } 51 return out 52 } 53 54 // batchRefToBatch parses a string into a Batch. 55 // The input is a comma-separated list of colon-separated ref/sha pairs, 56 // like "master:abcdef0,123:f00d,456:f00f". 57 func batchRefToBatch(batchRef string) (Batch, error) { 58 batch := Batch{} 59 for i, ref := range strings.Split(batchRef, ",") { 60 parts := strings.Split(ref, ":") 61 if len(parts) != 2 { 62 return Batch{}, errors.New("bad batchref: " + batchRef) 63 } 64 if i == 0 { 65 batch.BaseName = parts[0] 66 batch.BaseSha = parts[1] 67 } else { 68 num, err := strconv.ParseInt(parts[0], 10, 32) 69 if err != nil { 70 return Batch{}, fmt.Errorf("bad batchref: %s (%v)", batchRef, err) 71 } 72 batch.Pulls = append(batch.Pulls, batchPull{int(num), parts[1]}) 73 } 74 } 75 return batch, nil 76 } 77 78 // getCompleteBatches returns a list of Batches that passed all 79 // required tests. 80 func (sq *SubmitQueue) getCompleteBatches(jobs prowJobs) []Batch { 81 // for each batch specifier, a set of successful contexts 82 batchContexts := make(map[string]map[string]interface{}) 83 for _, job := range jobs { 84 if batchContexts[job.Refs] == nil { 85 batchContexts[job.Refs] = make(map[string]interface{}) 86 } 87 batchContexts[job.Refs][job.Context] = nil 88 } 89 batches := []Batch{} 90 for batchRef, contexts := range batchContexts { 91 match := true 92 // Did this succeed in all the contexts we want? 93 sq.opts.Lock() 94 mergeContexts := mungeopts.RequiredContexts.Merge 95 retestContexts := mungeopts.RequiredContexts.Retest 96 sq.opts.Unlock() 97 for _, ctx := range mergeContexts { 98 if _, ok := contexts[ctx]; !ok { 99 match = false 100 } 101 } 102 for _, ctx := range retestContexts { 103 if _, ok := contexts[ctx]; !ok { 104 match = false 105 } 106 } 107 if match { 108 batch, err := batchRefToBatch(batchRef) 109 if err != nil { 110 continue 111 } 112 batches = append(batches, batch) 113 } 114 } 115 return batches 116 } 117 118 // batchIntersectsQueue returns whether at least one PR in the batch is queued. 119 func (sq *SubmitQueue) batchIntersectsQueue(batch Batch) bool { 120 sq.Lock() 121 defer sq.Unlock() 122 for _, pull := range batch.Pulls { 123 if _, ok := sq.githubE2EQueue[pull.Number]; ok { 124 return true 125 } 126 } 127 return false 128 } 129 130 // matchesCommit determines if the batch can be merged given some commits. 131 // That is, does it contain exactly: 132 // 1) the batch's BaseSha 133 // 2) (optional) merge commits for PRs in the batch 134 // 3) any merged PRs in the batch are sequential from the beginning 135 // The return value is the number of PRs already merged, and any errors. 136 func (b *Batch) matchesCommits(commits []*githubapi.RepositoryCommit) (int, error) { 137 if len(commits) == 0 { 138 return 0, errors.New("no commits") 139 } 140 141 shaToPR := make(map[string]int) 142 143 for _, pull := range b.Pulls { 144 shaToPR[pull.Sha] = pull.Number 145 } 146 147 matchedPRs := []int{} 148 149 // convert the list of commits into a DAG for easy following 150 dag := make(map[string]*githubapi.RepositoryCommit) 151 for _, commit := range commits { 152 dag[*commit.SHA] = commit 153 } 154 155 ref := *commits[0].SHA 156 for { 157 if ref == b.BaseSha { 158 break // found the base ref (condition #1) 159 } 160 commit, ok := dag[ref] 161 if !ok { 162 return 0, errors.New("ran out of commits (missing ref " + ref + ")") 163 } 164 message := "" 165 if commit.Commit != nil && commit.Commit.Message != nil { 166 // The actual commit message is buried a little oddly. 167 message = *commit.Commit.Message 168 } 169 if len(commit.Parents) == 2 && strings.HasPrefix(message, "Merge") { 170 // looks like a merge commit! 171 172 // first parent is the normal branch 173 ref = *commit.Parents[0].SHA 174 // second parent is the PR 175 pr, ok := shaToPR[*commit.Parents[1].SHA] 176 if !ok { 177 return 0, errors.New("Merge of something not in batch") 178 } 179 matchedPRs = append(matchedPRs, pr) 180 } else { 181 return 0, errors.New("Unknown non-merge commit " + ref) 182 } 183 } 184 185 // Now, ensure that the merged PRs are ordered correctly. 186 for i, pr := range matchedPRs { 187 if b.Pulls[len(matchedPRs)-1-i].Number != pr { 188 return 0, errors.New("Batch PRs merged out-of-order") 189 } 190 } 191 return len(matchedPRs), nil 192 } 193 194 // batchIsApplicable returns whether a successful batch result can be used-- 195 // 1) some of the batch is still unmerged and in the queue. 196 // 2) the recent commits are the batch head ref or merges of batch PRs. 197 // 3) all unmerged PRs in the batch are still in the queue. 198 // The return value is the number of PRs already merged, and any errors. 199 func (sq *SubmitQueue) batchIsApplicable(batch Batch) (int, error) { 200 // batch must intersect the queue 201 if !sq.batchIntersectsQueue(batch) { 202 return 0, errors.New("batch has no PRs in Queue") 203 } 204 commits, err := sq.githubConfig.GetBranchCommits(batch.BaseName, 100) 205 if err != nil { 206 glog.Errorf("Error getting commits for batchIsApplicable: %v", err) 207 return 0, errors.New("failed to get branch commits: " + err.Error()) 208 } 209 return batch.matchesCommits(commits) 210 } 211 212 func (sq *SubmitQueue) handleGithubE2EBatchMerge() { 213 repo := sq.githubConfig.Org + "/" + sq.githubConfig.Project 214 for range time.Tick(1 * time.Minute) { 215 sq.opts.Lock() 216 url := sq.ProwURL + "/data.js" 217 sq.opts.Unlock() 218 allJobs, err := getJobs(url) 219 if err != nil { 220 glog.Errorf("Error reading batch jobs from Prow URL %v: %v", url, err) 221 continue 222 } 223 batchJobs := allJobs.batch().repo(repo) 224 jobs := batchJobs.successful() 225 batches := sq.getCompleteBatches(jobs) 226 batchErrors := make(map[string]string) 227 for _, batch := range batches { 228 _, err := sq.batchIsApplicable(batch) 229 if err != nil { 230 batchErrors[batch.String()] = err.Error() 231 continue 232 } 233 sq.doBatchMerge(batch) 234 } 235 sq.batchStatus.Error = batchErrors 236 sq.batchStatus.Running = batchJobs.firstUnfinished() 237 } 238 } 239 240 // doBatchMerge iteratively merges PRs in the batch if possible. 241 // If you modify this, consider modifying doGithubE2EAndMerge too. 242 func (sq *SubmitQueue) doBatchMerge(batch Batch) { 243 sq.mergeLock.Lock() 244 defer sq.mergeLock.Unlock() 245 246 // Test again inside the merge lock, in case some other merge snuck in. 247 match, err := sq.batchIsApplicable(batch) 248 if err != nil { 249 glog.Errorf("unexpected! batchIsApplicable failed after success %v", err) 250 return 251 } 252 if !sq.e2eStable(true) { 253 return 254 } 255 256 glog.Infof("merging batch: %s", batch) 257 prs := []*github.MungeObject{} 258 // Check entire batch's preconditions first. 259 for _, pull := range batch.Pulls[match:] { 260 obj, err := sq.githubConfig.GetObject(pull.Number) 261 if err != nil { 262 glog.Errorf("error getting object for pr #%d: %v", pull.Number, err) 263 return 264 } 265 if sha, _, ok := obj.GetHeadAndBase(); !ok { 266 glog.Errorf("error getting pr #%d sha: %v", pull.Number, err) 267 return 268 } else if sha != pull.Sha { 269 glog.Errorf("error: batch PR #%d HEAD changed: %s instead of %s", 270 pull.Number, sha, pull.Sha) 271 return 272 } 273 if !sq.validForMergeExt(obj, false) { 274 return 275 } 276 prs = append(prs, obj) 277 } 278 279 // Make the merge less confusing: describe the overall batch. 280 prStrings := []string{} 281 for _, pull := range batch.Pulls { 282 prStrings = append(prStrings, strconv.Itoa(pull.Number)) 283 } 284 extra := fmt.Sprintf(" (batch tested with PRs %s)", strings.Join(prStrings, ", ")) 285 286 // then merge each 287 for _, pr := range prs { 288 ok := sq.mergePullRequest(pr, mergedBatch, extra) 289 if !ok { 290 return 291 } 292 atomic.AddInt32(&sq.batchMerges, 1) 293 } 294 }