Skip to content

Commit ad21d23

Browse files
authored
Move Package Push to use Kong (#592)
* Move tests to new command structure * Remove package push from cobra's command root * Update tests to only validate command line arguments * Remove unused type * explicitly add package command on isHelpRequest * Update tests to catch error when stdin is set without stdin-file-name * Fix linter errors on merge
1 parent fb76511 commit ad21d23

File tree

7 files changed

+283
-442
lines changed

7 files changed

+283
-442
lines changed

cmd/pkg/push.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package pkg
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"os"
9+
10+
"github.com/alecthomas/kong"
11+
"github.com/buildkite/cli/v3/internal/cli"
12+
bkIO "github.com/buildkite/cli/v3/internal/io"
13+
"github.com/buildkite/cli/v3/internal/util"
14+
"github.com/buildkite/cli/v3/pkg/cmd/factory"
15+
"github.com/buildkite/go-buildkite/v4"
16+
)
17+
18+
var (
19+
ErrInvalidConfig = errors.New("invalid config")
20+
ErrAPIError = errors.New("API error")
21+
22+
// To be overridden in testing
23+
// Actually diddling an io.Reader so that it looks like a readable stdin is tricky
24+
// so we'll just stub this out
25+
isStdInReadableFunc = isStdinReadable
26+
)
27+
28+
type PushCmd struct {
29+
RegistrySlug string `arg:"" required:"" help:"The slug of the registry to push the package to" `
30+
FilePath string `xor:"input" help:"Path to the package file to push"`
31+
StdinFileName string `xor:"input" help:"The filename to use when reading the package from stdin"`
32+
StdInArg string `arg:"" optional:"" hidden:"" help:"Use '-' as value to pass package via stdin. Required if --stdin-file-name is used."`
33+
Web bool `short:"w" help:"Open the pipeline in a web browser." `
34+
}
35+
36+
func (c *PushCmd) Help() string {
37+
return `Push a new package to a Buildkite registry. The package can be passed as a path to a file with the --file-path flag,
38+
or via stdin. If passed via stdin, the filename must be provided with the --stdin-file-name flag, as a Buildkite
39+
registry requires a filename for the package.
40+
41+
Examples:
42+
Push a package to a Buildkite registry
43+
The web URL of the uploaded package will be printed to stdout.
44+
45+
# Push package from file
46+
$ bk package push my-registry --file-path my-package.tar.gz
47+
48+
# Push package via stdin
49+
$ cat my-package.tar.gz | bk package push my-registry --stdin-file-name my-package.tar.gz - # Pass package via stdin, note hyphen as the argument
50+
51+
# add -w to open the build in your web browser
52+
$ bk package push my-registry --file-path my-package.tar.gz -w
53+
`
54+
}
55+
56+
func (c *PushCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {
57+
f, err := factory.New()
58+
if err != nil {
59+
return err
60+
}
61+
62+
f.SkipConfirm = globals.SkipConfirmation()
63+
f.NoInput = globals.DisableInput()
64+
f.Quiet = globals.IsQuiet()
65+
66+
err = c.Validate()
67+
if err != nil {
68+
return fmt.Errorf("failed to validate flags and args: %w", err)
69+
}
70+
71+
var (
72+
from io.Reader
73+
packageName string
74+
)
75+
76+
switch {
77+
case c.FilePath != "":
78+
packageName = c.FilePath
79+
file, err := os.Open(c.FilePath)
80+
if err != nil {
81+
return fmt.Errorf("couldn't open file %s: %w", c.FilePath, err)
82+
}
83+
defer file.Close()
84+
85+
from = file
86+
case c.StdinFileName != "":
87+
packageName = c.StdinFileName
88+
from = os.Stdin
89+
90+
default:
91+
panic("Neither file path nor stdin file name are available, there has been an error in the config validation. Report this to [email protected]")
92+
}
93+
94+
ctx := context.Background()
95+
var pkg buildkite.Package
96+
spinErr := bkIO.SpinWhile(f, "Pushing file", func() {
97+
pkg, _, err = f.RestAPIClient.PackagesService.Create(ctx, f.Config.OrganizationSlug(), c.RegistrySlug, buildkite.CreatePackageInput{
98+
Filename: packageName,
99+
Package: from,
100+
})
101+
})
102+
if spinErr != nil {
103+
return spinErr
104+
}
105+
if err != nil {
106+
return fmt.Errorf("%w: request to create package failed: %w", ErrAPIError, err)
107+
}
108+
109+
return util.OpenInWebBrowser(c.Web, pkg.WebURL)
110+
}
111+
112+
func isStdinReadable() (bool, error) {
113+
stat, err := os.Stdin.Stat()
114+
if err != nil {
115+
return false, fmt.Errorf("failed to stat stdin: %w", err)
116+
}
117+
118+
readable := (stat.Mode() & os.ModeCharDevice) == 0
119+
return readable, nil
120+
}
121+
122+
func (c *PushCmd) Validate() error {
123+
124+
// Validate the args such that either a file path is provided or stdin is being used
125+
126+
// check if c.FilePath and c.Stdin cannot be both set or both empty
127+
if c.FilePath == "" && c.StdinFileName == "" {
128+
return fmt.Errorf("%w: either a file path argument or --stdin-file-name must be provided", ErrInvalidConfig)
129+
}
130+
131+
if c.FilePath != "" && c.StdinFileName != "" {
132+
return fmt.Errorf("%w: cannot provide both a file path argument and --stdin-file-name", ErrInvalidConfig)
133+
}
134+
135+
if c.StdinFileName != "" {
136+
if c.StdInArg != "-" {
137+
return fmt.Errorf("%w: when passing a package file via stdin, the final argument must be '-'", ErrInvalidConfig)
138+
}
139+
140+
stdInReadable, err := isStdInReadableFunc()
141+
if err != nil {
142+
return fmt.Errorf("failed to check if stdin is readable: %w", err)
143+
}
144+
145+
if !stdInReadable {
146+
return fmt.Errorf("%w: stdin is not readable", ErrInvalidConfig)
147+
}
148+
149+
return nil
150+
} else {
151+
//Validate if an std-in arg is provided without stdin-file-name
152+
if c.StdInArg == "-" {
153+
return fmt.Errorf("%w: when passing a package file via stdin, --stdin-file-name must be provided", ErrInvalidConfig)
154+
}
155+
//We have a file path, check it exists and is a regular file
156+
fi, err := os.Stat(c.FilePath)
157+
if err != nil {
158+
return fmt.Errorf("%w: %w", ErrInvalidConfig, err)
159+
}
160+
161+
if !fi.Mode().IsRegular() {
162+
mode := "directory"
163+
if !fi.Mode().IsDir() {
164+
mode = fi.Mode().String()
165+
}
166+
return fmt.Errorf("%w: file at %s is not a regular file, mode was: %s", ErrInvalidConfig, c.FilePath, mode)
167+
}
168+
169+
return nil
170+
}
171+
}

cmd/pkg/push_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package pkg
2+
3+
import (
4+
"errors"
5+
"io"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestPackagePushCommandArgs(t *testing.T) {
11+
t.Parallel()
12+
13+
cases := []struct {
14+
name string
15+
stdin io.Reader
16+
cmd PushCmd
17+
18+
wantErrContain string
19+
wantErr error
20+
}{
21+
// Config validation errors
22+
{
23+
name: "no args",
24+
cmd: PushCmd{
25+
RegistrySlug: "my-registry",
26+
FilePath: "",
27+
StdinFileName: "",
28+
StdInArg: "",
29+
},
30+
wantErrContain: "either a file path argument or --stdin-file-name must be provided",
31+
wantErr: ErrInvalidConfig,
32+
},
33+
{
34+
name: "file that's a directory",
35+
cmd: PushCmd{
36+
RegistrySlug: "my-registry",
37+
FilePath: "/",
38+
StdinFileName: "",
39+
StdInArg: "",
40+
},
41+
wantErr: ErrInvalidConfig,
42+
wantErrContain: "file at / is not a regular file, mode was: directory",
43+
},
44+
{
45+
name: "file that doesn't exist",
46+
cmd: PushCmd{
47+
RegistrySlug: "my-registry",
48+
FilePath: "/does-not-exist",
49+
StdinFileName: "",
50+
StdInArg: "",
51+
},
52+
wantErr: ErrInvalidConfig,
53+
wantErrContain: "stat /does-not-exist: no such file or directory",
54+
},
55+
{
56+
name: "cannot provide both file path and stdin file name",
57+
cmd: PushCmd{
58+
RegistrySlug: "my-registry",
59+
FilePath: "/a-test-package.pkg",
60+
StdinFileName: "a-test-package.pkg",
61+
StdInArg: "",
62+
},
63+
wantErr: ErrInvalidConfig,
64+
wantErrContain: "cannot provide both a file path argument and --stdin-file-name",
65+
},
66+
{
67+
name: "file path but with stdin arg '-'",
68+
cmd: PushCmd{
69+
RegistrySlug: "my-registry",
70+
FilePath: "/directory/test.pkg",
71+
StdinFileName: "",
72+
StdInArg: "-",
73+
},
74+
stdin: strings.NewReader("test package stream contents!"),
75+
wantErr: ErrInvalidConfig,
76+
wantErrContain: "when passing a package file via stdin, --stdin-file-name must be provided",
77+
},
78+
{
79+
name: "stdin without --stdin-file-name",
80+
cmd: PushCmd{
81+
RegistrySlug: "my-registry",
82+
FilePath: "",
83+
StdinFileName: "test",
84+
StdInArg: "",
85+
},
86+
stdin: strings.NewReader("test package stream contents!"),
87+
wantErr: ErrInvalidConfig,
88+
wantErrContain: "when passing a package file via stdin, the final argument must be '-'",
89+
},
90+
}
91+
92+
for _, tc := range cases {
93+
t.Run(tc.name, func(t *testing.T) {
94+
95+
t.Parallel()
96+
97+
err := tc.cmd.Validate()
98+
if !errors.Is(err, tc.wantErr) {
99+
t.Errorf("Expected error %v, got %v", tc.wantErr, err)
100+
}
101+
102+
if err != nil && !strings.Contains(err.Error(), tc.wantErrContain) {
103+
t.Errorf("Expected error to contain %q, got %q", tc.wantErrContain, err.Error())
104+
}
105+
})
106+
}
107+
}

main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/buildkite/cli/v3/cmd/job"
1616
"github.com/buildkite/cli/v3/cmd/organization"
1717
"github.com/buildkite/cli/v3/cmd/pipeline"
18+
"github.com/buildkite/cli/v3/cmd/pkg"
1819
"github.com/buildkite/cli/v3/cmd/use"
1920
"github.com/buildkite/cli/v3/cmd/user"
2021
"github.com/buildkite/cli/v3/cmd/version"
@@ -90,7 +91,7 @@ type (
9091
List organization.ListCmd `cmd:"" help:"List configured organizations." aliases:"ls"`
9192
}
9293
PackageCmd struct {
93-
Args []string `arg:"" optional:"" passthrough:"all"`
94+
Push pkg.PushCmd `cmd:"" help:"Push a new package to a Buildkite registry"`
9495
}
9596
PipelineCmd struct {
9697
Create pipeline.CreateCmd `cmd:"" help:"Create a new pipeline."`
@@ -111,7 +112,6 @@ type (
111112
)
112113

113114
// Delegation methods, we should delete when native Kong implementations ready
114-
func (p *PackageCmd) Run(cli *CLI) error { return cli.delegateToCobraSystem("package", p.Args) }
115115
func (c *ConfigureCmd) Run(cli *CLI) error { return cli.delegateToCobraSystem("configure", c.Args) }
116116

117117
// delegateToCobraSystem delegates execution to the legacy Cobra command system.
@@ -303,11 +303,12 @@ func isHelpRequest() bool {
303303
if len(os.Args) >= 2 && os.Args[1] == "organization" {
304304
return false
305305
}
306-
307306
if len(os.Args) >= 2 && os.Args[1] == "use" {
308307
return false
309308
}
310-
309+
if len(os.Args) >= 2 && os.Args[1] == "package" {
310+
return false
311+
}
311312
if len(os.Args) >= 2 && os.Args[1] == "user" {
312313
return false
313314
}

pkg/cmd/pkg/package.go

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)