diff --git a/.gitea/workflows/registry-gc.yaml b/.gitea/workflows/registry-gc.yaml index 31640e8..66fadf2 100644 --- a/.gitea/workflows/registry-gc.yaml +++ b/.gitea/workflows/registry-gc.yaml @@ -23,7 +23,9 @@ # repo variable REGISTRY_GC_ENABLE=true — so you can eyeball a scheduled dry-run # first, then flip the variable to let it prune automatically. # -# Needs the existing PACKAGE_TOKEN secret (write:package scope, which the +# TOKENS (two, by capability — a package token can't read branches, hence the +# original 403): branch reads use the automatic Actions token (github.token, +# repo read); package list + delete use PACKAGE_TOKEN (write:package, which the # registry-push token already has). Only touches images THIS repo builds; the # fleet-kiosk image is owned by the kiosk-app submodule's own CI. @@ -40,6 +42,9 @@ on: type: boolean default: true +permissions: + contents: read # branch listing via the auto Actions token + concurrency: group: registry-gc cancel-in-progress: false @@ -53,7 +58,8 @@ jobs: SERVER: "https://git.bitasylum.org" OWNER: ${{ github.repository_owner }} REPO: ${{ github.repository }} - TOKEN: ${{ secrets.PACKAGE_TOKEN }} + REPO_TOKEN: ${{ github.token }} # auto Actions token: repo/branch reads + PKG_TOKEN: ${{ secrets.PACKAGE_TOKEN }} # package list + delete (write:package) EVENT: ${{ github.event_name }} DISPATCH_DRY: ${{ inputs.dry_run }} GC_ENABLE: ${{ vars.REGISTRY_GC_ENABLE }} @@ -69,7 +75,27 @@ jobs: command -v jq >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq jq; } API="${SERVER}/api/v1" - auth=(-H "Authorization: token ${TOKEN}" -H "Accept: application/json") + + # Authenticated GET. $1 = which token (repo|pkg), $2 = url. Echoes the + # body on 2xx; on anything else prints an actionable error and exits + # (so a scope/auth problem can never be mistaken for "no results"). + get() { + local who="$1" url="$2" tok tmp code body + case "$who" in repo) tok="$REPO_TOKEN";; pkg) tok="$PKG_TOKEN";; esac + tmp="$(mktemp)" + code="$(curl -sS --retry 3 -o "$tmp" -w '%{http_code}' \ + -H "Authorization: token ${tok}" -H "Accept: application/json" "$url" || echo 000)" + body="$(cat "$tmp")"; rm -f "$tmp" + case "$code" in + 2*) printf '%s' "$body"; return 0 ;; + 401|403) + echo "::error::GET ${url} -> HTTP ${code} (token '${who}' lacks scope). branches need repo read (github.token); packages need read/write:package (PACKAGE_TOKEN)." >&2 + exit 1 ;; + *) + echo "::error::GET ${url} -> HTTP ${code}" >&2 + exit 1 ;; + esac + } # ---- dry-run resolution ------------------------------------------ if [ "$EVENT" = "workflow_dispatch" ]; then @@ -91,7 +117,7 @@ jobs: live=" main " # main is always protected, even if listing is odd total=0 page=1 while :; do - resp="$(curl -fsSL --retry 3 "${auth[@]}" "${API}/repos/${REPO}/branches?limit=50&page=${page}")" + resp="$(get repo "${API}/repos/${REPO}/branches?limit=50&page=${page}")" cnt="$(printf '%s' "$resp" | jq 'length')" [ "$cnt" -eq 0 ] && break while IFS= read -r b; do @@ -112,7 +138,7 @@ jobs: for img in $IMAGES; do page=1 while :; do - resp="$(curl -fsSL --retry 3 "${auth[@]}" "${API}/packages/${OWNER}?type=container&q=${img}&limit=50&page=${page}")" + resp="$(get pkg "${API}/packages/${OWNER}?type=container&q=${img}&limit=50&page=${page}")" cnt="$(printf '%s' "$resp" | jq 'length')" [ "$cnt" -eq 0 ] && break while IFS= read -r ver; do @@ -143,7 +169,9 @@ jobs: orphans=$((orphans + 1)) echo "ORPHAN ${img}:${ver}" if [ "$DRY" != "true" ]; then - code="$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE "${auth[@]}" "${API}/packages/${OWNER}/container/${img}/${ver}" || echo 000)" + code="$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE \ + -H "Authorization: token ${PKG_TOKEN}" \ + "${API}/packages/${OWNER}/container/${img}/${ver}" || echo 000)" case "$code" in 2*) deleted=$((deleted + 1)); echo " deleted ${img}:${ver}" ;; *) failed=$((failed + 1)); echo "::warning::delete ${img}:${ver} returned HTTP ${code}" ;;