Просмотр исходного кода

Workflow: .github/workflows/cleanup-ghcr.yml runs Sundays at 03:00 UTC across both packages, with a manual workflow_dispatch and a dry_run toggle. It builds the live-digest
set the same way we just did, so it won't ever delete a digest still referenced by a tagged manifest.

maziggy 3 недель назад
Родитель
Сommit
3407c5f088
1 измененных файлов с 103 добавлено и 0 удалено
  1. 103 0
      .github/workflows/cleanup-ghcr.yml

+ 103 - 0
.github/workflows/cleanup-ghcr.yml

@@ -0,0 +1,103 @@
+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
+
+jobs:
+  cleanup:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        package: [bambuddy, bambuddy-beta]
+    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 ]