diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cd76af82c580cc62260ab3ee8ac4e419e95c465d..1a0f663215901c43a469bcc989a28fcac8daf979 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,6 +43,7 @@ check_docs_update: - git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME && git checkout $CI_MERGE_REQUEST_TARGET_BRANCH_NAME && git checkout $CI_COMMIT_SHA - go run cmd/gen-docs/docs.go - |- + git status if [[ $(git add -A --dry-run) ]]; then echo '✖ ERROR: Documentation changes detected!'; echo '✖ These changes require a documentation update. To regenerate the docs, read https://gitlab.com/gitlab-org/cli/-/tree/main/docs#generating-the-docs.'; diff --git a/api/pipeline.go b/api/pipeline.go index 0a79be306bf342aa35d18704ce818c415edfb77a..86372a67ba95989ec3e4019bcd355f9d64229e24 100644 --- a/api/pipeline.go +++ b/api/pipeline.go @@ -160,6 +160,38 @@ var GetPipelines = func(client *gitlab.Client, l *gitlab.ListProjectPipelinesOpt return pipes, nil } +var GetPipeline = func(client *gitlab.Client, pid int, l *gitlab.RequestOptionFunc, repo interface{}) (*gitlab.Pipeline, error) { + if client == nil { + client = apiClient.Lab() + } + + pipe, _, err := client.Pipelines.GetPipeline(repo, pid) + + if err != nil { + return nil, err + } + return pipe, nil +} + +var GetPipelineVariables = func(client *gitlab.Client, pid int, l *gitlab.RequestOptionFunc, repo interface{}) ([]*gitlab.PipelineVariable, error) { + if client == nil { + client = apiClient.Lab() + } + + pipe, _, err := client.Pipelines.GetPipeline(repo, pid) + if err != nil { + return nil, err + } + projectID := pipe.ProjectID + + pipelineVars, _, err := client.Pipelines.GetPipelineVariables(projectID, pid) + + if err != nil { + return nil, err + } + return pipelineVars, nil +} + var GetPipelineJobs = func(client *gitlab.Client, pid int, repo string) ([]*gitlab.Job, error) { if client == nil { client = apiClient.Lab() diff --git a/commands/ci/ci.go b/commands/ci/ci.go index 785207814ce922f21280a10fdcf398a179d5abbc..89c816e6fde8abc948e245895882928efa64e54e 100644 --- a/commands/ci/ci.go +++ b/commands/ci/ci.go @@ -3,6 +3,7 @@ package ci import ( jobArtifactCmd "gitlab.com/gitlab-org/cli/commands/ci/artifact" pipeDeleteCmd "gitlab.com/gitlab-org/cli/commands/ci/delete" + pipeGetCmd "gitlab.com/gitlab-org/cli/commands/ci/get" legacyCICmd "gitlab.com/gitlab-org/cli/commands/ci/legacyci" ciLintCmd "gitlab.com/gitlab-org/cli/commands/ci/lint" pipeListCmd "gitlab.com/gitlab-org/cli/commands/ci/list" @@ -36,5 +37,6 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command { ciCmd.AddCommand(pipeRetryCmd.NewCmdRetry(f)) ciCmd.AddCommand(pipeRunCmd.NewCmdRun(f)) ciCmd.AddCommand(jobArtifactCmd.NewCmdRun(f)) + ciCmd.AddCommand(pipeGetCmd.NewCmdGet(f)) return ciCmd } diff --git a/commands/ci/get/get.go b/commands/ci/get/get.go new file mode 100644 index 0000000000000000000000000000000000000000..59cd55dcee846a49794d7f4277726227c8fbfc96 --- /dev/null +++ b/commands/ci/get/get.go @@ -0,0 +1,128 @@ +package status + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/xanzy/go-gitlab" + "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/tableprinter" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +type PipelineMergedResponse struct { + *gitlab.Pipeline + Jobs []*gitlab.Job `json:"jobs"` + Variables []*gitlab.PipelineVariable `json:"variables"` +} + +func NewCmdGet(f *cmdutils.Factory) *cobra.Command { + var pipelineGetCmd = &cobra.Command{ + Use: "get [flags]", + Short: `Get JSON of a running CI pipeline on current or other branch specified`, + Aliases: []string{"stats"}, + Example: heredoc.Doc(` + glab ci get + glab ci -R some/project -p 12345 + `), + Long: ``, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + c := f.IO.Color() + + apiClient, err := f.HttpClient() + if err != nil { + return err + } + + repo, err := f.BaseRepo() + if err != nil { + return err + } + + // Parse arguments into local vars + branch, _ := cmd.Flags().GetString("branch") + pipelineId, _ := cmd.Flags().GetInt("pipeline-id") + + if branch == "" { + branch, err = git.CurrentBranch() + if err != nil { + return err + } + } + pipeline, err := api.GetPipeline(apiClient, pipelineId, nil, repo.FullName()) + if err != nil { + redCheck := c.Red("✘") + fmt.Fprintf(f.IO.StdOut, "%s No pipelines running or available on %s branch\n", redCheck, branch) + return err + } + + jobs, err := api.GetPipelineJobs(apiClient, pipelineId, repo.FullName()) + variables, err := api.GetPipelineVariables(apiClient, pipelineId, nil, repo.FullName()) + + mergedPipelineObject := &PipelineMergedResponse{ + Pipeline: pipeline, + Jobs: jobs, + Variables: variables, + } + + outputFormat, _ := cmd.Flags().GetString("output-format") + if outputFormat == "json" { + printJSON(*mergedPipelineObject) + } else { + printTable(*mergedPipelineObject) + } + + return nil + }, + } + + pipelineGetCmd.Flags().StringP("branch", "b", "", "Check pipeline status for a branch. (Default is current branch)") + pipelineGetCmd.Flags().IntP("pipeline-id", "p", 0, "Provide pipeline ID") + pipelineGetCmd.Flags().StringP("output-format", "o", "text", "Format output as: text, json") + + return pipelineGetCmd +} + +func printJSON(p PipelineMergedResponse) { + JSONStr, _ := json.Marshal(p) + fmt.Println(string(JSONStr)) +} + +func printTable(p PipelineMergedResponse) { + fmt.Print("# Pipeline:\n") + pipelineTable := tableprinter.NewTablePrinter() + pipelineTable.AddRow("id:", strconv.Itoa(p.ID)) + pipelineTable.AddRow("status:", p.Status) + pipelineTable.AddRow("source:", p.Source) + pipelineTable.AddRow("ref:", p.Ref) + pipelineTable.AddRow("sha:", p.SHA) + pipelineTable.AddRow("tag:", p.Tag) + pipelineTable.AddRow("yaml Errors:", p.YamlErrors) + pipelineTable.AddRow("user:", p.User.Username) + pipelineTable.AddRow("created:", p.CreatedAt) + pipelineTable.AddRow("started:", p.StartedAt) + pipelineTable.AddRow("updated:", p.UpdatedAt) + fmt.Println(pipelineTable.String()) + + fmt.Print("# Jobs:\n") + jobTable := tableprinter.NewTablePrinter() + for _, j := range p.Jobs { + j := j + jobTable.AddRow(j.Name+":", j.Status) + } + fmt.Println(jobTable.String()) + + fmt.Print("# Variables:\n") + varTable := tableprinter.NewTablePrinter() + for _, v := range p.Variables { + varTable.AddRow(v.Key+":", v.Value) + } + fmt.Println(varTable.String()) +} diff --git a/commands/ci/run/run.go b/commands/ci/run/run.go index 75614ad2d9f3d088e8905ae9cf121f21e6ee7f3d..3cc290f933f00310569202876036c4cbbfda6522 100644 --- a/commands/ci/run/run.go +++ b/commands/ci/run/run.go @@ -1,8 +1,9 @@ package run import ( + "encoding/json" "fmt" - "regexp" + "os" "strings" "gitlab.com/gitlab-org/cli/api" @@ -14,9 +15,12 @@ import ( "github.com/xanzy/go-gitlab" ) -const keyValuePair = ".+:.+" +var ( + PipelineVarTypeEnv = "env_var" + PipelineVarTypeFile = "file" +) -var re = regexp.MustCompile(keyValuePair) +var envVariables = []string{} func getDefaultBranch(f *cmdutils.Factory) string { repo, err := f.BaseRepo() @@ -39,6 +43,47 @@ func getDefaultBranch(f *cmdutils.Factory) string { return branch } +func parseVarArg(s string) (*gitlab.PipelineVariableOptions, 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 nil, fmt.Errorf("invalid argument structure") + } + return &gitlab.PipelineVariableOptions{ + Key: &v[0], + Value: &v[1], + }, nil +} + +func extractEnvVar(s string) (*gitlab.PipelineVariableOptions, error) { + pvar, err := parseVarArg(s) + if err != nil { + return nil, err + } + pvar.VariableType = &PipelineVarTypeEnv + return pvar, nil +} + +func extractFileVar(s string) (*gitlab.PipelineVariableOptions, error) { + pvar, err := parseVarArg(s) + if err != nil { + return nil, err + } + b, err := os.ReadFile(*pvar.Value) + if err != nil { + return nil, err + } + content := string(b) + pvar.VariableType = &PipelineVarTypeFile + pvar.Value = &content + return pvar, nil +} + func NewCmdRun(f *cmdutils.Factory) *cobra.Command { var pipelineRunCmd = &cobra.Command{ Use: "run [flags]", @@ -47,8 +92,10 @@ func NewCmdRun(f *cmdutils.Factory) *cobra.Command { Example: heredoc.Doc(` glab ci run glab ci run -b main - glab ci run -b main --variables MYKEY:some_value - glab ci run -b main --variables MYKEY:some_value --variables KEY2:another_value + glab ci run -b main --variables-env key1:val1 + glab ci run -b main --variables-env key1:val1,key2:val2 + glab ci run -b main --variables-env key1:val1 --variables-env key2:val2 + glab ci run -b main --variables-file MYKEY:file1 --variables KEY2:some_value `), Long: ``, Args: cobra.ExactArgs(0), @@ -67,27 +114,54 @@ func NewCmdRun(f *cmdutils.Factory) *cobra.Command { pipelineVars := []*gitlab.PipelineVariableOptions{} - if customPipelineVars, _ := cmd.Flags().GetStringSlice("variables"); len(customPipelineVars) > 0 { - varType := "env_var" + if customPipelineVars, _ := cmd.Flags().GetStringSlice("variables-env"); len(customPipelineVars) > 0 { for _, v := range customPipelineVars { - if !re.MatchString(v) { - return fmt.Errorf("Bad pipeline variable : \"%s\" should be of format KEY:VALUE", v) + pvar, err := extractEnvVar(v) + if err != nil { + return fmt.Errorf("parsing pipeline variable expected format KEY:VALUE: %w", err) } - s := strings.SplitN(v, ":", 2) - pipelineVars = append(pipelineVars, &gitlab.PipelineVariableOptions{ - Key: &s[0], - Value: &s[1], - VariableType: &varType, - }) + pipelineVars = append(pipelineVars, pvar) } } + if customPipelineFileVars, _ := cmd.Flags().GetStringSlice("variables-file"); len(customPipelineFileVars) > 0 { + for _, v := range customPipelineFileVars { + pvar, err := extractFileVar(v) + if err != nil { + return fmt.Errorf("parsing pipeline variable expected format KEY:FILENAME: %w", err) + } + pipelineVars = append(pipelineVars, pvar) + } + } + + vf, err := cmd.Flags().GetString("variables-from") + if err != nil { + return err + } + if vf != "" { + b, err := os.ReadFile(vf) + if err != nil { + // Return the error encountered + return fmt.Errorf("opening variable file: %s", vf) + } + var result []*gitlab.PipelineVariableOptions + err = json.Unmarshal(b, &result) + if err != nil { + return fmt.Errorf("loading pipeline values: %w", err) + } + pipelineVars = append(pipelineVars, result...) + } + c := &gitlab.CreatePipelineOptions{ Variables: &pipelineVars, } - if m, _ := cmd.Flags().GetString("branch"); m != "" { - c.Ref = gitlab.String(m) + branch, err := cmd.Flags().GetString("branch") + if err != nil { + return err + } + if branch != "" { + c.Ref = gitlab.String(branch) } else { c.Ref = gitlab.String(getDefaultBranch(f)) } @@ -102,7 +176,10 @@ func NewCmdRun(f *cmdutils.Factory) *cobra.Command { }, } pipelineRunCmd.Flags().StringP("branch", "b", "", "Create pipeline on branch/ref ") - pipelineRunCmd.Flags().StringSliceP("variables", "", []string{}, "Pass variables to pipeline") + pipelineRunCmd.Flags().StringSliceVarP(&envVariables, "variables", "", []string{}, "Pass variables to pipeline in format :") + pipelineRunCmd.Flags().StringSliceVarP(&envVariables, "variables-env", "", []string{}, "Pass variables to pipeline in format :") + pipelineRunCmd.Flags().StringSliceP("variables-file", "", []string{}, "Pass file contents as a file variable to pipeline in format :") + pipelineRunCmd.Flags().StringP("variables-from", "f", "", "JSON file containing variables for pipeline execution") return pipelineRunCmd } diff --git a/docs/source/ci/get.md b/docs/source/ci/get.md new file mode 100644 index 0000000000000000000000000000000000000000..a670aa02a76fd8642cd18b1d64f0188a735a3208 --- /dev/null +++ b/docs/source/ci/get.md @@ -0,0 +1,41 @@ +--- +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 get` + +Get JSON of a running CI pipeline on current or other branch specified + +```plaintext +glab ci get [flags] +``` + +## Examples + +```plaintext +glab ci get +glab ci -R some/project -p 12345 + +``` + +## Options + +```plaintext + -b, --branch string Check pipeline status for a branch. (Default is current branch) + -o, --output-format string Format output as: text, json (default "text") + -p, --pipeline-id int Provide pipeline ID +``` + +## 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 +``` diff --git a/docs/source/ci/index.md b/docs/source/ci/index.md index 7416e537229083fdb5bc365e9d4c4e4d80ec8bd2..448a42e151df1e42e29e47433fa5d850a834c25a 100755 --- a/docs/source/ci/index.md +++ b/docs/source/ci/index.md @@ -30,6 +30,7 @@ Work with GitLab CI pipelines and jobs - [artifact](artifact.md) - [ci](ci/index.md) - [delete](delete.md) +- [get](get.md) - [lint](lint.md) - [list](list.md) - [retry](retry.md) diff --git a/docs/source/ci/run.md b/docs/source/ci/run.md index d07dc5316b9bfc54b95e6d3f86bc4032dfc3a6f2..e688c72b530fba731062e7e87bb7a8880f563aa9 100644 --- a/docs/source/ci/run.md +++ b/docs/source/ci/run.md @@ -22,16 +22,21 @@ glab ci run [flags] ```plaintext glab ci run glab ci run -b main -glab ci run -b main --variables MYKEY:some_value -glab ci run -b main --variables MYKEY:some_value --variables KEY2:another_value +glab ci run -b main --variables-env key1:val1 +glab ci run -b main --variables-env key1:val1,key2:val2 +glab ci run -b main --variables-env key1:val1 --variables-env key2:val2 +glab ci run -b main --variables-file MYKEY:file1 --variables KEY2:some_value ``` ## Options ```plaintext - -b, --branch string Create pipeline on branch/ref - --variables strings Pass variables to pipeline + -b, --branch string Create pipeline on branch/ref + --variables strings Pass variables to pipeline in format : + --variables-env strings Pass variables to pipeline in format : + --variables-file strings Pass file contents as a file variable to pipeline in format : + -f, --variables-from string JSON file containing variables for pipeline execution ``` ## Options inherited from parent commands