cli/bpbuild/build.go (119 lines of code) (raw):

package bpbuild import ( "context" "fmt" "os" "time" cloudbuild "google.golang.org/api/cloudbuild/v1" "gopkg.in/yaml.v3" ) const ( successStatus = "SUCCESS" failedStatus = "FAILURE" ) // getCBBuildsWithFilter returns a list of cloudbuild builds in projectID with a given filter. // Additional client side filters can be specified via cFilters. // TODO(bharathkkb): move https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/tree/main/infra/utils/fbf into CLI func getCBBuildsWithFilter(projectID string, filter string, cFilters []clientBuildFilter) ([]*cloudbuild.Build, error) { ctx := context.Background() cloudbuildService, err := cloudbuild.NewService(ctx) if err != nil { return nil, fmt.Errorf("error creating cloudbuild service: %w", err) } c, err := cloudbuildService.Projects.Builds.List(projectID).Filter(filter).Do() if err != nil { return nil, fmt.Errorf("error listing builds with filter %s in project %s: %w", filter, projectID, err) } cbBuilds := []*cloudbuild.Build{} appendClientFilteredBuilds := func(builds []*cloudbuild.Build) { for _, b := range builds { appendBuild := true for _, cFilter := range cFilters { // skip if any client side filter evaluates to false if !cFilter(b) { appendBuild = false break } } if appendBuild { cbBuilds = append(cbBuilds, b) } } } if len(c.Builds) < 1 { return nil, fmt.Errorf("no builds found with filter %s in project %s", filter, projectID) } appendClientFilteredBuilds(c.Builds) // pagination for { c, err = cloudbuildService.Projects.Builds.List(projectID).Filter(filter).PageToken(c.NextPageToken).Do() if err != nil { return nil, fmt.Errorf("error retrieving next page with token %s: %w", c.NextPageToken, err) } appendClientFilteredBuilds(c.Builds) if c.NextPageToken == "" { break } } return cbBuilds, nil } // clientside filter functions type clientBuildFilter func(*cloudbuild.Build) bool // filterRealBuilds filters out builds not triggered from source repos (i.e by automation). func filterRealBuilds(b *cloudbuild.Build) bool { for _, subs := range []string{"COMMIT_SHA", "REPO_NAME", "TRIGGER_NAME"} { _, substExists := b.Substitutions[subs] if !substExists { return false } } return true } // filterGHRepoBuilds filters builds from a particular repo name. // TODO:(bharathkkb): We should ideally be using a sever side filter for this https://cloud.google.com/build/docs/view-build-results#filtering_build_results_using_queries // but I was not able to figure out expected format for GH URLs. func filterGHRepoBuilds(repo string) clientBuildFilter { return func(b *cloudbuild.Build) bool { name, exists := b.Substitutions["REPO_NAME"] if !exists { return false } return name == repo } } // successBuildsBtwFilterExpr returns a CEL expression as string // for finding all successful builds between start and end time. func successBuildsBtwFilterExpr(start, end time.Time) string { return fmt.Sprintf( "create_time>=\"%s\" AND create_time<\"%s\" AND status=\"%s\"", start.Format(time.RFC3339), end.Format(time.RFC3339), successStatus) } // getBuildFromFile unmarshalls a CloudBuild file at path. func getBuildFromFile(path string) (*cloudbuild.Build, error) { content, err := os.ReadFile(path) if err != nil { return nil, err } var b cloudbuild.Build err = yaml.Unmarshal(content, &b) if err != nil { return nil, err } return &b, nil } // getBuildStepIDs retrieves a slice of build step IDs in a build. func getBuildStepIDs(b *cloudbuild.Build) []string { steps := []string{} for _, bs := range b.Steps { steps = append(steps, bs.Id) } return steps } // findBuildStageDurations computes duration for a given build stage across a slice of builds // if and only if stage is successful. func findBuildStageDurations(stepId string, builds []*cloudbuild.Build) ([]time.Duration, error) { durations := []time.Duration{} for _, b := range builds { for _, bs := range b.Steps { if bs.Id != stepId || bs.Status != successStatus { continue } parsedStartTime, err := time.Parse(time.RFC3339Nano, bs.Timing.StartTime) if err != nil { return []time.Duration{}, err } parsedEndTime, err := time.Parse(time.RFC3339Nano, bs.Timing.EndTime) if err != nil { return []time.Duration{}, err } durations = append(durations, parsedEndTime.Sub(parsedStartTime).Truncate(time.Second)) } } return durations, nil }