Skip to content

About

GitHub Action to delete old workflow runs, with retry/rate-limit handling, per-workflow filtering, and dry-run mode. Keeps noisy CI histories clean.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

28 stars

Watchers

1 watching

Forks

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

Clean Workflow Action

Delete old GitHub Actions workflow runs with retry logic, circuit breaker, rate-limit awareness, and per-workflow filtering. Built for repos that generate a lot of CI noise.

CI Check Dist Coverage OpenSSF Scorecard Marketplace License: MIT Node

GitHub Actions histories grow without bounds. Scheduled jobs, PR checks, and matrix builds can leave a repo with thousands of runs a year, cluttering the Actions tab and eating into your plan's storage. This action prunes the old ones on a schedule you control, with retries, rate-limit pacing, and a circuit breaker so a flaky API call doesn't leave you with a half-finished cleanup.

If this keeps your Actions tab clean and your storage quota in check, a ⭐ on the repo helps other teams find it too.

Quick start

- uses: igorjs/gh-actions-clean-workflow@v8
  with:
    runs_older_than: 7  # delete runs older than 7 days
    runs_to_keep: 5     # keep the 5 most recent runs per workflow

Need a full workflow file you can drop in? See Examples below.

Table of contents

Features

  • Workflow filtering by name, so you only touch the runs you want
  • Per-workflow retention via runs_to_keep (keep the N most recent per workflow, delete the rest)
  • Automatic retries with exponential backoff on transient failures (5xx, 429, network)
  • Circuit breaker that opens after repeated failures and recovers automatically
  • Rate-limit aware: respects GitHub's Retry-After header and paces requests
  • Dry run mode to preview deletions without touching anything
  • Detailed metrics exposed as action outputs for monitoring, alerting, or dashboards
  • Hardened CI: signed commits, pinned action SHAs, OSV-Scanner + betterleaks scanning, OpenSSF Scorecard, and dependency review

Inputs

Input Description Default Required
token GitHub token used for the REST API. Needs actions: write on the target repo. ${{ github.token }} No
owner Repository owner. ${{ github.repository_owner }} No
repo Repository name. extracted from ${{ github.repository }} No
runs_older_than Delete runs older than this many days (since the last rerun). 7 No
runs_to_keep Keep this many most-recent runs per workflow. Set to 0 to delete every eligible run. 0 No
workflow_names Comma-separated workflow names to filter. Empty = all workflows. Case-sensitive. "" No
dry_run If true, log what would be deleted but don't call the delete API. false No

Outputs

Output Description
total-runs-found Total runs matched by the filter
runs-deleted Runs successfully deleted
runs-failed Runs that failed to delete
total-api-requests Total API calls (list + delete + retries)
successful-requests API calls that returned 2xx
failed-requests API calls that ultimately failed
retry-attempts Retry attempts across the run
rate-limit-hits Times a 429 / secondary rate limit was observed
circuit-breaker-trips Times the circuit opened to back off

Permissions

The workflow that calls this action needs actions: write on the target repository so the GitHub API can delete runs.

permissions:
  actions: write

If the default GITHUB_TOKEN doesn't have enough scope (cross-repo cleanups, org-wide policies, ...), pass a PAT explicitly via the token input.

Examples

Basic, scheduled cleanup

Run every Monday at 00:00 UTC, delete runs older than 14 days, keep the 20 most recent per workflow.

name: Clean workflow runs

on:
  schedule:
    - cron: "0 0 * * 1"
  workflow_dispatch:

permissions:
  actions: write

jobs:
  clean:
    runs-on: ubuntu-latest
    steps:
      - uses: igorjs/gh-actions-clean-workflow@v8
        with:
          runs_older_than: 14
          runs_to_keep: 20

Filter by workflow name

Only clean CI / Deploy noise, leave other workflows untouched.

- uses: igorjs/gh-actions-clean-workflow@v8
  with:
    workflow_names: "CI, Deploy, Tests"
    runs_older_than: 14
    runs_to_keep: 5

Dry run

Preview what would be deleted without calling the delete API. Useful for the first cron run, or to sanity-check filters.

