Created
February 1, 2026 21:26
-
-
Save viperadnan-git/b27193df91f18d4e262aca8d168b4eb5 to your computer and use it in GitHub Desktop.
GitHub Actions Cache Cleanup - Delete old cache entries (3+ days) with filtering by repo/branch/key. Includes dry-run mode, auto repo detection, and size reporting. Requires: gh CLI, jq
This file contains hidden or 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 -euo pipefail | |
| # Default values | |
| DAYS=3 | |
| REPO="" | |
| BRANCH="" | |
| KEY_PATTERN="" | |
| DRY_RUN=false | |
| VERBOSE=false | |
| # Colors | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| NC='\033[0m' # No Color | |
| usage() { | |
| cat << EOF | |
| Usage: $(basename "$0") [OPTIONS] | |
| Delete GitHub Actions cache entries older than specified days. | |
| OPTIONS: | |
| -r, --repo REPO Repository (owner/name format, default: current repo) | |
| -d, --days DAYS Delete cache older than DAYS (default: 3) | |
| -b, --branch BRANCH Filter by branch name | |
| -k, --key PATTERN Filter by cache key pattern | |
| -n, --dry-run Show what would be deleted without deleting | |
| -v, --verbose Verbose output | |
| -h, --help Show this help message | |
| EXAMPLES: | |
| # Delete cache older than 3 days from current repo | |
| $(basename "$0") | |
| # Delete cache older than 7 days from specific repo | |
| $(basename "$0") -r owner/repo -d 7 | |
| # Delete cache for main branch only | |
| $(basename "$0") -b main -d 5 | |
| # Delete cache matching key pattern | |
| $(basename "$0") -k "Linux-" -d 1 | |
| # Dry run to see what would be deleted | |
| $(basename "$0") -d 3 --dry-run | |
| EOF | |
| exit 0 | |
| } | |
| log() { | |
| echo -e "${GREEN}[INFO]${NC} $*" | |
| } | |
| warn() { | |
| echo -e "${YELLOW}[WARN]${NC} $*" | |
| } | |
| error() { | |
| echo -e "${RED}[ERROR]${NC} $*" >&2 | |
| exit 1 | |
| } | |
| verbose() { | |
| if [[ "$VERBOSE" == true ]]; then | |
| echo -e "${NC}[DEBUG]${NC} $*" | |
| fi | |
| } | |
| check_dependencies() { | |
| if ! command -v gh &> /dev/null; then | |
| error "gh CLI is not installed. Install from: https://cli.github.com/" | |
| fi | |
| if ! command -v jq &> /dev/null; then | |
| error "jq is not installed. Install with: apt/brew install jq" | |
| fi | |
| # Check gh authentication | |
| if ! gh auth status &> /dev/null; then | |
| error "Not authenticated with gh. Run: gh auth login" | |
| fi | |
| } | |
| get_repo() { | |
| if [[ -n "$REPO" ]]; then | |
| echo "$REPO" | |
| return | |
| fi | |
| # Try to get repo from git remote | |
| if git rev-parse --git-dir &> /dev/null; then | |
| local remote_url | |
| remote_url=$(git config --get remote.origin.url || echo "") | |
| if [[ -n "$remote_url" ]]; then | |
| # Extract owner/repo from various URL formats | |
| echo "$remote_url" | sed -E 's#.*github\.com[:/](.+/.+?)(\.git)?$#\1#' | |
| return | |
| fi | |
| fi | |
| error "Could not determine repository. Use -r flag to specify." | |
| } | |
| parse_args() { | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -r|--repo) | |
| REPO="$2" | |
| shift 2 | |
| ;; | |
| -d|--days) | |
| DAYS="$2" | |
| shift 2 | |
| ;; | |
| -b|--branch) | |
| BRANCH="$2" | |
| shift 2 | |
| ;; | |
| -k|--key) | |
| KEY_PATTERN="$2" | |
| shift 2 | |
| ;; | |
| -n|--dry-run) | |
| DRY_RUN=true | |
| shift | |
| ;; | |
| -v|--verbose) | |
| VERBOSE=true | |
| shift | |
| ;; | |
| -h|--help) | |
| usage | |
| ;; | |
| *) | |
| error "Unknown option: $1" | |
| ;; | |
| esac | |
| done | |
| } | |
| delete_cache() { | |
| local repo | |
| repo=$(get_repo) | |
| log "Repository: $repo" | |
| log "Delete cache older than: $DAYS days" | |
| # Calculate cutoff date | |
| local cutoff_date | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| cutoff_date=$(date -u -v-"${DAYS}"d +"%Y-%m-%dT%H:%M:%SZ") | |
| else | |
| cutoff_date=$(date -u -d "$DAYS days ago" +"%Y-%m-%dT%H:%M:%SZ") | |
| fi | |
| verbose "Cutoff date: $cutoff_date" | |
| # Fetch all cache entries | |
| log "Fetching cache entries..." | |
| local cache_list | |
| cache_list=$(gh api -X GET "/repos/$repo/actions/caches" --paginate \ | |
| --jq '.actions_caches[]') | |
| if [[ -z "$cache_list" ]]; then | |
| log "No cache entries found." | |
| exit 0 | |
| fi | |
| local total_count=0 | |
| local deleted_count=0 | |
| local total_size=0 | |
| # Process each cache entry | |
| while IFS= read -r cache; do | |
| local id key ref created_at size_bytes | |
| id=$(echo "$cache" | jq -r '.id') | |
| key=$(echo "$cache" | jq -r '.key') | |
| ref=$(echo "$cache" | jq -r '.ref') | |
| created_at=$(echo "$cache" | jq -r '.created_at') | |
| size_bytes=$(echo "$cache" | jq -r '.size_in_bytes') | |
| total_count=$((total_count + 1)) | |
| # Apply filters | |
| if [[ -n "$BRANCH" ]] && [[ "$ref" != *"$BRANCH"* ]]; then | |
| verbose "Skipping (branch filter): $key" | |
| continue | |
| fi | |
| if [[ -n "$KEY_PATTERN" ]] && [[ "$key" != *"$KEY_PATTERN"* ]]; then | |
| verbose "Skipping (key filter): $key" | |
| continue | |
| fi | |
| # Check if older than cutoff | |
| if [[ "$created_at" < "$cutoff_date" ]]; then | |
| deleted_count=$((deleted_count + 1)) | |
| total_size=$((total_size + size_bytes)) | |
| local size_mb | |
| size_mb=$(echo "scale=2; $size_bytes / 1048576" | bc) | |
| if [[ "$DRY_RUN" == true ]]; then | |
| echo -e "${YELLOW}[DRY-RUN]${NC} Would delete: $key (${size_mb}MB, created: $created_at)" | |
| else | |
| verbose "Deleting: $key (${size_mb}MB, created: $created_at)" | |
| if gh api -X DELETE "/repos/$repo/actions/caches/$id" &> /dev/null; then | |
| echo -e "${GREEN}✓${NC} Deleted: $key (${size_mb}MB)" | |
| else | |
| warn "Failed to delete: $key" | |
| fi | |
| fi | |
| else | |
| verbose "Keeping (newer): $key (created: $created_at)" | |
| fi | |
| done < <(echo "$cache_list") | |
| # Summary | |
| local total_size_mb | |
| total_size_mb=$(echo "scale=2; $total_size / 1048576" | bc) | |
| echo "" | |
| log "Summary:" | |
| echo " Total cache entries: $total_count" | |
| echo " Entries to delete: $deleted_count" | |
| echo " Total size: ${total_size_mb}MB" | |
| if [[ "$DRY_RUN" == true ]]; then | |
| warn "DRY RUN - No cache was actually deleted. Remove --dry-run to delete." | |
| fi | |
| } | |
| main() { | |
| parse_args "$@" | |
| check_dependencies | |
| delete_cache | |
| } | |
| main "$@" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've created a comprehensive bash script for cleaning up GitHub Actions cache. Here's what it does:
Features:
Quick Install:
curl -fsSL https://gist.githubusercontent.com/viperadnan-git/b27193df91f18d4e262aca8d168b4eb5/raw/gh-cache-cleanup.sh | bash -s -- [OPTIONS]Or download and make executable:
Usage Examples:
Requirements:
ghCLI (authenticated)jqfor JSON parsingbcfor calculations (usually pre-installed)The script handles pagination automatically and provides a detailed summary of deleted entries and space freed.