Files
grm/scripts/prune_runner_images.py
emil cae4e2a860
Post-merge / detect-type (push) Successful in 1m3s
Post-merge / release (push) Successful in 1m8s
Post-merge / validate-commit-msg (push) Successful in 1m9s
Post-merge / vikunja (push) Successful in 1m13s
Post-merge / badges (push) Successful in 1m19s
Post-merge / sync-wiki (push) Successful in 1m44s
Post-merge / publish (push) Successful in 1m3s
Post-merge / configure-repo (push) Successful in 1m18s
GRM-107: refactor: use devx Makefile aliases, bump devx>=0.23.0
2026-06-27 20:24:29 +00:00

106 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Prune stale runner images from a rootless Docker daemon.
Usage:
python3 prune_runner_images.py [--dry-run]
Removes all images matching the runner-images pattern from the local
Docker daemon so the runner pulls a fresh :latest on the next job.
Environment variables:
DOCKER_HOST — Docker daemon socket (set by caller)
XDG_RUNTIME_DIR — Runtime directory (set by caller)
"""
from __future__ import annotations
import argparse
import re
import subprocess # nosec B404
import sys
from collections.abc import Sequence
#: Pattern for images we want to prune (repository:tag format).
IMAGE_PATTERN = re.compile(r"runner-images/(ci-base|ci-quality|ci-full)")
def list_docker_images() -> list[str]:
"""List all images in the local Docker daemon as repository:tag strings.
Returns:
List of ``repository:tag`` strings (excluding ``<none>`` entries).
"""
result = subprocess.run( # nosec B603
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
capture_output=True,
text=True,
check=True,
)
return [line.strip() for line in result.stdout.splitlines() if line.strip() and "<none>" not in line]
def filter_runner_images(images: Sequence[str]) -> list[str]:
"""Filter image list to only runner-images entries.
Args:
images: List of ``repository:tag`` strings.
Returns:
Subset matching the runner-images pattern.
"""
return [img for img in images if IMAGE_PATTERN.search(img)]
def remove_images(images: Sequence[str], dry_run: bool = False) -> list[str]:
"""Remove the given images from the local Docker daemon.
Args:
images: List of ``repository:tag`` strings to remove.
dry_run: If True, print what would be removed but don't execute.
Returns:
List of images that were removed (or would be removed in dry-run).
"""
removed: list[str] = []
for img in images:
if dry_run:
print(f"[dry-run] would remove: {img}")
removed.append(img)
continue
result = subprocess.run( # nosec B603
["docker", "rmi", "-f", img],
capture_output=True,
text=True,
)
if result.returncode == 0:
print(f"removed: {img}")
removed.append(img)
else:
print(f"failed to remove {img}: {result.stderr.strip()}", file=sys.stderr)
return removed
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Prune stale runner images.")
parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be removed without executing.",
)
args = parser.parse_args(argv)
all_images = list_docker_images()
runner_images = filter_runner_images(all_images)
if not runner_images:
print("no runner images found to prune")
return 0
removed = remove_images(runner_images, dry_run=args.dry_run)
print(f"pruned {len(removed)} image(s)")
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())