blob: 0dde9bc7dc48d95fd39fab99242dda503ec033e0 [file] [log] [blame]
// Copyright 2019 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.
// This tool returns the "Last Known Good Build" for a given Buildbucket builder.
// Builder ids are often duplicated across Buildbucket buckets, so the id must be
// fully qualified using the project and bucket.
//
// Example Usage:
//
// $ lkgb fuchsia/ci/fuchsia-x64-release
// $ lkgb fuchsia/try/fuchsia-x64-debug
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"go.chromium.org/luci/auth/client/authcli"
"go.chromium.org/luci/hardcoded/chromeinfra"
"go.fuchsia.dev/infra/buildbucket"
"google.golang.org/genproto/protobuf/field_mask"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
)
// Command line flags
var (
// LUCI authentication flags.
authFlags authcli.Flags
// A flag for reading a BuildBucket Builder ID from command line args.
builderIDFlag = buildbucket.BuilderID()
// A gitiles ref to filter builds by.
gitilesRef string
// SearchBuilds page limit to use when filtering for matching gitiles ref.
searchLimit int
// Whether to show help and exit.
help bool
)
func usage() {
fmt.Printf(`lkgb [flags] builder-id
lkgb returns the "Last Known Good Build" id for a given builder. builder-id must have the
form "project/bucket/builder". For example: "fuchsia/ci/fuchsia-x64-release". Optionally
filter for matching gitiles ref using -gitiles-ref option.
`)
flag.PrintDefaults()
}
func init() {
authFlags.Register(flag.CommandLine, chromeinfra.DefaultAuthOptions())
flag.StringVar(&gitilesRef, "gitiles-ref", "", "Optionally filter builds for matching gitiles ref (e.g. refs/heads/master). By default, no filter applied")
flag.IntVar(&searchLimit, "search-limit", 50, "If searching with -gitiles-ref, search this many builds. Defaults to 50")
flag.BoolVar(&help, "h", false, "Whether to show usage and exit")
flag.Usage = usage
}
func main() {
flag.Parse()
if help || flag.NArg() == 0 {
flag.Usage()
return
}
if err := execute(context.Background(), flag.Arg(0)); err != nil {
log.Println(err)
os.Exit(1)
}
os.Exit(0)
}
func execute(ctx context.Context, input string) error {
opts, err := authFlags.Options()
if err != nil {
return fmt.Errorf("failed to get auth options: %v", err)
}
if err := builderIDFlag.Set(input); err != nil {
return fmt.Errorf("invalid builder id %q: %v", input, err)
}
client, err := buildbucket.NewBuildsClient(ctx, buildbucket.DefaultHost, opts)
if err != nil {
return fmt.Errorf("failed to create builds client: %v", err)
}
builderID := builderIDFlag.Get().(buildbucketpb.BuilderID)
buildID, err := getBuildID(ctx, client, builderID, gitilesRef, int32(searchLimit))
if err != nil {
return fmt.Errorf("failed to get build: %v", err)
}
fmt.Printf("%d\n", buildID)
return nil
}
func getBuildID(ctx context.Context, client buildbucketpb.BuildsClient, builderID buildbucketpb.BuilderID, gitilesRef string, searchLimit int32) (int64, error) {
pageSize := int32(1)
paths := []string{"builds.*.id"}
// If filtering by gitiles ref, we need to search for more than one build.
if gitilesRef != "" {
pageSize = searchLimit
paths = append(paths, "builds.*.input")
}
response, err := client.SearchBuilds(ctx, &buildbucketpb.SearchBuildsRequest{
Predicate: &buildbucketpb.BuildPredicate{
Builder: &builderID,
Status: buildbucketpb.Status_SUCCESS,
},
Fields: &field_mask.FieldMask{
Paths: paths,
},
PageSize: pageSize,
})
if err != nil {
return 0, fmt.Errorf("search failed: %v", err)
}
if response.Builds == nil || len(response.Builds) == 0 {
return 0, errors.New("no build found")
}
// If filtering by gitiles ref, pick the first build which matches.
// The response is already descendingly sorted by build start time,
// so we'll pick the last known green build.
if gitilesRef != "" {
for _, build := range response.Builds {
if build.Input.GitilesCommit.Ref == gitilesRef {
return build.Id, nil
}
}
return 0, fmt.Errorf("no build found matching gitiles ref %q within last %d builds", gitilesRef, searchLimit)
}
return response.Builds[0].Id, nil
}