- uses: igorjs/gh-actions-clean-workflow@v8
  with:
    dry_run: true
    runs_older_than: 7

Consume the metrics outputs

- name: Clean workflow runs
  id: clean
  uses: igorjs/gh-actions-clean-workflow@v8
  with:
    runs_older_than: 7
    runs_to_keep: 10

- name: Summarize
  run: |
    echo "Found:        ${{ steps.clean.outputs.total-runs-found }}"
    echo "Deleted:      ${{ steps.clean.outputs.runs-deleted }}"
    echo "Failed:       ${{ steps.clean.outputs.runs-failed }}"
    echo "API requests: ${{ steps.clean.outputs.total-api-requests }}"
    echo "Rate limited: ${{ steps.clean.outputs.rate-limit-hits }}"
    echo "CB trips:     ${{ steps.clean.outputs.circuit-breaker-trips }}"
Manual trigger with inputs
name: Clean workflow runs

on:
  workflow_dispatch:
    inputs:
      runs_older_than:
        description: "Days old to delete"
        default: "7"
      runs_to_keep:
        description: "Most recent runs to keep per workflow"
        default: "0"
      workflow_names:
        description: "Comma-separated workflow names (empty = all)"
        default: ""
      dry_run:
        description: "Dry run mode"
        type: boolean
        default: false

permissions:
  actions: write

jobs:
  clean:
    runs-on: ubuntu-latest
    steps:
      - uses: igorjs/gh-actions-clean-workflow@v8
        with:
          runs_older_than: ${{ github.event.inputs.runs_older_than }}
          runs_to_keep: ${{ github.event.inputs.runs_to_keep }}
          workflow_names: ${{ github.event.inputs.workflow_names }}
          dry_run: ${{ github.event.inputs.dry_run }}
Scheduled + manual with shared defaults
name: Clean workflow runs

on:
  schedule:
    - cron: "0 0 * * 1"
  workflow_dispatch:
    inputs:
      runs_older_than:
        description: "Days old to delete"
        default: "7"
      dry_run:
        description: "Dry run mode"
        type: boolean
        default: false

permissions:
  actions: write

env:
  SCHEDULED_RUNS_OLDER_THAN: "7"
  SCHEDULED_RUNS_TO_KEEP: "20"

jobs:
  clean:
    runs-on: ubuntu-latest
    steps:
      - uses: igorjs/gh-actions-clean-workflow@v8
        with:
          runs_older_than: ${{ github.event.inputs.runs_older_than || env.SCHEDULED_RUNS_OLDER_THAN }}
          runs_to_keep: ${{ github.event.inputs.runs_to_keep || env.SCHEDULED_RUNS_TO_KEEP }}
          dry_run: ${{ github.event.inputs.dry_run || false }}

Advanced behavior

Retention

runs_to_keep and runs_older_than filter independently, then combine:

  1. runs_older_than selects the candidate pool: only runs older than this many days are considered for deletion at all. Anything newer is never touched, regardless of runs_to_keep.
  2. runs_to_keep then retains the N newest runs within that already-old pool, per workflow, and only the remainder is deleted.

In other words, runs_to_keep does not mean "keep the N newest runs overall". A run inside the retention window (newer than runs_older_than) is kept automatically and isn't counted against runs_to_keep at all; runs_to_keep only decides how many of the old runs survive.

Example: with runs_older_than: 30 and runs_to_keep: 5, a workflow with 3 runs newer than 30 days and 20 runs older than 30 days ends up with the 3 recent runs plus the 5 newest of the 20 old ones kept, 15 old runs deleted.

Retry logic

Failed requests are retried with exponential backoff before giving up.

  • Up to 3 retry attempts
  • Backoff: 1s, 2s, 4s
  • Retries on: 5xx server errors, 429 rate limit, transient network failures
  • Does not retry on 4xx client errors (except 429)

Circuit breaker

Prevents the action from hammering an unhealthy API.

  • Opens after 5 consecutive failures
  • Stays open for 60 seconds, then transitions to half-open
  • Half-open allows a probe request; success closes the circuit, failure reopens it
  • Trip count exported via circuit-breaker-trips

