Remove old images by tag, and test the step after a deploy (closes #216)
Check / check (pull_request) Skipped

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
This commit is contained in:
2026-09-23 10:09:47 +00:00
parent 8c61b5ae77
commit 4c6d3f464d
4 changed files with 170 additions and 77 deletions
+31 -7
View File
@@ -537,12 +537,13 @@ func (c *Client) RemoveImage(ctx context.Context, imageID ImageID) error {
return nil
}
// ListImageIDs returns the IDs of all images tagged in the given repository,
// such as "upaas-myapp".
func (c *Client) ListImageIDs(
// ListImageTags returns the tags in the given repository, such as
// "upaas-myapp:12" in "upaas-myapp", each with the ID of its image.
// Tags the same image has in other repositories are left out.
func (c *Client) ListImageTags(
ctx context.Context,
repository string,
) ([]ImageID, error) {
) (map[string]ImageID, error) {
if c.docker == nil {
return nil, ErrNotConnected
}
@@ -554,12 +555,35 @@ func (c *Client) ListImageIDs(
return nil, fmt.Errorf("failed to list images: %w", err)
}
ids := make([]ImageID, 0, len(images))
tags := make(map[string]ImageID)
for _, img := range images {
ids = append(ids, ImageID(img.ID))
for _, tag := range img.RepoTags {
if strings.HasPrefix(tag, repository+":") {
tags[tag] = ImageID(img.ID)
}
}
}
return ids, nil
return tags, nil
}
// RemoveImageTag removes a tag such as "upaas-myapp:12", without force.
// Docker then deletes the image, and the untagged images it was built on,
// only if no other tag and no container still uses it.
func (c *Client) RemoveImageTag(ctx context.Context, tag string) error {
if c.docker == nil {
return ErrNotConnected
}
_, err := c.docker.ImageRemove(ctx, tag, image.RemoveOptions{
PruneChildren: true,
})
if err != nil && !client.IsErrNotFound(err) {
return fmt.Errorf("failed to remove image tag %s: %w", tag, err)
}
return nil
}
func (c *Client) performBuild(