Maintaining a busy GitHub repository leads to an accumulation of hundreds, or even thousands, of old workflow runs. While GitHub provides retention policies, sometimes you need to manually purge old data to stay within storage limits or clean up your project's history.
Manually deleting runs through the UI is tedious. In this article, we’ll break down a powerful Bash script that automates this process using the GitHub CLI (gh), jq for JSON processing, and xargs for high-speed parallel deletion.
Prerequisites
Before running the script, ensure you have the these installed:
- GitHub CLI (
gh): The primary tool for interacting with GitHub's API jq: A lightweight command-line JSON processor used to filter run IDs- GNU Coreutils: Specifically
dateandxargs
NOTE: You must be authenticated with the GitHub CLI and have the necessary permissions (repo/workflow scopes) to delete runs in the target repository. Use
gh auth loginto sign in.
The Cleanup Script
Below is the complete script. It safely prompts for user input, calculates a cutoff date based on your requirements, and performs a parallel deletion.
#!/bin/env bash
set -euo pipefail
OWNER="<REPLACE-WITH-YOUR-USERNAME-OR-ORGANIZATION>"
PARALLEL_JOBS=5 # adjust for large repos
echo
# Prompt for the repository name
read -p "Enter the repository name: " REPO
if [[ -z "$REPO" ]]; then
echo "Error: Repository name cannot be empty."
exit 1
fi
read -p "Delete workflow runs older than how many days? " DAYS
if ! [[ "$DAYS" =~ ^[0-9]+$ ]]; then
echo "Please enter a valid number."
exit 1
fi
CUTOFF=$(date -u -d "$DAYS days ago" +"%Y-%m-%dT%H:%M:%SZ")
echo
echo "Finding workflow runs in $OWNER/$REPO older than $DAYS days (before $CUTOFF)..."
echo
# Fetch runs once
RUNS_JSON=$(gh api repos/$OWNER/$REPO/actions/runs --paginate)
# Filter runs older than cutoff
RUNS=$(echo "$RUNS_JSON" | jq -r --arg cutoff "$CUTOFF" '
.workflow_runs[]
| select(.created_at < $cutoff)
| "\(.id)|\(.created_at)|\(.name)"
')
if [ -z "$RUNS" ]; then
echo "No workflow runs older than $DAYS days found."
exit 0
fi
COUNT=$(echo "$RUNS" | wc -l | tr -d ' ')
echo "---------------------------------------------"
echo "Runs to delete: $COUNT"
echo "---------------------------------------------"
echo
echo "The following workflow runs will be deleted:"
echo "---------------------------------------------"
echo "$RUNS" | while IFS="|" read -r id created name; do
printf "ID: %-12s Date: %s Name: %s\n" "$id" "$created" "$name"
done
echo "---------------------------------------------"
echo
read -p "Proceed with deletion? (y/N): " CONFIRM
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
echo
echo "Deleting workflow runs in parallel ($PARALLEL_JOBS workers)..."
echo
export OWNER REPO
echo "$RUNS" | cut -d'|' -f1 |
xargs -n1 -P"$PARALLEL_JOBS" -I{} bash -c '
echo "Deleting run {}"
gh api --method DELETE repos/$OWNER/$REPO/actions/runs/{}
echo "Deleted run {}"
'
echo
echo "Cleanup complete."
How It Works
1. Change the Owner
To use it for your personal or organization account, change the OWNER variable in:
OWNER="<REPLACE-WITH-YOUR-USERNAME-OR-ORGANIZATION>"
2. Execution Safety with set -euo pipefail
This ensures the script exits immediately if a command fails, if an unset variable is used, or if any part of a piped command errors out. This prevents the script from running "blindly" if the GitHub API is unreachable.
3. Filter with date and jq
The script calculates a UTC timestamp (ISO 8601) using:
CUTOFF=$(date -u -d "$DAYS days ago" +"%Y-%m-%dT%H:%M:%SZ").
It then uses jq to compare this against the created_at field of every workflow run returned by the GitHub API.
4. Handle Large Datasets
For repositories with thousands of runs, the API doesn't return everything in one go. The --paginate flag in the gh api command automatically handles following the "next" page links until all runs are retrieved.
5. Speed Up with Parallelism
Deleting runs one-by-one can be slow due to network latency. Using xargs -P"$PARALLEL_JOBS" spawns multiple background processes:
-n1: Process one ID at a time per worker.-P5: Run up to 5 API calls simultaneously.-I{}: A placeholder for the ID passed from the pipe.
If you are cleaning up a massive repository (e.g., 5,000+ runs), you can increase PARALLEL_JOBS to 10 or 15. Be mindful of GitHub's API rate limits.
Troubleshooting
- "Resource not accessible by integration": Your GitHub token likely lacks the
workflowpermission. Rungh auth refresh -s workflow. datecommand errors: This script uses GNUdatesyntax. If you are on macOS, you may need to installcoreutils(brew install coreutils) and usegdate, or adjust the syntax for BSDdate.- Rate Limiting: If you see 403 errors during deletion, decrease the
PARALLEL_JOBScount.
Managing GitHub's Native Retention Policies
While scripts are excellent for surgical cleanups, you can also configure GitHub's native settings to automatically delete old workflow runs and artifacts. By default, GitHub retains this data for 90 days, but you can shorten this window to keep your storage usage low.
Configuring Retention via the Web UI
- Navigate to Settings: On your GitHub repository or Organization page, click the Settings tab.
- Access Actions Settings: In the left sidebar, click Actions > General.
- Set Retention Period: Scroll down to the Workflow run retention section.
- Update Days: Enter a new value (e.g.,
30or14days) and click Save.
NOTE: Retention periods can be set at the Repository, Organization, or Enterprise level. Settings at the organization or enterprise level will act as a maximum limit for the repositories within them.
Managing Retention via GitHub CLI
You can also view current repository settings using the gh tool:
# View current repository settings including retention
gh api repos/$OWNER/$REPO --jq '{name, visibility, default_retention: .retention_days}'