mirror of https://gitlab.com/gitlab-org/cli.git
302 lines
7.6 KiB
Go
302 lines
7.6 KiB
Go
package ciutils
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitlab.com/gitlab-org/cli/internal/glrepo"
|
|
"gitlab.com/gitlab-org/cli/pkg/git"
|
|
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
|
"gitlab.com/gitlab-org/cli/pkg/prompt"
|
|
|
|
"gitlab.com/gitlab-org/cli/api"
|
|
"gitlab.com/gitlab-org/cli/pkg/tableprinter"
|
|
"gitlab.com/gitlab-org/cli/pkg/utils"
|
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/AlecAivazis/survey/v2/terminal"
|
|
"github.com/pkg/errors"
|
|
"github.com/xanzy/go-gitlab"
|
|
)
|
|
|
|
var (
|
|
once sync.Once
|
|
offset int64
|
|
)
|
|
|
|
func makeHyperlink(s *iostreams.IOStreams, pipeline *gitlab.PipelineInfo) string {
|
|
return s.Hyperlink(fmt.Sprintf("%d", pipeline.ID), pipeline.WebURL)
|
|
}
|
|
|
|
func DisplaySchedules(i *iostreams.IOStreams, s []*gitlab.PipelineSchedule, projectID string) string {
|
|
if len(s) > 0 {
|
|
table := tableprinter.NewTablePrinter()
|
|
table.AddRow("ID", "Description", "Cron", "Owner", "Active")
|
|
for _, schedule := range s {
|
|
table.AddRow(schedule.ID, schedule.Description, schedule.Cron, schedule.Owner.Username, schedule.Active)
|
|
}
|
|
|
|
return table.Render()
|
|
}
|
|
|
|
// return empty string, since when there is no schedule, the title will already display it accordingly
|
|
return ""
|
|
}
|
|
|
|
func DisplayMultiplePipelines(s *iostreams.IOStreams, p []*gitlab.PipelineInfo, projectID string) string {
|
|
c := s.Color()
|
|
|
|
table := tableprinter.NewTablePrinter()
|
|
|
|
if len(p) > 0 {
|
|
|
|
for _, pipeline := range p {
|
|
duration := ""
|
|
|
|
if pipeline.CreatedAt != nil {
|
|
duration = c.Magenta("(" + utils.TimeToPrettyTimeAgo(*pipeline.CreatedAt) + ")")
|
|
}
|
|
|
|
var pipeState string
|
|
if pipeline.Status == "success" {
|
|
pipeState = c.Green(fmt.Sprintf("(%s) • #%s", pipeline.Status, makeHyperlink(s, pipeline)))
|
|
} else if pipeline.Status == "failed" {
|
|
pipeState = c.Red(fmt.Sprintf("(%s) • #%s", pipeline.Status, makeHyperlink(s, pipeline)))
|
|
} else {
|
|
pipeState = c.Gray(fmt.Sprintf("(%s) • #%s", pipeline.Status, makeHyperlink(s, pipeline)))
|
|
}
|
|
|
|
table.AddRow(pipeState, fmt.Sprintf("(#%d)", pipeline.IID), pipeline.Ref, duration)
|
|
}
|
|
|
|
return table.Render()
|
|
}
|
|
|
|
return "No Pipelines available on " + projectID
|
|
}
|
|
|
|
func RunTraceSha(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid interface{}, sha, name string) error {
|
|
job, err := api.PipelineJobWithSha(apiClient, pid, sha, name)
|
|
if err != nil || job == nil {
|
|
return errors.Wrap(err, "failed to find job")
|
|
}
|
|
return runTrace(ctx, apiClient, w, pid, job.ID)
|
|
}
|
|
|
|
func runTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid interface{}, jobId int) error {
|
|
fmt.Fprintln(w, "Getting job trace...")
|
|
for range time.NewTicker(time.Second * 3).C {
|
|
if ctx.Err() == context.Canceled {
|
|
break
|
|
}
|
|
job, _, err := apiClient.Jobs.GetJob(pid, jobId)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to find job")
|
|
}
|
|
switch job.Status {
|
|
case "pending":
|
|
fmt.Fprintf(w, "%s is pending... waiting for job to start\n", job.Name)
|
|
continue
|
|
case "manual":
|
|
fmt.Fprintf(w, "Manual job %s not started, waiting for job to start\n", job.Name)
|
|
continue
|
|
case "skipped":
|
|
fmt.Fprintf(w, "%s has been skipped\n", job.Name)
|
|
}
|
|
once.Do(func() {
|
|
fmt.Fprintf(w, "Showing logs for %s job #%d\n", job.Name, job.ID)
|
|
})
|
|
trace, _, err := apiClient.Jobs.GetTraceFile(pid, jobId)
|
|
if err != nil || trace == nil {
|
|
return errors.Wrap(err, "failed to find job")
|
|
}
|
|
_, _ = io.CopyN(io.Discard, trace, offset)
|
|
lenT, err := io.Copy(w, trace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
offset += lenT
|
|
|
|
if job.Status == "success" ||
|
|
job.Status == "failed" ||
|
|
job.Status == "cancelled" {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetJobId(inputs *JobInputs, opts *JobOptions) (int, error) {
|
|
// If the user hasn't supplied an argument, we display the jobs list interactively.
|
|
if inputs.JobName == "" {
|
|
return getJobIdInteractive(inputs, opts)
|
|
}
|
|
|
|
// If the user supplied a job ID, we can use it directly.
|
|
if jobID, err := strconv.Atoi(inputs.JobName); err == nil {
|
|
return jobID, nil
|
|
}
|
|
|
|
// Otherwise, we try to find the latest job ID based on the job name.
|
|
pipelineId, err := getPipelineId(inputs, opts)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("get pipeline: %w", err)
|
|
}
|
|
|
|
jobs, _, err := opts.ApiClient.Jobs.ListPipelineJobs(opts.Repo.FullName(), pipelineId, nil)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("list pipeline jobs: %w", err)
|
|
}
|
|
|
|
for _, job := range jobs {
|
|
if job.Name == inputs.JobName {
|
|
return job.ID, nil
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("pipeline %d contains no jobs", pipelineId)
|
|
}
|
|
|
|
func getPipelineId(inputs *JobInputs, opts *JobOptions) (int, error) {
|
|
if inputs.PipelineId != 0 {
|
|
return inputs.PipelineId, nil
|
|
}
|
|
|
|
branch, err := getBranch(inputs.Branch, opts)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("get branch: %w", err)
|
|
}
|
|
|
|
pipeline, err := api.GetLastPipeline(opts.ApiClient, opts.Repo.FullName(), branch)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("get last pipeline: %w", err)
|
|
}
|
|
return pipeline.ID, err
|
|
}
|
|
|
|
func getBranch(branch string, opts *JobOptions) (string, error) {
|
|
if branch != "" {
|
|
return branch, nil
|
|
}
|
|
|
|
branch, err := git.CurrentBranch()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return branch, nil
|
|
}
|
|
|
|
func getJobIdInteractive(inputs *JobInputs, opts *JobOptions) (int, error) {
|
|
pipelineId, err := getPipelineId(inputs, opts)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
fmt.Fprintf(opts.IO.StdOut, "Getting jobs for pipeline %d...\n\n", pipelineId)
|
|
|
|
jobs, err := api.GetPipelineJobs(opts.ApiClient, pipelineId, opts.Repo.FullName())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var jobOptions []string
|
|
var selectedJob string
|
|
|
|
for _, job := range jobs {
|
|
if inputs.SelectionPredicate == nil || inputs.SelectionPredicate(job) {
|
|
jobOptions = append(jobOptions, fmt.Sprintf("%s (%d) - %s", job.Name, job.ID, job.Status))
|
|
}
|
|
}
|
|
|
|
messagePrompt := inputs.SelectionPrompt
|
|
if messagePrompt == "" {
|
|
messagePrompt = "Select pipeline job to trace:"
|
|
}
|
|
|
|
promptOpts := &survey.Select{
|
|
Message: messagePrompt,
|
|
Options: jobOptions,
|
|
}
|
|
if len(jobOptions) > 0 {
|
|
|
|
err = prompt.AskOne(promptOpts, &selectedJob)
|
|
if err != nil {
|
|
if errors.Is(err, terminal.InterruptErr) {
|
|
return 0, nil
|
|
}
|
|
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
if selectedJob != "" {
|
|
re := regexp.MustCompile(`(?s)\((.*)\)`)
|
|
m := re.FindAllStringSubmatch(selectedJob, -1)
|
|
return utils.StringToInt(m[0][1]), nil
|
|
} else if len(jobs) > 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
pipeline, err := api.GetPipeline(opts.ApiClient, pipelineId, nil, opts.Repo.FullName())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
// use commit statuses to show external jobs
|
|
cs, err := api.GetCommitStatuses(opts.ApiClient, opts.Repo.FullName(), pipeline.SHA)
|
|
if err != nil {
|
|
return 0, nil
|
|
}
|
|
|
|
c := opts.IO.Color()
|
|
|
|
fmt.Fprint(opts.IO.StdOut, "Getting external jobs...\n")
|
|
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 0, nil
|
|
}
|
|
|
|
type JobInputs struct {
|
|
JobName string
|
|
Branch string
|
|
PipelineId int
|
|
SelectionPrompt string
|
|
SelectionPredicate func(s *gitlab.Job) bool
|
|
}
|
|
|
|
type JobOptions struct {
|
|
ApiClient *gitlab.Client
|
|
Repo glrepo.Interface
|
|
IO *iostreams.IOStreams
|
|
}
|
|
|
|
func TraceJob(inputs *JobInputs, opts *JobOptions) error {
|
|
jobID, err := GetJobId(inputs, opts)
|
|
if err != nil {
|
|
fmt.Fprintln(opts.IO.StdErr, "invalid job ID:", inputs.JobName)
|
|
return err
|
|
}
|
|
if jobID == 0 {
|
|
return nil
|
|
}
|
|
fmt.Fprintln(opts.IO.StdOut)
|
|
return runTrace(context.Background(), opts.ApiClient, opts.IO.StdOut, opts.Repo.FullName(), jobID)
|
|
}
|