Skip to content

Cleanup Pre-release Packages #27

Cleanup Pre-release Packages

Cleanup Pre-release Packages #27

name: Cleanup Pre-release Packages
# Unlist old pre-release versions from NuGet.org and delete from GitHub Packages.
# Runs weekly and can be triggered manually after a stable release.
#
# Logic:
# 1. Superseded pre-releases (base version <= latest stable): unlist/delete ALL
# 2. Current pre-releases (base version > latest stable): keep last 3 per channel (alpha/beta), unlist/delete the rest
#
# NuGet.org: unlists packages (still downloadable by exact version pin)
# GitHub Packages: permanently deletes old versions
on:
schedule:
- cron: "0 6 * * 0" # Every Sunday at 06:00 UTC
workflow_dispatch:
permissions:
packages: write
env:
PACKAGE_NAME: ALCops.Analyzers
PACKAGE_NAME_LOWER: alcops.analyzers
KEEP_LAST_N: 3
jobs:
cleanup:
runs-on: ubuntu-latest
name: Cleanup old pre-releases
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- name: Cleanup NuGet.org
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
set -euo pipefail
if [ -z "${NUGET_API_KEY:-}" ]; then
echo "::warning::NUGET_API_KEY not configured, skipping NuGet.org cleanup"
exit 0
fi
echo "Fetching version metadata from NuGet.org registration API..."
REGISTRATION_JSON=$(curl -sf --compressed \
"https://api.nuget.org/v3/registration5-gz-semver2/${PACKAGE_NAME_LOWER}/index.json") \
|| { echo "::warning::Failed to fetch registration index"; exit 0; }
# Extract listed versions from registration pages.
# Pages may have inlined items or require fetching by URL (@id).
ALL_VERSIONS=$(echo "$REGISTRATION_JSON" | jq -r '
[.items[] |
if .items then
.items[]
else
empty
end
| select(.catalogEntry.listed == true)
| .catalogEntry.version
] | .[]') || { echo "::warning::Failed to parse registration data"; exit 0; }
# Handle non-inlined pages: fetch each page URL if items were not inlined
PAGE_URLS=$(echo "$REGISTRATION_JSON" | jq -r '
[.items[] | select(.items == null) | .["@id"]] | .[]' 2>/dev/null)
if [ -n "$PAGE_URLS" ]; then
while IFS= read -r page_url; do
PAGE_VERSIONS=$(curl -sf --compressed "$page_url" | jq -r '
[.items[] | select(.catalogEntry.listed == true) | .catalogEntry.version] | .[]' 2>/dev/null)
if [ -n "$PAGE_VERSIONS" ]; then
ALL_VERSIONS=$(printf '%s\n%s' "$ALL_VERSIONS" "$PAGE_VERSIONS")
fi
done <<< "$PAGE_URLS"
fi
TOTAL_COUNT=$(echo "$REGISTRATION_JSON" | jq '[.items[].items // [] | length] | add // 0')
LISTED_COUNT=$(echo "$ALL_VERSIONS" | grep -c . || true)
echo "Found ${LISTED_COUNT} listed versions out of ${TOTAL_COUNT} total on NuGet.org"
if [ -z "$ALL_VERSIONS" ]; then
echo "No listed versions found"
exit 0
fi
# Separate stable and pre-release
STABLE=()
PRERELEASE=()
while IFS= read -r v; do
if [[ "$v" == *-* ]]; then
PRERELEASE+=("$v")
else
STABLE+=("$v")
fi
done <<< "$ALL_VERSIONS"
echo "Found ${#STABLE[@]} stable and ${#PRERELEASE[@]} pre-release versions"
if [ ${#PRERELEASE[@]} -eq 0 ]; then
echo "No pre-release versions to clean up"
exit 0
fi
# Find latest stable
LATEST_STABLE=""
if [ ${#STABLE[@]} -gt 0 ]; then
LATEST_STABLE=$(printf '%s\n' "${STABLE[@]}" | sort -V | tail -n1)
echo "Latest stable: $LATEST_STABLE"
else
echo "No stable versions found, will only trim current pre-releases"
fi
# Categorize pre-releases into superseded vs current
SUPERSEDED=()
CURRENT=()
for v in "${PRERELEASE[@]}"; do
base="${v%%-*}"
if [ -n "$LATEST_STABLE" ]; then
higher=$(printf '%s\n%s' "$base" "$LATEST_STABLE" | sort -V | tail -n1)
if [ "$higher" = "$LATEST_STABLE" ] || [ "$base" = "$LATEST_STABLE" ]; then
SUPERSEDED+=("$v")
else
CURRENT+=("$v")
fi
else
CURRENT+=("$v")
fi
done
echo "Superseded pre-releases: ${#SUPERSEDED[@]}"
echo "Current pre-releases: ${#CURRENT[@]}"
# Build unlist list
TO_UNLIST=()
# All superseded pre-releases
for v in "${SUPERSEDED[@]}"; do
TO_UNLIST+=("$v")
done
# Split current pre-releases by channel (alpha vs beta)
CURRENT_ALPHA=()
CURRENT_BETA=()
for v in "${CURRENT[@]}"; do
if [[ "$v" == *-beta.* ]]; then
CURRENT_BETA+=("$v")
else
CURRENT_ALPHA+=("$v")
fi
done
echo "Current alphas: ${#CURRENT_ALPHA[@]}, betas: ${#CURRENT_BETA[@]}"
# Keep last N per channel (by SemVer sort)
for channel_name in "alpha" "beta"; do
declare -n channel_arr="CURRENT_${channel_name^^}"
if [ ${#channel_arr[@]} -gt "$KEEP_LAST_N" ]; then
mapfile -t sorted < <(printf '%s\n' "${channel_arr[@]}" | sort -V)
remove_count=$(( ${#sorted[@]} - KEEP_LAST_N ))
for ((i=0; i<remove_count; i++)); do
TO_UNLIST+=("${sorted[$i]}")
done
fi
done
echo ""
echo "=== NuGet.org: unlisting ${#TO_UNLIST[@]} versions ==="
for v in "${TO_UNLIST[@]}"; do
echo " Unlisting: $v"
dotnet nuget delete "$PACKAGE_NAME" "$v" \
--source https://api.nuget.org/v3/index.json \
--api-key "$NUGET_API_KEY" \
--non-interactive 2>&1 || echo " (already unlisted or failed)"
sleep 1
done
echo "NuGet.org cleanup complete"
- name: Cleanup GitHub Packages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
OWNER="${{ github.repository_owner }}"
echo "Fetching GitHub Packages versions..."
GH_VERSIONS=$(gh api "/orgs/${OWNER}/packages/nuget/${PACKAGE_NAME}/versions" \
--paginate --jq '.[] | "\(.id)|\(.name)"' 2>/dev/null) \
|| GH_VERSIONS=$(gh api "/users/${OWNER}/packages/nuget/${PACKAGE_NAME}/versions" \
--paginate --jq '.[] | "\(.id)|\(.name)"' 2>/dev/null) \
|| { echo "::warning::Failed to list GitHub Packages versions"; exit 0; }
if [ -z "$GH_VERSIONS" ]; then
echo "No GitHub Packages versions found"
exit 0
fi
# Build lookup: version -> id
declare -A VERSION_IDS
while IFS='|' read -r id name; do
VERSION_IDS["$name"]="$id"
done <<< "$GH_VERSIONS"
echo "Found ${#VERSION_IDS[@]} versions on GitHub Packages"
# Separate stable and pre-release
STABLE=()
PRERELEASE=()
for v in "${!VERSION_IDS[@]}"; do
if [[ "$v" == *-* ]]; then
PRERELEASE+=("$v")
else
STABLE+=("$v")
fi
done
# Find latest stable
LATEST_STABLE=""
if [ ${#STABLE[@]} -gt 0 ]; then
LATEST_STABLE=$(printf '%s\n' "${STABLE[@]}" | sort -V | tail -n1)
fi
# Categorize
SUPERSEDED=()
CURRENT=()
for v in "${PRERELEASE[@]}"; do
base="${v%%-*}"
if [ -n "$LATEST_STABLE" ]; then
higher=$(printf '%s\n%s' "$base" "$LATEST_STABLE" | sort -V | tail -n1)
if [ "$higher" = "$LATEST_STABLE" ] || [ "$base" = "$LATEST_STABLE" ]; then
SUPERSEDED+=("$v")
else
CURRENT+=("$v")
fi
else
CURRENT+=("$v")
fi
done
# Build delete list
TO_DELETE=()
for v in "${SUPERSEDED[@]}"; do
TO_DELETE+=("$v")
done
# Split current pre-releases by channel (alpha vs beta)
CURRENT_ALPHA=()
CURRENT_BETA=()
for v in "${CURRENT[@]}"; do
if [[ "$v" == *-beta.* ]]; then
CURRENT_BETA+=("$v")
else
CURRENT_ALPHA+=("$v")
fi
done
echo "Current alphas: ${#CURRENT_ALPHA[@]}, betas: ${#CURRENT_BETA[@]}"
# Keep last N per channel (by SemVer sort)
for channel_name in "alpha" "beta"; do
declare -n channel_arr="CURRENT_${channel_name^^}"
if [ ${#channel_arr[@]} -gt "$KEEP_LAST_N" ]; then
mapfile -t sorted < <(printf '%s\n' "${channel_arr[@]}" | sort -V)
remove_count=$(( ${#sorted[@]} - KEEP_LAST_N ))
for ((i=0; i<remove_count; i++)); do
TO_DELETE+=("${sorted[$i]}")
done
fi
done
echo ""
echo "=== GitHub Packages: deleting ${#TO_DELETE[@]} versions ==="
# Determine API path (org vs user)
API_BASE="/orgs/${OWNER}/packages/nuget/${PACKAGE_NAME}/versions"
gh api "$API_BASE" --silent 2>/dev/null \
|| API_BASE="/users/${OWNER}/packages/nuget/${PACKAGE_NAME}/versions"
for v in "${TO_DELETE[@]}"; do
VID="${VERSION_IDS[$v]:-}"
if [ -z "$VID" ]; then
echo " Skipping $v (version ID not found)"
continue
fi
echo " Deleting: $v (id: $VID)"
gh api -X DELETE "${API_BASE}/${VID}" 2>&1 \
|| echo " (already deleted or failed)"
sleep 1
done
echo "GitHub Packages cleanup complete"