GitHub CLI: Bulk Delete Old Issues

Long-running projects accumulate outdated issues: bugs for old versions, features that are no longer relevant, test tickets. Manual deletion is slow and tedious. This script automatically finds and deletes (or marks) old issues by specified labels. 💡 Script uses dry-run by default - shows what would be deleted, without actual deletion. 📦 Script: delete-issues.sh Full code #!/bin/bash # Delete old GitHub Issues by labels and date # Usage: ./delete-issues.sh [--execute] set -euo pipefail # === CONFIG === REPO="owner/repo" # Repository in owner/repo format LABELS='label1,label2,label3' # Labels to filter (comma-separated) CUTOFF="2025-12-31T23:59:59Z" # Delete issues created BEFORE this date DRY_RUN=true # true = preview only, false = actually delete # Parse arguments if [[ "${1:-}" == "--execute" ]]; then DRY_RUN=false echo "⚠️ Mode: ACTUAL DELETION" else echo "ℹ️ Mode: DRY RUN (nothing will be deleted)" fi echo "🔍 Searching issues in $REPO with labels: $LABELS, created before $CUTOFF" echo "---" # Fetch and filter issues gh issue list --repo "$REPO" --state all --limit 1000 \ --json number,title,createdAt,labels,url | \ jq -r --arg labels "$LABELS" --arg cutoff "$CUTOFF" ' ($labels | split(",")) as $label_array | .[] | select(.createdAt < $cutoff) | select(.labels | map(.name) | any(. as $l | $label_array | index($l))) | "\(.number)|\(.title)|\(.url)" ' | \ while IFS='|' read -r number title url; do if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY RUN] ##$number - $title" echo " $url" else echo "🗑 Deleting ##$number - $title" gh issue delete "$REPO" "$number" --yes sleep 1 # Small pause to avoid rate limits fi done echo "---" echo "✅ Done" Install dependencies # GitHub CLI # Windows (winget): winget install GitHub.cli # Linux (Ubuntu/Debian): sudo apt install gh # macOS (Homebrew): brew install gh # Authenticate gh auth login # jq (JSON processor) # Windows (winget): winget install jq.jq # Linux: sudo apt install jq # macOS: brew install jq Run # Make script executable chmod +x delete-issues.sh # DRY RUN (safe mode - preview only) ./delete-issues.sh # ACTUAL DELETION (add --execute flag) ./delete-issues.sh --execute 🔍 How it works Step-by-step breakdown Step Command What it does 1 gh issue list --json ... Fetches issues as JSON with fields: number, title, date, labels, URL 2 jq -r ... Filters: date < cutoff AND has at least one of specified labels 3 while read ... Processes each matching issue 4 gh issue delete Deletes issue (only if DRY_RUN=false) jq filter logic ($labels | split(",")) as $label_array | # Split label string into array .[] | # For each issue select(.createdAt < $cutoff) | # Only if created before cutoff select( .labels | map(.name) | # Get list of label names any(. as $l | $label_array | index($l)) # Check if any matches our labels ) | "\(.number)|\(.title)|\(.url)" # Format output Why this way: ...

17 Mar 2026 · 5 min · 880 words · Potato Energy Team, ponfertato