GitHub Actions is excellent for CI/CD, but workflow runs accumulate quickly. For active repositories, especially those with pull request validation and matrix builds, you can easily end up with thousands of runs.

While GitHub retains logs according to repository settings, sometimes you want tighter control β€” for example:

  • Keep only the last 10 runs
  • Clean up old runs to reduce clutter
  • Script cleanup as part of maintenance

Using the GitHub CLI (gh), you can do this safely and deterministically.

This article walks through a structured approach:

  1. Preview what will be deleted
  2. Delete all but the latest N runs
  3. Use a compact one-liner
  4. Add an interactive safety prompt

Prerequisites

Make sure:

  • You have the GitHub CLI installed
  • You are authenticated (gh auth login)
  • You are in the target repository directory

On Windows you can install the GitHub CLI using winget:

winget install -e --id GitHub.cli

All examples below use PowerShell.


Step 1 β€” Preview What Will Be Deleted (Recommended)

Before deleting anything, inspect which runs would be removed.

$runs = gh run list --limit 1000 --json databaseId,createdAt `
    | ConvertFrom-Json

$sorted = $runs | Sort-Object {[datetime]$_.createdAt} -Descending
$toDelete = $sorted | Select-Object -Skip 10

$toDelete | Select-Object databaseId, createdAt

What this does

  1. Retrieves up to 1000 workflow runs
  2. Sorts them descending by creation date
  3. Skips the latest 10
  4. Displays the remainder (candidates for deletion)

This gives you a clear, deterministic preview of what will be removed.

Do not skip this step in production repositories.


Step 2 β€” Delete All But the Latest 10

Once you’ve verified the list, perform the cleanup:

$runs = gh run list --limit 1000 --json databaseId,createdAt `
    | ConvertFrom-Json

$sorted = $runs | Sort-Object {[datetime]$_.createdAt} -Descending
$toDelete = $sorted | Select-Object -Skip 10

foreach ($run in $toDelete) {
    gh run delete $run.databaseId
}

Notes

  • databaseId uniquely identifies the run.
  • Deletion is permanent.
  • There is no recycle bin.

If you need retention beyond 1000 runs, you must page manually (see gh run list --help).


Step 3 β€” Clean One-Liner

If you prefer a compact version:

gh run list --limit 1000 --json databaseId,createdAt `
| ConvertFrom-Json `
| Sort-Object {[datetime]$_.createdAt} -Descending `
| Select-Object -Skip 10 `
| ForEach-Object { gh run delete $_.databaseId }

This performs the same logic inline:

  • Fetch
  • Sort
  • Skip newest 10
  • Delete the rest

Use this only after you are confident in the behavior.


Step 4 β€” Interactive Safety Prompt

If you want a manual confirmation per run:

foreach ($run in $toDelete) {
    $confirm = Read-Host "Delete run $($run.databaseId)? (y/n)"
    if ($confirm -eq 'y') {
        gh run delete $run.databaseId
    }
}

This is useful for:

  • First-time cleanup
  • High-visibility repositories
  • Conservative governance environments

Understanding the Mechanics

Why Sort Explicitly?

gh run list does not guarantee stable ordering in all contexts. Sorting by createdAt ensures deterministic behavior.

Sort-Object {[datetime]$_.createdAt} -Descending

This guarantees:

  • The newest runs are always preserved
  • Deletion logic remains predictable

Why Skip Instead of Take?

Using:

Select-Object -Skip 10

ensures:

  • The 10 most recent runs are untouched
  • Everything older becomes a deletion candidate

This pattern is cleaner than filtering by date thresholds because:

  • It is relative
  • It does not depend on time zones
  • It avoids clock skew issues

Automating This in Maintenance

You could:

  • Run this manually once per month
  • Schedule a local task
  • Wrap it in a PowerShell script (cleanup-runs.ps1)
  • Trigger it from a secure admin repository

However:

Do not put destructive cleanup logic directly into CI without guardrails.

If you do automate:

  • Scope it to a single repository
  • Limit deletion count per execution
  • Log all deletions

Alternative: Built-in Retention Policies

GitHub also supports retention settings:

  • Repository β†’ Settings β†’ Actions β†’ Retention
  • Organization-level policies

If your use case is simple, prefer built-in retention over custom deletion.

Use CLI cleanup only when:

  • You need tighter control
  • You want β€œkeep latest N”
  • You’re cleaning up historical clutter

TL;DR

To keep only the latest 10 GitHub Actions runs:

Preview:

gh run list --limit 1000 --json databaseId,createdAt `
| ConvertFrom-Json `
| Sort-Object {[datetime]$_.createdAt} -Descending `
| Select-Object -Skip 10 `
| Select-Object databaseId, createdAt

Delete:

gh run list --limit 1000 --json databaseId,createdAt `
| ConvertFrom-Json `
| Sort-Object {[datetime]$_.createdAt} -Descending `
| Select-Object -Skip 10 `
| ForEach-Object { gh run delete $_.databaseId }

Deterministic. Scriptable. Reversible only by discipline.


Keeping repositories clean is part of operational hygiene. Treat workflow runs as operational data β€” not archival storage.

Happy Coding.