A command-line tool that measures how a GitHub organization actually delivers software: who contributes, which repositories are busy, and where the DORA metrics land.
The example above is generated from synthetic data by
scripts/generate_report_image.py.
Commits, lines added and deleted, pull requests opened, reviews given, and review comments — per person, over the chosen window.
Bot accounts are excluded, as is anything listed in exclude_users or --exclude-user — useful for AI agent accounts, which GitHub does not mark as bots. So are contributors who changed no lines, unless you pass --include-inactive (useful when you care about reviewers).
Line counts skip individual commits above 10,000 added lines, and the report says how many were dropped. Those commits are lockfiles, vendored dependencies, and generated output: in one real organization, 10 commits out of 3,921 accounted for 58% of every line added, against a median commit of 85 lines. The commits still count towards commit totals — it is only their line counts that are set aside. Tune with --bulk-commit-lines, or pass 0 to count everything.
Commits, pull requests, lead time, deployment counts and failure rate, mean time to restore, primary language, branch and contributor counts. Repositories with no activity in the window are omitted.
| Metric | How it is derived here |
|---|---|
| Lead time for changes | Hours from the first commit on a pull request's branch to the merge |
| Deployment frequency | Runs of the repository's deployment workflow, per day |
| Change failure rate | Share of those runs that concluded in failure |
| Time to restore service | Hours from a failed run to the next successful one |
Each is rated against the published DORA bands (Elite / High / Medium / Low). See Accuracy and limitations for what these approximations can and cannot tell you.
Requires uv and Python 3.12+.
git clone https://github.com/tre-systems/github-org-metrics.git
cd github-org-metrics
uv syncCreate a fine-grained personal access token with read-only access to the organization:
| Scope | Permission | Access |
|---|---|---|
| Repository | Actions | Read-only |
| Repository | Contents | Read-only |
| Repository | Metadata | Read-only |
| Repository | Pull requests | Read-only |
| Organization | Members | Read-only |
Then export it:
export GITHUB_TOKEN=your_token_hereGH_TOKEN is also accepted, so an existing gh CLI setup works. No token is needed when reading from the cache.
uv run github-metrics <organization> [options]Analyse the last three months:
uv run github-metrics my-orgLook further back:
uv run github-metrics my-org --months 6Keep a first run on a large organization cheap:
uv run github-metrics my-org --repos 10 --fastNarrow to specific repositories:
uv run github-metrics my-org --target-repos api-service web-appRe-analyse cached data with logins hidden:
uv run github-metrics my-org --use-cache --anonymize| Option | Description | Default |
|---|---|---|
--months N |
Calendar months to analyse | 3 |
--days N |
Days to analyse, instead of --months |
- |
--compare-previous |
Also analyse the preceding window and show how each metric moved | off |
--repos N |
Analyse only the N most recently active repositories | all |
--target-repos A B C |
Analyse specific repositories by name | - |
--fast |
Skip per-pull-request reviews, comments, and branch history | off |
--deploy-workflow [REPO=]NAME |
Treat this workflow as the deployment; repeatable, per repository or for all | inferred |
--strict-deployments |
Count only deployment-shaped workflows, never a repository's CI workflow | off |
--workers N |
Concurrent API requests | 8 |
--output-dir DIR |
Where to write CSVs and the cache | current directory |
--no-csv |
Skip writing CSV files | off |
--export-svg FILE |
Also save the console report as an SVG image | - |
--anonymize |
Replace logins with stable pseudonyms everywhere | off |
--bulk-commit-lines N |
Exclude commits adding more than this many lines from developer totals (0 counts everything) | 10,000 |
--exclude-user LOGIN |
Leave an account out entirely; repeatable | - |
--include-inactive |
Include contributors who changed no lines | off |
--use-cache |
Analyse cached data instead of calling the API | off |
--update-cache |
Fetch fresh data and rewrite the cache | off |
--no-cache |
Do not write a cache file | off |
--config FILE |
Settings file to read | .github-metrics.toml |
-v, --verbose / -q, --quiet |
Raise or lower log detail | - |
A single snapshot cannot answer the question the DORA metrics exist for: is delivery getting better or worse? --compare-previous runs the same analysis over the window immediately before this one and shows the movement.
uv run github-metrics my-org --months 1 --compare-previousLead time for changes 16.4 h ↑ 16.2 h High
Deployment frequency 4.43 / day ↓ 1.43 /day Elite
Change failure rate 10.5 % ↑ 4.8 pp Elite
Time to restore service 6.9 h ↑ 5.9 h High
Compared with 26 May 2026 to 25 Jun 2026: 191 commits (-19), 16 pull requests (-4), 1 active developer (no change)
Improvements are green and regressions red, per metric — deploying more often is progress, taking longer to recover is not. It doubles the data fetched, since the earlier window has to be collected too.
Some settings describe an organization rather than a run: which workflow deploys each repository, which accounts are automation, how large a commit has to be before it is generated output. Put those in .github-metrics.toml next to the project instead of retyping them:
bulk_commit_lines = 10000
exclude_users = ["cursoragent", "release-bot"]
strict_deployments = false
[deploy_workflows]
default = "Deploy"
api = "Release"
web = "Publish"Under [deploy_workflows], each key is a repository name and default is reserved for the fallback applied to the rest.
Command-line flags always win over the file. Point at a different file with --config FILE; unknown settings are reported and ignored, and a malformed one stops the run rather than being silently half-applied.
The cache is personal data. <org>_github_data_cache.json holds raw API responses — logins, commit authorship, timestamps, pull request titles — for everyone active in the window. A three-month run over 35 repositories produced 68 MB of it. The CSV exports name individuals too.
- Both are covered by
.gitignore, but nothing stops you copying them elsewhere. Treat them like any other export of colleague data. --anonymizecovers the console and the CSVs, not the cache. That is deliberate: re-analysing cached data needs the real logins. Use--no-cachewhen you want nothing durable on disk.- Delete caches you are no longer using; there is no retention policy and the tool will never remove one for you.
- Consider what you are measuring people for before you run it. See the note on lines changed under Accuracy and limitations.
Two CSV files are written alongside the console report.
<org>_github_developer_metrics.csv — Developer, Commits, Lines Added, Lines Deleted, PRs Opened, PRs Reviewed, PR Comments, Repositories.
<org>_github_repository_metrics.csv — Repository, Commits, PRs, Lead Time (h), Deploys, Fail %, MTTR (h), Deploy (m), Deploy Workflow, Created, Updated, Language, Branches, Contributors.
--anonymize applies to the CSVs as well as the console, so an anonymised run leaks no logins anywhere.
Raw API responses are cached to <org>_github_data_cache.json, which makes re-analysis instant and offline:
uv run github-metrics my-org --use-cache --months 1The cache records the window it was fetched for. Asking for a wider window than the cache covers produces a warning rather than a quietly incomplete report — re-run with --update-cache to refill it.
Commit line counts are batched through GraphQL, 50 commits per request, falling back to the REST per-commit endpoint for anything GraphQL cannot resolve. Branch and contributor counts take one request each rather than paginating a list to length it. What remains is up to three requests per pull request for reviews, comments, and branch history; these are issued concurrently (--workers), and pagination stops as soon as it walks past the window.
--fast skips the per-pull-request calls entirely. Reviews and comments are then reported as N/A, and lead time falls back to each pull request's creation date, which understates it by however long the branch existed beforehand.
The tool waits out primary rate limits, honours Retry-After on secondary ones, and retries transient server errors with backoff, so a long run survives being throttled.
Long runs are also resumable. Progress is checkpointed to the cache every ten repositories and whenever a run is interrupted, and re-running the same command continues from that checkpoint rather than starting over. Only an unfinished run is resumed — a completed cache is a finished answer, and --use-cache is how you ask for it.
-
Deployments are inferred, not observed. GitHub has no universal notion of a deployment, so the busiest deployment-shaped workflow (
deploy,release,publish) on the default branch stands in for one, falling back to a repository's CI workflow. The report names the workflow it counted for each repository and dims the name where it was only a CI fallback, and the deployment frequency says how many repositories that applies to. A repository that deploys outside GitHub Actions reports no deployments.That fallback is generous: a green CI run is not a release. Correct it per repository, or refuse to guess:
uv run github-metrics my-org --deploy-workflow api=Release --deploy-workflow web=Publish
uv run github-metrics my-org --strict-deployments
-
Lead time excludes branches older than 90 days, and time-to-restore excludes gaps longer than 7 days. Both are real events, but a branch abandoned for a quarter is not a lead time, and a pipeline left red for a fortnight is an abandoned workflow rather than a fortnight-long outage. Failures beyond the cap are still counted in the change failure rate, and reported alongside the recovery figure.
-
Line counts come from GitHub's own commit stats. Commits above
--bulk-commit-linesare excluded to keep generated and vendored files from swamping the totals, but everything below it still counts, including large reformats. -
Commits are counted on the default branch only, which is what the commits endpoint returns.
-
Lines changed is not productivity. Neither is commit count. These numbers are useful for seeing where activity and review load are concentrated; they make poor performance targets, and they will be gamed if used as such.
uv run pytestuv run ruff check . && uv run ruff format --check . && uv run mypyA pre-commit config runs the same lint and format checks on commit:
uv run pre-commit installThe layout keeps the analysis testable without a network:
| Module | Responsibility |
|---|---|
client.py |
GitHub REST access: pagination, retries, rate limits |
collect.py |
Which endpoints to call, and concurrency |
analyze.py |
Pure aggregation of raw payloads into metrics |
dora.py |
DORA performance bands |
report.py |
Console tables and CSV export |
cache.py |
Versioned on-disk cache and checkpoints |
config.py |
Optional .github-metrics.toml settings |
cli.py |
Argument handling and orchestration |
Regenerate the README image after changing the console output:
uv run scripts/generate_report_image.py"GitHub rejected the token (401)" — the token is missing, expired, or malformed. Re-export GITHUB_TOKEN.
"Permission denied" warnings — the token lacks a scope for that endpoint. Metrics depending on it report no data; the rest of the run continues.
"Repositories not found or inaccessible" — the names passed to --target-repos do not exist in the organization, or the token cannot see them.
A metric shows "no data" — nothing was observed to measure it, as opposed to a value of zero. Deployment metrics need a GitHub Actions workflow; lead time needs merged pull requests.
MIT.