From ada0cab137f1b59efb9e6f4bcf1dce2b7f52bd9f Mon Sep 17 00:00:00 2001 From: Andreas Weber Date: Sun, 26 Nov 2023 21:47:57 +0100 Subject: [PATCH 1/6] feat(ci): add ci trigger (#682) --- commands/ci/ci.go | 2 + commands/ci/trigger/trigger.go | 55 ++++++++++++++++++++++++++ commands/ci/trigger/trigger_test.go | 61 +++++++++++++++++++++++++++++ docs/source/ci/index.md | 1 + docs/source/ci/trigger.md | 32 +++++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 commands/ci/trigger/trigger.go create mode 100644 commands/ci/trigger/trigger_test.go create mode 100644 docs/source/ci/trigger.md diff --git a/commands/ci/ci.go b/commands/ci/ci.go index c6d50ce12..845ef0ef8 100644 --- a/commands/ci/ci.go +++ b/commands/ci/ci.go @@ -11,6 +11,7 @@ import ( pipeRunCmd "gitlab.com/gitlab-org/cli/commands/ci/run" pipeStatusCmd "gitlab.com/gitlab-org/cli/commands/ci/status" ciTraceCmd "gitlab.com/gitlab-org/cli/commands/ci/trace" + pipeTriggerCmd "gitlab.com/gitlab-org/cli/commands/ci/trigger" ciViewCmd "gitlab.com/gitlab-org/cli/commands/ci/view" "gitlab.com/gitlab-org/cli/commands/cmdutils" @@ -36,6 +37,7 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command { ciCmd.AddCommand(pipeStatusCmd.NewCmdStatus(f)) ciCmd.AddCommand(pipeRetryCmd.NewCmdRetry(f)) ciCmd.AddCommand(pipeRunCmd.NewCmdRun(f)) + ciCmd.AddCommand(pipeTriggerCmd.NewCmdTrigger(f)) ciCmd.AddCommand(jobArtifactCmd.NewCmdRun(f)) ciCmd.AddCommand(pipeGetCmd.NewCmdGet(f)) return ciCmd diff --git a/commands/ci/trigger/trigger.go b/commands/ci/trigger/trigger.go new file mode 100644 index 000000000..abfa5dbe1 --- /dev/null +++ b/commands/ci/trigger/trigger.go @@ -0,0 +1,55 @@ +package trigger + +import ( + "fmt" + + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/pkg/utils" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { + pipelineTriggerCmd := &cobra.Command{ + Use: "trigger ", + Short: `Trigger a manual CI/CD job`, + Aliases: []string{}, + Example: heredoc.Doc(` + glab ci trigger 871528 +`), + 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 + } + + jobID := utils.StringToInt(args[0]) + + if jobID < 1 { + fmt.Fprintln(f.IO.StdErr, "invalid job id:", args[0]) + return cmdutils.SilentError + } + + job, err := api.PlayPipelineJob(apiClient, jobID, repo.FullName()) + if err != nil { + return cmdutils.WrapError(err, fmt.Sprintf("Could not trigger job with ID: %d", jobID)) + } + fmt.Fprintln(f.IO.StdOut, "Triggered job (id:", job.ID, "), status:", job.Status, ", ref:", job.Ref, ", weburl: ", job.WebURL, ")") + + return nil + }, + } + + return pipelineTriggerCmd +} diff --git a/commands/ci/trigger/trigger_test.go b/commands/ci/trigger/trigger_test.go new file mode 100644 index 000000000..c1578d5a3 --- /dev/null +++ b/commands/ci/trigger/trigger_test.go @@ -0,0 +1,61 @@ +package trigger + +import ( + "net/http" + "testing" + + "gitlab.com/gitlab-org/cli/pkg/iostreams" + + "github.com/MakeNowJust/heredoc" + + "github.com/stretchr/testify/assert" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/test" +) + +func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) { + ios, _, stdout, stderr := iostreams.Test() + factory := cmdtest.InitFactory(ios, rt) + + _, _ = factory.HttpClient() + + cmd := NewCmdTrigger(factory) + + return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) +} + +func TestCiTrigger(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/play", + 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 trigger %s`: %v", jobId, err) + } + + out := output.String() + + assert.Equal(t, heredoc.Doc(` + Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 ) +`), out) + assert.Empty(t, output.Stderr()) +} diff --git a/docs/source/ci/index.md b/docs/source/ci/index.md index 07d7d4575..522f3d674 100644 --- a/docs/source/ci/index.md +++ b/docs/source/ci/index.md @@ -44,4 +44,5 @@ pipeline - [`run`](run.md) - [`status`](status.md) - [`trace`](trace.md) +- [`trigger`](trigger.md) - [`view`](view.md) diff --git a/docs/source/ci/trigger.md b/docs/source/ci/trigger.md new file mode 100644 index 000000000..5d15394b0 --- /dev/null +++ b/docs/source/ci/trigger.md @@ -0,0 +1,32 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + + + +# `glab ci trigger` + +Trigger a manual CI/CD job + +```plaintext +glab ci trigger [flags] +``` + +## Examples + +```plaintext +glab ci trigger 871528 + +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for command + -R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL +``` -- GitLab From 3dd5b7cad3824b0177a18caa2ce526aeeb8919c1 Mon Sep 17 00:00:00 2001 From: Andreas Weber Date: Tue, 28 Nov 2023 20:21:17 +0100 Subject: [PATCH 2/6] feat(ci): add trigger with job name (#682) --- commands/ci/trigger/trigger.go | 50 +++++++- commands/ci/trigger/trigger_test.go | 178 ++++++++++++++++++++++------ docs/source/ci/trigger.md | 7 ++ 3 files changed, 199 insertions(+), 36 deletions(-) diff --git a/commands/ci/trigger/trigger.go b/commands/ci/trigger/trigger.go index abfa5dbe1..460465e4c 100644 --- a/commands/ci/trigger/trigger.go +++ b/commands/ci/trigger/trigger.go @@ -5,10 +5,12 @@ import ( "gitlab.com/gitlab-org/cli/api" "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/pkg/git" "gitlab.com/gitlab-org/cli/pkg/utils" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" + "github.com/xanzy/go-gitlab" ) func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { @@ -37,8 +39,38 @@ func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { jobID := utils.StringToInt(args[0]) if jobID < 1 { - fmt.Fprintln(f.IO.StdErr, "invalid job id:", args[0]) - return cmdutils.SilentError + jobName := args[0] + + pipelineId, err := cmd.Flags().GetInt("pipeline-id") + if err != nil || pipelineId == 0 { + branch, _ := cmd.Flags().GetString("branch") + if branch == "" { + branch, err = git.CurrentBranch() + if err != nil { + return err + } + } + commit, err := api.GetCommit(apiClient, repo.FullName(), branch) + if err != nil { + return err + } + pipelineId = commit.LastPipeline.ID + } + + jobs, _, err := apiClient.Jobs.ListPipelineJobs(repo.FullName(), pipelineId, nil) + if err != nil { + return err + } + for _, job := range jobs { + if job.Name == jobName { + jobID = job.ID + break + } + } + if jobID < 1 { + fmt.Fprintln(f.IO.StdErr, "invalid job id:", args[0]) + return cmdutils.SilentError + } } job, err := api.PlayPipelineJob(apiClient, jobID, repo.FullName()) @@ -51,5 +83,19 @@ func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { }, } + pipelineTriggerCmd.Flags().StringP("branch", "b", "", "used branch to search for job name. (Default is current branch)") + pipelineTriggerCmd.Flags().IntP("pipeline-id", "p", 0, "Provide pipeline ID") return pipelineTriggerCmd } + +type Jobs []*gitlab.Job + +// FindByName returns the first Remote whose name matches the list +func (jobs Jobs) FindByName(name string) (*gitlab.Job, error) { + for _, job := range jobs { + if job.Name == name { + return job, nil + } + } + return nil, cmdutils.SilentError +} diff --git a/commands/ci/trigger/trigger_test.go b/commands/ci/trigger/trigger_test.go index c1578d5a3..9a8b0921f 100644 --- a/commands/ci/trigger/trigger_test.go +++ b/commands/ci/trigger/trigger_test.go @@ -4,58 +4,168 @@ 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/test" ) -func runCommand(rt http.RoundTripper, cli string) (*test.CmdOut, error) { - ios, _, stdout, stderr := iostreams.Test() +func runCommand(rt http.RoundTripper, isTTY bool, args string) (*test.CmdOut, error) { + ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "") + factory := cmdtest.InitFactory(ios, rt) _, _ = factory.HttpClient() cmd := NewCmdTrigger(factory) - return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) + return cmdtest.ExecuteCommand(cmd, args, stdout, stderr) } func TestCiTrigger(t *testing.T) { - fakeHTTP := httpmock.New() - defer fakeHTTP.Verify(t) + type httpMock struct { + method string + path string + status int + body string + } - // test will fail with unmatched HTTP stub if this POST is not performed - fakeHTTP.RegisterResponder(http.MethodPost, "/projects/OWNER/REPO/jobs/1122/play", - httpmock.NewStringResponse(http.StatusCreated, ` + tests := []struct { + name string + args string + httpMocks []httpMock + expectedOut string + }{ { - "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 trigger %s`: %v", jobId, err) + name: "when trigger with job-id is created", + args: "1122", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/jobs/1122/play", + 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" + }`, + }, + }, + expectedOut: "Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + }, + { + name: "when trigger with job-name is created", + args: "lint -b main -p 123", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/jobs/1122/play", + 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" + }]`, + }, + }, + expectedOut: "Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + }, + { + name: "when trigger with job-name and last pipeline is created", + args: "lint -b main", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/jobs/1122/play", + 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" + }]`, + }, + }, + expectedOut: "Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + }, } - out := output.String() + 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)) + } - assert.Equal(t, heredoc.Doc(` - Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 ) -`), out) - assert.Empty(t, output.Stderr()) + output, err := runCommand(fakeHTTP, false, tc.args) + require.Nil(t, err) + + assert.Equal(t, tc.expectedOut, output.String()) + assert.Empty(t, output.Stderr()) + }) + } } diff --git a/docs/source/ci/trigger.md b/docs/source/ci/trigger.md index 5d15394b0..e1fa8f86a 100644 --- a/docs/source/ci/trigger.md +++ b/docs/source/ci/trigger.md @@ -24,6 +24,13 @@ glab ci trigger 871528 ``` +## Options + +```plaintext + -b, --branch string used branch to search for job name. (Default is current branch) + -p, --pipeline-id int Provide pipeline ID +``` + ## Options inherited from parent commands ```plaintext -- GitLab From 0c8fca815d21774dfaf85b01cd9b178596851b01 Mon Sep 17 00:00:00 2001 From: Amy Qualls Date: Tue, 28 Nov 2023 19:48:48 +0000 Subject: [PATCH 3/6] Apply 1 suggestion(s) to 1 file(s) --- commands/ci/trigger/trigger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/ci/trigger/trigger.go b/commands/ci/trigger/trigger.go index 460465e4c..bc9afc6db 100644 --- a/commands/ci/trigger/trigger.go +++ b/commands/ci/trigger/trigger.go @@ -77,7 +77,7 @@ func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { if err != nil { return cmdutils.WrapError(err, fmt.Sprintf("Could not trigger job with ID: %d", jobID)) } - fmt.Fprintln(f.IO.StdOut, "Triggered job (id:", job.ID, "), status:", job.Status, ", ref:", job.Ref, ", weburl: ", job.WebURL, ")") + fmt.Fprintln(f.IO.StdOut, "Triggered job (ID:", job.ID, "), status:", job.Status, ", ref:", job.Ref, ", weburl: ", job.WebURL, ")") return nil }, -- GitLab From 9c193e17ceff69444874c734341b0cc6e5696442 Mon Sep 17 00:00:00 2001 From: Andreas Weber Date: Tue, 28 Nov 2023 20:50:11 +0100 Subject: [PATCH 4/6] fix: unit test changed to capitalize --- commands/ci/trigger/trigger_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/ci/trigger/trigger_test.go b/commands/ci/trigger/trigger_test.go index 9a8b0921f..100dc153d 100644 --- a/commands/ci/trigger/trigger_test.go +++ b/commands/ci/trigger/trigger_test.go @@ -60,7 +60,7 @@ func TestCiTrigger(t *testing.T) { }`, }, }, - expectedOut: "Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + expectedOut: "Triggered job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", }, { name: "when trigger with job-name is created", @@ -98,7 +98,7 @@ func TestCiTrigger(t *testing.T) { }]`, }, }, - expectedOut: "Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + expectedOut: "Triggered job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", }, { name: "when trigger with job-name and last pipeline is created", @@ -146,7 +146,7 @@ func TestCiTrigger(t *testing.T) { }]`, }, }, - expectedOut: "Triggered job (id: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", + expectedOut: "Triggered job (ID: 1123 ), status: pending , ref: branch-name , weburl: https://gitlab.com/OWNER/REPO/-/jobs/1123 )\n", }, } -- GitLab From 70d8dd05dd9dc7cdd9819d197e26c3de433b84da Mon Sep 17 00:00:00 2001 From: Amy Qualls Date: Thu, 30 Nov 2023 17:52:55 +0000 Subject: [PATCH 5/6] feat: add suggestions --- commands/ci/trigger/trigger.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/ci/trigger/trigger.go b/commands/ci/trigger/trigger.go index bc9afc6db..614b63f99 100644 --- a/commands/ci/trigger/trigger.go +++ b/commands/ci/trigger/trigger.go @@ -16,7 +16,7 @@ import ( func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { pipelineTriggerCmd := &cobra.Command{ Use: "trigger ", - Short: `Trigger a manual CI/CD job`, + Short: `Trigger a manual CI/CD job.`, Aliases: []string{}, Example: heredoc.Doc(` glab ci trigger 871528 @@ -83,8 +83,8 @@ func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { }, } - pipelineTriggerCmd.Flags().StringP("branch", "b", "", "used branch to search for job name. (Default is current branch)") - pipelineTriggerCmd.Flags().IntP("pipeline-id", "p", 0, "Provide pipeline ID") + pipelineTriggerCmd.Flags().StringP("branch", "b", "", "The branch to search for the job. (Default: current branch)") + pipelineTriggerCmd.Flags().IntP("pipeline-id", "p", 0, "The pipeline ID to search for the job.") return pipelineTriggerCmd } -- GitLab From 1b83ac9558d19efd18ae25dd15c7d92021d8ee4b Mon Sep 17 00:00:00 2001 From: Andreas Weber Date: Thu, 30 Nov 2023 18:56:42 +0100 Subject: [PATCH 6/6] feat: update docs, add more examples --- commands/ci/trigger/trigger.go | 6 +++++- docs/source/ci/trigger.md | 12 ++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/commands/ci/trigger/trigger.go b/commands/ci/trigger/trigger.go index 614b63f99..fb6ea23d9 100644 --- a/commands/ci/trigger/trigger.go +++ b/commands/ci/trigger/trigger.go @@ -19,7 +19,11 @@ func NewCmdTrigger(f *cmdutils.Factory) *cobra.Command { Short: `Trigger a manual CI/CD job.`, Aliases: []string{}, Example: heredoc.Doc(` - glab ci trigger 871528 + $ glab ci trigger 224356863 + #=> trigger manual job with id 224356863 + + $ glab ci trigger lint + #=> trigger manual job with name lint `), Long: ``, Args: cobra.ExactArgs(1), diff --git a/docs/source/ci/trigger.md b/docs/source/ci/trigger.md index e1fa8f86a..56156f6c8 100644 --- a/docs/source/ci/trigger.md +++ b/docs/source/ci/trigger.md @@ -11,7 +11,7 @@ Please do not edit this file directly. Run `make gen-docs` instead. # `glab ci trigger` -Trigger a manual CI/CD job +Trigger a manual CI/CD job. ```plaintext glab ci trigger [flags] @@ -20,15 +20,19 @@ glab ci trigger [flags] ## Examples ```plaintext -glab ci trigger 871528 +$ glab ci trigger 224356863 +#=> trigger manual job with id 224356863 + +$ glab ci trigger lint +#=> trigger manual job with name lint ``` ## Options ```plaintext - -b, --branch string used branch to search for job name. (Default is current branch) - -p, --pipeline-id int Provide pipeline ID + -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 -- GitLab