Files
sow-tools/scripts/prune-release-assets.sh
T
archvillainetteandClaude Opus 5 f3ca6be698
ci / ci (pull_request) Successful in 3m6s
feat(ci): prune release assets outside the newest two tags (#52)
Gitea has no release-asset retention, so every v* tag kept its full
cross-platform binary set forever — 31 tags had accumulated 1.8 GB on the
production host disk. Nothing consumes an old asset: CI takes the Crucible
binary from the Nix flake, and every deleted file is reproducible from its tag.

scripts/prune-release-assets.sh lists every release page, sorts by creation
date, skips the newest two published releases, and deletes the remaining
releases' attachments. Two rather than one so a release in flight never leaves
the previous download broken. Drafts never consume a keep slot. Releases, tags,
source archives and release notes are untouched.

Nothing is excluded — SHA256SUMS and the wrappers go with the binaries: the
checksums are only meaningful next to the files they cover, and the consumer
drift-check reads the wrappers from the latest release, which stays complete.

The step is best-effort: a stale asset is cheaper than a blocked publish, so
API failures warn and the step carries continue-on-error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:51:10 +02:00

68 lines
2.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Delete the binary assets of every release except the newest KEEP ones.
#
# Gitea has no asset retention, so 9 assets x ~57 MB per tag pile up forever on
# the host disk (sow-tools #52). Nothing consumes an old asset: CI takes the
# Crucible binary from the Nix flake, and every deleted file is reproducible
# from its tag. Tags, source archives, release notes and the releases
# themselves are never touched — only attachments.
#
# Nothing is excluded, SHA256SUMS and the wrappers included: the checksums are
# only meaningful next to the binaries they cover, and the drift-check reads
# the wrappers from the latest release, which always stays complete.
#
# Best-effort by design: a stale asset is cheaper than a blocked release, so
# every API failure is a warning, never a non-zero exit.
#
# Env:
# API repo API base, e.g. https://git.example/api/v1/repos/owner/repo [required]
# TOKEN Gitea token with releases:write [required]
# KEEP how many newest releases stay complete (default 2)
set -uo pipefail
: "${API:?set API}"
: "${TOKEN:?set TOKEN}"
keep="${KEEP:-2}"
auth="Authorization: token ${TOKEN}"
per_page=50
warn() { echo "prune-release-assets: $*" >&2; }
# Walk every page so an old backlog is cleared, not only the tag that fell out
# of the window on this run. Each page already embeds its releases' assets, so
# no follow-up request per release is needed.
releases='[]'
page=1
while :; do
body="$(curl -fsS -H "$auth" "${API}/releases?limit=${per_page}&page=${page}&draft=false")" || {
warn "listing releases failed on page ${page}; pruning what was listed so far"
break
}
count="$(jq 'length' <<<"$body")" || { warn "unreadable release page ${page}"; break; }
releases="$(jq -s 'add' <(printf '%s' "$releases") <(printf '%s' "$body"))"
# A short page is the last one; this also stops a server that ignores `page`.
(( count < per_page )) && break
page=$(( page + 1 ))
done
# Sort here rather than trusting the response order, and skip drafts in case
# the server ignored `draft=false` — a draft must not consume a keep slot and
# strip the binaries off the newest real release.
mapfile -t stale < <(jq -r --argjson keep "$keep" '
[.[] | select(.draft != true)]
| sort_by(.created_at) | reverse | .[$keep:]
| .[] | "\(.id) \(.assets // [] | map(.id) | join(","))"
' <<<"$releases")
(( ${#stale[@]} )) || { echo "prune-release-assets: nothing older than the newest ${keep} releases"; exit 0; }
for line in "${stale[@]}"; do
id="${line%% *}"
assets="${line#* }"
for asset in ${assets//,/ }; do
echo "deleting asset ${asset} of release ${id}"
curl -fsS -X DELETE -H "$auth" "${API}/releases/${id}/assets/${asset}" >/dev/null \
|| warn "deleting asset ${asset} of release ${id} failed"
done
done