diff --git a/commands/ci/ci.go b/commands/ci/ci.go index c6d50ce123702ff8496b513dfa3870a133ce71da..845ef0ef80c7b7a4c6173468964e202c99ca8379 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 0000000000000000000000000000000000000000..fb6ea23d934cd1710b05949999bac79f27ab7796 --- /dev/null +++ b/commands/ci/trigger/trigger.go @@ -0,0 +1,105 @@ +package trigger + +import ( + "fmt" + + "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 { + pipelineTriggerCmd := &cobra.Command{ + Use: "trigger ", + Short: `Trigger a manual CI/CD job.`, + Aliases: []string{}, + Example: heredoc.Doc(` + $ 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), + 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 { + 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()) + 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 + }, + } + + 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 +} + +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 new file mode 100644 index 0000000000000000000000000000000000000000..100dc153d0c5a8baf552dcc8c22894c93b1bba18 --- /dev/null +++ b/commands/ci/trigger/trigger_test.go @@ -0,0 +1,171 @@ +package trigger + +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/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, args, stdout, stderr) +} + +func TestCiTrigger(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + tests := []struct { + name string + args string + httpMocks []httpMock + expectedOut string + }{ + { + 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", + }, + } + + 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, 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/index.md b/docs/source/ci/index.md index 07d7d45755d4915475f4b4cc50fab2c81ad91fc8..522f3d674b6a0051914530266d54bd308786b6ac 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 0000000000000000000000000000000000000000..56156f6c89c6721dba133c38231e7b146aa50135 --- /dev/null +++ b/docs/source/ci/trigger.md @@ -0,0 +1,43 @@ +--- +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 224356863 +#=> trigger manual job with id 224356863 + +$ glab ci trigger lint +#=> trigger manual job with 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 + +```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 +```