feat: purge scripts for CI/CD artifacts
This commit is contained in:
91
scripts/ci/purge-action-runs.sh
Executable file
91
scripts/ci/purge-action-runs.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Defaults (override via CLI flags)
|
||||||
|
GITEA_BASE_URL="https://git.motovaultpro.com"
|
||||||
|
TOKEN=""
|
||||||
|
OWNER="egullickson"
|
||||||
|
REPO="motovaultpro"
|
||||||
|
PER_PAGE=50
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage:
|
||||||
|
$0 --token=YOUR_TOKEN [--base-url=https://git.motovaultpro.com] [--owner=egullickson] [--repo=motovaultpro] [--per-page=50]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Delete all Actions runs in egullickson/motovaultpro
|
||||||
|
$0 --token=XXXX
|
||||||
|
|
||||||
|
# Different repo
|
||||||
|
$0 --token=XXXX --owner=someone --repo=otherrepo
|
||||||
|
|
||||||
|
# Different server + page size
|
||||||
|
$0 --token=XXXX --base-url=https://git.example.com --per-page=100
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- CLI parsing ---
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--token=*) TOKEN="${arg#*=}" ;;
|
||||||
|
--base-url=*) GITEA_BASE_URL="${arg#*=}" ;;
|
||||||
|
--owner=*) OWNER="${arg#*=}" ;;
|
||||||
|
--repo=*) REPO="${arg#*=}" ;;
|
||||||
|
--per-page=*) PER_PAGE="${arg#*=}" ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown argument: $arg"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
echo "ERROR: --token= is required"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Basic validation
|
||||||
|
if ! [[ "$PER_PAGE" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "ERROR: --per-page must be a number"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
api() {
|
||||||
|
curl -fsS \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
page=1
|
||||||
|
deleted=0
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
echo "Fetching page ${page}..."
|
||||||
|
json="$(api "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${REPO}/actions/runs?limit=${PER_PAGE}&page=${page}")"
|
||||||
|
|
||||||
|
# Try both common shapes:
|
||||||
|
# - {"workflow_runs":[...]}
|
||||||
|
# - {"runs":[...]}
|
||||||
|
ids="$(echo "$json" | jq -r '(.workflow_runs // .runs // []) | .[].id')"
|
||||||
|
|
||||||
|
if [[ -z "${ids}" ]]; then
|
||||||
|
echo "No more runs found."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
while read -r id; do
|
||||||
|
[[ -z "$id" ]] && continue
|
||||||
|
echo "Deleting run id=${id}"
|
||||||
|
api -X DELETE "${GITEA_BASE_URL}/api/v1/repos/${OWNER}/${REPO}/actions/runs/${id}" >/dev/null
|
||||||
|
deleted=$((deleted + 1))
|
||||||
|
done <<< "${ids}"
|
||||||
|
|
||||||
|
page=$((page + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done. Deleted ${deleted} runs."
|
||||||
191
scripts/ci/purge-container-images.sh
Executable file
191
scripts/ci/purge-container-images.sh
Executable file
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Defaults (override via CLI flags)
|
||||||
|
# =============================================================================
|
||||||
|
REGISTRY="https://git.motovaultpro.com"
|
||||||
|
USERNAME="egullickson"
|
||||||
|
TOKEN=""
|
||||||
|
KEEP_TAG="latest"
|
||||||
|
DRY_RUN=1
|
||||||
|
|
||||||
|
REPOS=(
|
||||||
|
"egullickson/frontend"
|
||||||
|
"egullickson/backend"
|
||||||
|
)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
need() { command -v "$1" >/dev/null || { echo "ERROR: missing dependency: $1"; exit 1; }; }
|
||||||
|
need curl
|
||||||
|
need jq
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage:
|
||||||
|
$0 --token=YOUR_PAT [--user=egullickson] [--registry=https://git.motovaultpro.com]
|
||||||
|
[--keep-tag=latest] [--dry-run|--no-dry-run]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Preview
|
||||||
|
$0 --token=XXXX --dry-run
|
||||||
|
|
||||||
|
# Actually delete
|
||||||
|
$0 --token=XXXX --no-dry-run
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Deletes all tags except KEEP_TAG for repos:
|
||||||
|
${REPOS[*]}
|
||||||
|
- Protects KEEP_TAG by digest (won't delete anything pointing to KEEP_TAG digest).
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- CLI parsing -------------------------------------------------------------
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--token=*) TOKEN="${arg#*=}" ;;
|
||||||
|
--user=*) USERNAME="${arg#*=}" ;;
|
||||||
|
--registry=*) REGISTRY="${arg#*=}" ;;
|
||||||
|
--keep-tag=*) KEEP_TAG="${arg#*=}" ;;
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
--no-dry-run) DRY_RUN=0 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown argument: $arg"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
echo "ERROR: --token= is required"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ACCEPT_MANIFEST="application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json"
|
||||||
|
|
||||||
|
get_bearer_token() {
|
||||||
|
local repo="$1"
|
||||||
|
local scope="repository:${repo}:pull,delete"
|
||||||
|
curl -fsS -u "${USERNAME}:${TOKEN}" \
|
||||||
|
"${REGISTRY}/v2/token?service=container_registry&scope=${scope}" \
|
||||||
|
| jq -r '.token // .access_token // empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header_for_repo() {
|
||||||
|
local repo="$1"
|
||||||
|
local t
|
||||||
|
t="$(get_bearer_token "$repo")"
|
||||||
|
if [[ -z "$t" ]]; then
|
||||||
|
echo "ERROR: Could not obtain bearer token for ${repo} (check PAT permissions)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Authorization: Bearer ${t}"
|
||||||
|
}
|
||||||
|
|
||||||
|
tags_list() {
|
||||||
|
local repo="$1" auth="$2"
|
||||||
|
curl -fsS -H "$auth" "${REGISTRY}/v2/${repo}/tags/list"
|
||||||
|
}
|
||||||
|
|
||||||
|
digest_for_tag() {
|
||||||
|
local repo="$1" tag="$2" auth="$3"
|
||||||
|
curl -fsSI \
|
||||||
|
-H "$auth" \
|
||||||
|
-H "Accept: ${ACCEPT_MANIFEST}" \
|
||||||
|
"${REGISTRY}/v2/${repo}/manifests/${tag}" \
|
||||||
|
| tr -d '\r' \
|
||||||
|
| awk 'BEGIN{IGNORECASE=1} /^docker-content-digest:/ {print $2; exit}'
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_digest() {
|
||||||
|
local repo="$1" digest="$2" auth="$3"
|
||||||
|
if [[ "$DRY_RUN" == "1" ]]; then
|
||||||
|
echo "DRY_RUN: DELETE ${REGISTRY}/v2/${repo}/manifests/${digest}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsS -X DELETE -H "$auth" \
|
||||||
|
"${REGISTRY}/v2/${repo}/manifests/${digest}" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
purge_repo_keep_tag() {
|
||||||
|
local repo="$1"
|
||||||
|
echo "== Purging ${repo} (keep ${KEEP_TAG}) =="
|
||||||
|
|
||||||
|
local auth
|
||||||
|
auth="$(auth_header_for_repo "$repo")"
|
||||||
|
|
||||||
|
local json tags
|
||||||
|
json="$(tags_list "$repo" "$auth")"
|
||||||
|
tags="$(echo "$json" | jq -r '.tags[]?' | sort || true)"
|
||||||
|
|
||||||
|
if [[ -z "$tags" ]]; then
|
||||||
|
echo "No tags found."
|
||||||
|
echo
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! echo "$tags" | grep -qx "${KEEP_TAG}"; then
|
||||||
|
echo "WARN: No '${KEEP_TAG}' tag found; refusing to purge ${repo}."
|
||||||
|
echo "$tags" | sed 's/^/ - /'
|
||||||
|
echo
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local keep_digest
|
||||||
|
keep_digest="$(digest_for_tag "$repo" "$KEEP_TAG" "$auth" || true)"
|
||||||
|
if [[ -z "$keep_digest" ]]; then
|
||||||
|
echo "ERROR: Could not resolve digest for ${repo}:${KEEP_TAG}"
|
||||||
|
echo "Tip: curl -vI -H \"$auth\" ${REGISTRY}/v2/${repo}/manifests/${KEEP_TAG}"
|
||||||
|
echo
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "${KEEP_TAG} digest: ${keep_digest}"
|
||||||
|
|
||||||
|
# Collect digests referenced by non-KEEP_TAG tags (skipping those equal to keep_digest)
|
||||||
|
local digests=()
|
||||||
|
while read -r tag; do
|
||||||
|
[[ -z "$tag" || "$tag" == "$KEEP_TAG" ]] && continue
|
||||||
|
|
||||||
|
local d
|
||||||
|
d="$(digest_for_tag "$repo" "$tag" "$auth" || true)"
|
||||||
|
if [[ -z "$d" ]]; then
|
||||||
|
echo "WARN: couldn't resolve digest for tag=${tag} (skipping)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$d" == "$keep_digest" ]]; then
|
||||||
|
echo "SKIP: tag=${tag} points to ${KEEP_TAG} digest (deleting would remove ${KEEP_TAG} too)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
digests+=("$d")
|
||||||
|
done <<< "$tags"
|
||||||
|
|
||||||
|
if [[ "${#digests[@]}" -eq 0 ]]; then
|
||||||
|
echo "Nothing to delete."
|
||||||
|
echo
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Will delete (deduped) manifest(s) (and all tags pointing to them)."
|
||||||
|
printf '%s\n' "${digests[@]}" | sort -u | while read -r digest; do
|
||||||
|
[[ -z "$digest" ]] && continue
|
||||||
|
echo "Deleting digest: $digest"
|
||||||
|
if ! delete_digest "$repo" "$digest" "$auth"; then
|
||||||
|
echo "WARN: delete failed for $digest"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
for repo in "${REPOS[@]}"; do
|
||||||
|
purge_repo_keep_tag "$repo"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Complete."
|
||||||
Reference in New Issue
Block a user