X Tutup
Skip to content

Commit 577f29a

Browse files
authored
Support listing and removing user Codespaces secrets (cli#4714)
1 parent 90313fb commit 577f29a

File tree

4 files changed

+191
-33
lines changed

4 files changed

+191
-33
lines changed

pkg/cmd/secret/list/list.go

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"time"
1010

11+
"github.com/MakeNowJust/heredoc"
1112
"github.com/cli/cli/v2/api"
1213
"github.com/cli/cli/v2/internal/config"
1314
"github.com/cli/cli/v2/internal/ghinstance"
@@ -25,8 +26,9 @@ type ListOptions struct {
2526
Config func() (config.Config, error)
2627
BaseRepo func() (ghrepo.Interface, error)
2728

28-
OrgName string
29-
EnvName string
29+
OrgName string
30+
EnvName string
31+
UserSecrets bool
3032
}
3133

3234
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
@@ -39,13 +41,19 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
3941
cmd := &cobra.Command{
4042
Use: "list",
4143
Short: "List secrets",
42-
Long: "List secrets for a repository, environment, or organization",
43-
Args: cobra.NoArgs,
44+
Long: heredoc.Doc(`
45+
List secrets on one of the following levels:
46+
- repository (default): available to Actions runs in a repository
47+
- environment: available to Actions runs for a deployment environment in a repository
48+
- organization: available to Actions runs within an organization
49+
- user: available to Codespaces for your user
50+
`),
51+
Args: cobra.NoArgs,
4452
RunE: func(cmd *cobra.Command, args []string) error {
4553
// support `-R, --repo` override
4654
opts.BaseRepo = f.BaseRepo
4755

48-
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil {
56+
if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil {
4957
return err
5058
}
5159

@@ -59,6 +67,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
5967

6068
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization")
6169
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "List secrets for an environment")
70+
cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "List a secret for your user")
6271

6372
return cmd
6473
}
@@ -73,15 +82,16 @@ func listRun(opts *ListOptions) error {
7382
envName := opts.EnvName
7483

7584
var baseRepo ghrepo.Interface
76-
if orgName == "" {
85+
if orgName == "" && !opts.UserSecrets {
7786
baseRepo, err = opts.BaseRepo()
7887
if err != nil {
7988
return fmt.Errorf("could not determine base repo: %w", err)
8089
}
8190
}
8291

8392
var secrets []*Secret
84-
if orgName == "" {
93+
showSelectedRepoInfo := opts.IO.IsStdoutTTY()
94+
if orgName == "" && !opts.UserSecrets {
8595
if envName == "" {
8696
secrets, err = getRepoSecrets(client, baseRepo)
8797
} else {
@@ -101,7 +111,11 @@ func listRun(opts *ListOptions) error {
101111
return err
102112
}
103113

104-
secrets, err = getOrgSecrets(client, host, orgName)
114+
if opts.UserSecrets {
115+
secrets, err = getUserSecrets(client, host, showSelectedRepoInfo)
116+
} else {
117+
secrets, err = getOrgSecrets(client, host, orgName, showSelectedRepoInfo)
118+
}
105119
}
106120

107121
if err != nil {
@@ -117,7 +131,7 @@ func listRun(opts *ListOptions) error {
117131
}
118132
tp.AddField(updatedAt, nil, nil)
119133
if secret.Visibility != "" {
120-
if opts.IO.IsStdoutTTY() {
134+
if showSelectedRepoInfo {
121135
tp.AddField(fmtVisibility(*secret), nil, nil)
122136
} else {
123137
tp.AddField(strings.ToUpper(string(secret.Visibility)), nil, nil)
@@ -158,25 +172,32 @@ func fmtVisibility(s Secret) string {
158172
return ""
159173
}
160174

161-
func getOrgSecrets(client httpClient, host, orgName string) ([]*Secret, error) {
175+
func getOrgSecrets(client httpClient, host, orgName string, showSelectedRepoInfo bool) ([]*Secret, error) {
162176
secrets, err := getSecrets(client, host, fmt.Sprintf("orgs/%s/actions/secrets", orgName))
163177
if err != nil {
164178
return nil, err
165179
}
166180

167-
type responseData struct {
168-
TotalCount int `json:"total_count"`
181+
if showSelectedRepoInfo {
182+
err = getSelectedRepositoryInformation(client, secrets)
183+
if err != nil {
184+
return nil, err
185+
}
169186
}
187+
return secrets, nil
188+
}
170189

171-
for _, secret := range secrets {
172-
if secret.SelectedReposURL == "" {
173-
continue
174-
}
175-
var result responseData
176-
if _, err := apiGet(client, secret.SelectedReposURL, &result); err != nil {
177-
return nil, fmt.Errorf("failed determining selected repositories for %s: %w", secret.Name, err)
190+
func getUserSecrets(client httpClient, host string, showSelectedRepoInfo bool) ([]*Secret, error) {
191+
secrets, err := getSecrets(client, host, "user/codespaces/secrets")
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
if showSelectedRepoInfo {
197+
err = getSelectedRepositoryInformation(client, secrets)
198+
if err != nil {
199+
return nil, err
178200
}
179-
secret.NumSelectedRepos = result.TotalCount
180201
}
181202

182203
return secrets, nil
@@ -256,3 +277,22 @@ func findNextPage(link string) string {
256277
}
257278
return ""
258279
}
280+
281+
func getSelectedRepositoryInformation(client httpClient, secrets []*Secret) error {
282+
type responseData struct {
283+
TotalCount int `json:"total_count"`
284+
}
285+
286+
for _, secret := range secrets {
287+
if secret.SelectedReposURL == "" {
288+
continue
289+
}
290+
var result responseData
291+
if _, err := apiGet(client, secret.SelectedReposURL, &result); err != nil {
292+
return fmt.Errorf("failed determining selected repositories for %s: %w", secret.Name, err)
293+
}
294+
secret.NumSelectedRepos = result.TotalCount
295+
}
296+
297+
return nil
298+
}

pkg/cmd/secret/list/list_test.go

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ func Test_NewCmdList(t *testing.T) {
4747
EnvName: "Development",
4848
},
4949
},
50+
{
51+
name: "user",
52+
cli: "-u",
53+
wants: ListOptions{
54+
UserSecrets: true,
55+
},
56+
},
5057
}
5158

5259
for _, tt := range tests {
@@ -153,6 +160,30 @@ func Test_listRun(t *testing.T) {
153160
"SECRET_THREE\t1975-11-30",
154161
},
155162
},
163+
{
164+
name: "user tty",
165+
tty: true,
166+
opts: &ListOptions{
167+
UserSecrets: true,
168+
},
169+
wantOut: []string{
170+
"SECRET_ONE.*Updated 1988-10-11.*Visible to 1 selected repository",
171+
"SECRET_TWO.*Updated 2020-12-04.*Visible to 2 selected repositories",
172+
"SECRET_THREE.*Updated 1975-11-30.*Visible to 3 selected repositories",
173+
},
174+
},
175+
{
176+
name: "user not tty",
177+
tty: false,
178+
opts: &ListOptions{
179+
UserSecrets: true,
180+
},
181+
wantOut: []string{
182+
"SECRET_ONE\t1988-10-11\t",
183+
"SECRET_TWO\t2020-12-04\t",
184+
"SECRET_THREE\t1975-11-30\t",
185+
},
186+
},
156187
}
157188

158189
for _, tt := range tests {
@@ -203,11 +234,50 @@ func Test_listRun(t *testing.T) {
203234
}
204235
path = fmt.Sprintf("orgs/%s/actions/secrets", tt.opts.OrgName)
205236

206-
reg.Register(
207-
httpmock.REST("GET", fmt.Sprintf("orgs/%s/actions/secrets/SECRET_THREE/repositories", tt.opts.OrgName)),
208-
httpmock.JSONResponse(struct {
209-
TotalCount int `json:"total_count"`
210-
}{2}))
237+
if tt.tty {
238+
reg.Register(
239+
httpmock.REST("GET", fmt.Sprintf("orgs/%s/actions/secrets/SECRET_THREE/repositories", tt.opts.OrgName)),
240+
httpmock.JSONResponse(struct {
241+
TotalCount int `json:"total_count"`
242+
}{2}))
243+
}
244+
}
245+
246+
if tt.opts.UserSecrets {
247+
payload.Secrets = []*Secret{
248+
{
249+
Name: "SECRET_ONE",
250+
UpdatedAt: t0,
251+
Visibility: shared.Selected,
252+
SelectedReposURL: "https://api.github.com/user/codespaces/secrets/SECRET_ONE/repositories",
253+
},
254+
{
255+
Name: "SECRET_TWO",
256+
UpdatedAt: t1,
257+
Visibility: shared.Selected,
258+
SelectedReposURL: "https://api.github.com/user/codespaces/secrets/SECRET_TWO/repositories",
259+
},
260+
{
261+
Name: "SECRET_THREE",
262+
UpdatedAt: t2,
263+
Visibility: shared.Selected,
264+
SelectedReposURL: "https://api.github.com/user/codespaces/secrets/SECRET_THREE/repositories",
265+
},
266+
}
267+
268+
path = "user/codespaces/secrets"
269+
if tt.tty {
270+
for i, secret := range payload.Secrets {
271+
hostLen := len("https://api.github.com/")
272+
path := secret.SelectedReposURL[hostLen:len(secret.SelectedReposURL)]
273+
repositoryCount := i + 1
274+
reg.Register(
275+
httpmock.REST("GET", path),
276+
httpmock.JSONResponse(struct {
277+
TotalCount int `json:"total_count"`
278+
}{repositoryCount}))
279+
}
280+
}
211281
}
212282

213283
reg.Register(httpmock.REST("GET", path), httpmock.JSONResponse(payload))

pkg/cmd/secret/remove/remove.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"net/http"
66

7+
"github.com/MakeNowJust/heredoc"
78
"github.com/cli/cli/v2/api"
89
"github.com/cli/cli/v2/internal/config"
910
"github.com/cli/cli/v2/internal/ghrepo"
@@ -18,9 +19,10 @@ type RemoveOptions struct {
1819
Config func() (config.Config, error)
1920
BaseRepo func() (ghrepo.Interface, error)
2021

21-
SecretName string
22-
OrgName string
23-
EnvName string
22+
SecretName string
23+
OrgName string
24+
EnvName string
25+
UserSecrets bool
2426
}
2527

2628
func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Command {
@@ -33,13 +35,19 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co
3335
cmd := &cobra.Command{
3436
Use: "remove <secret-name>",
3537
Short: "Remove secrets",
36-
Long: "Remove a secret for a repository, environment, or organization",
37-
Args: cobra.ExactArgs(1),
38+
Long: heredoc.Doc(`
39+
Remove a secret on one of the following levels:
40+
- repository (default): available to Actions runs in a repository
41+
- environment: available to Actions runs for a deployment environment in a repository
42+
- organization: available to Actions runs within an organization
43+
- user: available to Codespaces for your user
44+
`),
45+
Args: cobra.ExactArgs(1),
3846
RunE: func(cmd *cobra.Command, args []string) error {
3947
// support `-R, --repo` override
4048
opts.BaseRepo = f.BaseRepo
4149

42-
if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil {
50+
if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil {
4351
return err
4452
}
4553

@@ -54,6 +62,7 @@ func NewCmdRemove(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co
5462
}
5563
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Remove a secret for an organization")
5664
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Remove a secret for an environment")
65+
cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "Remove a secret for your user")
5766

5867
return cmd
5968
}
@@ -69,7 +78,7 @@ func removeRun(opts *RemoveOptions) error {
6978
envName := opts.EnvName
7079

7180
var baseRepo ghrepo.Interface
72-
if orgName == "" {
81+
if orgName == "" && !opts.UserSecrets {
7382
baseRepo, err = opts.BaseRepo()
7483
if err != nil {
7584
return fmt.Errorf("could not determine base repo: %w", err)
@@ -81,6 +90,8 @@ func removeRun(opts *RemoveOptions) error {
8190
path = fmt.Sprintf("orgs/%s/actions/secrets/%s", orgName, opts.SecretName)
8291
} else if envName != "" {
8392
path = fmt.Sprintf("repos/%s/environments/%s/secrets/%s", ghrepo.FullName(baseRepo), envName, opts.SecretName)
93+
} else if opts.UserSecrets {
94+
path = fmt.Sprintf("user/codespaces/secrets/%s", opts.SecretName)
8495
} else {
8596
path = fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(baseRepo), opts.SecretName)
8697
}
@@ -102,7 +113,9 @@ func removeRun(opts *RemoveOptions) error {
102113

103114
if opts.IO.IsStdoutTTY() {
104115
target := orgName
105-
if orgName == "" {
116+
if opts.UserSecrets {
117+
target = "your user"
118+
} else if orgName == "" {
106119
target = ghrepo.FullName(baseRepo)
107120
}
108121
cs := opts.IO.ColorScheme()

pkg/cmd/secret/remove/remove_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ func TestNewCmdRemove(t *testing.T) {
4949
EnvName: "anEnv",
5050
},
5151
},
52+
{
53+
name: "user",
54+
cli: "cool -u",
55+
wants: RemoveOptions{
56+
SecretName: "cool",
57+
UserSecrets: true,
58+
},
59+
},
5260
}
5361

5462
for _, tt := range tests {
@@ -201,3 +209,30 @@ func Test_removeRun_org(t *testing.T) {
201209
}
202210

203211
}
212+
213+
func Test_removeRun_user(t *testing.T) {
214+
reg := &httpmock.Registry{}
215+
216+
reg.Register(
217+
httpmock.REST("DELETE", "user/codespaces/secrets/cool_secret"),
218+
httpmock.StatusStringResponse(204, "No Content"))
219+
220+
io, _, _, _ := iostreams.Test()
221+
222+
opts := &RemoveOptions{
223+
IO: io,
224+
HttpClient: func() (*http.Client, error) {
225+
return &http.Client{Transport: reg}, nil
226+
},
227+
Config: func() (config.Config, error) {
228+
return config.NewBlankConfig(), nil
229+
},
230+
SecretName: "cool_secret",
231+
UserSecrets: true,
232+
}
233+
234+
err := removeRun(opts)
235+
assert.NoError(t, err)
236+
237+
reg.Verify(t)
238+
}

0 commit comments

Comments
 (0)
X Tutup