mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
110 lines
4.3 KiB
YAML
110 lines
4.3 KiB
YAML
name: Cleanup GHCR untagged images
|
|
|
|
# Deletes untagged (orphan) container versions from GHCR while preserving
|
|
# any digest that is still referenced by a tagged multi-arch manifest list.
|
|
# Without that cross-reference, off-the-shelf cleanup actions can silently
|
|
# break multi-arch tags by deleting referenced platform manifests.
|
|
#
|
|
# Requires a repo secret `GHCR_CLEANUP_TOKEN` — a classic PAT with
|
|
# `read:packages` + `delete:packages` scope. GITHUB_TOKEN cannot delete
|
|
# versions of user-owned packages (only org-owned).
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 3 * * 0' # Sundays at 03:00 UTC
|
|
workflow_dispatch:
|
|
inputs:
|
|
dry_run:
|
|
description: 'List orphans without deleting'
|
|
type: boolean
|
|
default: false
|
|
|
|
# This workflow authenticates exclusively via GHCR_CLEANUP_TOKEN (a classic PAT)
|
|
# and never reads/writes via the default GITHUB_TOKEN. Strip every permission
|
|
# from the GITHUB_TOKEN so a stolen workflow run can't reach the repo at all
|
|
# — least privilege per CodeQL `actions/missing-workflow-permissions`.
|
|
permissions: {}
|
|
|
|
jobs:
|
|
cleanup:
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
package: [bambuddy]
|
|
steps:
|
|
- name: Cleanup ${{ matrix.package }}
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GHCR_CLEANUP_TOKEN }}
|
|
OWNER: ${{ github.repository_owner }}
|
|
PACKAGE: ${{ matrix.package }}
|
|
DRY_RUN: ${{ inputs.dry_run }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# 1. Fetch all versions for the package.
|
|
gh api "users/${OWNER}/packages/container/${PACKAGE}/versions?per_page=100" \
|
|
--paginate --slurp > all.json
|
|
total=$(jq 'add | length' all.json)
|
|
echo "Total versions: $total"
|
|
|
|
# 2. Build the live-digest set: digests referenced by any tagged
|
|
# multi-arch manifest list. Use the registry token (separate from
|
|
# GH_TOKEN) for ghcr.io manifest reads.
|
|
REG_TOKEN=$(curl -sS \
|
|
"https://ghcr.io/token?scope=repository:${OWNER}/${PACKAGE}:pull&service=ghcr.io" \
|
|
| jq -r .token)
|
|
|
|
jq -r 'add | map(select(.metadata.container.tags | length > 0))
|
|
| .[] | .metadata.container.tags[0]' all.json > tags.txt
|
|
|
|
: > live.txt
|
|
while IFS= read -r tag; do
|
|
curl -sS \
|
|
-H "Authorization: Bearer $REG_TOKEN" \
|
|
-H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" \
|
|
"https://ghcr.io/v2/${OWNER}/${PACKAGE}/manifests/${tag}" \
|
|
| jq -r '(.manifests // []) | .[].digest' >> live.txt 2>/dev/null || true
|
|
done < tags.txt
|
|
sort -u live.txt -o live.txt
|
|
echo "Live digests referenced by manifest lists: $(wc -l < live.txt)"
|
|
|
|
# 3. Untagged digests minus live = true orphans.
|
|
jq -r 'add | map(select(.metadata.container.tags | length == 0))
|
|
| .[] | .name' all.json | sort -u > untagged.txt
|
|
comm -23 untagged.txt live.txt > orphan_digests.txt
|
|
orphan_count=$(wc -l < orphan_digests.txt)
|
|
echo "Orphan digests safe to delete: $orphan_count"
|
|
|
|
if [ "$orphan_count" -eq 0 ]; then
|
|
echo "Nothing to delete."
|
|
exit 0
|
|
fi
|
|
|
|
# 4. Map orphan digests -> version IDs.
|
|
jq -r --slurpfile orphans <(jq -R . orphan_digests.txt) '
|
|
add
|
|
| map(select(.name as $n | ($orphans | flatten | index($n))))
|
|
| .[] | .id
|
|
' all.json > delete_ids.txt
|
|
echo "Version IDs queued for deletion: $(wc -l < delete_ids.txt)"
|
|
|
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
echo "Dry run — not deleting."
|
|
head -20 delete_ids.txt
|
|
exit 0
|
|
fi
|
|
|
|
# 5. Delete.
|
|
deleted=0
|
|
failed=0
|
|
while IFS= read -r id; do
|
|
if gh api -X DELETE "users/${OWNER}/packages/container/${PACKAGE}/versions/${id}" --silent; then
|
|
deleted=$((deleted + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
fi
|
|
done < delete_ids.txt
|
|
echo "Deleted: $deleted Failed: $failed"
|
|
[ "$failed" -eq 0 ]
|