Created
February 10, 2026 05:49
-
-
Save widnyana/8853cef9cdbdf3d8264f012ea53431c7 to your computer and use it in GitHub Desktop.
List GitHub container package versions (images) created in the last N days & delete it
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| List GitHub container package versions (images) created in the last N days. | |
| """ | |
| import argparse | |
| import os | |
| from datetime import datetime, timedelta, timezone | |
| from typing import Any, Dict, List | |
| import requests | |
| def get_package_versions(token: str, org: str, package_name: str, days: int | None = 5) -> List[Dict[str, Any]]: | |
| """ | |
| Fetch container package versions from GitHub Packages API. | |
| Args: | |
| token: GitHub personal access token | |
| org: Organization name | |
| package_name: Container package name | |
| days: Number of days to look back (None = no filter) | |
| Returns: | |
| List of package versions with tag, id, and age | |
| """ | |
| headers = { | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| "Authorization": f"Bearer {token}", | |
| } | |
| url = f"https://api.github.com/orgs/{org}/packages/container/{package_name}/versions" | |
| response = requests.get(url, headers=headers) | |
| response.raise_for_status() | |
| all_versions = response.json() | |
| cutoff_date = datetime.now(timezone.utc) - timedelta(days=days) if days else None | |
| filtered_versions = [] | |
| for version in all_versions: | |
| created_at = datetime.strptime(version["created_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) | |
| if cutoff_date is None or created_at >= cutoff_date: | |
| age = datetime.now(timezone.utc) - created_at | |
| # Get container tags (image tags) | |
| tags = version.get("metadata", {}).get("container", {}).get("tags", []) | |
| if isinstance(tags, list): | |
| tag_list = [t if isinstance(t, str) else str(t) for t in tags] | |
| tag_str = ", ".join(tag_list) if tag_list else "(no tags)" | |
| else: | |
| tag_str = "(no tags)" | |
| filtered_versions.append( | |
| { | |
| "id": version["id"], | |
| "tags": tag_str, | |
| "tag_list": tag_list if isinstance(tags, list) and tags else [], | |
| "created_at": version["created_at"], | |
| "age": age, | |
| } | |
| ) | |
| return filtered_versions | |
| def delete_package_version(token: str, org: str, package_name: str, version_id: int) -> bool: | |
| """ | |
| Delete a specific package version. | |
| Args: | |
| token: GitHub personal access token | |
| org: Organization name | |
| package_name: Container package name | |
| version_id: Version ID to delete | |
| Returns: | |
| True if deleted successfully | |
| """ | |
| headers = { | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| "Authorization": f"Bearer {token}", | |
| } | |
| url = f"https://api.github.com/orgs/{org}/packages/container/{package_name}/versions/{version_id}" | |
| response = requests.delete(url, headers=headers) | |
| response.raise_for_status() | |
| return response.status_code == 204 | |
| def format_age(timedelta_obj: timedelta) -> str: | |
| """Format timedelta into human-readable string.""" | |
| total_seconds = int(timedelta_obj.total_seconds()) | |
| days_count = total_seconds // 86400 | |
| hours = (total_seconds % 86400) // 3600 | |
| minutes = (total_seconds % 3600) // 60 | |
| if days_count > 0: | |
| return f"{days_count}d {hours}h" | |
| elif hours > 0: | |
| return f"{hours}h {minutes}m" | |
| else: | |
| return f"{minutes}m" | |
| def main(): | |
| parser = argparse.ArgumentParser(description="List GitHub container package versions") | |
| parser.add_argument("package", help="Container package name (e.g., appreal-backend)") | |
| parser.add_argument("--org", default="project-appreal", help="Organization name (default: project-appreal)") | |
| parser.add_argument("--all", action="store_true", help="Show all versions, no date filter") | |
| parser.add_argument("--days", type=int, default=5, help="Number of days to look back (default: 5)") | |
| parser.add_argument("--delete", action="store_true", help="Delete the versions found by the query") | |
| args = parser.parse_args() | |
| token = os.getenv("GITHUB_TOKEN", "") | |
| if not token: | |
| raise ValueError("GITHUB_TOKEN must be defined.") | |
| days = None if args.all else args.days | |
| try: | |
| versions = get_package_versions(token, args.org, args.package, days) | |
| if not versions: | |
| if args.all: | |
| print(f"No versions found for package '{args.package}'.") | |
| else: | |
| print(f"No versions found in the last {days} days for package '{args.package}'.") | |
| return | |
| if args.all: | |
| print(f"All versions of '{args.package}':") | |
| else: | |
| print(f"Versions of '{args.package}' updated in the last {days} days:") | |
| print("-" * 80) | |
| print(f"{'ID':<12} {'Tags':<30} {'Age':<12} {'Created At'}") | |
| print("-" * 80) | |
| for v in sorted(versions, key=lambda x: x["id"]): | |
| print(f"{v['id']:<12} {v['tags']:<30} {format_age(v['age']):<12} {v['created_at']}") | |
| # Delete mode | |
| if args.delete: | |
| print() | |
| confirm = input(f"Delete {len(versions)} version(s)? [y/N]: ") | |
| if confirm.lower() == "y": | |
| for v in sorted(versions, key=lambda x: x["id"]): | |
| try: | |
| print(f"Deleting version {v['id']} ({v['tags']})...", end=" ") | |
| if delete_package_version(token, args.org, args.package, v["id"]): | |
| print("OK") | |
| else: | |
| print("FAILED") | |
| except requests.exceptions.HTTPError as e: | |
| status = e.response.status_code | |
| reason = "" | |
| if status == 401: | |
| reason = " - Unauthorized: Invalid token or insufficient permissions" | |
| elif status == 403: | |
| reason = " - Forbidden: Token missing delete:packages scope or not an admin" | |
| elif status == 404: | |
| reason = " - Not found: Version may have been already deleted" | |
| print(f"FAILED - HTTP {status}{reason}") | |
| except Exception as e: | |
| print(f"FAILED - {e}") | |
| else: | |
| print("Aborted.") | |
| except requests.exceptions.HTTPError as e: | |
| print(f"HTTP Error: {e}") | |
| if e.response.status_code == 401: | |
| print("Invalid token. Please check your GitHub personal access token.") | |
| elif e.response.status_code == 404: | |
| print(f"Package '{args.package}' not found in organization '{args.org}'.") | |
| except Exception as e: | |
| print(f"Error: {e}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment