Merge branch 'refactor/ci_subgroups' into 'main'

feat: Create a job artifact command

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

Merged-by: Shekhar Patnaik <spatnaik@gitlab.com>
Approved-by: Gary Holtz <gholtz@gitlab.com>
Approved-by: Shekhar Patnaik <spatnaik@gitlab.com>
Reviewed-by: Gary Holtz <gholtz@gitlab.com>
Co-authored-by: Sylvain Nieuwlandt <nieuwlandt.s@an0rak.dev>
This commit is contained in:
Shekhar Patnaik 2024-04-22 11:42:30 +00:00
commit 00a2e08bbd
13 changed files with 404 additions and 100 deletions

View File

@ -147,7 +147,7 @@ endif
bin/golangci-lint-${GOLANGCI_VERSION}:
@mkdir -p bin
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ./bin/ v${GOLANGCI_VERSION}
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b ./bin v${GOLANGCI_VERSION}
@mv bin/golangci-lint $@
.PHONY: coverage

View File

@ -1,34 +1,12 @@
package ci
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/internal/config"
"gitlab.com/gitlab-org/cli/pkg/utils"
jobArtifact "gitlab.com/gitlab-org/cli/commands/job/artifact"
)
func ensurePathIsCreated(filename string) error {
dir, _ := filepath.Split(filename)
if _, err := os.Stat(filename); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0o700) // Create your file
if err != nil {
return fmt.Errorf("could not create new path: %v", err)
}
}
return nil
}
func NewCmdRun(f *cmdutils.Factory) *cobra.Command {
jobArtifactCmd := &cobra.Command{
Use: "artifact <refName> <jobName> [flags]",
@ -54,71 +32,9 @@ func NewCmdRun(f *cmdutils.Factory) *cobra.Command {
return err
}
artifact, err := api.DownloadArtifactJob(apiClient, repo.FullName(), args[0], &gitlab.DownloadArtifactsFileOptions{Job: &args[1]})
if err != nil {
return err
}
zipReader, err := zip.NewReader(artifact, artifact.Size())
if err != nil {
return err
}
if !config.CheckPathExists(path) {
if err := os.Mkdir(path, 0o755); err != nil {
return err
}
}
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
for _, v := range zipReader.File {
sanitizedAssetName := utils.SanitizePathName(v.Name)
destDir, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("resolving absolute download directory path: %v", err)
}
destPath := filepath.Join(destDir, sanitizedAssetName)
if !strings.HasPrefix(destPath, destDir) {
return fmt.Errorf("invalid file path name")
}
if v.FileInfo().IsDir() {
if err := os.Mkdir(destPath, v.Mode()); err != nil {
return err
}
} else {
srcFile, err := zipReader.Open(v.Name)
if err != nil {
return err
}
defer srcFile.Close()
err = ensurePathIsCreated(destPath)
if err != nil {
return err
}
symlinkCheck, _ := os.Lstat(destPath)
if symlinkCheck != nil && symlinkCheck.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("file in artifact would overwrite a symbolic link- cannot extract")
}
dstFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, v.Mode())
if err != nil {
return err
}
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
}
}
return nil
return jobArtifact.DownloadArtifacts(apiClient, repo, path, args[0], args[1])
},
Deprecated: "please use 'glab job artifact' instead",
}
jobArtifactCmd.Flags().StringP("path", "p", "./", "Path to download the artifact files")

View File

@ -1,6 +1,9 @@
package ci
import (
"fmt"
"os"
jobArtifactCmd "gitlab.com/gitlab-org/cli/commands/ci/artifact"
ciConfigCmd "gitlab.com/gitlab-org/cli/commands/ci/config"
pipeDeleteCmd "gitlab.com/gitlab-org/cli/commands/ci/delete"
@ -26,10 +29,13 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command {
Short: `Work with GitLab CI/CD pipelines and jobs`,
Long: ``,
Aliases: []string{"pipe", "pipeline"},
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintf(os.Stderr, "Aliases 'pipe' and 'pipeline' are deprecated. Please use 'ci' instead.\n\n")
_ = cmd.Help()
},
}
cmdutils.EnableRepoOverride(ciCmd, f)
ciCmd.AddCommand(legacyCICmd.NewCmdCI(f))
ciCmd.AddCommand(ciTraceCmd.NewCmdTrace(f))
ciCmd.AddCommand(ciViewCmd.NewCmdView(f))
@ -44,5 +50,6 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command {
ciCmd.AddCommand(jobArtifactCmd.NewCmdRun(f))
ciCmd.AddCommand(pipeGetCmd.NewCmdGet(f))
ciCmd.AddCommand(ciConfigCmd.NewCmdConfig(f))
return ciCmd
}

