mirror of https://gitlab.com/gitlab-org/cli.git
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:
commit
00a2e08bbd
2
Makefile
2
Makefile
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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)
|
Loading…
Reference in New Issue