Contact Us

Got questions, inquiries, or opportunities for collaboration? We are just a message away!

Automate GitHub Actions Cleanup with Bash

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 date and xargs

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 login to 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 workflow permission. Run gh auth refresh -s workflow.
  • date command errors: This script uses GNU date syntax. If you are on macOS, you may need to install coreutils (brew install coreutils) and use gdate, or adjust the syntax for BSD date.
  • Rate Limiting: If you see 403 errors during deletion, decrease the PARALLEL_JOBS count.

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

  1. Navigate to Settings: On your GitHub repository or Organization page, click the Settings tab.
  2. Access Actions Settings: In the left sidebar, click Actions > General.
  3. Set Retention Period: Scroll down to the Workflow run retention section.
  4. Update Days: Enter a new value (e.g., 30 or 14 days) 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}'

Further Reading & References