View File

@ -1,22 +1,72 @@
package ci
import (
"bytes"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/test"
)
func TestPipelineCmd(t *testing.T) {
old := os.Stdout // keep backup of the real stdout
r, w, _ := os.Pipe()
os.Stdout = w
assert.Nil(t, NewCmdCI(&cmdutils.Factory{}).Execute())
out := test.ReturnBuffer(old, r, w)
assert.Contains(t, out, "Use \"ci [command] --help\" for more information about a command.\n")
var tests = []struct {
name string
args string
expectedOut string
expectedErr string
}{
{
name: "when no args should display the help message",
args: "",
expectedOut: "Use \"ci [command] --help\" for more information about a command.\n",
expectedErr: "Aliases 'pipe' and 'pipeline' are deprecated. Please use 'ci' instead.",
},
}
func TestPipelineCmd(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
wantedErr := ""
if len(test.expectedErr) > 0 {
wantedErr = test.expectedErr
}
// Catching Stdout & Stderr
oldOut := os.Stdout
rOut, wOut, _ := os.Pipe()
os.Stdout = wOut
outC := make(chan string)
go func() {
var buf bytes.Buffer
_, _ = io.Copy(&buf, rOut)
outC <- buf.String()
}()
oldErr := os.Stderr
rErr, wErr, _ := os.Pipe()
os.Stderr = wErr
errC := make(chan string)
go func() {
var buf bytes.Buffer
_, _ = io.Copy(&buf, rErr)
errC <- buf.String()
}()
err := NewCmdCI(&cmdutils.Factory{}).Execute()
// Rollbacking Stdout & Stderr
wOut.Close()
os.Stdout = oldOut
stdout := <-outC
wErr.Close()
os.Stderr = oldErr
stderr := <-errC
if assert.NoErrorf(t, err, "error running `ci %s` : %v", test.args, err) {
assert.Contains(t, stderr, wantedErr)
assert.Contains(t, stdout, test.expectedOut)
}
})
}
}

View File

@ -0,0 +1,38 @@
package artifact
import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
)
func NewCmdArtifact(f *cmdutils.Factory) *cobra.Command {
jobArtifactCmd := &cobra.Command{
Use: "artifact <refName> <jobName> [flags]",
Short: `Download all artifacts from the last pipeline`,
Aliases: []string{"push"},
Example: heredoc.Doc(`
glab job artifact main build
glab job artifact main deploy --path="artifacts/"
`),
Long: ``,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
repo, err := f.BaseRepo()
if err != nil {
return err
}
apiClient, err := f.HttpClient()
if err != nil {
return err
}
path, err := cmd.Flags().GetString("path")
if err != nil {
return err
}
return DownloadArtifacts(apiClient, repo, path, args[0], args[1])
},
}
jobArtifactCmd.Flags().StringP("path", "p", "./", "Path to download the artifact files")
return jobArtifactCmd
}

View File

@ -0,0 +1,95 @@
package artifact
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/internal/config"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/utils"
)
func ensurePathIsCreated(filename string) error {
dir, _ := filepath.Split(filename)
if _, err := os.Stat(filename); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0o700) // Create your file
if err != nil {
return fmt.Errorf("could not create new path: %v", err)
}
}
return nil
}
func DownloadArtifacts(apiClient *gitlab.Client, repo glrepo.Interface, path string, refName string, jobName string) error {
artifact, err := api.DownloadArtifactJob(apiClient, repo.FullName(), refName, &gitlab.DownloadArtifactsFileOptions{Job: &jobName})
if err != nil {
return err
}
zipReader, err := zip.NewReader(artifact, artifact.Size())
if err != nil {
return err
}
if !config.CheckPathExists(path) {
if err := os.Mkdir(path, 0o755); err != nil {
return err
}
}
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
for _, v := range zipReader.File {
sanitizedAssetName := utils.SanitizePathName(v.Name)
destDir, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("resolving absolute download directory path: %v", err)
}
destPath := filepath.Join(destDir, sanitizedAssetName)
if !strings.HasPrefix(destPath, destDir) {
return fmt.Errorf("invalid file path name")
}
if v.FileInfo().IsDir() {
if err := os.Mkdir(destPath, v.Mode()); err != nil {
return err
}
} else {
srcFile, err := zipReader.Open(v.Name)
if err != nil {
return err
}
defer srcFile.Close()
err = ensurePathIsCreated(destPath)
if err != nil {
return err
}
symlinkCheck, _ := os.Lstat(destPath)
if symlinkCheck != nil && symlinkCheck.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("file in artifact would overwrite a symbolic link- cannot extract")
}
dstFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, v.Mode())
if err != nil {
return err
}
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
}
}
return nil
}

