agones.dev/agones@v1.54.0/build/report/report.go (about) 1 // Copyright 2023 Google LLC All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // build/report/report.go generates a flake report for the last N weeks 16 // on the configured build trigger. It creates files in `tmp/report`, a 17 // dated YYYY-MM-DD.html report and an index.html with a redirect to the 18 // new date, intended to upload to Google Cloud Storage. 19 package main 20 21 import ( 22 "context" 23 "fmt" 24 "html/template" 25 "log" 26 "os" 27 "sort" 28 "time" 29 30 cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2" 31 cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" 32 "google.golang.org/api/iterator" 33 ) 34 35 const ( 36 window = time.Hour * 24 * 7 * 4 // 4 weeks 37 wantBuildTriggerID = "da003bb8-e9bb-4983-a556-e77fb92f17ca" 38 outPath = "tmp/report" 39 40 reportTemplate = ` 41 <!DOCTYPE html> 42 <html lang="en"> 43 <head> 44 <meta charset="UTF-8"> 45 <title>Flake Report From {{ .WindowStart }} to {{ .WindowEnd }}</title> 46 <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> 47 </head> 48 <body> 49 <header> 50 Flake Report {{ .WindowStart }} to {{ .WindowEnd }} 51 </header> 52 <p><b>Flake Ratio:</b> {{ printf "%.3f" .FlakeRatio }} ({{ .FlakeCount }} flakes / {{ .BuildCount }} successful builds)</p> 53 54 <table> 55 <tr> 56 <th>Time</th> 57 <th>Flaky Build</th> 58 </tr> 59 {{- range .Flakes -}} 60 <tr> 61 <td>{{ .CreateTime }}</td> 62 <td><a href="https://console.cloud.google.com/cloud-build/builds;region=global/{{ .ID }}?project=agones-images">{{ .ID }}</a></td> 63 </tr> 64 {{- end -}} 65 </table> 66 67 <p><b>Methodology:</b> For every successful build of a given commit hash, we count the number of failed 68 builds on the same commit hash. The <em>Flake Ratio</em> is the ratio of flakes to successes, giving an 69 expected value for how many times a build has to be retried before succeeding, on the same SHA. 70 This methodology only covers manual re-runs in Cloud Build - builds retried via rebase are not counted, 71 as in general it's difficult to attribute flakes between commit hashes. 72 </body> 73 </html> 74 ` 75 76 redirectTemplate = ` 77 <html xmlns="http://www.w3.org/1999/xhtml"> 78 <head> 79 <title>Latest build</title> 80 <meta http-equiv="refresh" content="0;URL='https://agones-build-reports.storage.googleapis.com/{{ .Date }}.html'" /> 81 </head> 82 <body> 83 <p><a href="https://agones-build-reports.storage.googleapis.com/{{ .Date }}.html">Latest build report (redirecting now).</a></p> 84 </body> 85 </html> 86 ` 87 ) 88 89 type report struct { 90 WindowStart string 91 WindowEnd string 92 Flakes []flake 93 FlakeCount int 94 BuildCount int 95 FlakeRatio float32 96 } 97 98 type flake struct { 99 ID string 100 CreateTime string 101 } 102 103 type redirect struct { 104 Date string 105 } 106 107 func main() { 108 ctx := context.Background() 109 reportTmpl := newReportTemplate() 110 redirTmpl := newRedirectTemplate() 111 112 windowEnd := time.Now().UTC() 113 windowStart := windowEnd.Add(-window) 114 date := windowEnd.Format("2006-01-02") 115 116 err := os.MkdirAll(outPath, 0o755) 117 if err != nil { 118 log.Fatalf("failed to create output path %v: %v", outPath, err) 119 } 120 121 datePath := fmt.Sprintf("%s/%s.html", outPath, date) 122 reportFile, err := os.OpenFile(datePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) 123 if err != nil { 124 log.Fatalf("failed to open report %v: %v", datePath, err) 125 } 126 127 redirPath := fmt.Sprintf("%s/index.html", outPath) 128 redirFile, err := os.OpenFile(redirPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) 129 if err != nil { 130 log.Fatalf("failed to open redirect %v: %v", redirPath, err) 131 } 132 133 c, err := cloudbuild.NewClient(ctx) 134 if err != nil { 135 log.Fatalf("failed to initialize cloudbuild client: %v", err) 136 } 137 138 success := make(map[string]bool) // build SHA -> bool 139 failure := make(map[string][]string) // build SHA -> slice of build IDs that failed 140 idTime := make(map[string]time.Time) // build ID -> create time 141 142 // See https://pkg.go.dev/cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb#ListBuildsRequest. 143 req := &cloudbuildpb.ListBuildsRequest{ 144 ProjectId: "agones-images", 145 // TODO(zmerlynn): No idea why this is failing. 146 // Filter: `build_trigger_id = "da003bb8-e9bb-4983-a556-e77fb92f17ca"`, 147 } 148 it := c.ListBuilds(ctx, req) 149 for { 150 resp, err := it.Next() 151 if err == iterator.Done { 152 break 153 } 154 if err != nil { 155 log.Fatalf("error listing builds: %v", err) 156 } 157 createTime := resp.CreateTime.AsTime() 158 if createTime.Before(windowStart) { 159 break 160 } 161 // We only care about Agones builds. 162 if resp.BuildTriggerId != wantBuildTriggerID { 163 continue 164 } 165 // Ignore if it's still running. 166 if resp.FinishTime == nil { 167 continue 168 } 169 170 id := resp.Id 171 sha := resp.Substitutions["COMMIT_SHA"] 172 status := resp.Status 173 idTime[id] = createTime 174 log.Printf("id = %v, sha = %v, status = %v", id, sha, status) 175 176 // Record clear cut success/failure, not timeout, cancelled, etc. 177 switch status { 178 case cloudbuildpb.Build_SUCCESS: 179 success[sha] = true 180 case cloudbuildpb.Build_FAILURE: 181 failure[sha] = append(failure[sha], id) 182 default: 183 continue 184 } 185 } 186 187 buildCount := len(success) 188 flakeCount := 0 189 var flakes []flake 190 for sha := range success { 191 flakeCount += len(failure[sha]) 192 for _, id := range failure[sha] { 193 flakes = append(flakes, flake{ 194 ID: id, 195 CreateTime: idTime[id].Format(time.RFC3339), 196 }) 197 } 198 } 199 sort.Slice(flakes, func(i, j int) bool { return flakes[i].CreateTime > flakes[j].CreateTime }) 200 201 if err := reportTmpl.Execute(reportFile, report{ 202 WindowStart: windowStart.Format("2006-01-02"), 203 WindowEnd: windowEnd.Format("2006-01-02"), 204 BuildCount: buildCount, 205 FlakeCount: flakeCount, 206 FlakeRatio: float32(flakeCount) / float32(buildCount), 207 Flakes: flakes, 208 }); err != nil { 209 log.Fatalf("failure rendering report: %v", err) 210 } 211 212 if err := redirTmpl.Execute(redirFile, redirect{Date: date}); err != nil { 213 log.Fatalf("failure rendering redirect: %v", err) 214 } 215 } 216 217 func newReportTemplate() *template.Template { 218 return template.Must(template.New("report").Parse(reportTemplate)).Option("missingkey=error") 219 } 220 221 func newRedirectTemplate() *template.Template { 222 return template.Must(template.New("redir").Parse(redirectTemplate)).Option("missingkey=error") 223 }