A self-hosted dashboard that tracks code coverage, cyclomatic complexity, and code duplication across your GitHub repositories — with trend charts, per-PR diff checks, and README badges.
Runs entirely on your own Cloudflare account (Worker + D1). Your data stays in your own database. No SaaS, no subscriptions, no third-party access to your metrics.
Catppuccin |
Gruvbox |
Nord |
Solarized |
Dracula |
Tokyo Night |
Note
The button deploys the Worker and provisions the D1 database automatically. You still need to complete the GitHub App, Cloudflare Zero Trust, and secrets setup described in docs/INSTALLATION.md.
- Install the GitHub App on the repos you want to track. The Worker registers them automatically via webhook.
- Add a workflow step to your CI that runs the reporting Action after your test suite. You run your tests and coverage tools; the Action reads the report file they produce (LCOV, Cobertura, JaCoCo, or the Go coverage profile — auto-detected) and pushes the numbers to the Worker using a GitHub Actions OIDC token — no static secrets.
- View trends in the dashboard (served as static assets by the Worker), protected by Cloudflare Access so only you can see it.
- Optionally embed a badge in your README.
┌─────────────────────────────────────────────────────────┐
│ Your CI (GitHub Actions) │
│ │
│ run tests → write report → Action reads it → POST │
│ /api/ci/coverage (OIDC token, no static secret) │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Cloudflare Worker (coverage-tracker.yourdomain.com) │
│ │
│ POST /api/ci/coverage ← OIDC-verified, project-scoped │
│ GET /api/projects/* ← Cloudflare Access JWT │
│ GET /api/badge/* ← public (per-project opt-in) │
│ POST /api/webhooks/* ← GitHub HMAC │
│ POST /api/admin/* ← Cloudflare Access JWT │
│ GET /api/health ← public │
│ * ← dashboard SPA (static assets) │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Cloudflare D1 (your SQLite database) │
│ owners · projects · coverage_runs · coverage_daily │
└─────────────────────────────────────────────────────────┘
- No static ingest secret — CI authenticates with a GitHub Actions OIDC token. The Worker verifies the signature and checks the
repositoryclaim against your registered projects, so only your registered repos can push data. - Private by default — all data routes are behind Cloudflare Access. Badge numbers are opt-in per repo.
- Append-only metrics — trend history is never modified. PR jobs read baselines but never write.
- Multi-repo — one instance tracks all the repos you install the GitHub App on.
- Idempotent ingest — re-running CI for the same commit is a safe no-op.
- shields.io badge format — drop a badge into any README with a one-liner.
See docs/INSTALLATION.md for the full setup guide. The short version:
- Add your domain to Cloudflare (DNS must be proxied through Cloudflare)
- Create the D1 database and apply the migration
- Create a GitHub App (for webhooks + API access)
- Create a GitHub OAuth App (for Cloudflare Access login)
- Configure Cloudflare Zero Trust and deploy the Worker (the dashboard SPA deploys with it automatically)
- Install the GitHub App on your repos
Then add the reporting Action to each repo's CI. Run your tests first; the Action auto-detects the coverage report from the common default paths — zero config:
# .github/workflows/coverage.yml
permissions:
id-token: write # OIDC token for Worker auth
checks: write # PR Check Run
steps:
- uses: actions/checkout@v4
- run: npm test -- --coverage # writes coverage/lcov.info
- uses: CoverageTracker/coverage-tracker/.github/actions/report@v0.4.0
with:
worker-url: https://coverage-tracker.yourdomain.com
# coverage-path is optional — auto-detected. Thresholds are optional too:
# min-coverage: '80'
# max-coverage-drop: '2'The Action no longer runs your tests, coverage tools, or jscpd — run those yourself and point it at (or let it auto-detect) the report. See docs/generating-coverage-reports.md for the per-language commands and default paths.
Once a repo has metrics ingested and badge_enabled is turned on, add this to your README:
The endpoint returns shields.io endpoint format, so you can use the shields.io URL builder to customise the label and style:
Available metric names: coverage, complexity, duplication.
To enable the badge for a project:
# Find the project ID
npx wrangler d1 execute DB --remote --command "SELECT id, full_slug FROM projects"
# Enable badge
curl -X PATCH https://coverage-tracker.yourdomain.com/api/admin/projects/1/badge \
-H "Cf-Access-Jwt-Assertion: <your-access-token>" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'.
├── migrations/ # D1 schema migrations (0001 owners/projects, 0002 coverage_runs/daily)
├── src/
│ ├── index.ts # Hono app entry point + scheduled cron handler
│ ├── types.ts # Bindings and shared types
│ ├── middleware/
│ │ ├── access.ts # Cloudflare Access JWT verification
│ │ ├── oidc.ts # GitHub Actions OIDC JWT verification
│ │ └── webhook.ts # GitHub webhook HMAC + replay protection
│ ├── lib/
│ │ ├── db.ts # D1 query helpers
│ │ ├── github.ts # GitHub App token minting
│ │ ├── metrics.ts # Metric name → column mapping
│ │ └── resync.ts # Installation reconciliation
│ ├── db/
│ │ └── rollup.ts # Daily coverage_runs → coverage_daily rollup + prune
│ └── routes/
│ ├── ci.ts # POST /api/ci/coverage
│ ├── baseline.ts # GET /api/baseline/:owner/:repo
│ ├── api.ts # GET /api/projects/*
│ ├── badge.ts # GET /api/badge/*
│ ├── webhooks.ts # POST /api/webhooks/github
│ └── admin.ts # POST /api/admin/*
├── dashboard/ # SvelteKit 5 source; builds to dashboard/build/ (served by Worker)
│ └── src/routes/ # Overview + per-repo drill-in views
├── .github/
│ ├── actions/report/ # Node reporting Action (parse reports + ingest + Check Runs)
│ └── workflows/ # CI: action-test.yml, deploy.yml
├── scripts/
│ └── setup-waf-rules.mjs # WAF skip rule for /api/ci/coverage + /api/webhooks/github
├── test/
│ └── seed-local.sql # Local D1 seed data
├── docs/
│ ├── INSTALLATION.md # Setup guide
│ ├── PROGRESS.md # Phase implementation status
│ └── plans/ # Design documents
├── wrangler.example.jsonc # Config template — copy to wrangler.json and fill in
└── .dev.vars.example # Local secrets template — copy to .dev.vars and fill in
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/ci/coverage |
OIDC token | Push typed coverage metrics from CI |
GET |
/api/projects |
Access | List all registered owners and repos |
GET |
/api/projects/:owner/:repo/metrics |
Access | Trend data for one repo |
GET |
/api/baseline/:owner/:repo |
OIDC token | Latest value on default branch (threshold checks) |
GET |
/api/badge/:owner/:repo/:metric.json |
Public | shields.io endpoint — only for badge_enabled repos |
POST |
/api/webhooks/github |
GitHub HMAC | GitHub App installation events |
POST |
/api/admin/resync |
Access | Reconcile projects table against GitHub |
PATCH |
/api/admin/projects/:id/badge |
Access | Toggle badge visibility |
GET |
/api/health |
Public | Liveness check |
{
"line_coverage": 82.4,
"branch_coverage": 79.1,
"cyclomatic": 4.2,
"cognitive": 2.1,
"duplication_pct": 1.8,
"maintainability": 95.0
}line_coverage is required; all other fields are optional. repository, branch, and commit_sha are derived from the OIDC token claims — they are not accepted in the body.
npm install
npm --prefix dashboard install # install dashboard dependencies
cp wrangler.example.jsonc wrangler.json # fill in your D1 database ID
cp .dev.vars.example .dev.vars # fill in local secrets
npm run db:migrate:local # apply schema to local D1
npm run dev # start Worker + SPA (builds dashboard first)Type-check:
npm run typecheckDeploy:
npm run db:migrate:remote
npm run deploySee docs/PROGRESS.md for a full breakdown. Current state:
| Phase | Description | Status |
|---|---|---|
| 1–6 | Core Worker, webhooks, dashboard, reporting Action | Complete |
| — | Convergence refactor (single Worker + static assets) | Complete |
| 7 | "Deploy to Cloudflare" button | In progress |
| 8 | Docs, OSS hygiene, public release | In progress |
Copyright (c) 2026 coverage-tracker contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.