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