name: Container Registry Prune on: schedule: - cron: '0 3 * * 0' # Weekly: Sundays at 03:00 UTC workflow_dispatch: inputs: registries: description: 'Comma-separated registries to prune (ghcr,dockerhub)' required: false default: 'ghcr,dockerhub' keep_days: description: 'Number of days to retain images (unprotected)' required: false default: '30' dry_run: description: 'If true, only logs candidates and does not delete (default: false for active cleanup)' required: false default: 'false' keep_last_n: description: 'Keep last N newest images (global)' required: false default: '30' permissions: packages: write contents: read jobs: prune: runs-on: ubuntu-latest env: OWNER: ${{ github.repository_owner }} IMAGE_NAME: charon REGISTRIES: ${{ github.event.inputs.registries || 'ghcr,dockerhub' }} KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }} KEEP_LAST_N: ${{ github.event.inputs.keep_last_n || '30' }} DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} PROTECTED_REGEX: '["^v","^latest$","^main$","^develop$"]' steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install tools run: | sudo apt-get update && sudo apt-get install -y jq curl - name: Run container prune env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} run: | chmod +x scripts/prune-container-images.sh ./scripts/prune-container-images.sh 2>&1 | tee prune-${{ github.run_id }}.log - name: Summarize prune results (space reclaimed) if: ${{ always() }} run: | set -euo pipefail SUMMARY_FILE=prune-summary.env LOG_FILE=prune-${{ github.run_id }}.log human() { local bytes=${1:-0} if [ -z "$bytes" ] || [ "$bytes" -eq 0 ]; then echo "0 B" return fi awk -v b="$bytes" 'function human(x){ split("B KiB MiB GiB TiB",u," "); i=0; while(x>1024){x/=1024;i++} printf "%0.2f %s", x, u[i+1]} END{human(b)}' } if [ -f "$SUMMARY_FILE" ]; then TOTAL_CANDIDATES=$(grep -E '^TOTAL_CANDIDATES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0) TOTAL_CANDIDATES_BYTES=$(grep -E '^TOTAL_CANDIDATES_BYTES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0) TOTAL_DELETED=$(grep -E '^TOTAL_DELETED=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0) TOTAL_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0) { echo "## Container prune summary" echo "- candidates: ${TOTAL_CANDIDATES} (≈ $(human "${TOTAL_CANDIDATES_BYTES}"))" echo "- deleted: ${TOTAL_DELETED} (≈ $(human "${TOTAL_DELETED_BYTES}"))" } >> "$GITHUB_STEP_SUMMARY" printf 'PRUNE_SUMMARY: candidates=%s candidates_bytes=%s deleted=%s deleted_bytes=%s\n' \ "${TOTAL_CANDIDATES}" "${TOTAL_CANDIDATES_BYTES}" "${TOTAL_DELETED}" "${TOTAL_DELETED_BYTES}" echo "Deleted approximately: $(human "${TOTAL_DELETED_BYTES}")" echo "space_saved=$(human "${TOTAL_DELETED_BYTES}")" >> "$GITHUB_OUTPUT" else deleted_bytes=$(grep -oE '\( *approx +[0-9]+ bytes\)' "$LOG_FILE" | sed -E 's/.*approx +([0-9]+) bytes.*/\1/' | awk '{s+=$1} END {print s+0}' || true) deleted_count=$(grep -cE 'deleting |DRY RUN: would delete' "$LOG_FILE" || true) { echo "## Container prune summary" echo "- deleted (approx): ${deleted_count} (≈ $(human "${deleted_bytes}"))" } >> "$GITHUB_STEP_SUMMARY" printf 'PRUNE_SUMMARY: deleted_approx=%s deleted_bytes=%s\n' "${deleted_count}" "${deleted_bytes}" echo "Deleted approximately: $(human "${deleted_bytes}")" echo "space_saved=$(human "${deleted_bytes}")" >> "$GITHUB_OUTPUT" fi - name: Upload prune artifacts if: ${{ always() }} uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: prune-log-${{ github.run_id }} path: | prune-${{ github.run_id }}.log prune-summary.env