chore(ci): enable scheduled container pruning and report reclaimed space
- Make container prune run perform deletions by default (workflow_dispatch default now false for dry_run) - Enhance prune script to estimate candidate and deleted image sizes (Docker Hub best-effort; GHCR manifest fallback) - Emit machine-readable summary (`prune-summary.env`) and human-readable summary to the workflow run - Upload logs + summary as artifacts and expose `space_saved` in the run summary Why: - Previously the scheduled job used dry-run by default and only logged candidates; this change makes scheduled pruning effective and provides visibility into storage reclaimed. Impact: - Runs will now remove eligible images by default (use dry_run=true to test) - Size calculations are best-effort and may be incomplete if registry APIs do not expose sizes
This commit is contained in:
53
.github/workflows/container-prune.yml
vendored
53
.github/workflows/container-prune.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
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 || 'true' }}
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||
PROTECTED_REGEX: '["^v","^latest$","^main$","^develop$"]'
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y jq curl
|
||||
|
||||
- name: Run container prune (dry-run by default)
|
||||
- name: Run container prune
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -54,10 +54,57 @@ jobs:
|
||||
chmod +x scripts/prune-container-images.sh
|
||||
./scripts/prune-container-images.sh 2>&1 | tee prune-${{ github.run_id }}.log
|
||||
|
||||
- name: Upload 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
|
||||
|
||||
@@ -28,6 +28,12 @@ LOG_PREFIX="[prune]"
|
||||
now_ts=$(date +%s)
|
||||
cutoff_ts=$(date -d "$KEEP_DAYS days ago" +%s 2>/dev/null || date -d "-$KEEP_DAYS days" +%s)
|
||||
|
||||
# Totals
|
||||
TOTAL_CANDIDATES=0
|
||||
TOTAL_CANDIDATES_BYTES=0
|
||||
TOTAL_DELETED=0
|
||||
TOTAL_DELETED_BYTES=0
|
||||
|
||||
echo "$LOG_PREFIX starting with REGISTRIES=$REGISTRIES KEEP_DAYS=$KEEP_DAYS DRY_RUN=$DRY_RUN"
|
||||
|
||||
action_delete_ghcr() {
|
||||
@@ -92,12 +98,32 @@ action_delete_ghcr() {
|
||||
|
||||
echo "$LOG_PREFIX candidate: id=$id tags=$tags created=$created"
|
||||
|
||||
# Try to estimate size for GHCR by fetching manifest (best-effort)
|
||||
candidate_bytes=0
|
||||
for tag in $(echo "$tags" | sed 's/,/ /g'); do
|
||||
if [[ -n "$tag" && "$tag" != "null" ]]; then
|
||||
manifest_url="https://ghcr.io/v2/${OWNER}/${IMAGE_NAME}/manifests/${tag}"
|
||||
manifest=$(curl -sS -H "Accept: application/vnd.docker.distribution.manifest.v2+json" -H "Authorization: Bearer $GITHUB_TOKEN" "$manifest_url" || true)
|
||||
if [[ -n "$manifest" ]]; then
|
||||
bytes=$(echo "$manifest" | jq -r '.layers // [] | map(.size) | add // 0')
|
||||
if [[ "$bytes" != "null" && "$bytes" -gt 0 2>/dev/null ]]; then
|
||||
candidate_bytes=$((candidate_bytes + bytes))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES+1))
|
||||
TOTAL_CANDIDATES_BYTES=$((TOTAL_CANDIDATES_BYTES + candidate_bytes))
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "$LOG_PREFIX DRY RUN: would delete GHCR version id=$id"
|
||||
echo "$LOG_PREFIX DRY RUN: would delete GHCR version id=$id (approx ${candidate_bytes} bytes)"
|
||||
else
|
||||
echo "$LOG_PREFIX deleting GHCR version id=$id"
|
||||
echo "$LOG_PREFIX deleting GHCR version id=$id (approx ${candidate_bytes} bytes)"
|
||||
curl -sS -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/${namespace_type}/${OWNER}/packages/container/${IMAGE_NAME}/versions/$id"
|
||||
TOTAL_DELETED=$((TOTAL_DELETED+1))
|
||||
TOTAL_DELETED_BYTES=$((TOTAL_DELETED_BYTES + candidate_bytes))
|
||||
fi
|
||||
|
||||
done
|
||||
@@ -160,12 +186,25 @@ action_delete_dockerhub() {
|
||||
|
||||
echo "$LOG_PREFIX candidate: tag=$tag_name last_updated=$last_updated"
|
||||
|
||||
# Estimate size from Docker Hub tag JSON (images[].size or full_size)
|
||||
bytes=0
|
||||
bytes=$(echo "$tag" | jq -r '.images | map(.size) | add // empty') || true
|
||||
if [[ -z "$bytes" || "$bytes" == "null" ]]; then
|
||||
bytes=$(echo "$tag" | jq -r '.full_size // empty' 2>/dev/null || true)
|
||||
fi
|
||||
bytes=${bytes:-0}
|
||||
|
||||
TOTAL_CANDIDATES=$((TOTAL_CANDIDATES+1))
|
||||
TOTAL_CANDIDATES_BYTES=$((TOTAL_CANDIDATES_BYTES + bytes))
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "$LOG_PREFIX DRY RUN: would delete Docker Hub tag=$tag_name"
|
||||
echo "$LOG_PREFIX DRY RUN: would delete Docker Hub tag=$tag_name (approx ${bytes} bytes)"
|
||||
else
|
||||
echo "$LOG_PREFIX deleting Docker Hub tag=$tag_name"
|
||||
echo "$LOG_PREFIX deleting Docker Hub tag=$tag_name (approx ${bytes} bytes)"
|
||||
curl -sS -X DELETE -H "Authorization: JWT $hub_token" \
|
||||
"https://hub.docker.com/v2/repositories/${DOCKERHUB_USERNAME}/${IMAGE_NAME}/tags/${tag_name}/"
|
||||
TOTAL_DELETED=$((TOTAL_DELETED+1))
|
||||
TOTAL_DELETED_BYTES=$((TOTAL_DELETED_BYTES + bytes))
|
||||
fi
|
||||
|
||||
done
|
||||
@@ -190,4 +229,30 @@ for r in "${regs[@]}"; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Summary
|
||||
human_readable() {
|
||||
local bytes=$1
|
||||
if (( bytes == 0 )); then
|
||||
echo "0 B"
|
||||
return
|
||||
fi
|
||||
local unit=(B KiB MiB GiB TiB)
|
||||
local i=0
|
||||
local value=$bytes
|
||||
while (( value > 1024 )) && (( i < 4 )); do
|
||||
value=$((value / 1024))
|
||||
i=$((i + 1))
|
||||
done
|
||||
printf "%s %s" "${value}" "${unit[$i]}"
|
||||
}
|
||||
|
||||
echo "$LOG_PREFIX SUMMARY: total_candidates=${TOTAL_CANDIDATES} total_candidates_bytes=${TOTAL_CANDIDATES_BYTES} total_deleted=${TOTAL_DELETED} total_deleted_bytes=${TOTAL_DELETED_BYTES}"
|
||||
echo "$LOG_PREFIX SUMMARY_HUMAN: candidates=${TOTAL_CANDIDATES} candidates_size=$(human_readable ${TOTAL_CANDIDATES_BYTES}) deleted=${TOTAL_DELETED} deleted_size=$(human_readable ${TOTAL_DELETED_BYTES})"
|
||||
|
||||
# Export summary for workflow parsing
|
||||
echo "TOTAL_CANDIDATES=${TOTAL_CANDIDATES}" >> prune-summary.env
|
||||
echo "TOTAL_CANDIDATES_BYTES=${TOTAL_CANDIDATES_BYTES}" >> prune-summary.env
|
||||
echo "TOTAL_DELETED=${TOTAL_DELETED}" >> prune-summary.env
|
||||
echo "TOTAL_DELETED_BYTES=${TOTAL_DELETED_BYTES}" >> prune-summary.env
|
||||
|
||||
echo "$LOG_PREFIX done"
|
||||
|
||||
Reference in New Issue
Block a user