Skip to main content

runkeep

Archive a GitHub repo's CI history to a local SQLite file before GitHub's new 90-day retention window starts applying to it on Oct 1, 2026.

CI PyPI

demo

the same session as text
$ runkeep check pallets/click

  pallets/click

  total workflow runs   3,661
  older than 90 days    2,680
  oldest run            2025-07-13  (14 months ago)

  2,680 runs are outside the 90-day retention window.
  GitHub starts applying that window to run history on Oct 1, 2026.

  archive it:  runkeep rescue pallets/click

$ runkeep rescue pallets/click --since 2026-08-27 --db click.db

  pallets/click  ->  click.db

  workflow runs     45   (45 hydrated)
  check suites      49
  check runs        276
  legacy statuses   15
  third-party       4 suites, 0 checks
  missing           0

  236 KB written in 7s

  Core CI history      complete
  Third-party checks   complete

$ runkeep verify click.db pallets/click --sample 8

  verify pallets/click  (click.db)

  archive invariant   49/49 suites consistent
  suites vs GitHub    8/8 match  (spot-check)
  runs vs GitHub      4/4 match  (spot-check)
  core archive        complete
  third-party checks  complete

  OK  (15.7s)

(All output in this README is from real runs. Counts are live — yours will differ.)

Why

On 2026-08-27 GitHub announced that starting October 1, 2026, checks, workflow runs, and statuses will be governed by the same Actions retention setting — a maximum of 90 days for public repositories, which cannot be raised. Today that history is kept well beyond 90 days, so a lot of it is about to age out. runkeep copies it into a local SQLite file you keep.

Install

uvx runkeep check astral-sh/ruff

That's the zero-install path — uv downloads runkeep, runs it, and throws it away. To keep it around:

pipx install runkeep      # isolated
pip install runkeep       # or plain pip

Python 3.10+. No dependencies.

Commands

runkeep check OWNER/REPO

How much CI history the repo is about to lose. Read-only, usually a few seconds, no token needed for public repos.

$ runkeep check astral-sh/ruff

  astral-sh/ruff

  total workflow runs   ~150k
  older than 90 days    ~104k
  oldest run            2025-07-12  (14 months ago)

  ~104k runs are outside the 90-day retention window.
  GitHub starts applying that window to run history on Oct 1, 2026.

  archive it:  runkeep rescue astral-sh/ruff

  counts >= 5,000 are GitHub's live search estimates (they drift a few %).

GitHub's unfiltered run count is capped at 40,000, so check reconstructs the real total from two created= date-window counts. Those search counts wobble a few percent on large ranges, so counts at or above 5,000 are shown rounded, with a ~.

runkeep rescue OWNER/REPO [--since DATE] [--until DATE] [--db FILE] [--no-thirdparty]

Archive the history into SQLite (<repo>.db by default). It discovers every workflow run by recursively slicing created= time windows, hydrates each run's check suite and check runs via GraphQL, collects legacy commit statuses, and enumerates independent third-party check suites.

  • Resumable. Interrupt it (Ctrl-C, dropped connection, laptop lid) and run the same command again — it reads its own progress out of the SQLite file and continues. Completed time slices, hydrated runs, probed commits, and hydrated third-party suites are never redone.
  • --since 2026-01-01 bounds the backfill. A bare runkeep rescue OWNER/REPO walks the repo's entire surviving history (this can be slow and page-heavy for very large repos — bound it with --since).
  • Two independent verdicts. Core CI history — workflow runs, their check suites/runs, and legacy statuses — is what "the archive is usable" means. Third-party checks (CodSpeed, Codecov, Renovate, ...) are captured by default but treated as optional: if GitHub times out enumerating them on a huge repo, runkeep shrinks its batch size, records the commits it still couldn't reach, and finishes the core archive anyway. Re-run rescue to retry those gaps. --no-thirdparty skips them entirely.
$ runkeep rescue elastic/elasticsearch --since 2026-08-27 --until 2026-08-27 --db es.db
...
  Core CI history      complete
  Third-party checks   incomplete (2 commits could not be queried)

  The archive is usable, but third-party check coverage is incomplete.
  Re-run rescue to retry those gaps.

rescue needs a token (see Is this safe?).

runkeep verify FILE.db OWNER/REPO

A trust check. It:

  1. re-checks the archive's internal invariant on every suite — stored check-run count equals the recorded expected count;
  2. re-queries a random sample of suites and runs against live GitHub through a separate code path (direct REST filter=all pagination), so a bug in rescue can't hide behind a matching bug in its own verifier; and
  3. reports the archive's own core / third-party completeness flags.

It is a spot-check plus an invariant, not a mathematical re-verification of every remote record. A run that GitHub has since deleted is reported as "deleted at source", not a failure.

What it saves / what it does not

Saved (full fidelity) Not saved
Workflow runs — number, attempt, event, status, conclusion, SHA, branch, actor, timestamps, URL Job step details
Check suites — status, conclusion, timestamps, commit, branch Check run annotations
Check runs — all attempts (filter=all, never latest) Build logs
App identity — which GitHub App produced each suite Artifacts
Legacy commit status contexts — context, state, description, target URL Per-attempt job timing
Independent third-party check suites + their check runs (best-effort on very large repos)
The relationships between all of the above

