Skip to content

Commit ac720f9

Browse files
committed
fix: implement GHCR and Docker Hub prune scripts with summary reporting
1 parent 1913e9d commit ac720f9

4 files changed

Lines changed: 363 additions & 209 deletions

File tree

.github/workflows/container-prune.yml

Lines changed: 147 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ on:
66
- cron: '0 3 * * 0' # Weekly: Sundays at 03:00 UTC
77
workflow_dispatch:
88
inputs:
9-
registries:
10-
description: 'Comma-separated registries to prune (ghcr,dockerhub)'
11-
required: false
12-
default: 'ghcr,dockerhub'
139
keep_days:
1410
description: 'Number of days to retain images (unprotected)'
1511
required: false
@@ -28,55 +24,117 @@ permissions:
2824
contents: read
2925

3026
jobs:
31-
prune:
27+
prune-ghcr:
3228
runs-on: ubuntu-latest
3329
env:
3430
OWNER: ${{ github.repository_owner }}
3531
IMAGE_NAME: charon
36-
REGISTRIES: ${{ github.event.inputs.registries || 'ghcr,dockerhub' }}
3732
KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }}
3833
KEEP_LAST_N: ${{ github.event.inputs.keep_last_n || '30' }}
39-
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
34+
DRY_RUN: ${{ github.event_name == 'pull_request' && 'true' || github.event.inputs.dry_run || 'false' }}
4035
PROTECTED_REGEX: '["^v?[0-9]+\\.[0-9]+\\.[0-9]+$","^latest$","^main$","^develop$"]'
36+
PRUNE_UNTAGGED: 'true'
37+
PRUNE_SBOM_TAGS: 'true'
4138
steps:
4239
- name: Checkout
4340
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
4441

