Skip to content

Instantly share code, notes, and snippets.

@pythoninthegrass
Last active November 4, 2024 06:21
Show Gist options
  • Save pythoninthegrass/f141766b41889d9ee95eef1cabb9a4cb to your computer and use it in GitHub Desktop.
Save pythoninthegrass/f141766b41889d9ee95eef1cabb9a4cb to your computer and use it in GitHub Desktop.
#!/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 ))
@pythoninthegrass
Copy link
Author

git clone [email protected]:f141766b41889d9ee95eef1cabb9a4cb.git rm_gh_pkg
cd $_
ln -s $(pwd)/rm_gh_pkg.sh ~/.local/bin/rm-gh-pkg
rm-gh-pkg <package>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment