1212 retention_days :
1313 description : Delete non-SHA-only image versions older than this many days
1414 required : false
15- default : ' '
15+ default : ' 45 '
1616 delete_sha_only_tags :
1717 description : Delete image versions that only have SHA tags
1818 required : false
@@ -175,25 +175,20 @@ jobs:
175175 return JSON.stringify(deletedVersionIds);
176176
177177 - name : Delete container versions that only have SHA tags
178+ id : cleanup_sha_only
178179 if : ${{ env.DELETE_SHA_ONLY_TAGS == 'true' }}
179180 uses : actions/github-script@v8
180181 env :
181182 PACKAGE_NAME : ${{ env.PACKAGE_NAME }}
182- IMAGES_TO_KEEP : ${{ env.IMAGES_TO_KEEP }}
183183 DRY_RUN : ${{ env.DRY_RUN }}
184184 DELETED_VERSION_IDS : ${{ steps.cleanup_by_age.outputs.result }}
185185 with :
186186 script : |
187187 const owner = context.repo.owner;
188188 const packageType = 'container';
189189 const packageName = process.env.PACKAGE_NAME;
190- const imagesToKeep = Math.max(1, Number(process.env.IMAGES_TO_KEEP || '4'));
191190 const dryRun = process.env.DRY_RUN === 'true';
192191
193- if (!Number.isFinite(imagesToKeep)) {
194- throw new Error(`IMAGES_TO_KEEP must be a number, received: ${process.env.IMAGES_TO_KEEP}`);
195- }
196-
197192 const paginateVersions = async () => {
198193 try {
199194 return {
@@ -267,7 +262,7 @@ jobs:
267262 return;
268263 }
269264
270- for (const [index, version] of remainingVersions.entries() ) {
265+ for (const version of remainingVersions) {
271266 const tags = version.metadata?.container?.tags ?? [];
272267 const isShaOnly = tags.length > 0 && tags.every(isShaTag);
273268 if (!isShaOnly) {
@@ -279,5 +274,65 @@ jobs:
279274 if (!dryRun) {
280275 await deleteVersion(scope, version.id);
281276 }
282- retainedCount -= 1;
283277 }
278+
279+ const shaOnlyDeleted = remainingVersions.filter(v => {
280+ const tags = v.metadata?.container?.tags ?? [];
281+ return tags.length > 0 && tags.every(isShaTag);
282+ });
283+
284+ return JSON.stringify({
285+ totalBefore: sortedVersions.length,
286+ ageBasedDeleted: JSON.parse(process.env.DELETED_VERSION_IDS || '[]').length,
287+ shaOnlyDeleted: shaOnlyDeleted.length,
288+ remaining: sortedVersions.length - (dryRun ? JSON.parse(process.env.DELETED_VERSION_IDS || '[]').length + shaOnlyDeleted.length : 0),
289+ });
290+
291+ - name : Generate cleanup report
292+ if : always()
293+ env :
294+ AGE_BASED_RESULT : ${{ steps.cleanup_by_age.outputs.result }}
295+ SHA_ONLY_RESULT : ${{ steps.cleanup_sha_only.outputs.result }}
296+ DELETE_SHA_ONLY_TAGS : ${{ env.DELETE_SHA_ONLY_TAGS }}
297+ DRY_RUN : ${{ env.DRY_RUN }}
298+ IMAGES_TO_KEEP : ${{ env.IMAGES_TO_KEEP }}
299+ RETENTION_DAYS : ${{ env.RETENTION_DAYS }}
300+ run : |
301+ echo "## 🧹 GHCR Cleanup Report" >> $GITHUB_STEP_SUMMARY
302+ echo "" >> $GITHUB_STEP_SUMMARY
303+
304+ if [ "$DRY_RUN" = "true" ]; then
305+ echo "" >> $GITHUB_STEP_SUMMARY
306+ echo "> ⚠️ **Dry run mode** - no images were actually deleted" >> $GITHUB_STEP_SUMMARY
307+ echo "" >> $GITHUB_STEP_SUMMARY
308+ fi
309+ echo "" >> $GITHUB_STEP_SUMMARY
310+
311+ echo "### Configuration" >> $GITHUB_STEP_SUMMARY
312+ echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY
313+ echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY
314+ echo "| Images to keep | ${IMAGES_TO_KEEP} |" >> $GITHUB_STEP_SUMMARY
315+ echo "| Retention days | ${RETENTION_DAYS} |" >> $GITHUB_STEP_SUMMARY
316+ echo "| Delete SHA-only tags | ${DELETE_SHA_ONLY_TAGS} |" >> $GITHUB_STEP_SUMMARY
317+ echo "" >> $GITHUB_STEP_SUMMARY
318+
319+ if [ -n "$SHA_ONLY_RESULT" ] && [ "$SHA_ONLY_RESULT" != "null" ]; then
320+ AGE_DELETED=$(echo "$AGE_BASED_RESULT" | jq -r 'if type == "array" then length elif type == "object" then .ageBasedDeleted // 0 else 0 end')
321+ SHA_DELETED=$(echo "$SHA_ONLY_RESULT" | jq -r '.shaOnlyDeleted // 0')
322+ TOTAL_BEFORE=$(echo "$SHA_ONLY_RESULT" | jq -r '.totalBefore // 0')
323+ REMAINING=$(echo "$SHA_ONLY_RESULT" | jq -r '.remaining // 0')
324+
325+ echo "### Summary" >> $GITHUB_STEP_SUMMARY
326+ echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY
327+ echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
328+ echo "| Total versions before | ${TOTAL_BEFORE} |" >> $GITHUB_STEP_SUMMARY
329+ echo "| Age-based deletions | ${AGE_DELETED} |" >> $GITHUB_STEP_SUMMARY
330+ echo "| SHA-only deletions | ${SHA_DELETED} |" >> $GITHUB_STEP_SUMMARY
331+ echo "| **Remaining versions** | **${REMAINING}** |" >> $GITHUB_STEP_SUMMARY
332+ elif [ -n "$AGE_BASED_RESULT" ] && [ "$AGE_BASED_RESULT" != "null" ]; then
333+ AGE_DELETED=$(echo "$AGE_BASED_RESULT" | jq -r 'if type == "array" then length else 0 end')
334+ echo "### Summary" >> $GITHUB_STEP_SUMMARY
335+ echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY
336+ echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
337+ echo "| Age-based deletions | ${AGE_DELETED} |" >> $GITHUB_STEP_SUMMARY
338+ fi
0 commit comments