4542
- name: Install tools
4643
run: |
47-
sudo apt-get update && sudo apt-get install -y jq curl gh
44+
sudo apt-get update && sudo apt-get install -y jq curl
45+
46+
- name: Run GHCR prune
47+
env:
48+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
run: |
50+
chmod +x scripts/prune-ghcr.sh
51+
./scripts/prune-ghcr.sh 2>&1 | tee prune-ghcr-${{ github.run_id }}.log
52+
53+
- name: Summarize GHCR results
54+
if: always()
55+
run: |
56+
set -euo pipefail
57+
SUMMARY_FILE=prune-summary-ghcr.env
58+
LOG_FILE=prune-ghcr-${{ github.run_id }}.log
59+
60+
human() {
61+
local bytes=${1:-0}
62+
if [ -z "$bytes" ] || [ "$bytes" -eq 0 ]; then
63+
echo "0 B"
64+
return
65+
fi
66+
awk -v b="$bytes" 'BEGIN { split("B KiB MiB GiB TiB",u," "); i=0; x=b; while(x>1024){x/=1024;i++} printf "%0.2f %s", x, u[i+1] }'
67+
}
68+
69+
if [ -f "$SUMMARY_FILE" ]; then
70+
TOTAL_CANDIDATES=$(grep -E '^TOTAL_CANDIDATES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0)
71+
TOTAL_CANDIDATES_BYTES=$(grep -E '^TOTAL_CANDIDATES_BYTES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0)
72+
TOTAL_DELETED=$(grep -E '^TOTAL_DELETED=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0)
73+
TOTAL_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0)
74+
75+
{
76+
echo "## GHCR prune summary"
77+
echo "- candidates: ${TOTAL_CANDIDATES} (≈ $(human "${TOTAL_CANDIDATES_BYTES}"))"
78+
echo "- deleted: ${TOTAL_DELETED} (≈ $(human "${TOTAL_DELETED_BYTES}"))"
79+
} >> "$GITHUB_STEP_SUMMARY"
80+
else
81+
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)
82+
deleted_count=$(grep -cE 'deleting |DRY RUN: would delete' "$LOG_FILE" || true)
83+
84+
{
85+
echo "## GHCR prune summary"
86+
echo "- deleted (approx): ${deleted_count} (≈ $(human "${deleted_bytes}"))"
87+
} >> "$GITHUB_STEP_SUMMARY"
88+
fi
89+
90+
- name: Upload GHCR prune artifacts
91+
if: always()
92+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
93+
with:
94+
name: prune-ghcr-log-${{ github.run_id }}
95+
path: |
96+
prune-ghcr-${{ github.run_id }}.log
97+
prune-summary-ghcr.env
4898
49-
- name: Show prune script being executed
99+
prune-dockerhub:
100+
runs-on: ubuntu-latest
101+
env:
102+
OWNER: ${{ github.repository_owner }}
103+
IMAGE_NAME: charon
104+
KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }}
105+
KEEP_LAST_N: ${{ github.event.inputs.keep_last_n || '30' }}
106+
DRY_RUN: ${{ github.event_name == 'pull_request' && 'true' || github.event.inputs.dry_run || 'false' }}
107+
PROTECTED_REGEX: '["^v?[0-9]+\\.[0-9]+\\.[0-9]+$","^latest$","^main$","^develop$"]'
108+
steps:
109+
- name: Checkout
110+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
111+
112+
- name: Install tools
50113
run: |
51-
echo "===== SCRIPT PATH ====="
52-
pwd
53-
ls -la scripts
54-
echo "===== FIRST 20 LINES ====="
55-
head -n 20 scripts/prune-container-images.sh
114+
sudo apt-get update && sudo apt-get install -y jq curl
56115
57-
- name: Run container prune
116+
- name: Run Docker Hub prune
58117
env:
59-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60118
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
61119
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
62120
run: |
63-
chmod +x scripts/prune-container-images.sh
64-
./scripts/prune-container-images.sh 2>&1 | tee prune-${{ github.run_id }}.log
121+
chmod +x scripts/prune-dockerhub.sh
122+
./scripts/prune-dockerhub.sh 2>&1 | tee prune-dockerhub-${{ github.run_id }}.log
65123
66-
- name: Summarize prune results (space reclaimed)
67-
if: ${{ always() }}
124+
- name: Summarize Docker Hub results
125+
if: always()
68126
run: |
69127
set -euo pipefail
70-
SUMMARY_FILE=prune-summary.env
71-
LOG_FILE=prune-${{ github.run_id }}.log
128+
SUMMARY_FILE=prune-summary-dockerhub.env
129+
LOG_FILE=prune-dockerhub-${{ github.run_id }}.log
72130
73131
human() {
74132
local bytes=${1:-0}
75133
if [ -z "$bytes" ] || [ "$bytes" -eq 0 ]; then
76134
echo "0 B"
77135
return
78136
fi
79-
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)}'
137+
awk -v b="$bytes" 'BEGIN { split("B KiB MiB GiB TiB",u," "); i=0; x=b; while(x>1024){x/=1024;i++} printf "%0.2f %s", x, u[i+1] }'
80138
}
81139
82140
if [ -f "$SUMMARY_FILE" ]; then
@@ -86,34 +144,84 @@ jobs:
86144
TOTAL_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' "$SUMMARY_FILE" | cut -d= -f2 || echo 0)
87145
88146
{
89-
echo "## Container prune summary"
147+
echo "## Docker Hub prune summary"
90148
echo "- candidates: ${TOTAL_CANDIDATES} (≈ $(human "${TOTAL_CANDIDATES_BYTES}"))"
91149
echo "- deleted: ${TOTAL_DELETED} (≈ $(human "${TOTAL_DELETED_BYTES}"))"
92150
} >> "$GITHUB_STEP_SUMMARY"
93-
94-
printf 'PRUNE_SUMMARY: candidates=%s candidates_bytes=%s deleted=%s deleted_bytes=%s\n' \
95-
"${TOTAL_CANDIDATES}" "${TOTAL_CANDIDATES_BYTES}" "${TOTAL_DELETED}" "${TOTAL_DELETED_BYTES}"
96-
echo "Deleted approximately: $(human "${TOTAL_DELETED_BYTES}")"
97-
echo "space_saved=$(human "${TOTAL_DELETED_BYTES}")" >> "$GITHUB_OUTPUT"
98151
else
99152
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)
100153
deleted_count=$(grep -cE 'deleting |DRY RUN: would delete' "$LOG_FILE" || true)
101154
102155
{
103-
echo "## Container prune summary"
156+
echo "## Docker Hub prune summary"
104157
echo "- deleted (approx): ${deleted_count} (≈ $(human "${deleted_bytes}"))"
105158
} >> "$GITHUB_STEP_SUMMARY"
106-
107-
printf 'PRUNE_SUMMARY: deleted_approx=%s deleted_bytes=%s\n' "${deleted_count}" "${deleted_bytes}"
108-
echo "Deleted approximately: $(human "${deleted_bytes}")"
109-
echo "space_saved=$(human "${deleted_bytes}")" >> "$GITHUB_OUTPUT"
110159
fi
111160
112-
- name: Upload prune artifacts
113-
if: ${{ always() }}
161+
- name: Upload Docker Hub prune artifacts
162+
if: always()
114163
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
115164
with:
116-
name: prune-log-${{ github.run_id }}
165+
name: prune-dockerhub-log-${{ github.run_id }}
117166
path: |
118-
prune-${{ github.run_id }}.log
119-
prune-summary.env
167+
prune-dockerhub-${{ github.run_id }}.log
168+
prune-summary-dockerhub.env
169+
170+
summarize:
171+
runs-on: ubuntu-latest
172+
needs: [prune-ghcr, prune-dockerhub]
173+
if: always()
174+
steps:
175+
- name: Download all artifacts
176+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
177+
with:
178+
pattern: prune-*-log-${{ github.run_id }}
179+
merge-multiple: true
180+
181+
- name: Combined summary
182+
run: |
183+
set -euo pipefail
184+
185+
human() {
186+
local bytes=${1:-0}
187+
if [ -z "$bytes" ] || [ "$bytes" -eq 0 ]; then
188+
echo "0 B"
189+
return
190+
fi
191+
awk -v b="$bytes" 'BEGIN { split("B KiB MiB GiB TiB",u," "); i=0; x=b; while(x>1024){x/=1024;i++} printf "%0.2f %s", x, u[i+1] }'
192+
}
193+
194+
GHCR_CANDIDATES=0 GHCR_CANDIDATES_BYTES=0 GHCR_DELETED=0 GHCR_DELETED_BYTES=0
195+
if [ -f prune-summary-ghcr.env ]; then
196+
GHCR_CANDIDATES=$(grep -E '^TOTAL_CANDIDATES=' prune-summary-ghcr.env | cut -d= -f2 || echo 0)
197+
GHCR_CANDIDATES_BYTES=$(grep -E '^TOTAL_CANDIDATES_BYTES=' prune-summary-ghcr.env | cut -d= -f2 || echo 0)
198+
GHCR_DELETED=$(grep -E '^TOTAL_DELETED=' prune-summary-ghcr.env | cut -d= -f2 || echo 0)
199+
GHCR_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' prune-summary-ghcr.env | cut -d= -f2 || echo 0)
200+
fi
201+
202+
HUB_CANDIDATES=0 HUB_CANDIDATES_BYTES=0 HUB_DELETED=0 HUB_DELETED_BYTES=0
203+
if [ -f prune-summary-dockerhub.env ]; then
204+
HUB_CANDIDATES=$(grep -E '^TOTAL_CANDIDATES=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0)
205+
HUB_CANDIDATES_BYTES=$(grep -E '^TOTAL_CANDIDATES_BYTES=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0)
206+
HUB_DELETED=$(grep -E '^TOTAL_DELETED=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0)
207+
HUB_DELETED_BYTES=$(grep -E '^TOTAL_DELETED_BYTES=' prune-summary-dockerhub.env | cut -d= -f2 || echo 0)
208+
fi
209+
210+
TOTAL_CANDIDATES=$((GHCR_CANDIDATES + HUB_CANDIDATES))
211+
TOTAL_CANDIDATES_BYTES=$((GHCR_CANDIDATES_BYTES + HUB_CANDIDATES_BYTES))
212+
TOTAL_DELETED=$((GHCR_DELETED + HUB_DELETED))
213+
TOTAL_DELETED_BYTES=$((GHCR_DELETED_BYTES + HUB_DELETED_BYTES))
214+
215+
{
216+
echo "## Combined container prune summary"
217+
echo ""
218+
echo "| Registry | Candidates | Deleted | Space Reclaimed |"
219+
echo "|----------|------------|---------|-----------------|"
220+
echo "| GHCR | ${GHCR_CANDIDATES} | ${GHCR_DELETED} | $(human "${GHCR_DELETED_BYTES}") |"
221+
echo "| Docker Hub | ${HUB_CANDIDATES} | ${HUB_DELETED} | $(human "${HUB_DELETED_BYTES}") |"
222+
echo "| **Total** | **${TOTAL_CANDIDATES}** | **${TOTAL_DELETED}** | **$(human "${TOTAL_DELETED_BYTES}")** |"
223+
} >> "$GITHUB_STEP_SUMMARY"
224+
225+
printf 'PRUNE_SUMMARY: candidates=%s candidates_bytes=%s deleted=%s deleted_bytes=%s\n' \
226+
"${TOTAL_CANDIDATES}" "${TOTAL_CANDIDATES_BYTES}" "${TOTAL_DELETED}" "${TOTAL_DELETED_BYTES}"
227+
echo "Total space reclaimed: $(human "${TOTAL_DELETED_BYTES}")"

docs/implementation/WORKFLOW_REVIEW_2026-01-26.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ A new scheduled workflow and helper script were added to safely prune old contai
159159

160160
- **Files added**:
161161
- `.github/workflows/container-prune.yml` (weekly schedule, manual dispatch)
162-
- `scripts/prune-container-images.sh` (dry-run by default; supports GHCR and Docker Hub)
162+
- `scripts/prune-ghcr.sh` (GHCR cleanup)
163+
- `scripts/prune-dockerhub.sh` (Docker Hub cleanup)
163164

164165
- **Behavior**:
165166
- Default: **dry-run=true** (no destructive changes).

0 commit comments

Comments
 (0)