DEV Community

Christo
Christo

Posted on

GitHub starts deleting your Actions run history on October 1. There is no export button.

GitHub's changelog, 27 August:

Starting October 1, 2026, checks, workflow runs, and statuses will be governed by the same Actions retention setting.

Until now those "were retained for 400+ days regardless of your retention configuration". The setting defaults to 90 days, and for public repositories 90 days is also the maximum, "matching the existing limit for artifacts and logs".

So on a public repo, run history older than three months goes. On a private one it goes at whatever that setting says, which for most people is a number nobody has ever opened the page to look at.

The advice in the changelog is one sentence: "Export or archive anything you need to keep beyond your configured retention period, since older checks, workflow runs, and statuses will be automatically removed."

There is no export button.

What actually disappears

Workflow runs, check runs and commit statuses. The metadata, not the logs: what ran, when, against which commit, who triggered it, and whether it passed. Job logs are a separate retention problem and were already capped.

The consequence people are going to notice first is provenance. npm provenance, GitHub artifact attestations and the SLSA generators all embed a run ID, and the thing you verify against is github.com/<org>/<repo>/actions/runs/<id>. Those already expire after 400+ days, which is why a community thread has been open on it since 2024. From October they expire at whatever your retention says. On a public repo that is 90 days, and an attestation you can no longer resolve is an attestation you cannot check.

Two API facts that cost me a day

If you are writing your own exporter, these two are worth having up front.

GET /actions/runs serves at most 1,000 results per search. The docs say it plainly: "This endpoint will return up to 1,000 results for each search when using the following parameters: actor, branch, check_suite_id, created, event, head_sha, status." At 100 per page that is ten pages and then nothing:

$ for p in 9 10 11; do gh api "repos/cli/cli/actions/runs?created=2026-08-01..2026-08-31&per_page=100&page=$p" --jq '.workflow_runs | length'; done
100
100
0
Enter fullscreen mode Exit fullscreen mode

What the docs do not say is that total_count reports the true number anyway:

$ gh api "repos/cli/cli/actions/runs?created=2026-08-01..2026-08-31&per_page=1" --jq .total_count
4067
Enter fullscreen mode Exit fullscreen mode

Four thousand of them in a window that will only ever hand you a thousand. That discrepancy is the useful part: one request tells you a window is over the cap, so you split it in half and recurse. A month becomes two halves, a half becomes two quarters, and you stop when a window reports under 1,000. For that repo, August came apart into windows of roughly a week.

GITHUB_TOKEN gets 1,000 requests per hour per repository. One busy month at 100 runs per page is 40 requests just for the run list, before you have fetched a single check run, and check runs are per commit. A year of a moderately active repo does not fit in one hour. So whatever you write has to resume rather than restart.

The bug I would like to save you from

This is the part I got wrong, and it is the kind of wrong that passes every test.

My exporter capped itself at 800 requests per run to stay inside that 1,000/hour limit. The walk stopped cleanly at the ceiling, wrote a checkpoint, and exited zero. Next night it picked up where it left off. All of that worked.

Except the thing that writes the result also spends API requests. It commits into the repository over the Git Data API: create blobs, create a tree, create a commit, update a ref. So the sequence was: spend all 800 requests walking history, then try to save, then get refused by my own budget.

Every night that actually used its budget threw its work away. On a repo small enough to finish inside 800 requests it was invisible, and sixty tests were green, because the local CLI path writes to a directory and spends nothing. It only showed up when I exercised the Action's real path, where the archive lives on a git ref and both the reads and the commit cost requests, against a ceiling low enough to hit. The run captured hundreds of records and committed nothing at all, and would have done that every night forever.

The fix is a sentence, and it is the generalisable bit: whenever "stop working" and "save what you did" share one quota, they are not the same budget. Persisting data you already fetched has to outrank your self-imposed ceiling. The provider's real rate limit still applies, so you can overshoot your own number and never theirs.

Two smaller versions of the same mistake were sitting underneath it. A window was marked as captured when its pages were fetched rather than when they were stored, so an interruption in between lost those runs for good while the month reported complete. That one cost 150 runs and I only caught it by auditing the committed result against the API rather than trusting my own counters. And dedupe hides all of this beautifully: "made no progress" and "converged" look identical when the second run adds zero rows either way.

If you write one of these, assert monotonic progress across simulated interruptions, on the expensive path, not just "it eventually finished" on the cheap one.

Where I landed

I turned it into an Action. It commits runs, checks and statuses as JSONL into the repository itself, on refs/attic/archive, which is a ref rather than a branch so it stays out of the branch list, out of a normal clone, and out of on: push triggers. Nightly, resumable, one dependency.

name: attic
on:
  schedule: [{ cron: '17 3 * * *' }]
permissions: { actions: read, checks: read, statuses: read, contents: write }
jobs:
  archive:
    runs-on: ubuntu-latest
    steps:
      - uses: Booyaka101/actions-attic@v1
Enter fullscreen mode Exit fullscreen mode

It is at github.com/Booyaka101/actions-attic, MIT, and it is mine, so weigh that accordingly. Two months of cli/cli came out at 7,148 runs, matching what the API reported for both windows at the time I pulled them. That repo runs enough CI that the number has moved since, which is rather the point.

You have until October 1. The backfill only reaches as far as GitHub still has data, so the archive you start in September is bigger than the one you start in November.

Top comments (0)