fix(runner): prepared-image pruning is a silent no-op; fix + LRU #43
fix/prepare-image-pruning
into main
Closes part of the prepare epic (fangorn/anvil#354 → fangorn/anvil-cli#41).
prune_prepared_images formatted with {{.CreatedAt}}, which Docker renders with spaces (2026-07-22 10:06:54 +0000 UTC). splitn(3, ' ') then fed docker rmi the string "10:06:54 +0000 UTC anvil-prepared:<hash>" — a ref with spaces — so every removal failed and was swallowed. Verified: the prune has never deleted anything, so prepared images grow unbounded until the disk fills (cf fangorn/anvil#334).
Fixes
- Format a single field so the tag is never split on a timestamp’s spaces.
- Evict by last use, not build time. One content-addressed image is shared across every repo/step with the same prepare block, so retention keys on use, not age or origin — otherwise a busy repo’s churn evicts a quiet repo’s daily-used image. Tracked with a marker file per tag under
~/.anvil-runner/prepared-images/, touched on every hit and build. - Prune every 6h, not only at boot.
Tests
Policy factored into a pure select_for_prune, unit-tested: LRU-within-count, coldest-first eviction, stale-age dropping, and the key case — an old-but-still-used image survives. 193 tests pass, clippy + fmt clean.