blob: 2b5b98316bf9a7c9279539c76624b7bf6e8ac4f2 [file] [log] [blame]
package try
import (
"bufio"
"context"
"fmt"
"io"
"os"
"regexp"
"sort"
"strings"
"github.com/urfave/cli/v2"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/repo_root"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/task_scheduler/go/specs"
)
var (
// stdin is an abstraction of os.Stdin which is convenient for testing.
stdin io.Reader = os.Stdin
// tryjobs is an instance of tryJobReader which may be replaced for testing.
tryjobs tryJobReader = &tryJobReaderImpl{}
)
// Command returns a cli.Command instance which represents the "try" command.
func Command() *cli.Command {
yFlag := "y"
return &cli.Command{
Name: "try",
Usage: "try [-y] [job name or regex]...",
Description: "Run try jobs against the active CL",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: yFlag,
Value: false,
Usage: "Trigger all matching try jobs without asking for confirmation.",
},
},
Action: func(ctx *cli.Context) error {
if err := fixupIssue(ctx.Context); err != nil {
return err
}
return try(ctx.Context, ctx.Args().Slice(), ctx.Bool(yFlag))
},
}
}
// try loads the available try jobs, filters by the given request strings, and
// triggers the try jobs selected by the user.
func try(ctx context.Context, jobRequests []string, triggerWithoutPrompt bool) error {
// Setup.
jobs, err := tryjobs.getTryJobs(ctx)
if err != nil {
return err
}
// Filter by the given requested job names/regexes.
filteredJobs := jobs
if len(jobRequests) > 0 {
jobRegexes := make([]*regexp.Regexp, 0, len(jobRequests))
for _, jobRequest := range jobRequests {
jobRegex, err := regexp.Compile(jobRequest)
if err != nil {
return err
}
jobRegexes = append(jobRegexes, jobRegex)
}
filteredJobs = map[string][]string{}
for bucket, jobList := range jobs {
for _, job := range jobList {
for _, jobRegex := range jobRegexes {
if jobRegex.MatchString(job) {
filteredJobs[bucket] = append(filteredJobs[bucket], job)
break
}
}
}
}
}
// Prompt the user before triggering jobs.
count := 0
for _, jobList := range filteredJobs {
count += len(jobList)
sort.Strings(jobList)
}
if count == 0 {
return skerr.Fmt("Found no jobs matching %v", jobRequests)
}
fmt.Println(fmt.Sprintf("Found %d jobs:", count))
for bucket, jobList := range filteredJobs {
fmt.Println(fmt.Sprintf(" %s:", bucket))
for _, job := range jobList {
fmt.Println(fmt.Sprintf(" %s", job))
}
}
if len(jobRequests) == 0 || count == 0 {
return nil
}
jobsToTrigger := filteredJobs
if !triggerWithoutPrompt {
fmt.Printf("Do you want to trigger these jobs? (y/n or i for interactive): ")
reader := bufio.NewReader(stdin)
read, err := reader.ReadString('\n')
if err != nil {
return err
}
read = strings.TrimSpace(read)
if read != "y" && read != "i" {
return nil
}
if read == "i" {
jobsToTrigger = map[string][]string{}
for bucket, jobList := range filteredJobs {
for _, job := range jobList {
fmt.Printf("Trigger %s? (y/n): ", job)
trigger, err := reader.ReadString('\n')
if err != nil {
return err
}
if strings.TrimSpace(trigger) == "y" {
jobsToTrigger[bucket] = append(jobsToTrigger[bucket], job)
}
}
}
}
}
// Trigger the try jobs.
for bucket, jobList := range jobsToTrigger {
cmd := []string{"git", "cl", "try", "-B", bucket}
for _, job := range jobList {
cmd = append(cmd, "-b", job)
}
if _, err := exec.RunCwd(ctx, ".", cmd...); err != nil {
return err
}
}
return nil
}
// fixupIssue ensures that the Gerrit issue is set via "git cl issue",
// regardless of whether "git cl upload" was used to create the change.
func fixupIssue(ctx context.Context) error {
// If the change was uploaded using Depot Tools, the issue configuration
// will already be present in the git config.
output, err := exec.RunCwd(ctx, ".", "git", "branch", "--show-current")
if err != nil {
return err
}
branch := strings.TrimSpace(output)
if _, err := exec.RunCwd(ctx, ".", "git", "config", "--local", fmt.Sprintf("branch.%s.gerritissue", branch)); err == nil {
return nil
}
// If the above failed, it's probably because the CL was not uploaded
// using Depot Tools. Find the Change-Id line in the most recent commit
// and obtain the issue number using that.
output, err = exec.RunCwd(ctx, ".", "git", "log", "n1", branch)
if err != nil {
return err
}
changeId, err := gerrit.ParseChangeId(output)
if err != nil {
return err
}
g, err := gerrit.NewGerrit(gerrit.GerritSkiaURL, httputils.DefaultClientConfig().Client())
if err != nil {
return err
}
ci, err := g.GetChange(ctx, changeId)
if err != nil {
return err
}
issue := fmt.Sprintf("%d", ci.Issue)
if _, err := exec.RunCwd(ctx, ".", "git", "cl", "issue", issue); err != nil {
return err
}
return nil
}
// tryJobReader provides an abstraction for reading the available set of try
// jobs to facilitate testing.
type tryJobReader interface {
// getTryJobs reads tasks.json from the current repo and returns a
// map[string][]string of Buildbucket bucket names to try job names.
getTryJobs(context.Context) (map[string][]string, error)
}
// tryJobReaderImpl is the default tryJobReader implementation which reads from
// the tasks.json file in the current repo.
type tryJobReaderImpl struct{}
// GetTryJobs implements tryJobReader.
func (r *tryJobReaderImpl) getTryJobs(ctx context.Context) (map[string][]string, error) {
repoRoot, err := repo_root.GetLocal()
if err != nil {
return nil, err
}
tasksCfg, err := specs.ReadTasksCfg(repoRoot)
if err != nil {
return nil, err
}
jobs := make([]string, 0, len(tasksCfg.Jobs))
for name := range tasksCfg.Jobs {
jobs = append(jobs, name)
}
// TODO(borenet): This assumes that the current repo is associated with the
// skia.primary bucket. This will work for most repos but it would be better
// to look up the correct bucket to use.
return map[string][]string{
"skia/skia.primary": jobs,
}, nil
}