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 }