detached 12bb525

fix(images): surface runtime prune error in dangling-only mode

When PruneRegistry is false, return the PruneRuntime error to the caller
instead of swallowing it into a warning log. Both-scopes mode keeps the
existing behavior of logging and continuing to registry cleanup.
commit
12bb5253deb191d126034b8cafde8228a2635811
author
brice · · 2026-02-13 06:45 UTC
parent
9de4117
signature
unsigned
2 files · +18 -4
M internal/usecase/images/service.go +5 -4
@@ -4,6 +4,7 @@ package images
44 import (
55 "context"
66 "encoding/json"
7+ "fmt"
78 "sort"
89 "strings"
910 "time"
@@ -347,11 +348,11 @@ func (s *Service) Prune(ctx context.Context, opts domain.ImagePruneOptions) (dom
347348 if opts.PruneDangling {
348349 runtimeReport, err := s.PruneRuntime(ctx)
349350 if err != nil {
350- msg := "runtime prune failed"
351- if opts.PruneRegistry {
352- msg += "; continuing with registry prune"
351+ if !opts.PruneRegistry {
352+ // Dangling-only: surface the error to the caller.
353+ return report, fmt.Errorf("runtime prune failed: %w", err)
353354 }
354- log.Warn().Err(err).Msg(msg)
355+ log.Warn().Err(err).Msg("runtime prune failed; continuing with registry prune")
355356 } else {
356357 report.Runtime = runtimeReport.Runtime
357358 }
@@ -4,6 +4,7 @@ package images
4 import (4 import (
5 "context"5 "context"
6 "encoding/json"6 "encoding/json"
7+ "fmt"
7 "sort"8 "sort"
8 "strings"9 "strings"
9 "time"10 "time"
@@ -347,11 +348,11 @@ func (s *Service) Prune(ctx context.Context, opts domain.ImagePruneOptions) (dom
347 if opts.PruneDangling {348 if opts.PruneDangling {
348 runtimeReport, err := s.PruneRuntime(ctx)349 runtimeReport, err := s.PruneRuntime(ctx)
349 if err != nil {350 if err != nil {
350- msg := "runtime prune failed"351+ if !opts.PruneRegistry {
351- if opts.PruneRegistry {352+ // Dangling-only: surface the error to the caller.
352- msg += "; continuing with registry prune"353+ return report, fmt.Errorf("runtime prune failed: %w", err)
353 }354 }
354- log.Warn().Err(err).Msg(msg)355+ log.Warn().Err(err).Msg("runtime prune failed; continuing with registry prune")
355 } else {356 } else {
356 report.Runtime = runtimeReport.Runtime357 report.Runtime = runtimeReport.Runtime
357 }358 }
M internal/usecase/images/service_test.go +13 -0
@@ -529,6 +529,19 @@ func TestService_Prune_DanglingOnlySkipsRegistry(t *testing.T) {
529529 assert.Equal(t, 0, manifestStorage.listRepositoriesCalls, "should not list repositories when registry disabled")
530530 }
531531
532+func TestService_Prune_DanglingOnlyRuntimeFailureReturnsError(t *testing.T) {
533+ rt := &fakeRuntime{pruneErr: errors.New("docker daemon unavailable")}
534+ svc := NewService(rt, newFakeManifestStorage(), &fakeBlobStorage{}, zerowrap.Default())
535+
536+ _, err := svc.Prune(context.Background(), domain.ImagePruneOptions{
537+ KeepLast: 3, PruneDangling: true, PruneRegistry: false,
538+ })
539+
540+ require.Error(t, err)
541+ assert.Contains(t, err.Error(), "runtime prune failed")
542+ assert.Contains(t, err.Error(), "docker daemon unavailable")
543+}
544+
532545 func TestService_Prune_RegistryOnlySkipsRuntime(t *testing.T) {
533546 manifestStorage := newFakeManifestStorage()
534547 manifestStorage.repositories = []string{"gordon/api"}
@@ -529,6 +529,19 @@ func TestService_Prune_DanglingOnlySkipsRegistry(t *testing.T) {
529 assert.Equal(t, 0, manifestStorage.listRepositoriesCalls, "should not list repositories when registry disabled")529 assert.Equal(t, 0, manifestStorage.listRepositoriesCalls, "should not list repositories when registry disabled")
530 }530 }
531 531
532+func TestService_Prune_DanglingOnlyRuntimeFailureReturnsError(t *testing.T) {
533+ rt := &fakeRuntime{pruneErr: errors.New("docker daemon unavailable")}
534+ svc := NewService(rt, newFakeManifestStorage(), &fakeBlobStorage{}, zerowrap.Default())
535+
536+ _, err := svc.Prune(context.Background(), domain.ImagePruneOptions{
537+ KeepLast: 3, PruneDangling: true, PruneRegistry: false,
538+ })
539+
540+ require.Error(t, err)
541+ assert.Contains(t, err.Error(), "runtime prune failed")
542+ assert.Contains(t, err.Error(), "docker daemon unavailable")
543+}
544+
532 func TestService_Prune_RegistryOnlySkipsRuntime(t *testing.T) {545 func TestService_Prune_RegistryOnlySkipsRuntime(t *testing.T) {
533 manifestStorage := newFakeManifestStorage()546 manifestStorage := newFakeManifestStorage()
534 manifestStorage.repositories = []string{"gordon/api"}547 manifestStorage.repositories = []string{"gordon/api"}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close