go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/cl-util/wait_for_cq.go (about)

     1  // Copyright 2022 The Fuchsia Authors. All rights reserved.
     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  	"time"
    14  
    15  	"github.com/maruel/subcommands"
    16  	"go.chromium.org/luci/auth"
    17  	"go.chromium.org/luci/common/logging"
    18  	"go.chromium.org/luci/common/logging/gologger"
    19  	"go.chromium.org/luci/common/retry"
    20  	"go.chromium.org/luci/common/retry/transient"
    21  
    22  	"go.fuchsia.dev/infra/gerrit"
    23  )
    24  
    25  func cmdWaitForCQ(authOpts auth.Options) *subcommands.Command {
    26  	return &subcommands.Command{
    27  		UsageLine: "wait-for-cq -host <gerrit-host> -project <gerrit-project> -change-num <change-num> -json-output <json-output> [-timeout <timeout>]",
    28  		ShortDesc: "Wait for CQ result for a CL.",
    29  		LongDesc:  "wait for CQ result for a CL. Note this currently only supports CQ dry runs.",
    30  		CommandRun: func() subcommands.CommandRun {
    31  			c := &waitForCQRun{}
    32  			c.Init(authOpts)
    33  			return c
    34  		},
    35  	}
    36  }
    37  
    38  type waitForCQRun struct {
    39  	commonFlags
    40  	changeNum   int64
    41  	jsonOutput  string
    42  	timeout     time.Duration
    43  	gerritLabel string
    44  }
    45  
    46  func (c *waitForCQRun) Init(defaultAuthOpts auth.Options) {
    47  	c.commonFlags.Init(defaultAuthOpts)
    48  	c.Flags.Int64Var(&c.changeNum, "change-num", 0, "Gerrit change number.")
    49  	c.Flags.StringVar(&c.jsonOutput, "json-output", "", "Path to write CQ status to.")
    50  	c.Flags.DurationVar(&c.timeout, "timeout", 0, "Wait this long for CQ to finish; indefinite if not set.")
    51  	c.Flags.StringVar(&c.gerritLabel, "gerrit-label", "", "Gerrit label indicating CQ pass/fail status. If not set, comment text is used to determine pass/fail status.")
    52  }
    53  
    54  func (c *waitForCQRun) Parse(a subcommands.Application, args []string) error {
    55  	if err := c.commonFlags.Parse(); err != nil {
    56  		return err
    57  	}
    58  	if c.changeNum == 0 {
    59  		return errors.New("-change-num is required")
    60  	}
    61  	if c.jsonOutput == "" {
    62  		return errors.New("-json-output is required")
    63  	}
    64  	return nil
    65  }
    66  
    67  func (c *waitForCQRun) main(a subcommands.Application) error {
    68  	ctx := context.Background()
    69  	ctx = logging.SetLevel(ctx, c.logLevel)
    70  	ctx = gologger.StdConfig.Use(ctx)
    71  	authClient, err := newAuthClient(ctx, c.parsedAuthOpts)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	client, err := gerrit.NewClient(c.gerritHost, c.gerritProject, authClient)
    76  	if err != nil {
    77  		return err
    78  	}
    79  	// Check for CQ completion once a minute.
    80  	retryPolicy := transient.Only(func() retry.Iterator {
    81  		return &retry.ExponentialBackoff{
    82  			Limited: retry.Limited{
    83  				Delay:   time.Minute,
    84  				Retries: -1,
    85  			},
    86  			Multiplier: 1,
    87  		}
    88  	})
    89  	var cancel context.CancelFunc
    90  	if c.timeout > 0 {
    91  		ctx, cancel = context.WithTimeout(ctx, c.timeout)
    92  		defer cancel()
    93  	}
    94  	if err := retry.Retry(ctx, retryPolicy, func() error {
    95  		return client.CheckCQCompletion(ctx, c.changeNum, c.gerritLabel)
    96  	}, nil); err != nil {
    97  		return err
    98  	}
    99  	// Retry transient failures once when looking for CQ pass/fail message.
   100  	retryPolicy = transient.Only(func() retry.Iterator {
   101  		return &retry.ExponentialBackoff{
   102  			Limited: retry.Limited{
   103  				Delay:   time.Minute,
   104  				Retries: 1,
   105  			},
   106  			Multiplier: 1,
   107  		}
   108  	})
   109  	var passed bool
   110  	if err := retry.Retry(ctx, retryPolicy, func() error {
   111  		var err error
   112  		passed, err = client.CQDryRunPassed(ctx, c.changeNum, c.gerritLabel)
   113  		return err
   114  	}, nil); err != nil {
   115  		return err
   116  	}
   117  
   118  	out := os.Stdout
   119  	if c.jsonOutput != "-" {
   120  		out, err = os.Create(c.jsonOutput)
   121  		if err != nil {
   122  			return err
   123  		}
   124  		defer out.Close()
   125  	}
   126  	if err := json.NewEncoder(out).Encode(passed); err != nil {
   127  		return fmt.Errorf("failed to encode: %w", err)
   128  	}
   129  	return nil
   130  }
   131  
   132  func (c *waitForCQRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   133  	if err := c.Parse(a, args); err != nil {
   134  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   135  		return 1
   136  	}
   137  
   138  	if err := c.main(a); err != nil {
   139  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
   140  		return 1
   141  	}
   142  	return 0
   143  }