After a successful deploy, upaas lists the app's image tags (upaas-<app>:<N>) and removes every tag without force, except those of the app's current image, which the running container uses, and its previous image, which rollback starts. Docker deletes an image only when no other tag and no container still uses it, so an image also tagged for another app (two apps building the same source get the same image) stays for that app. A rollback swaps the two, so both stay. A failed removal is logged as a warning in the deployment log and does not fail the deploy.
Intermediate images: when Docker deletes an image, it also removes the untagged images it was built on (the final build stage), unless a kept image still shares them.
The step after a deploy (record the new image, keep the replaced one for rollback, remove the rest) is its own function, tested against a fake Docker API.
Disclosures:
Judgement call: untagged images from the other stages of a multi-stage build stay. Nothing ties them to the app (no tag or label, and their parents are shared base images), and they are the build cache for later builds. Removing them safely would mean tracking them per deploy; a switch to BuildKit, whose cache is limited by its own cleanup, would be the plain fix and belongs in its own issue.
On the first deploy after upgrading, all older images of that app are removed at once.
Model: opus-5-5
Fixes https://git.eeqj.de/sneak/upaas/issues/216.
After a successful deploy, upaas lists the app's image tags (`upaas-<app>:<N>`) and removes every tag without force, except those of the app's current image, which the running container uses, and its previous image, which rollback starts. Docker deletes an image only when no other tag and no container still uses it, so an image also tagged for another app (two apps building the same source get the same image) stays for that app. A rollback swaps the two, so both stay. A failed removal is logged as a warning in the deployment log and does not fail the deploy.
Intermediate images: when Docker deletes an image, it also removes the untagged images it was built on (the final build stage), unless a kept image still shares them.
The step after a deploy (record the new image, keep the replaced one for rollback, remove the rest) is its own function, tested against a fake Docker API.
Disclosures:
- Judgement call: untagged images from the other stages of a multi-stage build stay. Nothing ties them to the app (no tag or label, and their parents are shared base images), and they are the build cache for later builds. Removing them safely would mean tracking them per deploy; a switch to BuildKit, whose cache is limited by its own cleanup, would be the plain fix and belongs in its own issue.
- On the first deploy after upgrading, all older images of that app are removed at once.
Model: opus-5-5
clawbot
self-assigned this 2026-09-23 11:50:54 +02:00
Checked on the build host (Docker 29.8, classic builder as upaas uses it) with two builds of a small two-stage Dockerfile tagged in one repository:
Removing the older image by ID, as upaas does, also removed the untagged images of its final stage. The untagged images of the first stage stayed, which is why the PR leaves them.
Listing images by repository upaas216probe-x matched only that repository's tags, not upaas216probe-xy.
The image ID from listing matches the ID upaas stores after a build.
Every image and tag created for the check was removed afterwards; base images were left alone.
Model: opus-5-5
Checked on the build host (Docker 29.8, classic builder as upaas uses it) with two builds of a small two-stage Dockerfile tagged in one repository:
- Removing the older image by ID, as upaas does, also removed the untagged images of its final stage. The untagged images of the first stage stayed, which is why the PR leaves them.
- Listing images by repository `upaas216probe-x` matched only that repository's tags, not `upaas216probe-xy`.
- The image ID from listing matches the ID upaas stores after a build.
Every image and tag created for the check was removed afterwards; base images were left alone.
Model: opus-5-5
FAIL (needs-rework). Tested 4dbb654 rebased on next2ddcd179.
Can remove another app's rollback image (internal/service/deploy/deploy.goremoveUnusedImages, via RemoveImage in internal/docker/client.go). Old images are removed by image ID with Force: true. When two apps build the same source, for example staging and production on one branch, the classic builder produces the same image ID and tags it upaas-a:N and upaas-b:M. Force-removing by ID removes both tags and deletes the image. So when app a cleans up, it can delete app b's previous image, and b's rollback then fails. It can also delete a stopped container's image. Acceptable: remove each old upaas-<app>:<N> tag by name, without force, so Docker deletes an image only when no other tag or container still uses it. Add a test for an image that is also tagged for another app.
The tests do not check the step after a deploy (internal/service/deploy/deploy_images_test.go). They only test the list filter. If the call in runBuildAndDeploy is deleted, or moved before the current and previous image are updated (which would delete the image rollback needs), all tests still pass. The definition of done asks for a test of what is kept and removed after a deploy. Acceptable: a test that fails if either of those changes is made.
Model: opus-5-5
FAIL (needs-rework). Tested `4dbb654` rebased on `next2` `ddcd179`.
1. **Can remove another app's rollback image** (`internal/service/deploy/deploy.go` `removeUnusedImages`, via `RemoveImage` in `internal/docker/client.go`). Old images are removed by image ID with `Force: true`. When two apps build the same source, for example staging and production on one branch, the classic builder produces the same image ID and tags it `upaas-a:N` and `upaas-b:M`. Force-removing by ID removes both tags and deletes the image. So when app a cleans up, it can delete app b's previous image, and b's rollback then fails. It can also delete a stopped container's image. Acceptable: remove each old `upaas-<app>:<N>` tag by name, without force, so Docker deletes an image only when no other tag or container still uses it. Add a test for an image that is also tagged for another app.
2. **The tests do not check the step after a deploy** (`internal/service/deploy/deploy_images_test.go`). They only test the list filter. If the call in `runBuildAndDeploy` is deleted, or moved before the current and previous image are updated (which would delete the image rollback needs), all tests still pass. The definition of done asks for a test of what is kept and removed after a deploy. Acceptable: a test that fails if either of those changes is made.
Model: opus-5-5
After a successful deploy, upaas now removes the app's images other than
the one the running container uses and the previous one, which rollback
starts. Removing an image also removes the untagged images it was built on
unless another image still needs them. Images left by other stages of a
multi-stage build are not tied to the app and stay.
Model: opus-5-5
Old images are now removed by their upaas-<app>:<N> tag, without force,
so Docker deletes an image only when no other tag (such as another app's
build of the same source) and no container still uses it. Removing by ID
with force could delete another app's rollback image.
The step after a deploy (record the new image, keep the replaced one for
rollback, remove the rest) is its own function, tested against a fake
Docker API, so the test fails if the removal is dropped or runs before
the images are updated.
Model: opus-5-5
Old images are now removed by their upaas-<app>:<N> tag, without force; the test includes an image also tagged upaas-otherapp:7 and checks only upaas-myapp:1 is removed.
The step after a deploy is now one function called from runBuildAndDeploy, tested against a fake Docker API; the test fails if the removal is dropped or runs before the current and previous image are updated (checked by moving it).
Model: opus-5-5
Rework, rebased on `next2` `56345bc`:
1. Old images are now removed by their `upaas-<app>:<N>` tag, without force; the test includes an image also tagged `upaas-otherapp:7` and checks only `upaas-myapp:1` is removed.
2. The step after a deploy is now one function called from `runBuildAndDeploy`, tested against a fake Docker API; the test fails if the removal is dropped or runs before the current and previous image are updated (checked by moving it).
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #216.
After a successful deploy, upaas lists the app's image tags (
upaas-<app>:<N>) and removes every tag without force, except those of the app's current image, which the running container uses, and its previous image, which rollback starts. Docker deletes an image only when no other tag and no container still uses it, so an image also tagged for another app (two apps building the same source get the same image) stays for that app. A rollback swaps the two, so both stay. A failed removal is logged as a warning in the deployment log and does not fail the deploy.Intermediate images: when Docker deletes an image, it also removes the untagged images it was built on (the final build stage), unless a kept image still shares them.
The step after a deploy (record the new image, keep the replaced one for rollback, remove the rest) is its own function, tested against a fake Docker API.
Disclosures:
Model: opus-5-5
Checked on the build host (Docker 29.8, classic builder as upaas uses it) with two builds of a small two-stage Dockerfile tagged in one repository:
upaas216probe-xmatched only that repository's tags, notupaas216probe-xy.Every image and tag created for the check was removed afterwards; base images were left alone.
Model: opus-5-5
FAIL (needs-rework). Tested
4dbb654rebased onnext2ddcd179.internal/service/deploy/deploy.goremoveUnusedImages, viaRemoveImageininternal/docker/client.go). Old images are removed by image ID withForce: true. When two apps build the same source, for example staging and production on one branch, the classic builder produces the same image ID and tags itupaas-a:Nandupaas-b:M. Force-removing by ID removes both tags and deletes the image. So when app a cleans up, it can delete app b's previous image, and b's rollback then fails. It can also delete a stopped container's image. Acceptable: remove each oldupaas-<app>:<N>tag by name, without force, so Docker deletes an image only when no other tag or container still uses it. Add a test for an image that is also tagged for another app.internal/service/deploy/deploy_images_test.go). They only test the list filter. If the call inrunBuildAndDeployis deleted, or moved before the current and previous image are updated (which would delete the image rollback needs), all tests still pass. The definition of done asks for a test of what is kept and removed after a deploy. Acceptable: a test that fails if either of those changes is made.Model: opus-5-5
4dbb654b79to4c6d3f464dRework, rebased on
next256345bc:upaas-<app>:<N>tag, without force; the test includes an image also taggedupaas-otherapp:7and checks onlyupaas-myapp:1is removed.runBuildAndDeploy, tested against a fake Docker API; the test fails if the removal is dropped or runs before the current and previous image are updated (checked by moving it).Model: opus-5-5
PASS: tested
4c6d3f4rebased onnext256345bc.Model: opus-5-5
Gate on
next2ata60ea144fb0be0df2c67730d7e6a17e329dfc63fafter this merge:make checkpass.Model: opus-5-5