Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions __tests__/cleanup-integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import Docker from 'dockerode'
import {ImageService} from '../src/image-service'
import {integration, delay} from './helpers'
import {integration} from './helpers'
import {run, cleanupOldImageVersions} from '../src/cleanup'
import {PROXY_IMAGE_NAME, digestName} from '../src/docker-tags'

Expand Down Expand Up @@ -65,9 +65,6 @@ integration('cleanupOldImageVersions', () => {
expect(initialImages.length).toEqual(2)

await cleanupOldImageVersions(docker, currentImage)
// The Docker API seems to ack the removal before it is carried out, so let's wait briefly to ensure
// the verification query doesn't race the deletion
await delay(200)

const remainingImages = await docker.listImages(imageOptions)
expect(remainingImages.length).toEqual(1)
Expand All @@ -83,9 +80,6 @@ integration('cleanupOldImageVersions', () => {
expect(imageCount).toEqual(2)

await run()
// The Docker API seems to ack the removal before it is carried out, so let's wait briefly to ensure
// the verification query doesn't race the deletion
await delay(200)

const remainingImages = await docker.listImages(imageOptions)
expect(remainingImages.length).toEqual(2)
Expand Down
53 changes: 24 additions & 29 deletions dist/cleanup/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/cleanup/index.js.map

Large diffs are not rendered by default.

51 changes: 23 additions & 28 deletions src/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ export async function run(cutoff = '24h'): Promise<void> {
core.info(`Pruning containers older than ${cutoff}`)
await docker.pruneContainers({filters: untilFilter})
await Promise.all(
updaterImages().map(async image => {
return cleanupOldImageVersions(docker, image)
})
updaterImages().map(async image => cleanupOldImageVersions(docker, image))
)
await cleanupOldImageVersions(docker, PROXY_IMAGE_NAME)
} catch (error: unknown) {
Expand All @@ -49,34 +47,31 @@ export async function cleanupOldImageVersions(

core.info(`Cleaning up images for ${repo}`)

docker.listImages(options, async function (err, imageInfoList) {
if (imageInfoList && imageInfoList.length > 0) {
for (const imageInfo of imageInfoList) {
// The given imageName is expected to be a tag + digest, however to avoid any surprises in future
// we fail over to check for a match on just tags as well.
//
// This means we won't remove any image which matches an imageName of either of these notations:
// - dependabot/image:$TAG@sha256:$REF (current implementation)
// - dependabot/image:v1
//
// Without checking imageInfo.RepoTags for a match, we would actually remove the latter even if
// this was the active version.
if (imageMatches(imageInfo, imageName)) {
core.info(`Skipping current image ${imageInfo.Id}`)
continue
}
const imageInfoList = await docker.listImages(options)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot flagged this:

Now that we await docker.listImages(options) directly, a rejection here propagates up and rejects cleanupOldImageVersions. In run(), since the updater images are cleaned via Promise.all(...), a listImages failure for one repo will reject the whole batch and skip cleanup of the remaining images (and the subsequent proxy-image cleanup) — landing in the outer try/catch.

The old callback-based code silently ignored the err argument, so a listing failure for one repo didn't stop the others. To preserve that isolation, consider scoping the listing failure per repo, e.g.:

let imageInfoList: Docker.ImageInfo[]
try {
  imageInfoList = await docker.listImages(options)
} catch (error: unknown) {
  if (error instanceof Error) {
    core.info(`Unable to list images for ${repo} -- ${error.message}`)
  }
  return
}

That keeps one repo's listing failure from short-circuiting cleanup of the others, while still awaiting removals as intended. Alternatively, Promise.allSettled in run() would achieve similar isolation across images.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd spotted that error propagation change originally and saw it as a feature not a bug! Since it'll stop swallowing errors...

But I hadn't considered there might be a way to both report the errors and continue for non-error ones. I'll poke at this.

for (const imageInfo of imageInfoList) {
// The given imageName is expected to be a tag + digest, however to avoid any surprises in future
// we fail over to check for a match on just tags as well.
//
// This means we won't remove any image which matches an imageName of either of these notations:
// - dependabot/image:$TAG@sha256:$REF (current implementation)
// - dependabot/image:v1
//
// Without checking imageInfo.RepoTags for a match, we would actually remove the latter even if
// this was the active version.
if (imageMatches(imageInfo, imageName)) {
core.info(`Skipping current image ${imageInfo.Id}`)
continue
}

core.info(`Removing image ${imageInfo.Id}`)
try {
await docker.getImage(imageInfo.Id).remove()
} catch (error: unknown) {
if (error instanceof Error) {
core.info(`Unable to remove ${imageInfo.Id} -- ${error.message}`)
}
}
core.info(`Removing image ${imageInfo.Id}`)
try {
await docker.getImage(imageInfo.Id).remove()
} catch (error: unknown) {
if (error instanceof Error) {
core.info(`Unable to remove ${imageInfo.Id} -- ${error.message}`)
}
}
})
}
}

function imageMatches(imageInfo: Docker.ImageInfo, imageName: string): boolean {
Expand Down
Loading