Last active
November 4, 2024 06:21
-
-
Save pythoninthegrass/f141766b41889d9ee95eef1cabb9a4cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -eo pipefail | |
# Default values | |
PACKAGE_NAME="" | |
USER_NAME="" | |
DRY_RUN=false | |
VERBOSE=false | |
# Colors | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
NC='\033[0m' # No Color | |
# Help message | |
usage() { | |
cat << EOF | |
Usage: $(basename "$0") -p PACKAGE_NAME [-u USERNAME] [-d] [-v] | |
Delete all versions of a GitHub Container Registry package. | |
Required arguments: | |
-p, --package Package name in GHCR | |
Optional arguments: | |
-u, --user GitHub username (defaults to authenticated user) | |
-n, --dry-run Show what would be deleted without actually deleting | |
-v, --verbose Increase output verbosity | |
-h, --help Show this help message | |
Example: | |
$(basename "$0") -p my-container -u myusername | |
EOF | |
} | |
# Log messages with different levels | |
log() { | |
local level=$1 | |
shift | |
case $level in | |
"INFO") | |
echo -e "${GREEN}[INFO]${NC} $*" | |
;; | |
"WARN") | |
echo -e "${YELLOW}[WARN]${NC} $*" >&2 | |
;; | |
"ERROR") | |
echo -e "${RED}[ERROR]${NC} $*" >&2 | |
;; | |
esac | |
} | |
# Parse arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
-p|--package) | |
PACKAGE_NAME="$2" | |
shift 2 | |
;; | |
-u|--user) | |
USER_NAME="$2" | |
shift 2 | |
;; | |
-n|--dry-run) | |
DRY_RUN=true | |
shift | |
;; | |
-v|--verbose) | |
VERBOSE=true | |
shift | |
;; | |
-h|--help) | |
usage | |
exit 0 | |
;; | |
*) | |
log "ERROR" "Unknown option: $1" | |
usage | |
exit 1 | |
;; | |
esac | |
done | |
# Validate required arguments | |
if [[ -z "$PACKAGE_NAME" ]]; then | |
log "ERROR" "Package name is required" | |
usage | |
exit 1 | |
fi | |
# Ensure GitHub CLI is installed and authenticated | |
if ! command -v gh &> /dev/null; then | |
log "ERROR" "GitHub CLI (gh) is not installed" | |
exit 1 | |
fi | |
# Get GitHub token using gh cli | |
TOKEN=$(gh auth token) | |
if [[ -z "$TOKEN" ]]; then | |
log "ERROR" "Failed to get GitHub token. Please run 'gh auth login' first" | |
exit 1 | |
fi | |
# Get username if not provided | |
if [[ -z "$USER_NAME" ]]; then | |
USER_NAME=$(gh api user -q .login) | |
if [[ -z "$USER_NAME" ]]; then | |
log "ERROR" "Failed to get GitHub username" | |
exit 1 | |
fi | |
fi | |
# get package versions | |
get_versions() { | |
local response | |
response=$(curl -s -L \ | |
-H "Accept: application/vnd.github+json" \ | |
-H "Authorization: Bearer ${TOKEN}" \ | |
"https://api.github.com/users/${USER_NAME}/packages/container/${PACKAGE_NAME}/versions") | |
# First check if response is valid JSON | |
if ! echo "$response" | jq . >/dev/null 2>&1; then | |
log "ERROR" "Invalid JSON response from API" | |
exit 1 | |
fi | |
# Then check if it's an error response (has message field) | |
if echo "$response" | jq 'type == "object" and has("message")' | grep -q true; then | |
local error_message | |
error_message=$(echo "$response" | jq -r '.message') | |
log "ERROR" "Failed to get package versions: $error_message" | |
exit 1 | |
fi | |
# At this point we should have a valid array response | |
if ! echo "$response" | jq 'type == "array"' | grep -q true; then | |
log "ERROR" "Unexpected API response format" | |
exit 1 | |
fi | |
echo "$response" | |
} | |
# delete a version | |
delete_version() { | |
local version_id=$1 | |
local tags=$2 | |
if [[ "$DRY_RUN" = true ]]; then | |
log "INFO" "Would delete version $version_id (tags: $tags)" | |
return 0 | |
fi | |
local response | |
local response_body | |
local http_code | |
# Capture both response body and HTTP code | |
response=$(curl -s -L -w "\n%{http_code}" -X DELETE \ | |
-H "Accept: application/vnd.github+json" \ | |
-H "Authorization: Bearer ${TOKEN}" \ | |
"https://api.github.com/users/${USER_NAME}/packages/container/${PACKAGE_NAME}/versions/${version_id}") | |
# Split response into body and status code | |
response_body=$(echo "$response" | sed '$d') | |
http_code=$(echo "$response" | tail -n1) | |
case $http_code in | |
204) | |
[[ "$VERBOSE" = true ]] && log "INFO" "Deleted version $version_id (tags: $tags)" | |
return 0 | |
;; | |
400) | |
if echo "$response_body" | grep -q "5000 downloads"; then | |
log "WARN" "Cannot delete version $version_id (tags: $tags) - More than 5000 downloads" | |
log "INFO" "It may be possible to remove the package entirely using the GitHub web interface" | |
log "INFO" "See: https://docs.github.com/en/packages/learn-github-packages/deleting-and-restoring-a-package#deleting-an-entire-user-scoped-package-on-github" | |
log "WARN" "Otherwise, please contact GitHub support to delete this version" | |
return 2 # Special return code for this case | |
else | |
log "ERROR" "Failed to delete version $version_id: $(echo "$response_body" | jq -r '.message')" | |
return 1 | |
fi | |
;; | |
403) | |
log "ERROR" "Permission denied for version $version_id" | |
return 1 | |
;; | |
404) | |
log "WARN" "Version $version_id not found" | |
return 0 # Consider this a "success" as the version is already gone | |
;; | |
*) | |
log "ERROR" "Failed to delete version $version_id (HTTP $http_code): $(echo "$response_body" | jq -r '.message // .')" | |
return 1 | |
;; | |
esac | |
} | |
# Main execution | |
log "INFO" "Processing package: ${PACKAGE_NAME}" | |
[[ "$DRY_RUN" = true ]] && log "WARN" "Running in dry-run mode" | |
# Get all versions | |
versions=$(get_versions) | |
version_count=$(echo "$versions" | jq length) | |
if [[ $version_count -eq 0 ]]; then | |
log "INFO" "No versions found for package ${PACKAGE_NAME}" | |
exit 0 | |
fi | |
log "INFO" "Found $version_count versions" | |
# Process each version | |
popular_versions=0 | |
deleted_count=0 | |
failed_count=0 | |
echo "$versions" | jq -c '.[]' | while read -r version; do | |
version_id=$(echo "$version" | jq -r '.id') | |
tags=$(echo "$version" | jq -r '.metadata.container.tags | join(", ")') | |
tags=${tags:-"<untagged>"} | |
if delete_version "$version_id" "$tags"; then | |
((deleted_count++)) | |
else | |
case $? in | |
2) # Downloads limit case | |
((popular_versions++)) | |
;; | |
*) # Other failures | |
((failed_count++)) | |
;; | |
esac | |
fi | |
done | |
# Update summary to include popular versions | |
log "INFO" "Deletion complete" | |
log "INFO" "Successfully deleted: $deleted_count" | |
[[ $popular_versions -gt 0 ]] && log "WARN" "$popular_versions version(s) could not be deleted due to having >5000 downloads" | |
[[ $failed_count -gt 0 ]] && log "WARN" "Failed to delete: $failed_count" | |
# Modify exit code to indicate if any versions couldn't be deleted | |
exit $(( failed_count > 0 ? 1 : popular_versions > 0 ? 2 : 0 )) |
Author
pythoninthegrass
commented
Nov 4, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment