mirror of https://gitlab.com/gitlab-org/cli.git
feat(ci): align job id resolution ( #7422)
This commit is contained in:
parent
d6a7b20cd2
commit
54dfd62e5e
|
@ -50,7 +50,7 @@ func NewCmdSet(f *cmdutils.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
Example: heredoc.Doc(`
|
||||
$ glab alias set mrv 'mr view'
|
||||
$ glab mrv -w 123
|
||||
#=> glab mr view -w 123
|
||||
# glab mr view -w 123
|
||||
|
||||
$ glab alias set createissue 'glab create issue --title "$1"'
|
||||
$ glab createissue "My Issue" --description "Something is broken."
|
||||
|
@ -58,7 +58,7 @@ func NewCmdSet(f *cmdutils.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
|
||||
$ glab alias set --shell igrep 'glab issue list --assignee="$1" | grep $2'
|
||||
$ glab igrep user foo
|
||||
#=> glab issue list --assignee="user" | grep "foo"
|
||||
# glab issue list --assignee="user" | grep "foo"
|
||||
`),
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
|
|
@ -28,7 +28,7 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command {
|
|||
cmdutils.EnableRepoOverride(ciCmd, f)
|
||||
|
||||
ciCmd.AddCommand(legacyCICmd.NewCmdCI(f))
|
||||
ciCmd.AddCommand(ciTraceCmd.NewCmdTrace(f, nil))
|
||||
ciCmd.AddCommand(ciTraceCmd.NewCmdTrace(f))
|
||||
ciCmd.AddCommand(ciViewCmd.NewCmdView(f))
|
||||
ciCmd.AddCommand(ciLintCmd.NewCmdLint(f))
|
||||
ciCmd.AddCommand(pipeDeleteCmd.NewCmdDelete(f))
|
||||
|
|
|
@ -4,15 +4,22 @@ 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"
|
||||
)
|
||||
|
@ -78,17 +85,17 @@ func RunTraceSha(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid
|
|||
if err != nil || job == nil {
|
||||
return errors.Wrap(err, "failed to find job")
|
||||
}
|
||||
return RunTrace(ctx, apiClient, w, pid, job, name)
|
||||
return runTrace(ctx, apiClient, w, pid, job.ID)
|
||||
}
|
||||
|
||||
func RunTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid interface{}, job *gitlab.Job, name string) error {
|
||||
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
|
||||
}
|
||||
trace, _, err := apiClient.Jobs.GetTraceFile(pid, job.ID)
|
||||
if err != nil || trace == nil {
|
||||
job, _, err := apiClient.Jobs.GetJob(pid, jobId)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to find job")
|
||||
}
|
||||
switch job.Status {
|
||||
|
@ -102,11 +109,12 @@ func RunTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid in
|
|||
fmt.Fprintf(w, "%s has been skipped\n", job.Name)
|
||||
}
|
||||
once.Do(func() {
|
||||
if name == "" {
|
||||
name = job.Name
|
||||
}
|
||||
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 {
|
||||
|
@ -122,3 +130,158 @@ func RunTrace(ctx context.Context, apiClient *gitlab.Client, w io.Writer, pid in
|
|||
}
|
||||
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 {
|
||||
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 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 jobs[0].ID, nil
|
||||
} else {
|
||||
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...")
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
fmt.Fprintln(opts.IO.StdOut)
|
||||
return runTrace(context.Background(), opts.ApiClient, opts.IO.StdOut, opts.Repo.FullName(), jobID)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
package ciutils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/gitlab-org/cli/commands/cmdtest"
|
||||
"gitlab.com/gitlab-org/cli/pkg/httpmock"
|
||||
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
||||
"gitlab.com/gitlab-org/cli/pkg/prompt"
|
||||
)
|
||||
|
||||
func TestGetJobId(t *testing.T) {
|
||||
type httpMock struct {
|
||||
method string
|
||||
path string
|
||||
status int
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
jobName string
|
||||
pipelineId int
|
||||
httpMocks []httpMock
|
||||
askOneStubs []string
|
||||
expectedOut int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "when getJobId with integer is requested",
|
||||
jobName: "1122",
|
||||
expectedOut: 1122,
|
||||
httpMocks: []httpMock{},
|
||||
}, {
|
||||
name: "when getJobId with name and pipelineId is requested",
|
||||
jobName: "lint",
|
||||
pipelineId: 123,
|
||||
expectedOut: 1122,
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "when getJobId with name and pipelineId is requested and listJobs throws error",
|
||||
jobName: "lint",
|
||||
pipelineId: 123,
|
||||
expectedError: "list pipeline jobs: GET https://gitlab.com/api/v4/projects/OWNER/REPO/pipelines/123/jobs: 403 ",
|
||||
expectedOut: 0,
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "when getJobId with name and last pipeline is requested",
|
||||
jobName: "lint",
|
||||
pipelineId: 0,
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/repository/commits/main",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"last_pipeline" : {
|
||||
"id": 123
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
expectedOut: 1122,
|
||||
}, {
|
||||
name: "when getJobId with name and last pipeline is requested and getCommits throws error",
|
||||
jobName: "lint",
|
||||
pipelineId: 0,
|
||||
expectedError: "get pipeline: get last pipeline: GET https://gitlab.com/api/v4/projects/OWNER/REPO/repository/commits/main: 403 ",
|
||||
expectedOut: 0,
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/repository/commits/main",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "when getJobId with name and last pipeline is requested and getJobs throws error",
|
||||
jobName: "lint",
|
||||
pipelineId: 0,
|
||||
expectedError: "list pipeline jobs: GET https://gitlab.com/api/v4/projects/OWNER/REPO/pipelines/123/jobs: 403 ",
|
||||
expectedOut: 0,
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/repository/commits/main",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"last_pipeline" : {
|
||||
"id": 123
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "when getJobId with pipelineId is requested, ask for job and answer",
|
||||
jobName: "",
|
||||
pipelineId: 123,
|
||||
expectedOut: 1122,
|
||||
askOneStubs: []string{"lint (1122) - failed"},
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs?per_page=100",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "when getJobId with pipelineId is requested, ask for job and give no answer",
|
||||
jobName: "",
|
||||
pipelineId: 123,
|
||||
expectedOut: 1122,
|
||||
askOneStubs: []string{""},
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs?per_page=100",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeHTTP := &httpmock.Mocker{
|
||||
MatchURL: httpmock.PathAndQuerystring,
|
||||
}
|
||||
|
||||
if tc.askOneStubs != nil {
|
||||
as, restoreAsk := prompt.InitAskStubber()
|
||||
defer restoreAsk()
|
||||
for _, value := range tc.askOneStubs {
|
||||
as.StubOne(value)
|
||||
}
|
||||
}
|
||||
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
for _, mock := range tc.httpMocks {
|
||||
fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body))
|
||||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := cmdtest.InitFactory(ios, fakeHTTP)
|
||||
|
||||
_, _ = f.HttpClient()
|
||||
|
||||
apiClient, _ := f.HttpClient()
|
||||
repo, _ := f.BaseRepo()
|
||||
|
||||
output, err := GetJobId(&JobInputs{
|
||||
JobName: tc.jobName,
|
||||
PipelineId: tc.pipelineId,
|
||||
Branch: "main",
|
||||
}, &JobOptions{
|
||||
IO: f.IO,
|
||||
Repo: repo,
|
||||
ApiClient: apiClient,
|
||||
})
|
||||
|
||||
if tc.expectedError == "" {
|
||||
require.Nil(t, err)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, tc.expectedError, err.Error())
|
||||
}
|
||||
assert.Equal(t, tc.expectedOut, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceJob(t *testing.T) {
|
||||
type httpMock struct {
|
||||
method string
|
||||
path string
|
||||
status int
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
jobName string
|
||||
pipelineId int
|
||||
httpMocks []httpMock
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "when traceJob is requested",
|
||||
jobName: "1122",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/trace",
|
||||
http.StatusOK,
|
||||
`Lorem ipsum`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "success"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when traceJob is requested and getJob throws error",
|
||||
jobName: "1122",
|
||||
expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122: 403 ",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when traceJob is requested and getJob throws error",
|
||||
jobName: "1122",
|
||||
expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122/trace: 403 ",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/trace",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "success"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeHTTP := &httpmock.Mocker{
|
||||
MatchURL: httpmock.PathAndQuerystring,
|
||||
}
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
for _, mock := range tc.httpMocks {
|
||||
fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body))
|
||||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := cmdtest.InitFactory(ios, fakeHTTP)
|
||||
|
||||
_, _ = f.HttpClient()
|
||||
|
||||
apiClient, _ := f.HttpClient()
|
||||
repo, _ := f.BaseRepo()
|
||||
|
||||
err := TraceJob(&JobInputs{
|
||||
JobName: tc.jobName,
|
||||
PipelineId: tc.pipelineId,
|
||||
Branch: "main",
|
||||
}, &JobOptions{
|
||||
IO: f.IO,
|
||||
Repo: repo,
|
||||
ApiClient: apiClient,
|
||||
})
|
||||
|
||||
if tc.expectedError == "" {
|
||||
require.Nil(t, err)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, tc.expectedError, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command {
|
|||
`),
|
||||
}
|
||||
|
||||
pipelineCICmd.AddCommand(ciTraceCmd.NewCmdTrace(f, nil))
|
||||
pipelineCICmd.AddCommand(ciTraceCmd.NewCmdTrace(f))
|
||||
pipelineCICmd.AddCommand(ciViewCmd.NewCmdView(f))
|
||||
pipelineCICmd.AddCommand(ciLintCmd.NewCmdLint(f))
|
||||
pipelineCICmd.Deprecated = "This command is deprecated. All the commands under it has been moved to `ci` or `pipeline` command. See https://gitlab.com/gitlab-org/cli/issues/372 for more info."
|
||||
|
|
|
@ -22,7 +22,7 @@ func NewCmdLint(f *cmdutils.Factory) *cobra.Command {
|
|||
Args: cobra.MaximumNArgs(1),
|
||||
Example: heredoc.Doc(`
|
||||
$ glab ci lint
|
||||
#=> Uses .gitlab-ci.yml in the current directory
|
||||
# Uses .gitlab-ci.yml in the current directory
|
||||
|
||||
$ glab ci lint .gitlab-ci.yml
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"fmt"
|
||||
|
||||
"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/utils"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -17,39 +17,60 @@ func NewCmdRetry(f *cmdutils.Factory) *cobra.Command {
|
|||
Short: `Retry a CI/CD job`,
|
||||
Aliases: []string{},
|
||||
Example: heredoc.Doc(`
|
||||
glab ci retry 871528
|
||||
$ glab ci retry
|
||||
# Interactively select a job to retry
|
||||
|
||||
$ glab ci retry 224356863
|
||||
# Retry job with ID 224356863
|
||||
|
||||
$ glab ci retry lint
|
||||
# Retry job with the name 'lint'
|
||||
`),
|
||||
Long: ``,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
|
||||
apiClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo, err := f.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jobID := utils.StringToInt(args[0])
|
||||
jobName := ""
|
||||
if len(args) != 0 {
|
||||
jobName = args[0]
|
||||
}
|
||||
branch, _ := cmd.Flags().GetString("branch")
|
||||
pipelineId, _ := cmd.Flags().GetInt("pipeline-id")
|
||||
|
||||
if jobID < 1 {
|
||||
fmt.Fprintln(f.IO.StdErr, "invalid job id:", args[0])
|
||||
return cmdutils.SilentError
|
||||
jobID, err := ciutils.GetJobId(&ciutils.JobInputs{
|
||||
JobName: jobName,
|
||||
Branch: branch,
|
||||
PipelineId: pipelineId,
|
||||
}, &ciutils.JobOptions{
|
||||
ApiClient: apiClient,
|
||||
IO: f.IO,
|
||||
Repo: repo,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(f.IO.StdErr, "invalid job ID:", args[0])
|
||||
return err
|
||||
}
|
||||
|
||||
job, err := api.RetryPipelineJob(apiClient, jobID, repo.FullName())
|
||||
if err != nil {
|
||||
return cmdutils.WrapError(err, fmt.Sprintf("Could not retry job with ID: %d", jobID))
|
||||
}
|
||||
fmt.Fprintln(f.IO.StdOut, "Retried job (id:", job.ID, "), status:", job.Status, ", ref:", job.Ref, ", weburl: ", job.WebURL, ")")
|
||||
fmt.Fprintln(f.IO.StdOut, "Retried job (ID:", job.ID, "), status:", job.Status, ", ref:", job.Ref, ", weburl: ", job.WebURL, ")")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
pipelineRetryCmd.Flags().StringP("branch", "b", "", "The branch to search for the job. (Default: current branch)")
|
||||
pipelineRetryCmd.Flags().IntP("pipeline-id", "p", 0, "The pipeline ID to search for the job.")
|
||||
return pipelineRetryCmd
|
||||
}
|
||||
|
|
|
@ -4,17 +4,16 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"gitlab.com/gitlab-org/cli/commands/cmdtest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitlab.com/gitlab-org/cli/commands/cmdtest"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/gitlab-org/cli/pkg/httpmock"
|
||||
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
||||
"gitlab.com/gitlab-org/cli/test"
|
||||
)
|
||||
|
||||
func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) {
|
||||
func runCommand(rt http.RoundTripper, args string) (*test.CmdOut, error) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
factory := cmdtest.InitFactory(ios, rt)
|
||||
|
||||
|
@ -22,40 +21,191 @@ func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) {
|
|||
|
||||
cmd := NewCmdRetry(factory)
|
||||
|
||||
return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr)
|
||||
return cmdtest.ExecuteCommand(cmd, args, stdout, stderr)
|
||||
}
|
||||
|
||||
func TestCiRetry(t *testing.T) {
|
||||
fakeHTTP := httpmock.New()
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
// test will fail with unmatched HTTP stub if this POST is not performed
|
||||
fakeHTTP.RegisterResponder(http.MethodPost, "/projects/OWNER/REPO/jobs/1122/retry",
|
||||
httpmock.NewStringResponse(http.StatusCreated, `
|
||||
{
|
||||
"id": 1123,
|
||||
"status": "pending",
|
||||
"stage": "build",
|
||||
"name": "build-job",
|
||||
"ref": "branch-name",
|
||||
"tag": false,
|
||||
"coverage": null,
|
||||
"allow_failure": false,
|
||||
"created_at": "2022-12-01T05:13:13.703Z",
|
||||
"web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123"
|
||||
}
|
||||
`))
|
||||
|
||||
jobId := "1122"
|
||||
output, err := runCommand(fakeHTTP, jobId)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `ci retry %s`: %v", jobId, err)
|
||||
type httpMock struct {
|
||||
method string
|
||||
path string
|
||||
status int
|
||||
body string
|
||||
}
|
||||
|
||||
out := output.String()
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
httpMocks []httpMock
|
||||
expectedError string
|
||||
expectedStderr string
|
||||
expectedOut string
|
||||
}{
|
||||
{
|
||||
name: "when retry with job-id",
|
||||
args: "1122",
|
||||
expectedOut: "Retried job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodPost,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/retry",
|
||||
http.StatusCreated,
|
||||
`{
|
||||
"id": 1123,
|
||||
"status": "pending",
|
||||
"stage": "build",
|
||||
"name": "build-job",
|
||||
"ref": "branch-name",
|
||||
"tag": false,
|
||||
"coverage": null,
|
||||
"allow_failure": false,
|
||||
"created_at": "2022-12-01T05:13:13.703Z",
|
||||
"web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when retry with job-id throws error",
|
||||
args: "1122",
|
||||
expectedError: "POST https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122/retry: 403 ",
|
||||
expectedOut: "",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodPost,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/retry",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when retry with job-name",
|
||||
args: "lint -b main -p 123",
|
||||
expectedOut: "Retried job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodPost,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/retry",
|
||||
http.StatusCreated,
|
||||
`{
|
||||
"id": 1123,
|
||||
"status": "pending",
|
||||
"stage": "build",
|
||||
"name": "build-job",
|
||||
"ref": "branch-name",
|
||||
"tag": false,
|
||||
"coverage": null,
|
||||
"allow_failure": false,
|
||||
"created_at": "2022-12-01T05:13:13.703Z",
|
||||
"web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when retry with job-name throws error",
|
||||
args: "lint -b main -p 123",
|
||||
expectedError: "list pipeline jobs: GET https://gitlab.com/api/v4/projects/OWNER/REPO/pipelines/123/jobs: 403 ",
|
||||
expectedStderr: "invalid job ID: lint\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when retry with job-name and last pipeline",
|
||||
args: "lint -b main",
|
||||
expectedOut: "Retried job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodPost,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/retry",
|
||||
http.StatusCreated,
|
||||
`{
|
||||
"id": 1123,
|
||||
"status": "pending",
|
||||
"stage": "build",
|
||||
"name": "build-job",
|
||||
"ref": "branch-name",
|
||||
"tag": false,
|
||||
"coverage": null,
|
||||
"allow_failure": false,
|
||||
"created_at": "2022-12-01T05:13:13.703Z",
|
||||
"web_url": "https://gitlab.com/OWNER/REPO/-/jobs/1123"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/repository/commits/main",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"last_pipeline" : {
|
||||
"id": 123
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER%2FREPO/pipelines/123/jobs",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, heredoc.Doc(`
|
||||
Retried job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )
|
||||
`), out)
|
||||
assert.Empty(t, output.Stderr())
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeHTTP := &httpmock.Mocker{
|
||||
MatchURL: httpmock.PathAndQuerystring,
|
||||
}
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
for _, mock := range tc.httpMocks {
|
||||
fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body))
|
||||
}
|
||||
|
||||
output, err := runCommand(fakeHTTP, tc.args)
|
||||
|
||||
if tc.expectedError == "" {
|
||||
require.Nil(t, err)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, tc.expectedError, err.Error())
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expectedOut, output.String())
|
||||
if tc.expectedStderr != "" {
|
||||
assert.Equal(t, tc.expectedStderr, output.Stderr())
|
||||
} else {
|
||||
assert.Empty(t, output.Stderr())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"gitlab.com/gitlab-org/cli/api"
|
||||
ciTraceCmd "gitlab.com/gitlab-org/cli/commands/ci/trace"
|
||||
"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"
|
||||
|
@ -144,16 +144,13 @@ func NewCmdStatus(f *cmdutils.Factory) *cobra.Command {
|
|||
isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
if retry == "View Logs" {
|
||||
// ToDo: bad idea to call another sub-command. should be fixed to avoid cyclo imports
|
||||
// and the a shared function placed in the ciutils sub-module
|
||||
return ciTraceCmd.TraceRun(&ciTraceCmd.TraceOpts{
|
||||
Branch: branch,
|
||||
JobID: 0,
|
||||
BaseRepo: f.BaseRepo,
|
||||
HTTPClient: f.HttpClient,
|
||||
IO: f.IO,
|
||||
return ciutils.TraceJob(&ciutils.JobInputs{
|
||||
Branch: branch,
|
||||
}, &ciutils.JobOptions{
|
||||
Repo: repo,
|
||||
ApiClient: apiClient,
|
||||
IO: f.IO,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,174 +1,57 @@
|
|||
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,
|
||||
}
|
||||
func NewCmdTrace(f *cmdutils.Factory) *cobra.Command {
|
||||
pipelineCITraceCmd := &cobra.Command{
|
||||
Use: "trace [<job-id>] [flags]",
|
||||
Short: `Trace a CI/CD job log in real time`,
|
||||
Example: heredoc.Doc(`
|
||||
$ glab ci trace
|
||||
#=> interactively select a job to trace
|
||||
# Interactively select a job to trace
|
||||
|
||||
$ glab ci trace 224356863
|
||||
#=> trace job with id 224356863
|
||||
# Trace job with ID 224356863
|
||||
|
||||
$ glab ci trace lint
|
||||
# Trace job with the name 'lint'
|
||||
`),
|
||||
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
|
||||
|
||||
repo, err := f.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jobName := ""
|
||||
if len(args) != 0 {
|
||||
opts.JobID = utils.StringToInt(args[0])
|
||||
jobName = args[0]
|
||||
}
|
||||
if opts.Branch == "" {
|
||||
opts.Branch, err = git.CurrentBranch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if runE != nil {
|
||||
return runE(opts)
|
||||
}
|
||||
return TraceRun(opts)
|
||||
branch, _ := cmd.Flags().GetString("branch")
|
||||
pipelineId, _ := cmd.Flags().GetInt("pipeline-id")
|
||||
|
||||
return ciutils.TraceJob(&ciutils.JobInputs{
|
||||
JobName: jobName,
|
||||
Branch: branch,
|
||||
PipelineId: pipelineId,
|
||||
}, &ciutils.JobOptions{
|
||||
ApiClient: apiClient,
|
||||
IO: f.IO,
|
||||
Repo: repo,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
pipelineCITraceCmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "Check pipeline status for a branch. (Default is the current branch)")
|
||||
pipelineCITraceCmd.Flags().StringP("branch", "b", "", "The branch to search for the job. (Default: current branch)")
|
||||
pipelineCITraceCmd.Flags().IntP("pipeline-id", "p", 0, "The pipeline ID to search for the job.")
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
package trace
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"gitlab.com/gitlab-org/cli/test"
|
||||
|
||||
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitlab.com/gitlab-org/cli/pkg/prompt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gitlab.com/gitlab-org/cli/commands/cmdutils"
|
||||
"gitlab.com/gitlab-org/cli/internal/config"
|
||||
|
||||
"gitlab.com/gitlab-org/cli/commands/cmdtest"
|
||||
)
|
||||
|
||||
var (
|
||||
stubFactory *cmdutils.Factory
|
||||
cmd *cobra.Command
|
||||
stdout *bytes.Buffer
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
cmdtest.InitTest(m, "ci_trace_test")
|
||||
}
|
||||
|
||||
func TestNewCmdTrace_Integration(t *testing.T) {
|
||||
glTestHost := test.GetHostOrSkip(t)
|
||||
|
||||
defer config.StubConfig(`---
|
||||
git_protocol: https
|
||||
hosts:
|
||||
gitlab.com:
|
||||
username: root
|
||||
`, "")()
|
||||
|
||||
var io *iostreams.IOStreams
|
||||
io, _, stdout, _ = iostreams.Test()
|
||||
stubFactory, _ = cmdtest.StubFactoryWithConfig(glTestHost + "/cli-automated-testing/test.git")
|
||||
stubFactory.IO = io
|
||||
stubFactory.IO.IsaTTY = true
|
||||
stubFactory.IO.IsErrTTY = true
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
wantOpts *TraceOpts
|
||||
}{
|
||||
{
|
||||
name: "Has no arg",
|
||||
args: ``,
|
||||
wantOpts: &TraceOpts{
|
||||
Branch: "master",
|
||||
JobID: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has arg with job-id",
|
||||
args: `3509632552`,
|
||||
wantOpts: &TraceOpts{
|
||||
Branch: "master",
|
||||
JobID: 3509632552,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "On a specified repo with job ID",
|
||||
args: "3509632552 -X cli-automated-testing/test",
|
||||
wantOpts: &TraceOpts{
|
||||
Branch: "master",
|
||||
JobID: 3509632552,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var actualOpts *TraceOpts
|
||||
cmd = NewCmdTrace(stubFactory, func(opts *TraceOpts) error {
|
||||
actualOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.Flags().StringP("repo", "X", "", "")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.wantOpts.IO = stubFactory.IO
|
||||
|
||||
argv, err := shlex.Split(tt.args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantOpts.JobID, actualOpts.JobID)
|
||||
assert.Equal(t, tt.wantOpts.Branch, actualOpts.Branch)
|
||||
assert.Equal(t, tt.wantOpts.Branch, actualOpts.Branch)
|
||||
assert.Equal(t, tt.wantOpts.IO, actualOpts.IO)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceRun(t *testing.T) {
|
||||
glTestHost := test.GetHostOrSkip(t)
|
||||
|
||||
var io *iostreams.IOStreams
|
||||
io, _, stdout, _ = iostreams.Test()
|
||||
stubFactory = cmdtest.StubFactory(glTestHost + "/cli-automated-testing/test.git")
|
||||
stubFactory.IO = io
|
||||
stubFactory.IO.IsaTTY = true
|
||||
stubFactory.IO.IsErrTTY = true
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
args string
|
||||
assertContains func(t *testing.T, out string)
|
||||
}{
|
||||
{
|
||||
desc: "Has no arg",
|
||||
args: ``,
|
||||
assertContains: func(t *testing.T, out string) {
|
||||
assert.Contains(t, out, "Getting job trace...")
|
||||
assert.Contains(t, out, "Showing logs for ")
|
||||
assert.Contains(t, out, `Preparing the "docker+machine"`)
|
||||
assert.Contains(t, out, `$ echo "This is a after script section"`)
|
||||
assert.Contains(t, out, "Job succeeded")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Has arg with job-id",
|
||||
args: `3509632552`,
|
||||
assertContains: func(t *testing.T, out string) {
|
||||
assert.Contains(t, out, "Getting job trace...\n")
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "On a specified repo with job ID",
|
||||
args: "3509632552 -X cli-automated-testing/test",
|
||||
assertContains: func(t *testing.T, out string) {
|
||||
assert.Contains(t, out, "Getting job trace...\n")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmd = NewCmdTrace(stubFactory, nil)
|
||||
cmd.Flags().StringP("repo", "X", "", "")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if tt.args == "" {
|
||||
as, teardown := prompt.InitAskStubber()
|
||||
defer teardown()
|
||||
|
||||
as.StubOne("cleanup4 (3509632552) - success")
|
||||
}
|
||||
argv, err := shlex.Split(tt.args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tt.assertContains(t, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package trace
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitlab.com/gitlab-org/cli/commands/cmdtest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gitlab.com/gitlab-org/cli/pkg/httpmock"
|
||||
"gitlab.com/gitlab-org/cli/pkg/iostreams"
|
||||
"gitlab.com/gitlab-org/cli/test"
|
||||
)
|
||||
|
||||
func runCommand(rt http.RoundTripper, args string) (*test.CmdOut, error) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
factory := cmdtest.InitFactory(ios, rt)
|
||||
|
||||
_, _ = factory.HttpClient()
|
||||
|
||||
cmd := NewCmdTrace(factory)
|
||||
|
||||
return cmdtest.ExecuteCommand(cmd, args, stdout, stderr)
|
||||
}
|
||||
|
||||
func TestCiTrace(t *testing.T) {
|
||||
type httpMock struct {
|
||||
method string
|
||||
path string
|
||||
status int
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
httpMocks []httpMock
|
||||
expectedOut string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "when trace for job-id is requested",
|
||||
args: "1122",
|
||||
expectedOut: "\nGetting job trace...\nShowing logs for lint job #1122\nLorem ipsum",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/trace",
|
||||
http.StatusOK,
|
||||
`Lorem ipsum`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "success"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when trace for job-id is requested and getTrace throws error",
|
||||
args: "1122",
|
||||
expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122/trace: 403 ",
|
||||
expectedOut: "\nGetting job trace...\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "success"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/trace",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when trace for job-id is requested and getJob throws error",
|
||||
args: "1122",
|
||||
expectedError: "failed to find job: GET https://gitlab.com/api/v4/projects/OWNER/REPO/jobs/1122: 403 ",
|
||||
expectedOut: "\nGetting job trace...\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusForbidden,
|
||||
`{}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when trace for job-name is requested",
|
||||
args: "lint -b main -p 123",
|
||||
expectedOut: "\nGetting job trace...\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/trace",
|
||||
http.StatusOK,
|
||||
`Lorem ipsum`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "success"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/pipelines/123/jobs",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when trace for job-name and last pipeline is requested",
|
||||
args: "lint -b main",
|
||||
expectedOut: "\nGetting job trace...\n",
|
||||
httpMocks: []httpMock{
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122/trace",
|
||||
http.StatusOK,
|
||||
`Lorem ipsum`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/jobs/1122",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "success"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/repository/commits/main",
|
||||
http.StatusOK,
|
||||
`{
|
||||
"last_pipeline" : {
|
||||
"id": 123
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
http.MethodGet,
|
||||
"/api/v4/projects/OWNER/REPO/pipelines/123/jobs",
|
||||
http.StatusOK,
|
||||
`[{
|
||||
"id": 1122,
|
||||
"name": "lint",
|
||||
"status": "failed"
|
||||
}, {
|
||||
"id": 1124,
|
||||
"name": "publish",
|
||||
"status": "failed"
|
||||
}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeHTTP := &httpmock.Mocker{
|
||||
MatchURL: httpmock.PathAndQuerystring,
|
||||
}
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
for _, mock := range tc.httpMocks {
|
||||
fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body))
|
||||
}
|
||||
|
||||
output, err := runCommand(fakeHTTP, tc.args)
|
||||
|
||||
if tc.expectedError == "" {
|
||||
require.Nil(t, err)
|
||||
} else {
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, tc.expectedError, err.Error())
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expectedOut, output.String())
|
||||
assert.Empty(t, output.Stderr())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ func NewCmdContributors(f *cmdutils.Factory) *cobra.Command {
|
|||
$ glab repo contributors
|
||||
|
||||
$ glab repo contributors -R gitlab-com/www-gitlab-com
|
||||
#=> Supports repo override
|
||||
# Supports repo override
|
||||
`),
|
||||
Args: cobra.ExactArgs(0),
|
||||
Aliases: []string{"users"},
|
||||
|
|
|
@ -39,7 +39,7 @@ glab alias set <alias name> '<command>' [flags]
|
|||
```plaintext
|
||||
$ glab alias set mrv 'mr view'
|
||||
$ glab mrv -w 123
|
||||
#=> glab mr view -w 123
|
||||
# glab mr view -w 123
|
||||
|
||||
$ glab alias set createissue 'glab create issue --title "$1"'
|
||||
$ glab createissue "My Issue" --description "Something is broken."
|
||||
|
@ -47,7 +47,7 @@ $ glab createissue "My Issue" --description "Something is broken."
|
|||
|
||||
$ glab alias set --shell igrep 'glab issue list --assignee="$1" | grep $2'
|
||||
$ glab igrep user foo
|
||||
#=> glab issue list --assignee="user" | grep "foo"
|
||||
# glab issue list --assignee="user" | grep "foo"
|
||||
|
||||
```
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ glab ci ci lint [flags]
|
|||
|
||||
```plaintext
|
||||
$ glab ci lint
|
||||
#=> Uses .gitlab-ci.yml in the current directory
|
||||
# Uses .gitlab-ci.yml in the current directory
|
||||
|
||||
$ glab ci lint .gitlab-ci.yml
|
||||
|
||||
|
|
|
@ -21,17 +21,21 @@ glab ci ci trace [<job-id>] [flags]
|
|||
|
||||
```plaintext
|
||||
$ glab ci trace
|
||||
#=> interactively select a job to trace
|
||||
# Interactively select a job to trace
|
||||
|
||||
$ glab ci trace 224356863
|
||||
#=> trace job with id 224356863
|
||||
# Trace job with ID 224356863
|
||||
|
||||
$ glab ci trace lint
|
||||
# Trace job with the name 'lint'
|
||||
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```plaintext
|
||||
-b, --branch string Check pipeline status for a branch. (Default is the current branch)
|
||||
-b, --branch string The branch to search for the job. (Default: current branch)
|
||||
-p, --pipeline-id int The pipeline ID to search for the job.
|
||||
```
|
||||
|
||||
## Options inherited from parent commands
|
||||
|
|
|
@ -21,7 +21,7 @@ glab ci lint [flags]
|
|||
|
||||
```plaintext
|
||||
$ glab ci lint
|
||||
#=> Uses .gitlab-ci.yml in the current directory
|
||||
# Uses .gitlab-ci.yml in the current directory
|
||||
|
||||
$ glab ci lint .gitlab-ci.yml
|
||||
|
||||
|
|
|
@ -20,8 +20,22 @@ glab ci retry <job-id> [flags]
|
|||
## Examples
|
||||
|
||||
```plaintext
|
||||
glab ci retry 871528
|
||||
$ glab ci retry
|
||||
# Interactively select a job to retry
|
||||
|
||||
$ glab ci retry 224356863
|
||||
# Retry job with ID 224356863
|
||||
|
||||
$ glab ci retry lint
|
||||
# Retry job with the name 'lint'
|
||||
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```plaintext
|
||||
-b, --branch string The branch to search for the job. (Default: current branch)
|
||||
-p, --pipeline-id int The pipeline ID to search for the job.
|
||||
```
|
||||
|
||||
## Options inherited from parent commands
|
||||
|
|
|
@ -21,17 +21,21 @@ glab ci trace [<job-id>] [flags]
|
|||
|
||||
```plaintext
|
||||
$ glab ci trace
|
||||
#=> interactively select a job to trace
|
||||
# Interactively select a job to trace
|
||||
|
||||
$ glab ci trace 224356863
|
||||
#=> trace job with id 224356863
|
||||
# Trace job with ID 224356863
|
||||
|
||||
$ glab ci trace lint
|
||||
# Trace job with the name 'lint'
|
||||
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
```plaintext
|
||||
-b, --branch string Check pipeline status for a branch. (Default is the current branch)
|
||||
-b, --branch string The branch to search for the job. (Default: current branch)
|
||||
-p, --pipeline-id int The pipeline ID to search for the job.
|
||||
```
|
||||
|
||||
## Options inherited from parent commands
|
||||
|
|
|
@ -29,7 +29,7 @@ users
|
|||
$ glab repo contributors
|
||||
|
||||
$ glab repo contributors -R gitlab-com/www-gitlab-com
|
||||
#=> Supports repo override
|
||||
# Supports repo override
|
||||
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in New Issue