v0.284.2 — the image clean-up sees digest-pulled (untagged) images (decision 53, R-736)
gates / gates (push) Successful in 26s

Found live on 9202: the product pins tag@digest, and such images are stored untagged (repo:<none>);
`docker image ls` without -a did not list them, so the retention saw almost no app image. Now `image ls -a`;
an anonymous <none>:<none> entry is never a candidate; the one-time marker is v2 so the corrected sweep runs
once everywhere. Test TestImageRetention_SeesUntaggedDigestPulledImages, red-proofed. 0.284.0/0.284.1 were
never floored.

MinAgent: 0.131.0 (unchanged).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-30 23:03:32 +02:00
parent 00fddd3816
commit a70c398dee
6 changed files with 53 additions and 5 deletions
@@ -22,8 +22,15 @@ type fakeImages struct {
func (f *fakeImages) run(args ...string) (string, error) {
switch {
case args[0] == "image" && args[1] == "ls":
all := false
for _, a := range args {
all = all || a == "-a"
}
var b strings.Builder
for _, im := range f.imgs {
if im.Tag == "<none>" && !all {
continue // measured on 9202: `docker image ls` without -a hides untagged (digest-pulled) images
}
fmt.Fprintf(&b, "%s\t%s\t%s\t%s\t%s\n", im.ID, im.Repo, im.Tag, im.Digest, im.Size)
}
return b.String(), nil
@@ -290,3 +297,24 @@ func TestImageRetention_ADoneUpdateRunsItWithThePrevious(t *testing.T) {
t.Fatal("the done update did not run the retention")
}
}
// The product pins tag@digest, so app images sit UNTAGGED (`repo:<none>`): the pass must see them.
// COMPANION RED-PROOF: drop "-a" from listLocalImages → "an untagged old image was not deleted".
func TestImageRetention_SeesUntaggedDigestPulledImages(t *testing.T) {
m := retentionManager(t)
f := baseImages()
f.imgs = append(f.imgs, localImage{ID: "sha256:W0", Repo: "acme/web", Tag: "<none>", Digest: "sha256:w0", Size: "100MB"},
localImage{ID: "sha256:ANON", Repo: "<none>", Tag: "<none>", Digest: "<none>", Size: "1MB"})
withFakeImages(t, f)
st, _ := m.GetStack("web")
if _, err := m.deleteUnkeptImages("test", appImageRepos(filepath.Dir(st.ComposePath), st.AppConfig), ""); err != nil {
t.Fatal(err)
}
got := strings.Join(f.rmi, ",")
if !strings.Contains(got, "sha256:W0") {
t.Fatalf("an untagged old image was not deleted: %s", got)
}
if strings.Contains(got, "sha256:ANON") {
t.Fatalf("an anonymous <none>:<none> entry was touched: %s", got)
}
}