-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a
gh variable get FOO
command (cli#9106)
Closes cli#9103. --------- Co-authored-by: William Martin <[email protected]>
- Loading branch information
1 parent
99568e6
commit 08a5589
Showing
3 changed files
with
336 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package get | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/MakeNowJust/heredoc" | ||
"github.com/cli/cli/v2/api" | ||
"github.com/cli/cli/v2/internal/gh" | ||
"github.com/cli/cli/v2/internal/ghrepo" | ||
"github.com/cli/cli/v2/pkg/cmd/variable/shared" | ||
"github.com/cli/cli/v2/pkg/cmdutil" | ||
"github.com/cli/cli/v2/pkg/iostreams" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type GetOptions struct { | ||
HttpClient func() (*http.Client, error) | ||
IO *iostreams.IOStreams | ||
Config func() (gh.Config, error) | ||
BaseRepo func() (ghrepo.Interface, error) | ||
|
||
VariableName string | ||
OrgName string | ||
EnvName string | ||
} | ||
|
||
type getVariableResponse struct { | ||
Value string `json:"value"` | ||
// Other available but unused fields | ||
// Name string `json:"name"` | ||
// UpdatedAt time.Time `json:"updated_at"` | ||
// Visibility shared.Visibility `json:"visibility"` | ||
// SelectedReposURL string `json:"selected_repositories_url"` | ||
// NumSelectedRepos int `json:"num_selected_repos"` | ||
} | ||
|
||
func NewCmdGet(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Command { | ||
opts := &GetOptions{ | ||
IO: f.IOStreams, | ||
Config: f.Config, | ||
HttpClient: f.HttpClient, | ||
} | ||
|
||
cmd := &cobra.Command{ | ||
Use: "get <variable-name>", | ||
Short: "Get variables", | ||
Long: heredoc.Doc(` | ||
Get a variable on one of the following levels: | ||
- repository (default): available to GitHub Actions runs or Dependabot in a repository | ||
- environment: available to GitHub Actions runs for a deployment environment in a repository | ||
- organization: available to GitHub Actions runs or Dependabot within an organization | ||
`), | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
// support `-R, --repo` override | ||
opts.BaseRepo = f.BaseRepo | ||
|
||
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil { | ||
return err | ||
} | ||
|
||
opts.VariableName = args[0] | ||
|
||
if runF != nil { | ||
return runF(opts) | ||
} | ||
|
||
return getRun(opts) | ||
}, | ||
} | ||
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Get a variable for an organization") | ||
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Get a variable for an environment") | ||
|
||
return cmd | ||
} | ||
|
||
func getRun(opts *GetOptions) error { | ||
c, err := opts.HttpClient() | ||
if err != nil { | ||
return fmt.Errorf("could not create http client: %w", err) | ||
} | ||
client := api.NewClientFromHTTP(c) | ||
|
||
orgName := opts.OrgName | ||
envName := opts.EnvName | ||
|
||
variableEntity, err := shared.GetVariableEntity(orgName, envName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var baseRepo ghrepo.Interface | ||
if variableEntity == shared.Repository || variableEntity == shared.Environment { | ||
baseRepo, err = opts.BaseRepo() | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
var path string | ||
switch variableEntity { | ||
case shared.Organization: | ||
path = fmt.Sprintf("orgs/%s/actions/variables/%s", orgName, opts.VariableName) | ||
case shared.Environment: | ||
path = fmt.Sprintf("repos/%s/environments/%s/variables/%s", ghrepo.FullName(baseRepo), envName, opts.VariableName) | ||
case shared.Repository: | ||
path = fmt.Sprintf("repos/%s/actions/variables/%s", ghrepo.FullName(baseRepo), opts.VariableName) | ||
} | ||
|
||
cfg, err := opts.Config() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
host, _ := cfg.Authentication().DefaultHost() | ||
|
||
var response getVariableResponse | ||
if err = client.REST(host, "GET", path, nil, &response); err != nil { | ||
var httpErr api.HTTPError | ||
if errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound { | ||
return fmt.Errorf("variable %s was not found", opts.VariableName) | ||
} | ||
|
||
return fmt.Errorf("failed to get variable %s: %w", opts.VariableName, err) | ||
} | ||
|
||
fmt.Fprintf(opts.IO.Out, "%s\n", response.Value) | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package get | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/cli/cli/v2/internal/config" | ||
"github.com/cli/cli/v2/internal/gh" | ||
"github.com/cli/cli/v2/internal/ghrepo" | ||
"github.com/cli/cli/v2/pkg/cmdutil" | ||
"github.com/cli/cli/v2/pkg/httpmock" | ||
"github.com/cli/cli/v2/pkg/iostreams" | ||
"github.com/google/shlex" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestNewCmdGet(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
cli string | ||
wants GetOptions | ||
wantErr error | ||
}{ | ||
{ | ||
name: "repo", | ||
cli: "FOO", | ||
wants: GetOptions{ | ||
OrgName: "", | ||
VariableName: "FOO", | ||
}, | ||
}, | ||
{ | ||
name: "org", | ||
cli: "-o TestOrg BAR", | ||
wants: GetOptions{ | ||
OrgName: "TestOrg", | ||
VariableName: "BAR", | ||
}, | ||
}, | ||
{ | ||
name: "env", | ||
cli: "-e Development BAZ", | ||
wants: GetOptions{ | ||
EnvName: "Development", | ||
VariableName: "BAZ", | ||
}, | ||
}, | ||
{ | ||
name: "org and env", | ||
cli: "-o TestOrg -e Development QUX", | ||
wantErr: cmdutil.FlagErrorf("%s", "specify only one of `--org` or `--env`"), | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
ios, _, _, _ := iostreams.Test() | ||
f := &cmdutil.Factory{ | ||
IOStreams: ios, | ||
} | ||
|
||
argv, err := shlex.Split(tt.cli) | ||
assert.NoError(t, err) | ||
|
||
var gotOpts *GetOptions | ||
cmd := NewCmdGet(f, func(opts *GetOptions) error { | ||
gotOpts = opts | ||
return nil | ||
}) | ||
cmd.SetArgs(argv) | ||
cmd.SetIn(&bytes.Buffer{}) | ||
cmd.SetOut(&bytes.Buffer{}) | ||
cmd.SetErr(&bytes.Buffer{}) | ||
|
||
_, err = cmd.ExecuteC() | ||
if tt.wantErr != nil { | ||
require.Equal(t, err, tt.wantErr) | ||
return | ||
} | ||
require.NoError(t, err) | ||
|
||
require.Equal(t, tt.wants.OrgName, gotOpts.OrgName) | ||
require.Equal(t, tt.wants.EnvName, gotOpts.EnvName) | ||
require.Equal(t, tt.wants.VariableName, gotOpts.VariableName) | ||
}) | ||
} | ||
} | ||
|
||
func Test_getRun(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
opts *GetOptions | ||
httpStubs func(*httpmock.Registry) | ||
wantOut string | ||
wantErr error | ||
}{ | ||
{ | ||
name: "getting repo variable", | ||
opts: &GetOptions{ | ||
VariableName: "VARIABLE_ONE", | ||
}, | ||
httpStubs: func(reg *httpmock.Registry) { | ||
reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/variables/VARIABLE_ONE"), | ||
httpmock.JSONResponse(getVariableResponse{ | ||
Value: "repo_var", | ||
})) | ||
}, | ||
wantOut: "repo_var\n", | ||
}, | ||
{ | ||
name: "getting org variable", | ||
opts: &GetOptions{ | ||
OrgName: "TestOrg", | ||
VariableName: "VARIABLE_ONE", | ||
}, | ||
httpStubs: func(reg *httpmock.Registry) { | ||
reg.Register(httpmock.REST("GET", "orgs/TestOrg/actions/variables/VARIABLE_ONE"), | ||
httpmock.JSONResponse(getVariableResponse{ | ||
Value: "org_var", | ||
})) | ||
}, | ||
wantOut: "org_var\n", | ||
}, | ||
{ | ||
name: "getting env variable", | ||
opts: &GetOptions{ | ||
EnvName: "Development", | ||
VariableName: "VARIABLE_ONE", | ||
}, | ||
httpStubs: func(reg *httpmock.Registry) { | ||
reg.Register(httpmock.REST("GET", "repos/owner/repo/environments/Development/variables/VARIABLE_ONE"), | ||
httpmock.JSONResponse(getVariableResponse{ | ||
Value: "env_var", | ||
})) | ||
}, | ||
wantOut: "env_var\n", | ||
}, | ||
{ | ||
name: "when the variable is not found, an error is returned", | ||
opts: &GetOptions{ | ||
VariableName: "VARIABLE_ONE", | ||
}, | ||
httpStubs: func(reg *httpmock.Registry) { | ||
reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/variables/VARIABLE_ONE"), | ||
httpmock.StatusStringResponse(404, "not found"), | ||
) | ||
}, | ||
wantErr: fmt.Errorf("variable VARIABLE_ONE was not found"), | ||
}, | ||
{ | ||
name: "when getting any variable from API fails, the error is bubbled with context", | ||
opts: &GetOptions{ | ||
VariableName: "VARIABLE_ONE", | ||
}, | ||
httpStubs: func(reg *httpmock.Registry) { | ||
reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/variables/VARIABLE_ONE"), | ||
httpmock.StatusStringResponse(400, "not found"), | ||
) | ||
}, | ||
wantErr: fmt.Errorf("failed to get variable VARIABLE_ONE: HTTP 400 (https://api.github.com/repos/owner/repo/actions/variables/VARIABLE_ONE)"), | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
var runTest = func(tty bool) func(t *testing.T) { | ||
return func(t *testing.T) { | ||
reg := &httpmock.Registry{} | ||
tt.httpStubs(reg) | ||
defer reg.Verify(t) | ||
|
||
ios, _, stdout, _ := iostreams.Test() | ||
ios.SetStdoutTTY(tty) | ||
|
||
tt.opts.IO = ios | ||
tt.opts.BaseRepo = func() (ghrepo.Interface, error) { | ||
return ghrepo.FromFullName("owner/repo") | ||
} | ||
tt.opts.HttpClient = func() (*http.Client, error) { | ||
return &http.Client{Transport: reg}, nil | ||
} | ||
tt.opts.Config = func() (gh.Config, error) { | ||
return config.NewBlankConfig(), nil | ||
} | ||
|
||
err := getRun(tt.opts) | ||
if err != nil { | ||
require.EqualError(t, tt.wantErr, err.Error()) | ||
return | ||
} | ||
|
||
require.NoError(t, err) | ||
require.Equal(t, tt.wantOut, stdout.String()) | ||
} | ||
} | ||
|
||
t.Run(tt.name+" tty", runTest(true)) | ||
t.Run(tt.name+" no-tty", runTest(false)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters