mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The cleanup step ran silently, so a no-op run was indistinguishable from a broken one. Add a config banner, per-deletion lines, and a per-package plus grand-total summary with a kept-reason breakdown, and an explicit message when nothing matches. Add DRY_RUN support. Deletion rule: delete a version only when it carries a ci-* tag, has no semver (v*) or latest tag, and is older than MAX_AGE_DAYS. Released images and untagged multi-arch child layers are never touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
3.7 KiB
Bash
Executable File
146 lines
3.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)"
|
|
|
|
# shellcheck source=scripts/meta/env/load.sh
|
|
# shellcheck disable=SC1091
|
|
source "${REPO_ROOT}/scripts/meta/env/load.sh"
|
|
|
|
: "${GH_TOKEN:?Missing GH_TOKEN}"
|
|
: "${ORG:?Missing ORG}"
|
|
|
|
package_prefix="${PACKAGE_PREFIX:-core}"
|
|
max_age_days="${MAX_AGE_DAYS:-30}"
|
|
tag_prefix="${TAG_PREFIX:-ci-}"
|
|
dry_run="${DRY_RUN:-false}"
|
|
|
|
if [[ ! "${max_age_days}" =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: MAX_AGE_DAYS must be a non-negative integer." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${tag_prefix}" ]]; then
|
|
echo "ERROR: TAG_PREFIX must not be empty." >&2
|
|
exit 1
|
|
fi
|
|
|
|
for tool in gh jq; do
|
|
if ! command -v "${tool}" >/dev/null 2>&1; then
|
|
echo "ERROR: ${tool} not found." >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
encode_package_name() {
|
|
local package="${1}"
|
|
jq -rn --arg value "${package}" '$value | @uri'
|
|
}
|
|
|
|
resolve_packages() {
|
|
if [[ -n "${PACKAGES:-}" ]]; then
|
|
printf '%s\n' "${PACKAGES}"
|
|
return 0
|
|
fi
|
|
|
|
: "${INFINITO_DISTROS:?Missing INFINITO_DISTROS}"
|
|
|
|
for distro in ${INFINITO_DISTROS}; do
|
|
printf '%s/%s\n' "${package_prefix}" "${distro}"
|
|
done
|
|
}
|
|
|
|
classify_version() {
|
|
local version="${1}" cutoff_ts="${2}"
|
|
local tags updated_ts
|
|
|
|
tags="$(jq -r '.tags[]?' <<<"${version}")"
|
|
[[ -n "${tags}" ]] || {
|
|
echo "keep:untagged"
|
|
return 0
|
|
}
|
|
grep -Eq "^(latest|v[0-9].*)$" <<<"${tags}" && {
|
|
echo "keep:release-tag"
|
|
return 0
|
|
}
|
|
grep -q "^${tag_prefix}" <<<"${tags}" || {
|
|
echo "keep:no-ci-tag"
|
|
return 0
|
|
}
|
|
|
|
updated_ts="$(date -u -d "$(jq -r '.updated_at' <<<"${version}")" +%s)"
|
|
[[ "${updated_ts}" -lt "${cutoff_ts}" ]] || {
|
|
echo "keep:too-new"
|
|
return 0
|
|
}
|
|
|
|
echo "delete"
|
|
}
|
|
|
|
cutoff="$(date -u -d "${max_age_days} days ago" +%s)"
|
|
|
|
echo "GHCR CI image cleanup"
|
|
echo " org=${ORG} tag-prefix=${tag_prefix} max-age=${max_age_days}d dry-run=${dry_run}"
|
|
echo " cutoff=$(date -u -d "@${cutoff}" +%Y-%m-%dT%H:%M:%SZ) (delete ${tag_prefix}* versions without a semver/latest tag, older than this)"
|
|
|
|
total_scanned=0
|
|
total_deleted=0
|
|
declare -A kept_reasons=()
|
|
|
|
while IFS= read -r package; do
|
|
package="${package#"${package%%[![:space:]]*}"}"
|
|
package="${package%"${package##*[![:space:]]}"}"
|
|
[[ -n "${package}" ]] || continue
|
|
|
|
encoded_package="$(encode_package_name "${package}")"
|
|
|
|
pkg_scanned=0
|
|
pkg_deleted=0
|
|
|
|
echo "== ${package} =="
|
|
|
|
while IFS= read -r version; do
|
|
[[ -n "${version}" ]] || continue
|
|
pkg_scanned=$((pkg_scanned + 1))
|
|
|
|
decision="$(classify_version "${version}" "${cutoff}")"
|
|
|
|
if [[ "${decision}" != "delete" ]]; then
|
|
kept_reasons["${decision}"]=$((${kept_reasons["${decision}"]:-0} + 1))
|
|
continue
|
|
fi
|
|
|
|
id="$(jq -r '.id' <<<"${version}")"
|
|
updated_at="$(jq -r '.updated_at' <<<"${version}")"
|
|
tags_csv="$(jq -r '[.tags[]?] | join(",")' <<<"${version}")"
|
|
|
|
if [[ "${dry_run}" == "true" ]]; then
|
|
echo " would delete id=${id} tags=${tags_csv} updated=${updated_at}"
|
|
else
|
|
echo " deleting id=${id} tags=${tags_csv} updated=${updated_at}"
|
|
gh api \
|
|
--method DELETE \
|
|
"/orgs/${ORG}/packages/container/${encoded_package}/versions/${id}"
|
|
fi
|
|
pkg_deleted=$((pkg_deleted + 1))
|
|
done < <(gh api --paginate \
|
|
"/orgs/${ORG}/packages/container/${encoded_package}/versions" \
|
|
--jq '.[] | {id: .id, updated_at: .updated_at, tags: .metadata.container.tags}' |
|
|
jq -c '.')
|
|
|
|
echo " -> scanned ${pkg_scanned}, deleted ${pkg_deleted}"
|
|
total_scanned=$((total_scanned + pkg_scanned))
|
|
total_deleted=$((total_deleted + pkg_deleted))
|
|
done < <(resolve_packages)
|
|
|
|
echo "== Summary =="
|
|
echo " scanned ${total_scanned} versions, deleted ${total_deleted}"
|
|
for reason in "${!kept_reasons[@]}"; do
|
|
echo " kept ${kept_reasons[${reason}]} (${reason})"
|
|
done
|
|
|
|
if [[ "${total_deleted}" -eq 0 ]]; then
|
|
echo " no versions matched the deletion criteria"
|
|
fi
|