diff --git a/build/includes/website.mk b/build/includes/website.mk index f152554689..09716e0207 100644 --- a/build/includes/website.mk +++ b/build/includes/website.mk @@ -109,3 +109,8 @@ test-gen-api-docs: ensure-build-image $(GEN_API_DOCS) sort $(expected_docs) > /tmp/result.sorted diff -bB /tmp/result.sorted /tmp/generated.html.sorted + +build-report: BUILD_REPORT_BUCKET ?= agones-build-reports +build-report: ensure-build-image + docker run --rm $(common_mounts) --workdir=$(mount_path) $(DOCKER_RUN_ARGS) $(build_tag) bash -c \ + "go work use build/report; go run build/report/report.go; gcloud storage cp tmp/report/* gs://$(BUILD_REPORT_BUCKET)/" diff --git a/build/report/cloudbuild.yaml b/build/report/cloudbuild.yaml new file mode 100644 index 0000000000..30459ed43a --- /dev/null +++ b/build/report/cloudbuild.yaml @@ -0,0 +1,41 @@ +# Copyright 2023 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Google Cloud Builder -- Generate report on recently flaky builds + +steps: + +# +# Creates the initial make + docker build platform +# + +- name: "ubuntu" + args: ["bash", "-c", "echo 'FROM gcr.io/cloud-builders/docker\nRUN apt-get install make\nENTRYPOINT [\"/usr/bin/make\"]' > Dockerfile.build"] +- name: "gcr.io/cloud-builders/docker" + args: ['build', '-f', 'Dockerfile.build', '-t', 'make-docker', '.'] # we need docker and make to run everything. +- name: "make-docker" + dir: "build" + env: + - "REGISTRY=${_REGISTRY}" + args: ["pull-build-image"] # since we are past CI build, we can assume that the build image exists. + +# Run build report +- name: "make-docker" + dir: "build" + args: ["build-report"] + +substitutions: + _REGISTRY: us-docker.pkg.dev/${PROJECT_ID}/ci +tags: ["build-report"] diff --git a/build/report/report.go b/build/report/report.go new file mode 100644 index 0000000000..f9e08ec9a8 --- /dev/null +++ b/build/report/report.go @@ -0,0 +1,199 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "html/template" + "log" + "os" + "sort" + "time" + + cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2" + cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" + "google.golang.org/api/iterator" +) + +const ( + window = time.Hour * 24 * 7 * 4 // 4 weeks + wantBuildTriggerId = "da003bb8-e9bb-4983-a556-e77fb92f17ca" + outPath = "tmp/report" + reportBucket = "agones-build-reports" + + reportTemplate = ` + + + + + {{ .FlakePercent }}% of {{ .Builds }} successful builds from {{ .WindowStart }} to {{ .WindowEnd }} + required at least one re-run to succeed. Examples flakes: + + + + + + +{{- range .Flakes -}} + + + + +{{- end -}} +
TimeFlaky Build
{{ .CreateTime }}{{ .Id }}
+ + +` + + redirectTemplate = ` + + + Latest build + + + +

Latest build report (redirecting now).

+ + +` +) + +type Report struct { + WindowStart string + WindowEnd string + Builds int + FlakePercent int + Flakes []Flake +} + +type Flake struct { + Id string + CreateTime string +} + +func main() { + ctx := context.Background() + reportTmpl := template.Must(template.New("report").Parse(reportTemplate)) + redirTmpl := template.Must(template.New("redir").Parse(redirectTemplate)) + + windowEnd := time.Now().UTC() + windowStart := windowEnd.Add(-window) + date := windowEnd.Format("2006-01-02") + + err := os.MkdirAll(outPath, 0755) + if err != nil { + log.Fatalf("failed to create output path %v: %v", outPath, err) + } + + datePath := fmt.Sprintf("%s/%s.html", outPath, date) + reportFile, err := os.OpenFile(datePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Fatalf("failed to open output file: %v", err) + } + + redirPath := fmt.Sprintf("%s/index.html", outPath) + redirFile, err := os.OpenFile(redirPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + + } + + c, err := cloudbuild.NewClient(ctx) + if err != nil { + log.Fatalf("failed to initialize cloudbuild client: %v", err) + } + defer c.Close() + + success := make(map[string]bool) // build SHA -> bool + failure := make(map[string][]string) // build SHA -> slice of build IDs that failed + idTime := make(map[string]time.Time) // build ID -> create time + + // See https://pkg.go.dev/cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb#ListBuildsRequest. + req := &cloudbuildpb.ListBuildsRequest{ + ProjectId: "agones-images", + // TODO(zmerlynn): No idea why this is failing. + // Filter: `build_trigger_id = "da003bb8-e9bb-4983-a556-e77fb92f17ca"`, + } + it := c.ListBuilds(ctx, req) + for { + resp, err := it.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Fatalf("error listing builds: %v", err) + break + } + createTime := resp.CreateTime.AsTime() + if createTime.Before(windowStart) { + break + } + // We only care about Agones builds. + if resp.BuildTriggerId != wantBuildTriggerId { + continue + } + // Ignore if it's still running. + if resp.FinishTime == nil { + continue + } + + id := resp.Id + sha := resp.Substitutions["COMMIT_SHA"] + status := resp.Status + idTime[id] = createTime + log.Printf("id = %v, sha = %v, status = %v", id, sha, status) + + // Record clear cut success/failure, not timeout, cancelled, etc. + switch status { + case cloudbuildpb.Build_SUCCESS: + success[sha] = true + case cloudbuildpb.Build_FAILURE: + failure[sha] = append(failure[sha], id) + default: + continue + } + } + + buildCount := 0 + flakeCount := 0 + var flakes []Flake + for sha := range success { + buildCount += 1 + if ids, ok := failure[sha]; ok { + flakeCount += 1 + for _, id := range ids { + flakes = append(flakes, Flake{ + Id: id, + CreateTime: idTime[id].Format(time.RFC3339), + }) + } + } + } + sort.Slice(flakes, func(i, j int) bool { return flakes[i].CreateTime > flakes[j].CreateTime }) + + if err := reportTmpl.Execute(reportFile, Report{ + WindowStart: windowStart.Format(time.RFC3339), + WindowEnd: windowEnd.Format(time.RFC3339), + Builds: buildCount, + FlakePercent: 100 * flakeCount / buildCount, + Flakes: flakes, + }); err != nil { + log.Fatalf("failure rendering report: %v", err) + } + + if err := redirTmpl.Execute(redirFile, struct{ Date string }{date}); err != nil { + log.Fatalf("failure rendering redirect: %v", err) + } +}