Skip to content

Cleanup All Workflow Runs #4

Cleanup All Workflow Runs

Cleanup All Workflow Runs #4

name: Cleanup All Workflow Runs
on:
workflow_dispatch: # Manual trigger
schedule:
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
permissions:
actions: write # Required to delete workflow runs
contents: read
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Python 3.13
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Run Cleanup Script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
DELETE_WITHIN_DAYS: 30
KEEP_MINIMUM_RUNS: 0
run: |
python <<'EOF'
#!/usr/bin/env python3
"""
Author: Diwas Neupane
Purpose: Delete all GitHub Actions workflow runs (except in-progress ones) created within the last DELETE_WITHIN_DAYS.
Keeps KEEP_MINIMUM_RUNS most recent runs if specified.
Pure stdlib (urllib), handles pagination.
"""
import os
import json
from datetime import datetime, timedelta, timezone
from urllib import request, error, parse
# --- Configuration ---
token = os.getenv("GITHUB_TOKEN")
repo = os.getenv("GITHUB_REPOSITORY")
delete_within_days = int(os.getenv("DELETE_WITHIN_DAYS", "30"))
keep_minimum = int(os.getenv("KEEP_MINIMUM_RUNS", "0"))
if not token or not repo:
print("❌ ERROR: Missing GITHUB_TOKEN or GITHUB_REPOSITORY environment variables.")
exit(1)
if keep_minimum < 0:
print(f"❌ ERROR: KEEP_MINIMUM_RUNS must be zero or positive (got {keep_minimum}).")
exit(1)
headers = {
"Authorization": f"token {token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "cleanup-script"
}
cutoff_date = datetime.now(timezone.utc) - timedelta(days=delete_within_days)
print(f"🧹 Deletion policy: targeting runs created after {cutoff_date.isoformat()}")
print(f"🛡️ Safety policy: keeping {keep_minimum} most recent runs")
def github_get(url, params=None):
if params:
url += "?" + parse.urlencode(params)
req = request.Request(url, headers=headers)
with request.urlopen(req) as resp:
return json.loads(resp.read().decode())
def github_delete(url):
req = request.Request(url, headers=headers, method="DELETE")
try:
with request.urlopen(req) as resp:
return resp.status == 204
except error.HTTPError as e:
print(f"❌ Failed to delete run: {e}")
return False
# --- Fetch all workflow runs (paginated) ---
all_runs = []
page = 1
while True:
api_url = f"https://api.github.com/repos/{repo}/actions/runs"
data = github_get(api_url, {"per_page": 100, "page": page})
runs = data.get("workflow_runs", [])
if not runs:
break
all_runs.extend(runs)
page += 1
print(f"📦 Fetched {len(all_runs)} total workflow runs")
# --- Filter runs: skip in_progress, filter by cutoff date ---
eligible_runs = []
for run in all_runs:
if run.get("status") == "in_progress":
continue # Skip runs still running
try:
created_at = datetime.strptime(run["created_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
except (KeyError, ValueError):
print(f"⚠️ Skipping run with invalid 'created_at': {run}")
continue
if created_at >= cutoff_date:
eligible_runs.append(run)
if not eligible_runs:
print("✅ No workflow runs eligible for deletion within the time window.")
exit(0)
# Sort runs newest first to preserve the most recent ones
eligible_runs.sort(key=lambda r: datetime.strptime(r["created_at"], "%Y-%m-%dT%H:%M:%SZ"), reverse=True)
if keep_minimum > 0 and len(eligible_runs) <= keep_minimum:
print(f"ℹ️ Found {len(eligible_runs)} runs but minimum keep threshold is {keep_minimum}. Nothing to delete.")
exit(0)
# Determine which runs to delete based on keep_minimum
runs_to_delete = eligible_runs if keep_minimum == 0 else eligible_runs[keep_minimum:]
print(f"🗑️ Deleting {len(runs_to_delete)} runs...")
# Delete runs one-by-one
for run in runs_to_delete:
run_id = run.get("id")
if not run_id:
print(f"⚠️ Skipping run without an ID: {run}")
continue
delete_url = f"https://api.github.com/repos/{repo}/actions/runs/{run_id}"
if github_delete(delete_url):
print(f"✅ Deleted run ID {run_id} (Created at {run['created_at']})")
print("🎉 Cleanup finished successfully.")
EOF