diff --git a/pkg/cmd/variable/delete/delete.go b/pkg/cmd/variable/delete/delete.go new file mode 100644 index 000000000..743036bf3 --- /dev/null +++ b/pkg/cmd/variable/delete/delete.go @@ -0,0 +1,135 @@ +package delete + +import ( + "fmt" + "net/http" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/config" + "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 DeleteOptions struct { + HttpClient func() (*http.Client, error) + IO *iostreams.IOStreams + Config func() (config.Config, error) + BaseRepo func() (ghrepo.Interface, error) + + VariableName string + OrgName string + EnvName string + Application string +} + +func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command { + opts := &DeleteOptions{ + IO: f.IOStreams, + Config: f.Config, + HttpClient: f.HttpClient, + } + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete variables", + Long: heredoc.Doc(` + Delete a variable on one of the following levels: + - repository (default): available to Actions runs or Dependabot in a repository + - environment: available to Actions runs for a deployment environment in a repository + - organization: available to 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 removeRun(opts) + }, + Aliases: []string{ + "remove", + }, + } + cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Delete a variable for an organization") + cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Delete a variable for an environment") + + return cmd +} + +func removeRun(opts *DeleteOptions) 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("repositories/%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() + + err = client.REST(host, "DELETE", path, nil, nil) + if err != nil { + return fmt.Errorf("failed to delete variable %s: %w", opts.VariableName, err) + } + + if opts.IO.IsStdoutTTY() { + var target string + switch variableEntity { + case shared.Organization: + target = orgName + case shared.Repository, shared.Environment: + target = ghrepo.FullName(baseRepo) + } + + cs := opts.IO.ColorScheme() + if envName != "" { + fmt.Fprintf(opts.IO.Out, "%s Deleted variable %s from %s environment on %s\n", cs.SuccessIconWithColor(cs.Red), opts.VariableName, envName, target) + } else { + fmt.Fprintf(opts.IO.Out, "%s Deleted Actions variable %s from %s\n", cs.SuccessIconWithColor(cs.Red), opts.VariableName, target) + } + } + + return nil +} diff --git a/pkg/cmd/variable/delete/delete_test.go b/pkg/cmd/variable/delete/delete_test.go new file mode 100644 index 000000000..578ddae38 --- /dev/null +++ b/pkg/cmd/variable/delete/delete_test.go @@ -0,0 +1,203 @@ +package delete + +import ( + "bytes" + "net/http" + "testing" + + "github.com/cli/cli/v2/internal/config" + "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" +) + +func TestNewCmdDelete(t *testing.T) { + tests := []struct { + name string + cli string + wants DeleteOptions + wantsErr bool + }{ + { + name: "no args", + wantsErr: true, + }, + { + name: "repo", + cli: "cool", + wants: DeleteOptions{ + VariableName: "cool", + }, + }, + { + name: "org", + cli: "cool --org anOrg", + wants: DeleteOptions{ + VariableName: "cool", + OrgName: "anOrg", + }, + }, + { + name: "env", + cli: "cool --env anEnv", + wants: DeleteOptions{ + VariableName: "cool", + EnvName: "anEnv", + }, + }, + } + + 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 *DeleteOptions + cmd := NewCmdDelete(f, func(opts *DeleteOptions) 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.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tt.wants.VariableName, gotOpts.VariableName) + assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName) + assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName) + }) + } +} + +func Test_removeRun_repo(t *testing.T) { + tests := []struct { + name string + opts *DeleteOptions + wantPath string + }{ + { + name: "Actions", + opts: &DeleteOptions{ + Application: "actions", + VariableName: "cool_variable", + }, + wantPath: "repos/owner/repo/actions/variables/cool_variable", + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + + reg.Register( + httpmock.REST("DELETE", tt.wantPath), + httpmock.StatusStringResponse(204, "No Content")) + + ios, _, _, _ := iostreams.Test() + + tt.opts.IO = ios + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + tt.opts.Config = func() (config.Config, error) { + return config.NewBlankConfig(), nil + } + tt.opts.BaseRepo = func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + } + + err := removeRun(tt.opts) + assert.NoError(t, err) + + reg.Verify(t) + } +} + +func Test_removeRun_env(t *testing.T) { + reg := &httpmock.Registry{} + + reg.Register( + httpmock.REST("DELETE", "repositories/owner/repo/environments/development/variables/cool_variable"), + httpmock.StatusStringResponse(204, "No Content")) + + ios, _, _, _ := iostreams.Test() + + opts := &DeleteOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + Config: func() (config.Config, error) { + return config.NewBlankConfig(), nil + }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + }, + VariableName: "cool_variable", + EnvName: "development", + } + + err := removeRun(opts) + assert.NoError(t, err) + + reg.Verify(t) +} + +func Test_removeRun_org(t *testing.T) { + tests := []struct { + name string + opts *DeleteOptions + wantPath string + }{ + { + name: "org", + opts: &DeleteOptions{ + OrgName: "UmbrellaCorporation", + }, + wantPath: "orgs/UmbrellaCorporation/actions/variables/tVirus", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + + reg.Register( + httpmock.REST("DELETE", tt.wantPath), + httpmock.StatusStringResponse(204, "No Content")) + + ios, _, _, _ := iostreams.Test() + + tt.opts.Config = func() (config.Config, error) { + return config.NewBlankConfig(), nil + } + 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.IO = ios + tt.opts.VariableName = "tVirus" + + err := removeRun(tt.opts) + assert.NoError(t, err) + + reg.Verify(t) + }) + } +} diff --git a/pkg/cmd/variable/list/list_test.go b/pkg/cmd/variable/list/list_test.go index fd09c9ba8..d8982ef42 100644 --- a/pkg/cmd/variable/list/list_test.go +++ b/pkg/cmd/variable/list/list_test.go @@ -89,6 +89,7 @@ func Test_listRun(t *testing.T) { tty: true, opts: &ListOptions{}, wantOut: []string{ + "NAME VALUE UPDATED AT", "VARIABLE_ONE one Updated 1988-10-11", "VARIABLE_TWO two Updated 2020-12-04", "VARIABLE_THREE three Updated 1975-11-30", @@ -111,6 +112,7 @@ func Test_listRun(t *testing.T) { OrgName: "UmbrellaCorporation", }, wantOut: []string{ + "NAME VALUE UPDATED AT VISIBILITY", "VARIABLE_ONE org_one Updated 1988-10-11 Visible to all repositories", "VARIABLE_TWO org_two Updated 2020-12-04 Visible to private repositories", "VARIABLE_THREE org_three Updated 1975-11-30 Visible to 2 selected reposito...", @@ -135,6 +137,7 @@ func Test_listRun(t *testing.T) { EnvName: "Development", }, wantOut: []string{ + "NAME VALUE UPDATED AT", "VARIABLE_ONE one Updated 1988-10-11", "VARIABLE_TWO two Updated 2020-12-04", "VARIABLE_THREE three Updated 1975-11-30", @@ -239,13 +242,8 @@ func Test_listRun(t *testing.T) { reg.Verify(t) outputLines := strings.Split(stdout.String(), "\n") - idx := 0 - if strings.Contains(outputLines[0], "NAME") { - idx = 1 - } - for _, l := range tt.wantOut { - assert.Equal(t, outputLines[idx], l) - idx++ + for i, l := range tt.wantOut { + assert.Equal(t, outputLines[i], l) } }) } diff --git a/pkg/cmd/variable/variable.go b/pkg/cmd/variable/variable.go index ec5964aab..1768227c8 100644 --- a/pkg/cmd/variable/variable.go +++ b/pkg/cmd/variable/variable.go @@ -2,6 +2,7 @@ package variable import ( "github.com/MakeNowJust/heredoc" + cmdDelete "github.com/cli/cli/v2/pkg/cmd/variable/delete" cmdList "github.com/cli/cli/v2/pkg/cmd/variable/list" cmdSet "github.com/cli/cli/v2/pkg/cmd/variable/set" "github.com/cli/cli/v2/pkg/cmdutil" @@ -22,5 +23,6 @@ func NewCmdVariable(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(cmdSet.NewCmdSet(f, nil)) cmd.AddCommand(cmdList.NewCmdList(f, nil)) + cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil)) return cmd }