Rate limiting

  • Honors GitHub's Retry-After header on 429 responses
  • Deletes in concurrent batches of 20, with a paced delay between batches (~170 deletions/min) to stay under secondary rate limits
  • Rate-limit hits exported via rate-limit-hits

Workflow filtering

  • Comma-separated names (workflow_names: "CI, Deploy")
  • Case-sensitive match against the workflow's name: field
  • Permitted characters: alphanumeric, spaces, dots, dashes, underscores

FAQ

Will this delete workflow runs that are still in progress?

No. The action only ever lists runs with status completed; anything queued or running is never touched.

What does runs_to_keep: 0 do?

It deletes every run inside the runs_older_than window for that workflow, with nothing kept back. Combine both inputs if you want a fixed floor of recent runs per workflow instead.

Can I see what would be deleted before it actually happens?

Yes. Set dry_run: true and the action logs every run it would delete without calling the delete API. See Dry run.

Does one run of this action clean multiple repositories?

No. owner and repo each take a single repository. For multiple repos, add one job (or a matrix) per repository, each pointing at its own owner/repo.

Versioning

Pin one of the following based on how aggressively you want updates:

Tag style Example Behavior
Floating major (recommended) @v8 Auto-updates to the latest v8.x.y patch/minor on each run. Breaking changes only on major bumps.
Specific tag @v8.0.0 Pinned; never moves. Update manually when you want a new version.
Commit SHA @<sha> Strictest pin. Use when you need byte-for-byte reproducibility (Dependabot can still bump this).
@main @main Not recommended for production; pulls whatever is on main at run time.

Releases follow Semantic Versioning and are cut automatically by semantic-release from Conventional Commits: every push to main is analyzed, and a qualifying commit (fix:, feat:, or a breaking change) tags a release and moves the floating major tag with no manual step. See the Releases page for changelogs.

Development

See CONTRIBUTING.md for prerequisites, setup, common commands, and testing details. Shared org-wide contribution rules (DCO, commit conventions, the code style baseline) are one hop further, linked from CONTRIBUTING.md.

Migrating from v7

Breaking

  • days_old has been removed. It was deprecated since v4, and in v7.0.1 it was silently ignored: the action always used runs_older_than regardless of what days_old was set to. GitHub Actions now warns on the undeclared input instead of accepting it silently.
  • result output has been removed. It was never written since v7.0.1; the metrics output has always been runs-deleted.

Upgrade

- uses: igorjs/gh-actions-clean-workflow@v7
+ uses: igorjs/gh-actions-clean-workflow@v8
  with:
-   days_old: 30
+   runs_older_than: 30

- name: Summarize
  run: |
-   echo "Deleted: ${{ steps.clean.outputs.result }}"
+   echo "Deleted: ${{ steps.clean.outputs.runs-deleted }}"

If your workflow already used runs_older_than, no change is needed beyond replacing result with runs-deleted and bumping the version tag.

Migrating from v6

v7 keeps the same input names but adds new ones; existing v6 workflows continue to work.

Breaking

  • Minimum local Node.js version raised to 26.0.0 (only affects contributors; the published action still runs on GitHub's node24 runtime)
  • Internal tooling switched from ESLint + Prettier to Biome, and from Jest/tsup to Vitest + esbuild. End-user surface is unchanged.

New

  • workflow_names input for per-workflow filtering
  • dry_run input for preview mode
  • 9 new metric outputs (total-api-requests, circuit-breaker-trips, etc.)
  • Circuit breaker + retry + rate-limit handling under the hood

Upgrade

- uses: igorjs/gh-actions-clean-workflow@v6
+ uses: igorjs/gh-actions-clean-workflow@v7
  with:
    runs_older_than: 7
+   # Optional new inputs:
+   workflow_names: "CI, Deploy"
+   dry_run: false

Security

If you find a vulnerability, please report it privately to the maintainer rather than opening a public issue. Continuous security scanning runs in CI via OSV-Scanner, betterleaks, OpenSSF Scorecard, and dependency review.

License

MIT

About

GitHub Action to delete old workflow runs, with retry/rate-limit handling, per-workflow filtering, and dry-run mode. Keeps noisy CI histories clean.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

28 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages