blob: e18f99501d5755f29dc1ca08c4e08c892a829045 [file] [log] [blame]
// Copyright 2022 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"
"encoding/json"
"errors"
"fmt"
"os"
"time"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/common/retry"
"go.chromium.org/luci/common/retry/transient"
"go.fuchsia.dev/infra/gerrit"
)
func cmdWaitForCQ(authOpts auth.Options) *subcommands.Command {
return &subcommands.Command{
UsageLine: "wait-for-cq -host <gerrit-host> -project <gerrit-project> -change-num <change-num> -json-output <json-output> [-timeout <timeout>]",
ShortDesc: "Wait for CQ result for a CL.",
LongDesc: "wait for CQ result for a CL. Note this currently only supports CQ dry runs.",
CommandRun: func() subcommands.CommandRun {
c := &waitForCQRun{}
c.Init(authOpts)
return c
},
}
}
type waitForCQRun struct {
commonFlags
changeNum int64
jsonOutput string
timeout time.Duration
}
func (c *waitForCQRun) Init(defaultAuthOpts auth.Options) {
c.commonFlags.Init(defaultAuthOpts)
c.Flags.Int64Var(&c.changeNum, "change-num", 0, "Gerrit change number.")
c.Flags.StringVar(&c.jsonOutput, "json-output", "", "Path to write CQ status to.")
c.Flags.DurationVar(&c.timeout, "timeout", 0, "Wait this long for CQ to finish; indefinite if not set.")
}
func (c *waitForCQRun) Parse(a subcommands.Application, args []string) error {
if err := c.commonFlags.Parse(); err != nil {
return err
}
if c.changeNum == 0 {
return errors.New("-change-num is required")
}
if c.jsonOutput == "" {
return errors.New("-json-output is required")
}
return nil
}
func (c *waitForCQRun) main(a subcommands.Application) error {
ctx := context.Background()
authClient, err := newAuthClient(ctx, c.parsedAuthOpts)
if err != nil {
return err
}
client, err := gerrit.NewClient(c.gerritHost, c.gerritProject, authClient)
if err != nil {
return err
}
// Check for CQ completion once a minute.
retryPolicy := transient.Only(func() retry.Iterator {
return &retry.ExponentialBackoff{
Limited: retry.Limited{
Delay: time.Minute,
Retries: -1,
},
Multiplier: 1,
}
})
var cancel context.CancelFunc
if c.timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, c.timeout)
defer cancel()
}
if err := retry.Retry(ctx, retryPolicy, func() error {
return client.CheckCQCompletion(ctx, c.changeNum)
}, nil); err != nil {
return err
}
// Retry transient failures once when looking for CQ pass/fail message.
retryPolicy = transient.Only(func() retry.Iterator {
return &retry.ExponentialBackoff{
Limited: retry.Limited{
Delay: time.Minute,
Retries: 1,
},
Multiplier: 1,
}
})
var passed bool
if err := retry.Retry(ctx, retryPolicy, func() error {
var err error
passed, err = client.CQDryRunPassed(ctx, c.changeNum)
return err
}, nil); err != nil {
return err
}
out := os.Stdout
if c.jsonOutput != "-" {
out, err = os.Create(c.jsonOutput)
if err != nil {
return err
}
defer out.Close()
}
if err := json.NewEncoder(out).Encode(passed); err != nil {
return fmt.Errorf("failed to encode: %w", err)
}
return nil
}
func (c *waitForCQRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if err := c.Parse(a, args); err != nil {
fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
return 1
}
if err := c.main(a); err != nil {
fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
return 1
}
return 0
}