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 ]
