github.com/abayer/test-infra@v0.0.5/prow/report/report.go (about) 1 /* 2 Copyright 2017 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 report contains helpers for writing comments and updating 18 // statuses in Github. 19 package report 20 21 import ( 22 "bytes" 23 "fmt" 24 "strings" 25 "text/template" 26 27 "k8s.io/test-infra/prow/github" 28 "k8s.io/test-infra/prow/kube" 29 "k8s.io/test-infra/prow/pjutil" 30 "k8s.io/test-infra/prow/plugins" 31 ) 32 33 const ( 34 commentTag = "<!-- test report -->" 35 ) 36 37 type GithubClient interface { 38 BotName() (string, error) 39 CreateStatus(org, repo, ref string, s github.Status) error 40 ListIssueComments(org, repo string, number int) ([]github.IssueComment, error) 41 CreateComment(org, repo string, number int, comment string) error 42 DeleteComment(org, repo string, ID int) error 43 EditComment(org, repo string, ID int, comment string) error 44 } 45 46 // reportStatus should be called on status different from Success. 47 // Once a parent ProwJob is pending, all children should be marked as Pending 48 // Same goes for failed status. 49 func reportStatus(ghc GithubClient, pj kube.ProwJob, childDescription string) error { 50 refs := pj.Spec.Refs 51 if pj.Spec.Report { 52 contextState := pj.Status.State 53 if contextState == kube.AbortedState { 54 contextState = kube.FailureState 55 } 56 if err := ghc.CreateStatus(refs.Org, refs.Repo, refs.Pulls[0].SHA, github.Status{ 57 // The state of the status. Can be one of error, failure, pending, or success. 58 // https://developer.github.com/v3/repos/statuses/#create-a-status 59 State: string(contextState), 60 Description: pj.Status.Description, 61 Context: pj.Spec.Context, 62 TargetURL: pj.Status.URL, 63 }); err != nil { 64 return err 65 } 66 } 67 68 // Updating Children 69 if pj.Status.State != kube.SuccessState { 70 for _, nj := range pj.Spec.RunAfterSuccess { 71 cpj := pjutil.NewProwJob(nj, pj.ObjectMeta.Labels) 72 cpj.Status.State = pj.Status.State 73 cpj.Status.Description = childDescription 74 cpj.Spec.Refs = refs 75 if err := reportStatus(ghc, cpj, childDescription); err != nil { 76 return err 77 } 78 } 79 } 80 return nil 81 } 82 83 // Report is creating/updating/removing reports in Github based on the state of 84 // the provided ProwJob. 85 func Report(ghc GithubClient, reportTemplate *template.Template, pj kube.ProwJob) error { 86 if !pj.Spec.Report { 87 return nil 88 } 89 refs := pj.Spec.Refs 90 if len(refs.Pulls) != 1 { 91 return fmt.Errorf("prowjob %s has %d pulls, not 1", pj.ObjectMeta.Name, len(refs.Pulls)) 92 } 93 childDescription := fmt.Sprintf("Waiting on: %s", pj.Spec.Context) 94 if err := reportStatus(ghc, pj, childDescription); err != nil { 95 return fmt.Errorf("error setting status: %v", err) 96 } 97 // Report manually aborted Jenkins jobs and jobs with invalid pod specs alongside 98 // test successes/failures. 99 if !pj.Complete() { 100 return nil 101 } 102 ics, err := ghc.ListIssueComments(refs.Org, refs.Repo, refs.Pulls[0].Number) 103 if err != nil { 104 return fmt.Errorf("error listing comments: %v", err) 105 } 106 botName, err := ghc.BotName() 107 if err != nil { 108 return fmt.Errorf("error getting bot name: %v", err) 109 } 110 deletes, entries, updateID := parseIssueComments(pj, botName, ics) 111 for _, delete := range deletes { 112 if err := ghc.DeleteComment(refs.Org, refs.Repo, delete); err != nil { 113 return fmt.Errorf("error deleting comment: %v", err) 114 } 115 } 116 if len(entries) > 0 { 117 comment, err := createComment(reportTemplate, pj, entries) 118 if err != nil { 119 return fmt.Errorf("generating comment: %v", err) 120 } 121 if updateID == 0 { 122 if err := ghc.CreateComment(refs.Org, refs.Repo, refs.Pulls[0].Number, comment); err != nil { 123 return fmt.Errorf("error creating comment: %v", err) 124 } 125 } else { 126 if err := ghc.EditComment(refs.Org, refs.Repo, updateID, comment); err != nil { 127 return fmt.Errorf("error updating comment: %v", err) 128 } 129 } 130 } 131 return nil 132 } 133 134 // parseIssueComments returns a list of comments to delete, a list of table 135 // entries, and the ID of the comment to update. If there are no table entries 136 // then don't make a new comment. Otherwise, if the comment to update is 0, 137 // create a new comment. 138 func parseIssueComments(pj kube.ProwJob, botName string, ics []github.IssueComment) ([]int, []string, int) { 139 var delete []int 140 var previousComments []int 141 var latestComment int 142 var entries []string 143 // First accumulate result entries and comment IDs 144 for _, ic := range ics { 145 if ic.User.Login != botName { 146 continue 147 } 148 // Old report comments started with the context. Delete them. 149 // TODO(spxtr): Delete this check a few weeks after this merges. 150 if strings.HasPrefix(ic.Body, pj.Spec.Context) { 151 delete = append(delete, ic.ID) 152 } 153 if !strings.Contains(ic.Body, commentTag) { 154 continue 155 } 156 if latestComment != 0 { 157 previousComments = append(previousComments, latestComment) 158 } 159 latestComment = ic.ID 160 var tracking bool 161 for _, line := range strings.Split(ic.Body, "\n") { 162 line = strings.TrimSpace(line) 163 if strings.HasPrefix(line, "---") { 164 tracking = true 165 } else if len(line) == 0 { 166 tracking = false 167 } else if tracking { 168 entries = append(entries, line) 169 } 170 } 171 } 172 var newEntries []string 173 // Next decide which entries to keep. 174 for i := range entries { 175 keep := true 176 f1 := strings.Split(entries[i], " | ") 177 for j := range entries { 178 if i == j { 179 continue 180 } 181 f2 := strings.Split(entries[j], " | ") 182 // Use the newer results if there are multiple. 183 if j > i && f2[0] == f1[0] { 184 keep = false 185 } 186 } 187 // Use the current result if there is an old one. 188 if pj.Spec.Context == f1[0] { 189 keep = false 190 } 191 if keep { 192 newEntries = append(newEntries, entries[i]) 193 } 194 } 195 var createNewComment bool 196 if string(pj.Status.State) == github.StatusFailure { 197 newEntries = append(newEntries, createEntry(pj)) 198 createNewComment = true 199 } 200 delete = append(delete, previousComments...) 201 if (createNewComment || len(newEntries) == 0) && latestComment != 0 { 202 delete = append(delete, latestComment) 203 latestComment = 0 204 } 205 return delete, newEntries, latestComment 206 } 207 208 func createEntry(pj kube.ProwJob) string { 209 return strings.Join([]string{ 210 pj.Spec.Context, 211 pj.Spec.Refs.Pulls[0].SHA, 212 fmt.Sprintf("[link](%s)", pj.Status.URL), 213 fmt.Sprintf("`%s`", pj.Spec.RerunCommand), 214 }, " | ") 215 } 216 217 // createComment take a ProwJob and a list of entries generated with 218 // createEntry and returns a nicely formatted comment. It may fail if template 219 // execution fails. 220 func createComment(reportTemplate *template.Template, pj kube.ProwJob, entries []string) (string, error) { 221 plural := "" 222 if len(entries) > 1 { 223 plural = "s" 224 } 225 var b bytes.Buffer 226 if reportTemplate != nil { 227 if err := reportTemplate.Execute(&b, &pj); err != nil { 228 return "", err 229 } 230 } 231 lines := []string{ 232 fmt.Sprintf("@%s: The following test%s **failed**, say `/retest` to rerun them all:", pj.Spec.Refs.Pulls[0].Author, plural), 233 "", 234 "Test name | Commit | Details | Rerun command", 235 "--- | --- | --- | ---", 236 } 237 lines = append(lines, entries...) 238 if reportTemplate != nil { 239 lines = append(lines, "", b.String()) 240 } 241 lines = append(lines, []string{ 242 "", 243 "<details>", 244 "", 245 plugins.AboutThisBot, 246 "</details>", 247 commentTag, 248 }...) 249 return strings.Join(lines, "\n"), nil 250 }