Deletes old workflow runs from a repository, or only their logs and artifacts. You choose the retention in days, how many of the newest runs always stay, and optional filters on actor, branch, event and status. A dry run lists what would go without deleting anything.
name: GitHub Maintenance
on:
workflow_dispatch:
schedule:
- cron: "0 3 * * *"
jobs:
maintenance:
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- uses: viascom/github-maintenance-action@v1
with:
retention_days: 31
keep_minimum_runs: 5Run it once with dry_run: true first. The log and the job summary show every run it would
touch.
- Lists the newest
keep_minimum_runsruns of the repository. Those are never touched. - Lists the runs created on or before today minus
retention_days(UTC), one GitHub query per combination of theactors,branches,eventsandstatusesvalues. GitHub returns at most 1000 runs per query, so a repository with more old runs than that is cleaned over several scheduled runs. - Drops the kept runs, the run that is executing the action, queued and running runs unless
statusesnames them, and, withkeep_pull_requests, every run triggered bypull_requestorpull_request_target. - For each remaining run: deletes its logs if
delete_logsis set, its artifacts ifdelete_artifactsis set, and the whole run if neither is set. It waits one second between deletions, as GitHub asks for mutating requests. - Writes the counts to the step outputs and the job summary. If any single deletion failed, the step fails after all runs were processed, so a missing permission cannot pass silently.
| Input | Default | Description |
|---|---|---|
github_token |
${{ github.token }} |
Token with actions: write on the repository. |
repository |
${{ github.repository }} |
Repository to clean, as owner/repo. |
retention_days |
31 |
Runs created more than this many days ago are candidates. 0 means every run. |
keep_minimum_runs |
5 |
The newest runs that always stay, whatever their age. |
github_base_url |
https://api.github.com |
REST API base URL. For GitHub Enterprise Server: https://HOST/api/v3. |
delete_logs |
false |
Delete only the logs of matching runs. |
delete_artifacts |
false |
Delete only the artifacts of matching runs. |
actors |
Comma-separated logins. Only runs triggered by these users are candidates. | |
branches |
Comma-separated branch names. | |
events |
Comma-separated trigger events, for example push,schedule. |
|
statuses |
Comma-separated statuses or conclusions: completed, success, failure, cancelled, skipped, timed_out, action_required, neutral, stale, in_progress, queued, requested, waiting, pending. |
|
keep_pull_requests |
false |
Keep runs triggered by pull_request and pull_request_target. |
dry_run |
false |
Log what would be deleted and delete nothing. |
debug |
false |
Log every API request. |
Filters are optional. An empty filter means no restriction on that dimension. With
delete_logs and delete_artifacts both set, the run itself stays and loses its logs and
artifacts. An input with a value GitHub does not accept, such as statuses: failed, fails the
step before anything is listed.
| Output | Description |
|---|---|
runs_deleted |
Workflow runs deleted, or that would be deleted in a dry run. |
logs_deleted |
Run logs deleted, or that would be deleted in a dry run. |
artifacts_deleted |
Artifacts deleted, or that would be deleted in a dry run. |
failures |
Operations that failed. The step fails when this is not 0. |
Keep the runs but reclaim storage by removing logs and artifacts older than a week:
- uses: viascom/github-maintenance-action@v1
with:
retention_days: 7
keep_minimum_runs: 20
delete_logs: true
delete_artifacts: trueRemove failed and cancelled runs of scheduled workflows on main after three days, keep
pull request runs:
- uses: viascom/github-maintenance-action@v1
with:
retention_days: 3
branches: main
events: schedule
statuses: failure,cancelled
keep_pull_requests: trueClean another repository with a fine-grained personal access token that has
Actions: Read and write on it:
- uses: viascom/github-maintenance-action@v1
with:
github_token: ${{ secrets.MAINTENANCE_TOKEN }}
repository: viascom/other-repositoryThe job needs permissions: actions: write. Listing runs and artifacts needs actions: read,
which write includes. Deleting a run also removes its logs and artifacts.
The GITHUB_TOKEN is limited to 1000 API requests per hour per repository, and GitHub applies
secondary limits to bursts of deletions. The action honours the retry-after and
x-ratelimit-reset headers: when a request is rate limited it waits as instructed and retries
up to three times before counting the operation as failed.
Releases follow semantic versioning. @v1 follows the newest 1.x release, @v1.0.0 pins one.
Every release ships a Docker image under the same version to ghcr.io/viascom/github-maintenance-action
and docker.io/viascom/github-maintenance-action, and action.yml at a release tag references
that image by digest, not by tag. A pinned action therefore runs the same image bytes on every
run, whatever happens to the tags in the registry.
Kotlin 2.4 on Java 25 (Zulu), Spring Boot 4.1, OkHttp 5 and Gson. Tests use JUnit 5, MockK,
atrium and MockWebServer; ./gradlew check runs them with the JaCoCo coverage gate.
./gradlew build
docker build -f docker/Dockerfile -t github-maintenance-action .
docker run --rm \
-e INPUT_GITHUB_TOKEN="$(gh auth token)" \
-e INPUT_REPOSITORY=viascom/github-maintenance-action \
-e INPUT_DRY_RUN=true \
github-maintenance-actionThe Actions runner hands inputs to a Docker container action as INPUT_<NAME> variables, so
running the image directly behaves exactly like the action.
To release, set the version in build.gradle, merge, and push the tag vX.Y.Z. The release
workflow verifies the version, runs the build, pushes the multi-arch image, pins action.yml to
the digest of that image in a commit it pushes to main, moves the tags vX.Y.Z and vX onto
that commit, and publishes the GitHub release notes. When main moved on since the tag, the
tags stay on the release commit and main gets its own pin commit, as long as main still
declares the same version. GitHub creates a new
container package as private, so after the first release set the package
ghcr.io/viascom/github-maintenance-action to public, or no workflow can pull the image.
MIT, Viascom Ltd liab. Co.