cli/commands/ci/trace/trace.go

175 lines
4.0 KiB
Go

package trace
import (
"context"
"errors"
"fmt"
"regexp"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/prompt"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/ci/ciutils"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/pkg/git"
"gitlab.com/gitlab-org/cli/pkg/utils"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
)
type TraceOpts struct {
Branch string
JobID int
BaseRepo func() (glrepo.Interface, error)
HTTPClient func() (*gitlab.Client, error)
IO *iostreams.IOStreams
}
func NewCmdTrace(f *cmdutils.Factory, runE func(traceOpts *TraceOpts) error) *cobra.Command {
opts := &TraceOpts{
IO: f.IO,
}
pipelineCITraceCmd := &cobra.Command{
Use: "trace [<job-id>] [flags]",
Short: `Trace a CI job log in real time`,
Example: heredoc.Doc(`
$ glab ci trace
#=> interactively select a job to trace
$ glab ci trace 224356863
#=> trace job with id 224356863
`),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
// support `-R, --repo` override
//
// NOTE: it is important to assign the BaseRepo and HTTPClient in RunE because
// they are overridden in a PersistentRun hook (when `-R, --repo` is specified)
// which runs before RunE is executed
opts.BaseRepo = f.BaseRepo
opts.HTTPClient = f.HttpClient
if len(args) != 0 {
opts.JobID = utils.StringToInt(args[0])
}
if opts.Branch == "" {
opts.Branch, err = git.CurrentBranch()
if err != nil {
return err
}
}
if runE != nil {
return runE(opts)
}
return TraceRun(opts)
},
}
pipelineCITraceCmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "Check pipeline status for a branch. (Default is the current branch)")
return pipelineCITraceCmd
}
func TraceRun(opts *TraceOpts) error {
apiClient, err := opts.HTTPClient()
if err != nil {
return err
}
repo, err := opts.BaseRepo()
if err != nil {
return err
}
if opts.JobID < 1 {
fmt.Fprintf(opts.IO.StdOut, "\nSearching for latest pipeline on %s...\n", opts.Branch)
pipeline, err := api.GetLastPipeline(apiClient, repo.FullName(), opts.Branch)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.StdOut, "Getting jobs for pipeline %d...\n\n", pipeline.ID)
jobs, err := api.GetPipelineJobs(apiClient, pipeline.ID, repo.FullName())
if err != nil {
return err
}
var jobOptions []string
var selectedJob string
for _, job := range jobs {
jobOptions = append(jobOptions, fmt.Sprintf("%s (%d) - %s", job.Name, job.ID, job.Status))
}
promptOpts := &survey.Select{
Message: "Select pipeline job to trace:",
Options: jobOptions,
}
err = prompt.AskOne(promptOpts, &selectedJob)
if err != nil {
if errors.Is(err, terminal.InterruptErr) {
return nil
}
return err
}
if selectedJob != "" {
re := regexp.MustCompile(`(?s)\((.*)\)`)
m := re.FindAllStringSubmatch(selectedJob, -1)
opts.JobID = utils.StringToInt(m[0][1])
} else if len(jobs) > 0 {
opts.JobID = jobs[0].ID
} else {
// use commit statuses to show external jobs
cs, err := api.GetCommitStatuses(apiClient, repo.FullName(), pipeline.SHA)
if err != nil {
return nil
}
c := opts.IO.Color()
fmt.Fprint(opts.IO.StdOut, "Getting external jobs...")
for _, status := range cs {
var s string
switch status.Status {
case "success":
s = c.Green(status.Status)
case "error":
s = c.Red(status.Status)
default:
s = c.Gray(status.Status)
}
fmt.Fprintf(opts.IO.StdOut, "(%s) %s\nURL: %s\n\n", s, c.Bold(status.Name), c.Gray(status.TargetURL))
}
return nil
}
}
job, err := api.GetPipelineJob(apiClient, opts.JobID, repo.FullName())
if err != nil {
return err
}
fmt.Fprintln(opts.IO.StdOut)
err = ciutils.RunTrace(context.Background(), apiClient, opts.IO.StdOut, repo.FullName(), job, job.Name)
if err != nil {
return err
}
return nil
}