Skip to content

Commit 32a9c4b

Browse files
authored
Merge pull request cli#11838 from luxass/handle-correct-count-for-cache-delete
fix(cache delete): report correct deleted count for key and key+ref deletions
2 parents 52ba836 + ab9c99e commit 32a9c4b

2 files changed

Lines changed: 77 additions & 28 deletions

File tree

pkg/cmd/cache/delete/delete.go

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -164,33 +164,18 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface
164164
cs := opts.IO.ColorScheme()
165165
repoName := ghrepo.FullName(repo)
166166
opts.IO.StartProgressIndicator()
167-
base := fmt.Sprintf("repos/%s/actions/caches", repoName)
168167

168+
totalDeleted := 0
169169
for _, cache := range toDelete {
170-
// TODO(babakks): We use two different endpoints here which have different
171-
// response schemas:
172-
//
173-
// 1. /repos/OWNER/REPO/actions/caches/ID (for deleting by cache ID)
174-
// - returns HTTP 204 (NO CONTENT) on success
175-
// 2. /repos/OWNER/REPO/actions/caches?key=KEY[&ref=REF] (for deleting by cache key, and optionally a ref)
176-
// - returns HTTP 200 on success including information about the deleted caches
177-
//
178-
// So, if/when we decided to use the data in the response body we need
179-
// to be careful with parsing. Probably want to split these API calls
180-
// into separate functions.
181-
182-
path := ""
170+
var count int
171+
var err error
183172
if id, ok := parseCacheID(cache); ok {
184-
path = fmt.Sprintf("%s/%d", base, id)
173+
err = deleteCacheByID(client, repo, id)
174+
count = 1
185175
} else {
186-
path = fmt.Sprintf("%s?key=%s", base, url.QueryEscape(cache))
187-
188-
if opts.Ref != "" {
189-
path += fmt.Sprintf("&ref=%s", url.QueryEscape(opts.Ref))
190-
}
176+
count, err = deleteCacheByKey(client, repo, cache, opts.Ref)
191177
}
192178

193-
err := client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
194179
if err != nil {
195180
var httpErr api.HTTPError
196181
if errors.As(err, &httpErr) {
@@ -207,17 +192,45 @@ func deleteCaches(opts *DeleteOptions, client *api.Client, repo ghrepo.Interface
207192
opts.IO.StopProgressIndicator()
208193
return err
209194
}
195+
196+
totalDeleted += count
210197
}
211198

212199
opts.IO.StopProgressIndicator()
213200

214201
if opts.IO.IsStdoutTTY() {
215-
fmt.Fprintf(opts.IO.Out, "%s Deleted %s from %s\n", cs.SuccessIcon(), text.Pluralize(len(toDelete), "cache"), repoName)
202+
fmt.Fprintf(opts.IO.Out, "%s Deleted %s from %s\n", cs.SuccessIcon(), text.Pluralize(totalDeleted, "cache"), repoName)
216203
}
217204

218205
return nil
219206
}
220207

208+
func deleteCacheByID(client *api.Client, repo ghrepo.Interface, id int) error {
209+
// returns HTTP 204 (NO CONTENT) on success
210+
path := fmt.Sprintf("repos/%s/actions/caches/%d", ghrepo.FullName(repo), id)
211+
return client.REST(repo.RepoHost(), "DELETE", path, nil, nil)
212+
}
213+
214+
// deleteCacheByKey deletes cache entries by given key (and optional ref) and
215+
// returns the number of deleted entries.
216+
//
217+
// Note that a key/ref combination does not necessarily map to a single cache
218+
// entry. There may be more than one entries with the same key/ref combination,
219+
// but those entries will have different IDs.
220+
func deleteCacheByKey(client *api.Client, repo ghrepo.Interface, key, ref string) (int, error) {
221+
path := fmt.Sprintf("repos/%s/actions/caches?key=%s", ghrepo.FullName(repo), url.QueryEscape(key))
222+
if ref != "" {
223+
path += fmt.Sprintf("&ref=%s", url.QueryEscape(ref))
224+
}
225+
var payload shared.CachePayload
226+
err := client.REST(repo.RepoHost(), "DELETE", path, nil, &payload)
227+
if err != nil {
228+
return 0, err
229+
}
230+
231+
return payload.TotalCount, nil
232+
}
233+
221234
func parseCacheID(arg string) (int, bool) {
222235
id, err := strconv.Atoi(arg)
223236
return id, err == nil

pkg/cmd/cache/delete/delete_test.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,30 @@ func TestDeleteRun(t *testing.T) {
235235
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
236236
"key": []string{"a weird_cache+key"},
237237
}),
238-
// The response is a JSON object but we don't need it here.
239-
httpmock.StatusStringResponse(200, "{}"),
238+
httpmock.JSONResponse(shared.CachePayload{
239+
TotalCount: 1,
240+
}),
240241
)
241242
},
242243
tty: true,
243244
wantStdout: "✓ Deleted 1 cache from OWNER/REPO\n",
244245
},
246+
{
247+
name: "deletes multiple caches by key",
248+
opts: DeleteOptions{Identifier: "shared-cache-key"},
249+
stubs: func(reg *httpmock.Registry) {
250+
reg.Register(
251+
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
252+
"key": []string{"shared-cache-key"},
253+
}),
254+
httpmock.JSONResponse(shared.CachePayload{
255+
TotalCount: 5,
256+
}),
257+
)
258+
},
259+
tty: true,
260+
wantStdout: "✓ Deleted 5 caches from OWNER/REPO\n",
261+
},
245262
{
246263
name: "no caches to delete when deleting all",
247264
opts: DeleteOptions{DeleteAll: true},
@@ -299,8 +316,9 @@ func TestDeleteRun(t *testing.T) {
299316
"key": []string{"cache-key"},
300317
"ref": []string{"refs/heads/main"},
301318
}),
302-
// The response is a JSON object but we don't need it here.
303-
httpmock.StatusStringResponse(200, "{}"),
319+
httpmock.JSONResponse(shared.CachePayload{
320+
TotalCount: 1,
321+
}),
304322
)
305323
},
306324
tty: true,
@@ -315,13 +333,31 @@ func TestDeleteRun(t *testing.T) {
315333
"key": []string{"cache-key"},
316334
"ref": []string{"refs/heads/main"},
317335
}),
318-
// The response is a JSON object but we don't need it here.
319-
httpmock.StatusStringResponse(200, "{}"),
336+
httpmock.JSONResponse(shared.CachePayload{
337+
TotalCount: 1,
338+
}),
320339
)
321340
},
322341
tty: false,
323342
wantStdout: "",
324343
},
344+
{
345+
name: "deletes multiple caches by key and ref",
346+
opts: DeleteOptions{Identifier: "cache-key", Ref: "refs/heads/feature"},
347+
stubs: func(reg *httpmock.Registry) {
348+
reg.Register(
349+
httpmock.QueryMatcher("DELETE", "repos/OWNER/REPO/actions/caches", url.Values{
350+
"key": []string{"cache-key"},
351+
"ref": []string{"refs/heads/feature"},
352+
}),
353+
httpmock.JSONResponse(shared.CachePayload{
354+
TotalCount: 3,
355+
}),
356+
)
357+
},
358+
tty: true,
359+
wantStdout: "✓ Deleted 3 caches from OWNER/REPO\n",
360+
},
325361
{
326362
// As of now, the API returns HTTP 404 for invalid or non-existent refs.
327363
name: "cache key exists but ref is invalid/not-found",

0 commit comments

Comments
 (0)