20
commands/job/job.go Normal file
View File

@ -0,0 +1,20 @@
package job
import (
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/job/artifact"
"github.com/spf13/cobra"
)
func NewCmdJob(f *cmdutils.Factory) *cobra.Command {
jobCmd := &cobra.Command{
Use: "job <command> [flags]",
Short: `Work with GitLab CI/CD jobs`,
Long: ``,
}
cmdutils.EnableRepoOverride(jobCmd, f)
jobCmd.AddCommand(artifact.NewCmdArtifact(f))
return jobCmd
}

72
commands/job/job_test.go Normal file
View File

@ -0,0 +1,72 @@
package job
import (
"bytes"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
)
var tests = []struct {
name string
args string
expectedOut string
expectedErr string
}{
{
name: "when no args should display the help message",
args: "",
expectedOut: "Use \"job [command] --help\" for more information about a command.\n",
expectedErr: "",
},
}
func TestJobCmd(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
wantedErr := ""
if len(test.expectedErr) > 0 {
wantedErr = test.expectedErr
}
// Catching Stdout & Stderr
oldOut := os.Stdout
rOut, wOut, _ := os.Pipe()
os.Stdout = wOut
outC := make(chan string)
go func() {
var buf bytes.Buffer
_, _ = io.Copy(&buf, rOut)
outC <- buf.String()
}()
oldErr := os.Stderr
rErr, wErr, _ := os.Pipe()
os.Stderr = wErr
errC := make(chan string)
go func() {
var buf bytes.Buffer
_, _ = io.Copy(&buf, rErr)
errC <- buf.String()
}()
err := NewCmdJob(&cmdutils.Factory{}).Execute()
// Rollbacking Stdout & Stderr
wOut.Close()
os.Stdout = oldOut
stdout := <-outC
wErr.Close()
os.Stderr = oldErr
stderr := <-errC
if assert.NoErrorf(t, err, "error running `job %s` : %v", test.args, err) {
assert.Contains(t, stderr, wantedErr)
assert.Contains(t, stdout, test.expectedOut)
}
})
}
}

View File

@ -19,6 +19,7 @@ import (
"gitlab.com/gitlab-org/cli/commands/help"
incidentCmd "gitlab.com/gitlab-org/cli/commands/incident"
issueCmd "gitlab.com/gitlab-org/cli/commands/issue"
jobCmd "gitlab.com/gitlab-org/cli/commands/job"
labelCmd "gitlab.com/gitlab-org/cli/commands/label"
mrCmd "gitlab.com/gitlab-org/cli/commands/mr"
projectCmd "gitlab.com/gitlab-org/cli/commands/project"
@ -114,6 +115,7 @@ func NewCmdRoot(f *cmdutils.Factory, version, buildDate string) *cobra.Command {
rootCmd.AddCommand(clusterCmd.NewCmdCluster(f))
rootCmd.AddCommand(issueCmd.NewCmdIssue(f))
rootCmd.AddCommand(incidentCmd.NewCmdIncident(f))
rootCmd.AddCommand(jobCmd.NewCmdJob(f))
rootCmd.AddCommand(labelCmd.NewCmdLabel(f))
rootCmd.AddCommand(mrCmd.NewCmdMR(f))
rootCmd.AddCommand(pipelineCmd.NewCmdCI(f))

View File

@ -13,6 +13,10 @@ Please do not edit this file directly. Run `make gen-docs` instead.
Work with GitLab CI/CD pipelines and jobs
```plaintext
glab ci <command> [flags]
```
## Aliases
```plaintext

View File

@ -0,0 +1,45 @@
---
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 job artifact`
Download all artifacts from the last pipeline
```plaintext
glab job artifact <refName> <jobName> [flags]
```
## Aliases
```plaintext
push
```
## Examples
```plaintext
glab job artifact main build
glab job artifact main deploy --path="artifacts/"
```
## Options
```plaintext
-p, --path string Path to download the artifact files (default "./")
```
## 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
```

25
docs/source/job/help.md Normal file
View File

@ -0,0 +1,25 @@
---
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 job help`
Help about any command
```plaintext
glab job help [command] [flags]
```
## 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
```

30
docs/source/job/index.md Normal file
View File

@ -0,0 +1,30 @@
---
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 job`
Work with GitLab CI/CD jobs
## Options
```plaintext
-R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL
```
## Options inherited from parent commands
```plaintext
--help Show help for command
```
## Subcommands
- [`artifact`](artifact.md)