diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 65a9d696a..ab76368ad 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -22,8 +22,9 @@ type DeleteOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams - DeleteAll bool - Identifier string + DeleteAll bool + SucceedOnNoCaches bool + Identifier string } func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command { @@ -33,7 +34,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd := &cobra.Command{ - Use: "delete [| | --all]", + Use: "delete [ | | --all]", Short: "Delete GitHub Actions caches", Long: heredoc.Docf(` Delete GitHub Actions caches. @@ -50,8 +51,11 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co # Delete a cache by id in a specific repo $ gh cache delete 1234 --repo cli/cli - # Delete all caches + # Delete all caches (exit code 1 on no caches) $ gh cache delete --all + + # Delete all caches (exit code 0 on no caches) + $ gh cache delete --all --succeed-on-no-caches `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -65,6 +69,10 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co return err } + if !opts.DeleteAll && opts.SucceedOnNoCaches { + return cmdutil.FlagErrorf("--succeed-on-no-caches must be used in conjunction with --all") + } + if !opts.DeleteAll && len(args) == 0 { return cmdutil.FlagErrorf("must provide either cache id, cache key, or use --all") } @@ -82,6 +90,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Delete all caches") + cmd.Flags().BoolVar(&opts.SucceedOnNoCaches, "succeed-on-no-caches", false, "Return exit code 0 if no caches found. Must be used in conjunction with `--all`") return cmd } @@ -100,12 +109,21 @@ func deleteRun(opts *DeleteOptions) error { var toDelete []string if opts.DeleteAll { + opts.IO.StartProgressIndicator() caches, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: -1}) + opts.IO.StopProgressIndicator() if err != nil { return err } if len(caches.ActionsCaches) == 0 { - return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon()) + if opts.SucceedOnNoCaches { + if opts.IO.IsStdoutTTY() { + fmt.Fprintf(opts.IO.Out, "%s No caches to delete\n", opts.IO.ColorScheme().SuccessIcon()) + } + return nil + } else { + return fmt.Errorf("%s No caches to delete", opts.IO.ColorScheme().FailureIcon()) + } } for _, cache := range caches.ActionsCaches { toDelete = append(toDelete, strconv.Itoa(cache.Id)) diff --git a/pkg/cmd/cache/delete/delete_test.go b/pkg/cmd/cache/delete/delete_test.go index 43fc7d213..8660d693b 100644 --- a/pkg/cmd/cache/delete/delete_test.go +++ b/pkg/cmd/cache/delete/delete_test.go @@ -43,6 +43,21 @@ func TestNewCmdDelete(t *testing.T) { cli: "--all", wants: DeleteOptions{DeleteAll: true}, }, + { + name: "delete all and succeed-on-no-caches flags", + cli: "--all --succeed-on-no-caches", + wants: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, + }, + { + name: "succeed-on-no-caches flag", + cli: "--succeed-on-no-caches", + wantsErr: "--succeed-on-no-caches must be used in conjunction with --all", + }, + { + name: "succeed-on-no-caches flag and id argument", + cli: "--succeed-on-no-caches 123", + wantsErr: "--succeed-on-no-caches must be used in conjunction with --all", + }, { name: "id argument and delete all flag", cli: "1 --all", @@ -72,6 +87,7 @@ func TestNewCmdDelete(t *testing.T) { } assert.NoError(t, err) assert.Equal(t, tt.wants.DeleteAll, gotOpts.DeleteAll) + assert.Equal(t, tt.wants.SucceedOnNoCaches, gotOpts.SucceedOnNoCaches) assert.Equal(t, tt.wants.Identifier, gotOpts.Identifier) }) } @@ -160,6 +176,19 @@ func TestDeleteRun(t *testing.T) { tty: true, wantStdout: "✓ Deleted 2 caches from OWNER/REPO\n", }, + { + name: "attempts to delete all caches but api errors", + opts: DeleteOptions{DeleteAll: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.StatusStringResponse(500, ""), + ) + }, + tty: true, + wantErr: true, + wantErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/actions/caches?per_page=100)", + }, { name: "displays delete error", opts: DeleteOptions{Identifier: "123"}, @@ -186,6 +215,54 @@ func TestDeleteRun(t *testing.T) { tty: true, wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n", }, + { + name: "no caches to delete when deleting all", + opts: DeleteOptions{DeleteAll: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.JSONResponse(shared.CachePayload{ + ActionsCaches: []shared.Cache{}, + TotalCount: 0, + }), + ) + }, + tty: false, + wantErr: true, + wantErrMsg: "X No caches to delete", + }, + { + name: "no caches to delete when deleting all but succeed on no cache tty", + opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.JSONResponse(shared.CachePayload{ + ActionsCaches: []shared.Cache{}, + TotalCount: 0, + }), + ) + }, + tty: true, + wantErr: false, + wantStdout: "✓ No caches to delete\n", + }, + { + name: "no caches to delete when deleting all but succeed on no cache non-tty", + opts: DeleteOptions{DeleteAll: true, SucceedOnNoCaches: true}, + stubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "repos/OWNER/REPO/actions/caches"), + httpmock.JSONResponse(shared.CachePayload{ + ActionsCaches: []shared.Cache{}, + TotalCount: 0, + }), + ) + }, + tty: false, + wantErr: false, + wantStdout: "", + }, } for _, tt := range tests {