cleanup-images.yml.disabled•14.8 kB
name: Cleanup Old Docker Images
on:
# Run after successful releases
workflow_run:
workflows: ["Main CI/CD Pipeline", "Docker Publish (Tags)", "Publish and Test (Tags)"]
types:
- completed
# Run weekly on Sunday at 2 AM UTC
schedule:
- cron: '0 2 * * 0'
# Allow manual trigger with options
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (no deletions)'
required: false
type: boolean
default: true
keep_versions:
description: 'Number of versions to keep'
required: false
type: string
default: '5'
delete_untagged:
description: 'Delete untagged images'
required: false
type: boolean
default: true
env:
DOCKER_IMAGE: doobidoo/mcp-memory-service
GHCR_IMAGE: ghcr.io/doobidoo/mcp-memory-service
jobs:
# Cleanup GitHub Container Registry images
cleanup-ghcr:
runs-on: ubuntu-latest
name: Cleanup GHCR Images
permissions:
packages: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Delete untagged images from GHCR
if: github.event.inputs.delete_untagged != 'false'
uses: actions/delete-package-versions@v4
continue-on-error: true
with:
package-name: 'mcp-memory-service'
package-type: 'container'
token: ${{ secrets.GITHUB_TOKEN }}
min-versions-to-keep: 0
delete-only-untagged-versions: 'true'
- name: Clean old GHCR versions (keep last N)
uses: actions/delete-package-versions@v4
continue-on-error: true
with:
package-name: 'mcp-memory-service'
package-type: 'container'
token: ${{ secrets.GITHUB_TOKEN }}
min-versions-to-keep: ${{ github.event.inputs.keep_versions || '5' }}
ignore-versions: '^(latest|slim|main|v[0-9]+\.[0-9]+\.[0-9]+)$'
delete-only-pre-release-versions: 'false'
- name: Clean buildcache tags older than 7 days
uses: snok/container-retention-policy@v1
continue-on-error: true
with:
image-names: mcp-memory-service
cut-off: 7 days ago UTC
account-type: personal
token: ${{ secrets.GITHUB_TOKEN }}
filter-tags: 'buildcache-*'
filter-include-untagged: false
skip-tags: latest,slim,main
dry-run: ${{ github.event.inputs.dry_run || 'false' }}
# Cleanup Docker Hub images
cleanup-dockerhub:
runs-on: ubuntu-latest
name: Cleanup Docker Hub Images
if: github.event_name != 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
cache-dependency-path: '**/pyproject.toml'
- name: Install dependencies
run: |
pip install requests python-dateutil
- name: Validate Docker Hub credentials
run: |
if [ -z "${{ secrets.DOCKER_USERNAME }}" ] || [ -z "${{ secrets.DOCKER_PASSWORD }}" ]; then
echo "⚠️ Docker Hub credentials not available, skipping Docker Hub cleanup"
echo "SKIP_DOCKER_CLEANUP=true" >> $GITHUB_ENV
else
echo "✓ Docker Hub credentials available"
echo "SKIP_DOCKER_CLEANUP=false" >> $GITHUB_ENV
fi
- name: Create cleanup script
run: |
cat > cleanup_docker_hub.py << 'EOF'
#!/usr/bin/env python3
"""
Docker Hub image cleanup script
Deletes old image tags based on retention policy
"""
import os
import sys
import json
import requests
from datetime import datetime, timedelta, timezone
from dateutil import parser
import re
class DockerHubCleaner:
def __init__(self, username, password, repository):
self.username = username
self.password = password
self.repository = repository
self.token = None
self.api_base = "https://hub.docker.com/v2"
def authenticate(self):
"""Get authentication token from Docker Hub"""
url = f"{self.api_base}/users/login/"
data = {"username": self.username, "password": self.password}
try:
response = requests.post(url, json=data)
response.raise_for_status()
self.token = response.json()["token"]
print("✓ Authenticated with Docker Hub")
return True
except Exception as e:
print(f"✗ Authentication failed: {e}")
return False
def get_tags(self):
"""Get all tags for the repository"""
url = f"{self.api_base}/repositories/{self.repository}/tags"
headers = {"Authorization": f"Bearer {self.token}"}
tags = []
while url:
try:
response = requests.get(url, headers=headers, params={"page_size": 100})
response.raise_for_status()
data = response.json()
tags.extend(data["results"])
url = data.get("next")
except Exception as e:
print(f"✗ Failed to get tags: {e}")
break
print(f"✓ Found {len(tags)} tags in repository")
return tags
def should_keep_tag(self, tag_name, tag_date, keep_versions, cutoff_date):
"""Determine if a tag should be kept based on retention policy"""
# Always keep these tags
protected_tags = ["latest", "slim", "main", "stable"]
if tag_name in protected_tags:
return True, "Protected tag"
# Keep semantic version tags (v1.2.3)
if re.match(r'^v?\d+\.\d+\.\d+$', tag_name):
# Check if it's one of the most recent versions
return True, "Semantic version"
# Keep major.minor tags (1.0, 2.1)
if re.match(r'^v?\d+\.\d+$', tag_name):
return True, "Major.minor version"
# Delete buildcache tags older than cutoff
if tag_name.startswith("buildcache-"):
if tag_date < cutoff_date:
return False, "Old buildcache tag"
return True, "Recent buildcache tag"
# Delete sha/digest tags older than cutoff
if tag_name.startswith("sha256-") or len(tag_name) == 7:
if tag_date < cutoff_date:
return False, "Old sha/digest tag"
return True, "Recent sha/digest tag"
# Delete test/dev tags older than cutoff
if any(x in tag_name.lower() for x in ["test", "dev", "tmp", "temp"]):
if tag_date < cutoff_date:
return False, "Old test/dev tag"
return True, "Recent test/dev tag"
# Keep if recent
if tag_date >= cutoff_date:
return True, "Recent tag"
return False, "Old tag"
def delete_tag(self, tag_name, dry_run=False):
"""Delete a specific tag from Docker Hub"""
if dry_run:
print(f" [DRY RUN] Would delete: {tag_name}")
return True
url = f"{self.api_base}/repositories/{self.repository}/tags/{tag_name}/"
headers = {"Authorization": f"Bearer {self.token}"}
try:
response = requests.delete(url, headers=headers)
response.raise_for_status()
print(f" ✓ Deleted: {tag_name}")
return True
except Exception as e:
print(f" ✗ Failed to delete {tag_name}: {e}")
return False
def cleanup(self, keep_versions=5, days_to_keep=30, dry_run=False):
"""Main cleanup function"""
if not self.authenticate():
return False
tags = self.get_tags()
if not tags:
return False
# Sort tags by date (newest first)
tags.sort(key=lambda x: parser.parse(x["last_updated"]), reverse=True)
# Calculate cutoff date
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days_to_keep)
# Separate semantic versions from other tags
version_tags = []
other_tags = []
for tag in tags:
if re.match(r'^v?\d+\.\d+\.\d+$', tag["name"]):
version_tags.append(tag)
else:
other_tags.append(tag)
print(f"\n📊 Repository Statistics:")
print(f" - Total tags: {len(tags)}")
print(f" - Version tags: {len(version_tags)}")
print(f" - Other tags: {len(other_tags)}")
print(f" - Keep versions: {keep_versions}")
print(f" - Days to keep: {days_to_keep}")
print(f" - Dry run: {dry_run}")
deleted_count = 0
kept_count = 0
print(f"\n🔍 Analyzing tags...")
# Process version tags (keep only the most recent N)
for i, tag in enumerate(version_tags):
tag_date = parser.parse(tag["last_updated"])
if i < keep_versions:
print(f" ✓ Keep {tag['name']} (recent version #{i+1})")
kept_count += 1
else:
print(f" ✗ Delete {tag['name']} (old version #{i+1})")
if self.delete_tag(tag["name"], dry_run):
deleted_count += 1
# Process other tags based on policy
for tag in other_tags:
tag_date = parser.parse(tag["last_updated"])
should_keep, reason = self.should_keep_tag(
tag["name"], tag_date, keep_versions, cutoff_date
)
if should_keep:
print(f" ✓ Keep {tag['name']} ({reason})")
kept_count += 1
else:
print(f" ✗ Delete {tag['name']} ({reason})")
if self.delete_tag(tag["name"], dry_run):
deleted_count += 1
print(f"\n📈 Summary:")
print(f" - Kept: {kept_count} tags")
print(f" - Deleted: {deleted_count} tags")
return True
def main():
# Get environment variables
username = os.environ.get("DOCKER_USERNAME")
password = os.environ.get("DOCKER_PASSWORD")
repository = os.environ.get("DOCKER_REPOSITORY", "doobidoo/mcp-memory-service")
# Get parameters
dry_run = os.environ.get("DRY_RUN", "false").lower() == "true"
keep_versions = int(os.environ.get("KEEP_VERSIONS", "5"))
days_to_keep = int(os.environ.get("DAYS_TO_KEEP", "30"))
if not username or not password:
print("✗ Missing DOCKER_USERNAME or DOCKER_PASSWORD")
sys.exit(1)
print(f"🐳 Docker Hub Cleanup for {repository}")
print("=" * 50)
cleaner = DockerHubCleaner(username, password, repository)
success = cleaner.cleanup(keep_versions, days_to_keep, dry_run)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
EOF
- name: Run Docker Hub cleanup
if: env.SKIP_DOCKER_CLEANUP != 'true'
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_REPOSITORY: doobidoo/mcp-memory-service
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
KEEP_VERSIONS: ${{ github.event.inputs.keep_versions || '5' }}
DAYS_TO_KEEP: '30'
run: python cleanup_docker_hub.py
- name: Docker Hub cleanup skipped
if: env.SKIP_DOCKER_CLEANUP == 'true'
run: echo "⚠️ Docker Hub cleanup was skipped due to missing credentials"
# Report cleanup results
report:
needs: [cleanup-ghcr, cleanup-dockerhub]
runs-on: ubuntu-latest
if: always()
name: Cleanup Report
steps:
- name: Generate cleanup report
run: |
echo "## 🧹 Docker Image Cleanup Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Run Type**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Dry Run**: ${{ github.event.inputs.dry_run || 'false' }}" >> $GITHUB_STEP_SUMMARY
echo "**Keep Versions**: ${{ github.event.inputs.keep_versions || '5' }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "- GHCR Cleanup: ${{ needs.cleanup-ghcr.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Docker Hub Cleanup: ${{ needs.cleanup-dockerhub.result }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Retention Policy" >> $GITHUB_STEP_SUMMARY
echo "- **Protected tags**: latest, slim, main, stable" >> $GITHUB_STEP_SUMMARY
echo "- **Version tags**: Keep last ${{ github.event.inputs.keep_versions || '5' }} versions" >> $GITHUB_STEP_SUMMARY
echo "- **Build cache**: Delete after 7 days" >> $GITHUB_STEP_SUMMARY
echo "- **Test/dev tags**: Delete after 30 days" >> $GITHUB_STEP_SUMMARY
echo "- **Untagged images**: Delete immediately" >> $GITHUB_STEP_SUMMARY