go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/size_diff/ci.go (about)

     1  // Copyright 2021 The Fuchsia Authors.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  
    14  	"github.com/maruel/subcommands"
    15  	"go.chromium.org/luci/auth"
    16  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    17  	"go.chromium.org/luci/luciexe/exe"
    18  
    19  	"go.fuchsia.dev/infra/cmd/size_check/sizes"
    20  	"go.fuchsia.dev/infra/cmd/size_diff/diff"
    21  )
    22  
    23  // The exit code to emit when the CI build does not have Buildbucket status
    24  // SUCCESS. Exit code 2 is avoided as this would clobber with the exit code
    25  // returned upon hitting a panic.
    26  const buildNotSuccessfulExitCode = 20
    27  
    28  // The output property containing binary size JSON data.
    29  const binarySizesOutputProp = "binary_sizes"
    30  
    31  type buildNotSuccessfulError struct {
    32  	msg    string
    33  	status buildbucketpb.Status
    34  }
    35  
    36  func (e buildNotSuccessfulError) Error() string { return e.msg }
    37  
    38  func cmdCI(authOpts auth.Options) *subcommands.Command {
    39  	return &subcommands.Command{
    40  		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>",
    41  		ShortDesc: "Compute diff of the input binary sizes object against a binary sizes object from CI.",
    42  		LongDesc:  "Compute diff of the input binary sizes object against a binary sizes object from CI.",
    43  		CommandRun: func() subcommands.CommandRun {
    44  			c := &ciRun{}
    45  			c.Init(authOpts)
    46  			return c
    47  		},
    48  	}
    49  }
    50  
    51  type ciRun struct {
    52  	commonFlags
    53  	binarySizesJSONInput string
    54  }
    55  
    56  func (c *ciRun) Init(defaultAuthOpts auth.Options) {
    57  	c.commonFlags.Init(defaultAuthOpts)
    58  	c.Flags.StringVar(&c.binarySizesJSONInput, "binary-sizes-json-input", "", "Path for input binary sizes object as JSON.")
    59  }
    60  
    61  func (c *ciRun) Parse() error {
    62  	if err := c.commonFlags.Parse(); err != nil {
    63  		return err
    64  	}
    65  	if c.binarySizesJSONInput == "" {
    66  		return errors.New("-binary-sizes-json-input is required")
    67  	}
    68  	return nil
    69  }
    70  
    71  func (c *ciRun) main() error {
    72  	ctx := context.Background()
    73  	build, err := getBuild(ctx, c.commonFlags, []string{binarySizesOutputProp})
    74  	if err != nil {
    75  		return err
    76  	}
    77  	buildLink := fmt.Sprintf("https://%s/build/%d", c.bbHost, build.Id)
    78  	if build.Status != buildbucketpb.Status_SUCCESS {
    79  		return buildNotSuccessfulError{
    80  			msg:    fmt.Sprintf("a successful build is needed to perform the size diff but got status %s, see %s", build.Status, buildLink),
    81  			status: build.Status,
    82  		}
    83  	}
    84  
    85  	var rawCIBinarySizes map[string]any
    86  	exe.ParseProperties(build.Output.Properties, map[string]any{
    87  		binarySizesOutputProp: &rawCIBinarySizes,
    88  	})
    89  	if len(rawCIBinarySizes) == 0 {
    90  		return fmt.Errorf("%q output property is not set, see %s", binarySizesOutputProp, buildLink)
    91  	}
    92  	ciBinarySizes, err := sizes.Parse(rawCIBinarySizes)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	// Read binary sizes JSON input.
    98  	jsonInput, err := os.ReadFile(c.binarySizesJSONInput)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	var rawBinarySizes map[string]any
   103  	if err := json.Unmarshal(jsonInput, &rawBinarySizes); err != nil {
   104  		return err
   105  	}
   106  	binarySizes, err := sizes.Parse(rawBinarySizes)
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	diff := diff.DiffBinarySizes(binarySizes, ciBinarySizes)
   112  	diff.BaselineBuildID = build.Id
   113  
   114  	// Emit diff to -json-output.
   115  	out := os.Stdout
   116  	if c.jsonOutput != "-" {
   117  		out, err = os.Create(c.jsonOutput)
   118  		if err != nil {
   119  			return err
   120  		}
   121  		defer out.Close()
   122  	}
   123  	data, err := json.MarshalIndent(diff, "", "  ")
   124  	if err != nil {
   125  		return fmt.Errorf("failed to marshal JSON: %w", err)
   126  	}
   127  	_, err = out.Write(data)
   128  	return err
   129  }
   130  
   131  func (c *ciRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   132  	if err := c.Parse(); err != nil {
   133  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   134  		return 1
   135  	}
   136  
   137  	if err := c.main(); err != nil {
   138  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   139  		var maybeBuildNotSuccessfulError buildNotSuccessfulError
   140  		if errors.As(err, &maybeBuildNotSuccessfulError) {
   141  			return buildNotSuccessfulExitCode
   142  		}
   143  		return 1
   144  	}
   145  	return 0
   146  }