Logs and artifacts are deliberately out of scope. For history old enough to matter here they are almost always already deleted at source by GitHub's existing 90-day artifact/log window. If you need those, download them separately while they still exist.

runkeep is not a complete GitHub or GitHub Actions backup, it does not preserve logs or artifacts, and verify does not re-verify every archived record against GitHub. Full detail and how each guarantee is checked: docs/fidelity.md.

Is this safe?

Yes, by construction:

  • Read-only. Every request is a GET or a read-only GraphQL query. runkeep never issues a mutation and refuses to send any GraphQL document containing mutation.
  • Your token is never printed, logged, or written to disk — not into the SQLite file, not into --json output, nowhere. It is read from $GITHUB_TOKEN (or $GH_TOKEN) and used only as an Authorization header.
  • The only host contacted is api.github.com. No telemetry, no analytics, no other network egress.
  • The archive is a plain SQLite file you own. Open it with any SQLite tool.

Token scopes. check needs no token for public repos. rescue and verify do:

Token type What to grant
Classic PAT public_repo for public repos, or repo for private ones
Fine-grained PAT Repository access to the target repo, then Read-only for Actions, Checks, Commit statuses, and Metadata. If a call fails with a permission error, it names the one to add.

Do I even need this?

Maybe not.

  • Private repositories: you can raise the Actions retention period to up to 400 days in Settings → Actions → General → Artifact and log retention (GitHub docs). If 400 days is enough, do that instead — nothing to install or maintain.
  • Public repositories: the limit is 90 days and cannot be raised (GitHub docs). A local archive is the only option.
  • Changing the retention setting only affects new runs — it does not bring back history that has already aged past the limit.

runkeep check tells you how much is actually at stake for a given repo before you decide.

Schema

One SQLite file (WAL mode). The tables you'll query:

Table One row per Key columns
workflow_run workflow run database_id, node_id, run_number, run_attempt, event, status, conclusion, head_sha, head_branch, actor_login, created_at, hydrated
check_suite check suite database_id, workflow_run_id (NULL = independent third-party), app_id, conclusion, head_sha, checkrun_total_count, checkrun_complete
check_run check run database_id, check_suite_id, name, status, conclusion, started_at, completed_at, app_slug
app GitHub App database_id, slug, name
status_context (commit, legacy status context) commit_sha, context, state, description, target_url
commit_status_probe commit checked for legacy statuses commit_sha, has_status, context_count
hydration_gap anything not fully archived kind, ref, detail — empty for a complete archive
discovery_slice, thirdparty_probe, archive_meta resume bookkeeping / metadata —
-- failure rate by workflow, last 90 days in the archive
SELECT workflow_name,
       count(*)                                                  AS runs,
       round(100.0 * sum(conclusion = 'failure') / count(*), 1)  AS pct_failed
FROM workflow_run
WHERE created_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now', '-90 days')
GROUP BY workflow_name
ORDER BY runs DESC;
-- slowest check runs ever recorded
SELECT cr.name,
       r.run_number,
       round((julianday(cr.completed_at) - julianday(cr.started_at)) * 1440, 1) AS minutes
FROM check_run cr
JOIN check_suite s  ON s.database_id = cr.check_suite_id
JOIN workflow_run r ON r.database_id = s.workflow_run_id
WHERE cr.completed_at IS NOT NULL AND cr.started_at IS NOT NULL
ORDER BY minutes DESC
LIMIT 20;
-- which non-Actions apps post checks on this repo, and how often
SELECT a.slug, a.name, count(*) AS suites
FROM check_suite s
JOIN app a ON a.database_id = s.app_id
WHERE a.slug <> 'github-actions'
GROUP BY a.slug
ORDER BY suites DESC;

License

MIT — see LICENSE.

Metadata

Release files for runkeep 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for runkeep 0.1.0
File Size Uploaded
runkeep-0.1.0.tar.gz 52.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for runkeep 0.1.0
File Interpreter ABI Platform
runkeep-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 96.1 kB

Release files / runkeep-0.1.0.tar.gz

Download URL runkeep-0.1.0.tar.gz
Size 52.1 kB
Tags Source
SHA-256 checksum
How to use checksums
e35ed599f3cd05e75075411ab0702512aa45b7d5738c58886ac0ee42f460d207
BLAKE2b-256 checksum
How to use checksums
517eb29fa59f00e50045b0bcef4566fe72ee983e86935e0f7a2e1829efa0ff2a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release files / runkeep-0.1.0-py3-none-any.whl

Download URL runkeep-0.1.0-py3-none-any.whl
Size 44.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5143c2b6d904fe622773c2107082f44b1434f287745ed31c61c8ecc9f66de4cb
BLAKE2b-256 checksum
How to use checksums
bbc40255f7a680254525d85c04eb30f7e9c3ed8bee28f5032cd57d0cd2797af1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page