Merge branch 'pipeline-trigger-support' into 'main'

feat: Add new command to run Pipeline Triggers

See merge request https://gitlab.com/gitlab-org/cli/-/merge_requests/1414

Merged-by: Jay McCure <jmccure@gitlab.com>
Approved-by: Timo Furrer <tfurrer@gitlab.com>
Approved-by: Jay McCure <jmccure@gitlab.com>
Reviewed-by: Clément Saccoccio <git-clement@saccoccio.me>
Reviewed-by: Jay McCure <jmccure@gitlab.com>
Reviewed-by: Timo Furrer <tfurrer@gitlab.com>
Co-authored-by: Clément Saccoccio <git-clement@saccoccio.me>
This commit is contained in:
Jay McCure 2024-04-11 08:30:58 +00:00
commit 44dc7b7250
8 changed files with 318 additions and 25 deletions

View File

@ -438,6 +438,14 @@ var CreatePipeline = func(client *gitlab.Client, projectID interface{}, opts *gi
return pipe, err
}
var RunPipelineTrigger = func(client *gitlab.Client, projectID interface{}, opts *gitlab.RunPipelineTriggerOptions) (*gitlab.Pipeline, error) {
if client == nil {
client = apiClient.Lab()
}
pipe, _, err := client.PipelineTriggers.RunPipelineTrigger(projectID, opts)
return pipe, err
}
var DownloadArtifactJob = func(client *gitlab.Client, repo string, ref string, opts *gitlab.DownloadArtifactsFileOptions) (*bytes.Reader, error) {
if client == nil {
client = apiClient.Lab()

View File

@ -10,9 +10,10 @@ import (
pipeListCmd "gitlab.com/gitlab-org/cli/commands/ci/list"
pipeRetryCmd "gitlab.com/gitlab-org/cli/commands/ci/retry"
pipeRunCmd "gitlab.com/gitlab-org/cli/commands/ci/run"
pipeRunTrigCmd "gitlab.com/gitlab-org/cli/commands/ci/run_trig"
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"
jobPlayCmd "gitlab.com/gitlab-org/cli/commands/ci/trigger"
ciViewCmd "gitlab.com/gitlab-org/cli/commands/ci/view"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
@ -38,7 +39,8 @@ 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(jobPlayCmd.NewCmdTrigger(f))
ciCmd.AddCommand(pipeRunTrigCmd.NewCmdRunTrig(f))
ciCmd.AddCommand(jobArtifactCmd.NewCmdRun(f))
ciCmd.AddCommand(pipeGetCmd.NewCmdGet(f))
ciCmd.AddCommand(ciConfigCmd.NewCmdConfig(f))

View File

@ -9,6 +9,7 @@ import (
"sync"
"time"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/git"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
@ -179,6 +180,27 @@ func getPipelineId(inputs *JobInputs, opts *JobOptions) (int, error) {
return pipeline.ID, err
}
func GetDefaultBranch(f *cmdutils.Factory) string {
repo, err := f.BaseRepo()
if err != nil {
return "master"
}
remotes, err := f.Remotes()
if err != nil {
return "master"
}
repoRemote, err := remotes.FindByRepo(repo.RepoOwner(), repo.RepoName())
if err != nil {
return "master"
}
branch, _ := git.GetDefaultBranch(repoRemote.Name)
return branch
}
func getBranch(branch string, opts *JobOptions) (string, error) {
if branch != "" {
return branch, nil

View File

@ -7,8 +7,8 @@ import (
"strings"
"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"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
@ -22,27 +22,6 @@ var (
var envVariables = []string{}
func getDefaultBranch(f *cmdutils.Factory) string {
repo, err := f.BaseRepo()
if err != nil {
return "master"
}
remotes, err := f.Remotes()
if err != nil {
return "master"
}
repoRemote, err := remotes.FindByRepo(repo.RepoOwner(), repo.RepoName())
if err != nil {
return "master"
}
branch, _ := git.GetDefaultBranch(repoRemote.Name)
return branch
}
func parseVarArg(s string) (*gitlab.PipelineVariableOptions, error) {
// From https://pkg.go.dev/strings#Split:
//
@ -167,7 +146,7 @@ func NewCmdRun(f *cmdutils.Factory) *cobra.Command {
} else {
// `ci run` is running out of a git repo
fmt.Fprintln(f.IO.StdOut, "not in a git repository, using repository argument")
c.Ref = gitlab.String(getDefaultBranch(f))
c.Ref = gitlab.String(ciutils.GetDefaultBranch(f))
}
pipe, err := api.CreatePipeline(apiClient, repo.FullName(), c)

View File

@ -0,0 +1,115 @@
package run_trig
import (
"errors"
"fmt"
"os"
"strings"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/ci/ciutils"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
)
var envVariables = []string{}
func parseVarArg(s string) (key string, val string, err error) {
// From https://pkg.go.dev/strings#Split:
//
// > If s does not contain sep and sep is not empty,
// > Split returns a slice of length 1 whose only element is s.
//
// Therefore, the function will always return a slice of min length 1.
v := strings.SplitN(s, ":", 2)
if len(v) == 1 {
return "", "", fmt.Errorf("invalid argument structure")
}
return v[0], v[1], nil
}
func NewCmdRunTrig(f *cmdutils.Factory) *cobra.Command {
pipelineRunCmd := &cobra.Command{
Use: "run-trig [flags]",
Short: `Run a CI/CD pipeline trigger`,
Aliases: []string{"run-trig"},
Example: heredoc.Doc(`
glab ci run-trig -t xxxx
glab ci run-trig -t xxxx -b main
glab ci run-trig -t xxxx -b main --variables key1:val1
glab ci run-trig -t xxxx -b main --variables key1:val1,key2:val2
glab ci run-trig -t xxxx -b main --variables key1:val1 --variables key2:val2
`),
Long: ``,
Args: cobra.ExactArgs(0),
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
}
c := &gitlab.RunPipelineTriggerOptions{
Variables: make(map[string]string),
}
if customPipelineVars, _ := cmd.Flags().GetStringSlice("variables"); len(customPipelineVars) > 0 {
for _, v := range customPipelineVars {
key, val, err := parseVarArg(v)
if err != nil {
return fmt.Errorf("parsing pipeline variable expected format KEY:VALUE: %w", err)
}
c.Variables[key] = val
}
}
branch, err := cmd.Flags().GetString("branch")
if err != nil {
return err
}
if branch != "" {
c.Ref = gitlab.String(branch)
} else if currentBranch, err := f.Branch(); err == nil {
c.Ref = gitlab.String(currentBranch)
} else {
// `ci run-trig` is running out of a git repo
fmt.Fprintln(f.IO.StdOut, "not in a git repository, using repository argument")
c.Ref = gitlab.String(ciutils.GetDefaultBranch(f))
}
token, err := cmd.Flags().GetString("token")
if err != nil {
return err
}
if token == "" {
token = os.Getenv("CI_JOB_TOKEN")
}
if token == "" {
return errors.New("--token parameter can be omitted only if CI_JOB_TOKEN environment variable is set")
}
c.Token = &token
pipe, err := api.RunPipelineTrigger(apiClient, repo.FullName(), c)
if err != nil {
return err
}
fmt.Fprintln(f.IO.StdOut, "Created pipeline (id:", pipe.ID, "), status:", pipe.Status, ", ref:", pipe.Ref, ", weburl: ", pipe.WebURL, ")")
return nil
},
}
pipelineRunCmd.Flags().StringP("token", "t", "", "Pipeline trigger token (can be omitted only if CI_JOB_TOKEN environment variable is set)")
pipelineRunCmd.Flags().StringP("branch", "b", "", "Create pipeline on branch/ref <string>")
pipelineRunCmd.Flags().StringSliceVarP(&envVariables, "variables", "", []string{}, "Pass variables to pipeline in format <key>:<value>")
return pipelineRunCmd
}

View File

@ -0,0 +1,116 @@
package run_trig
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"testing"
"gitlab.com/gitlab-org/cli/commands/cmdtest"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/cli/pkg/httpmock"
"gitlab.com/gitlab-org/cli/test"
)
type ResponseJSON struct {
Token string `json:"token"`
Ref string `json:"ref"`
}
func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "")
factory := cmdtest.InitFactory(ios, rt)
factory.Branch = func() (string, error) {
return "custom-branch-123", nil
}
_, _ = factory.HttpClient()
cmd := NewCmdRunTrig(factory)
return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr)
}
func TestCIRun(t *testing.T) {
tests := []struct {
name string
cli string
ciJobToken string
expectedPOSTBody string
expectedOut string
}{
{
name: "when running `ci run-trig` without branch parameter, defaults to current branch",
cli: "-t foobar",
ciJobToken: "",
expectedPOSTBody: fmt.Sprintf(`"ref":"%s"`, "custom-branch-123"),
expectedOut: fmt.Sprintf("Created pipeline (id: 123 ), status: created , ref: %s , weburl: https://gitlab.com/OWNER/REPO/-/pipelines/123 )\n", "custom-branch-123"),
},
{
name: "when running `ci run-trig` with branch parameter, run CI at branch",
cli: "-t foobar -b ci-cd-improvement-399",
ciJobToken: "",
expectedPOSTBody: `"ref":"ci-cd-improvement-399"`,
expectedOut: "Created pipeline (id: 123 ), status: created , ref: ci-cd-improvement-399 , weburl: https://gitlab.com/OWNER/REPO/-/pipelines/123 )\n",
},
{
name: "when running `ci run-trig` without any parameter, takes trigger token from env variable",
cli: "",
ciJobToken: "foobar",
expectedPOSTBody: fmt.Sprintf(`"ref":"%s"`, "custom-branch-123"),
expectedOut: fmt.Sprintf("Created pipeline (id: 123 ), status: created , ref: %s , weburl: https://gitlab.com/OWNER/REPO/-/pipelines/123 )\n", "custom-branch-123"),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fakeHTTP := &httpmock.Mocker{
MatchURL: httpmock.PathAndQuerystring,
}
defer fakeHTTP.Verify(t)
initialEnvValue := os.Getenv("CI_JOB_TOKEN")
os.Setenv("CI_JOB_TOKEN", tc.ciJobToken)
defer os.Setenv("CI_JOB_TOKEN", initialEnvValue)
fakeHTTP.RegisterResponder(http.MethodPost, "/api/v4/projects/OWNER/REPO/trigger/pipeline",
func(req *http.Request) (*http.Response, error) {
rb, _ := io.ReadAll(req.Body)
var response ResponseJSON
err := json.Unmarshal(rb, &response)
if err != nil {
fmt.Printf("Error when parsing response body %s\n", rb)
}
if response.Token != "foobar" {
fmt.Printf("Invalid token %s\n", rb)
}
// ensure CLI runs CI on correct branch
assert.Contains(t, string(rb), tc.expectedPOSTBody)
resp, _ := httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf(`{
"id": 123,
"iid": 123,
"project_id": 3,
"status": "created",
"ref": "%s",
"web_url": "https://gitlab.com/OWNER/REPO/-/pipelines/123"}`, response.Ref))(req)
return resp, nil
},
)
output, _ := runCommand(fakeHTTP, false, tc.cli)
out := output.String()
assert.Equal(t, tc.expectedOut, out)
assert.Empty(t, output.Stderr())
})
}
}

View File

@ -43,6 +43,7 @@ pipeline
- [`list`](list.md)
- [`retry`](retry.md)
- [`run`](run.md)
- [`run-trig`](run-trig.md)
- [`status`](status.md)
- [`trace`](trace.md)
- [`trigger`](trigger.md)

View File

@ -0,0 +1,50 @@
---
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
---
<!--
This documentation is auto generated by a script.
Please do not edit this file directly. Run `make gen-docs` instead.
-->
# `glab ci run-trig`
Run a CI/CD pipeline trigger
```plaintext
glab ci run-trig [flags]
```
## Aliases
```plaintext
run-trig
```
## Examples
```plaintext
glab ci run-trig -t xxxx
glab ci run-trig -t xxxx -b main
glab ci run-trig -t xxxx -b main --variables key1:val1
glab ci run-trig -t xxxx -b main --variables key1:val1,key2:val2
glab ci run-trig -t xxxx -b main --variables key1:val1 --variables key2:val2
```
## Options
```plaintext
-b, --branch string Create pipeline on branch/ref <string>
-t, --token string Pipeline trigger token (can be omitted only if CI_JOB_TOKEN environment variable is set)
--variables strings Pass variables to pipeline in format <key>:<value>
```
## 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
```