Community health metrics for a GitHub repository, built as static files. Implements CHAOSS metric definitions over Python and DuckDB, and renders a dashboard you can host on GitHub Pages.
No server, no database to run, no SaaS.
A maintainer can see stars and open issue counts. What they cannot see is whether anybody is waiting on them, whether newcomers who show up ever come back, and how much of the project rests on one person.
Those are the questions this measures.
→ drkrillo.github.io/oss-health-metrics
Regenerated daily against drkrillo/good-first-issues.
| page | question |
|---|---|
| Waiting On You | which open items owe somebody a reply, and for how long |
| Weekly Pulse | WIP, throughput and cycle time as a time series (Little's Law) |
| Time to First Response | median hours to a first human reply, by window and item type |
| Contributors | the funnel from fork to repeat contributor, and per-account velocity |
Four CHAOSS metrics are implemented directly: Time to First Response, Contributor Absence Factor, Change Request Duration and Closure Ratio, and Contributor Conversion Rate.
Implementing a metric definition means deciding what it does not say. These three changed the numbers enough to be worth stating up front:
A merge counts as a first response. Counting only comments dropped 64 of 114 pull requests, the fastest ones, and published the median of what was left.
Conversion levels are not nested. CHAOSS developer levels D0/D1/D2 describe independent cohorts: somebody can comment without ever forking, or land a merge without a single comment. Accumulating them sums disjoint populations.
Response debt is not the same as ownership. An item a maintainer opened and nobody answered is backlog, not somebody being kept waiting. Reading only the last actor's role misfiles every one of them.
| this | GitHub Insights | issue-metrics | OpenSauced | |
|---|---|---|---|---|
| Who has the ball, per open item | yes | no | no | no |
| Contributor timeline, 6 event types | yes | commits only | no | no |
| Time to first response as a trend | yes | no | snapshot | no |
| Little's Law / cycle time | yes | no | no | no |
| Contributor funnel | yes | no | no | partial |
| Per-account velocity signals | yes | no | no | no |
| Runs with no infrastructure | yes | n/a | yes | no, SaaS |
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # set GITHUB_TOKEN and GITHUB_REPOThen run the three stages:
PYTHONPATH=src python src/extract.py # GitHub API -> data/raw/*.csv
PYTHONPATH=src python src/transform.py # CSVs -> data/oss_health.duckdb
PYTHONPATH=src python -m src.render # DuckDB -> output/*.htmlGITHUB_REPO is one owner/repo. A dashboard describes a single repository: every
mart aggregates over whatever is in the CSVs, so two repositories in one warehouse
would share a funnel, a median and a bus factor that describe neither of them.
GITHUB_TOKEN is a personal access token. For a public repository it needs no
scopes at all: a token with nothing ticked still raises the API limit from 60
requests an hour to 5,000, and one run costs a few hundred. Private repositories
need the repo scope.
.github/workflows/refresh-dashboard.yml refreshes the dashboard daily and
publishes it to Pages. Fork the repository, enable Actions, and set the
TARGET_REPO variable to the repository you want to measure.
You do not need to create a token for this. Actions mints its own GITHUB_TOKEN
per run, which reads any public repository. Add a personal access token as the
ANALYSIS_TOKEN secret only if you need one of these:
- the repository you measure is private (the built-in token cannot read it)
- the repository is large: the built-in token allows 1,000 requests an hour, a personal one 5,000
The workflow picks up ANALYSIS_TOKEN when it exists and falls back to the
built-in token when it does not.
Three stages, and none of them knows about the others.
extract.py GitHub REST, paginated, rate-limit aware -> CSV
transform.py staging views -> mart tables (DuckDB) -> .duckdb
render/ DuckDB -> Plotly + hand-written HTML -> output/*.html
SQL lives in sql/, never inside Python. Staging views type the raw CSVs; ten mart
tables hold the metrics. Adding a metric means adding a .sql file and a chart
builder. See CONTRIBUTING.md.
python -m pytest -q88 tests. They do not chase coverage: each one pins an invariant a metric can
violate while still rendering a plausible chart. The SQL tests run the real
Transformer over a small fixture repository using the production queries, so
breaking a mart fails here.
Apache-2.0. See LICENSE.