| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| |
| "github.com/golang-collections/collections/set" |
| "go.chromium.org/luci/auth" |
| buildbucketpb "go.chromium.org/luci/buildbucket/proto" |
| "go.chromium.org/luci/grpc/prpc" |
| "google.golang.org/genproto/protobuf/field_mask" |
| ) |
| |
| // buildbucketClientWrapper provides utilities for interacting with Buildbucket. |
| type buildbucketClientWrapper struct { |
| client buildbucketpb.BuildsClient |
| } |
| |
| // newBuildbucketClient returns an authenticated buildbucketClientWrapper. |
| func newBuildbucketClient(ctx context.Context, authOpts auth.Options, host string) (*buildbucketClientWrapper, error) { |
| authClient, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, authOpts).Client() |
| if err != nil { |
| return nil, err |
| } |
| client := buildbucketpb.NewBuildsPRPCClient(&prpc.Client{ |
| C: authClient, |
| Host: host, |
| }) |
| return &buildbucketClientWrapper{client: client}, nil |
| } |
| |
| // GetBuilds gets the latest n successful builds for each input builder, from most to least recent. |
| // The response is in the same order as the order of the input builders. |
| func (c *buildbucketClientWrapper) GetBuilds(ctx context.Context, builders []*buildbucketpb.BuilderID, mask *field_mask.FieldMask, n int) ([][]*buildbucketpb.Build, error) { |
| // Always grab input by default. |
| mask.Paths = append(mask.Paths, "builds.*.input") |
| // Construct requests to execute in batch. |
| reqs := make([]*buildbucketpb.BatchRequest_Request, len(builders)) |
| for i, builder := range builders { |
| reqs[i] = &buildbucketpb.BatchRequest_Request{ |
| Request: &buildbucketpb.BatchRequest_Request_SearchBuilds{ |
| SearchBuilds: &buildbucketpb.SearchBuildsRequest{ |
| Predicate: &buildbucketpb.BuildPredicate{ |
| Builder: builder, |
| Status: buildbucketpb.Status_SUCCESS, |
| }, |
| Fields: mask, |
| PageSize: int32(n), |
| }, |
| }, |
| } |
| } |
| batchResp, err := c.client.Batch(ctx, &buildbucketpb.BatchRequest{Requests: reqs}) |
| if err != nil { |
| return nil, fmt.Errorf("failed to retrieve latest %d successful builds for %s: %w", n, builders, err) |
| } |
| builds := make([][]*buildbucketpb.Build, len(builders)) |
| for i, resp := range batchResp.Responses { |
| switch resp.Response.(type) { |
| case *buildbucketpb.BatchResponse_Response_SearchBuilds: |
| builds[i] = resp.Response.(*buildbucketpb.BatchResponse_Response_SearchBuilds).SearchBuilds.Builds |
| case *buildbucketpb.BatchResponse_Response_Error: |
| return nil, fmt.Errorf("got batch response error: %s", resp.GetError().String()) |
| default: |
| return nil, fmt.Errorf("unexpected response type: %T", resp.Response) |
| } |
| } |
| return builds, nil |
| } |
| |
| // accessorFunc accesses a value from a Build. |
| type accessorFunc func(*buildbucketpb.Build) any |
| |
| // getLastKnownGood gets the latest common attribute from the input slices of builds. |
| func getLastKnownGood(builds [][]*buildbucketpb.Build, gitilesRef string, f accessorFunc) (any, error) { |
| if len(builds) == 0 { |
| return nil, errors.New("input builds is of length 0") |
| } |
| // Compute common attributes to all slices but the first. |
| var commonAttrSet *set.Set |
| for _, buildSlice := range builds[1:] { |
| attrSet := set.New() |
| for _, build := range buildSlice { |
| buildAttr := f(build) |
| // Skip nil attributes. |
| if buildAttr != nil { |
| attrSet.Insert(buildAttr) |
| } |
| } |
| // Initialize set on first iteration. |
| if commonAttrSet == nil { |
| commonAttrSet = attrSet |
| // Otherwise, take the intersection between this set and the common set. |
| } else { |
| commonAttrSet = attrSet.Intersection(commonAttrSet) |
| } |
| } |
| |
| // Iterate through the first slice, returning the first attribute match. |
| for _, build := range builds[0] { |
| // Skip non-matching refs. |
| if gitilesRef != "" && gitilesRef != build.Input.GitilesCommit.Ref { |
| continue |
| } |
| attr := f(build) |
| // For length-1 inputs, the set is nil, so return the first build. |
| if commonAttrSet == nil || commonAttrSet.Has(attr) { |
| return attr, nil |
| } |
| } |
| |
| return nil, errors.New("no common last-known-good attribute found") |
| } |