blob: 593fad24dd31d0cb8fa6d4e7afb2b9bf9e189327 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/luciexe/exe"
"go.fuchsia.dev/infra/cmd/size_check/sizes"
"go.fuchsia.dev/infra/cmd/size_diff/diff"
)
// The exit code to emit when the CI build does not have Buildbucket status
// SUCCESS. Exit code 2 is avoided as this would clobber with the exit code
// returned upon hitting a panic.
const buildNotSuccessfulExitCode = 20
type buildNotSuccessfulError struct {
msg string
status buildbucketpb.Status
}
func (e buildNotSuccessfulError) Error() string { return e.msg }
func cmdCI(authOpts auth.Options) *subcommands.Command {
return &subcommands.Command{
UsageLine: "ci -gitiles-remote <gitiles-remote> -base-commit <sha1> -builder <project/bucket/builder> -binary-sizes-json-input <binary-sizes-json-input> -json-output <json-output>",
ShortDesc: "Compute diff of the input binary sizes object against a binary sizes object from CI.",
LongDesc: "Compute diff of the input binary sizes object against a binary sizes object from CI.",
CommandRun: func() subcommands.CommandRun {
c := &ciRun{}
c.Init(authOpts)
return c
},
}
}
type ciRun struct {
commonFlags
binarySizesJSONInput string
}
func (c *ciRun) Init(defaultAuthOpts auth.Options) {
c.commonFlags.Init(defaultAuthOpts)
c.Flags.StringVar(&c.binarySizesJSONInput, "binary-sizes-json-input", "", "Path for input binary sizes object as JSON.")
}
func (c *ciRun) Parse() error {
if err := c.commonFlags.Parse(); err != nil {
return err
}
if c.binarySizesJSONInput == "" {
return errors.New("-binary-sizes-json-input is required")
}
return nil
}
func (c *ciRun) main() error {
ctx := context.Background()
fieldMaskPaths1 := []string{
"builds.*.id",
"builds.*.status",
"builds.*.output.properties.fields.binary_sizes",
}
fieldMaskPaths2 := []string{
"id",
"status",
"output.properties.fields.binary_sizes",
}
build, err := getBuild(ctx, c.commonFlags, fieldMaskPaths1, fieldMaskPaths2)
if err != nil {
return err
}
buildLink := fmt.Sprintf("https://%s/build/%d", c.bbHost, build.Id)
if build.Status != buildbucketpb.Status_SUCCESS {
return buildNotSuccessfulError{
msg: fmt.Sprintf("a successful build is needed to perform the size diff but got status %s, see %s", build.Status, buildLink),
status: build.Status,
}
}
var rawCIBinarySizes map[string]any
exe.ParseProperties(build.Output.Properties, map[string]any{
"binary_sizes": &rawCIBinarySizes,
})
if len(rawCIBinarySizes) == 0 {
return fmt.Errorf("binary_sizes output property is not set, see %s", buildLink)
}
ciBinarySizes, err := sizes.Parse(rawCIBinarySizes)
if err != nil {
return err
}
// Read binary sizes JSON input.
jsonInput, err := os.ReadFile(c.binarySizesJSONInput)
if err != nil {
return err
}
var rawBinarySizes map[string]any
if err := json.Unmarshal(jsonInput, &rawBinarySizes); err != nil {
return err
}
binarySizes, err := sizes.Parse(rawBinarySizes)
if err != nil {
return err
}
diff := diff.DiffBinarySizes(binarySizes, ciBinarySizes)
diff.BaselineBuildID = build.Id
// Emit diff to -json-output.
out := os.Stdout
if c.jsonOutput != "-" {
out, err = os.Create(c.jsonOutput)
if err != nil {
return err
}
defer out.Close()
}
data, err := json.MarshalIndent(diff, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
_, err = out.Write(data)
return err
}
func (c *ciRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if err := c.Parse(); err != nil {
fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
return 1
}
if err := c.main(); err != nil {
fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
var maybeBuildNotSuccessfulError buildNotSuccessfulError
if errors.As(err, &maybeBuildNotSuccessfulError) {
return buildNotSuccessfulExitCode
}
return 1
}
return 0
}