HomeDocumentation

Documentation

Everything you need to deploy Coverage Tracker to your own Cloudflare account, install the GitHub App, and start pushing metrics from CI.

// getting started

Overview

Coverage Tracker is 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.

It runs entirely on your own Cloudflare account: a single Worker serves both the dashboard SPA and the API, backed by a D1 database. Your data stays in your own database — no SaaS, no subscriptions, no third-party access to your metrics.

Self-hosted by design

One instance per deployer, not multi-tenant. A single Worker and one wrangler.json on one apex domain. Everything below assumes you are deploying your own instance.

// architecture

How it works

The flow is four moving parts: your CI pushes metrics, the Worker validates and stores them, D1 keeps the history, and the dashboard reads trends back.

  1. Install the GitHub App

    On the repos you want to track. The Worker registers them automatically via an installation webhook.

  2. Push from CI

    A reporting step runs after your test suite, collects coverage / complexity / duplication numbers, and posts them using a GitHub Actions OIDC token — no static secret.

  3. View trends

    The dashboard, served as static assets by the same Worker, is protected by Cloudflare Access so only you can see it.

  4. Embed a badge

    Optionally opt a repo into the public, shields.io-compatible badge endpoint and drop the Markdown into your README.

Request routing

Only /api/* hits the Worker first; everything else is served asset-first as a single-page app. Each API route enforces its own auth in code.

routing
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 App HMAC
GET  /api/health        ← public
*                       ← dashboard SPA (static assets)
// the short version

Quick start

If you already run on Cloudflare, the whole setup is six moves. Each links to its full section below.

  1. Add your domain to Cloudflare (DNS proxied through Cloudflare).
  2. Create the D1 database and apply the migration.
  3. Create a GitHub App (webhooks + API access).
  4. Create a GitHub OAuth App (Cloudflare Access login).
  5. Configure Cloudflare Zero Trust, set secrets, and deploy the Worker.
  6. Install the GitHub App on your repos.

Or skip the manual route entirely — the Deploy to Cloudflare button provisions the Worker and D1 automatically. You still complete the GitHub App, Zero Trust, and secrets steps afterward.

clone & install
git clone https://github.com/your-org/coverage-tracker
cd coverage-tracker
npm install
// installation

Prerequisites

  • A Cloudflare account (free tier is sufficient).
  • A domain managed by Cloudflare — DNS must be proxied through Cloudflare.
  • A GitHub account (personal or org) with admin access to the repos you want to track.
  • Node.js 18+ and npm installed locally.
  • Wrangler authenticated: npx wrangler login.

If your domain's DNS lives elsewhere, add the domain in the Cloudflare dashboard, pick the Free plan, and replace the registrar's nameservers with the two Cloudflare provides. You do not need to create a DNS record for the Worker subdomain — the deploy step handles that.

// installation

Domain & database

Create your D1 database, then wire its id into wrangler.jsonc and apply the schema migration.

create D1
npx wrangler d1 create coverage

Copy the database_id from the output into the d1_databases entry of wrangler.jsonc:

wrangler.jsonc jsonc
"d1_databases": [
  {
    "binding": "DB",
    "database_name": "coverage",
    "database_id": "paste-your-id-here",   // ← add this line
    "migrations_dir": "migrations"
  }
]

Then apply the migration to your remote database:

migrate
npm run db:migrate:remote
Note

The committed wrangler.jsonc intentionally omits database_id so the Deploy to Cloudflare button can provision D1 automatically. For manual installs, add the field as shown.

// installation

GitHub App

Two separate integrations

The GitHub App (this step) handles webhook events and API access. The GitHub OAuth App (next section) handles dashboard login via Cloudflare Access. Create them separately — do not conflate them.

From the account or org that will host the app, go to Settings → Developer settings → GitHub Apps → New GitHub App and fill in:

FieldValue
GitHub App nameGlobally unique, e.g. your-coverage-tracker
Homepage URLhttps://coverage-tracker.yourdomain.com
Webhook → Activechecked
Webhook URL…/webhooks/github
Webhook secretGenerate 32 random bytes — save this value

Leave Callback URL, OAuth during installation, and Setup URL blank. Under repository permissions set Metadata: read-only and Checks: read & write — nothing else. Subscribe to both Installation target and Installation repositories events. For a private instance, choose Only on this account.

Convert the private key

GitHub downloads a PKCS#1 .pem; the Worker requires PKCS#8. Convert it with Node — no OpenSSL needed:

convert key
node -e "const c=require('crypto'), fs=require('fs');
const key=c.createPrivateKey(fs.readFileSync(process.argv[1],'utf8'));
process.stdout.write(key.export({type:'pkcs8',format:'pem'}));" \
  your-app.private-key.pem | npx wrangler secret put GITHUB_APP_PRIVATE_KEY

From the app's settings page, note the App ID and Client ID — you set these as secrets next.

// installation

Cloudflare Access

In Zero Trust, choose a team name (becomes myteam.cloudflareaccess.com). Then create a GitHub OAuth App for dashboard login with callback URL https://myteam.cloudflareaccess.com/cdn-cgi/access/callback, and add GitHub as an identity provider in Settings → Authentication using that OAuth App's client id and secret.

You will create two Access applications for the same hostname:

AppPathPolicy
Dashboard (Allow)blank — whole hostAllow → your email. Copy the AUD tagCF_ACCESS_AUD
API Bypass/apiBypass → Everyone
Critical invariant

Never put an Access Allow policy on /api/*. Machine callers (CI OIDC, webhooks, health) must reach the Worker unauthenticated at the edge — API auth is enforced in code. The bypass only removes the edge OAuth redirect; no /api/* route is left unprotected.

// installation

Secrets

Set every value with wrangler secret put — secrets are never committed. wrangler.json references names only.

wrangler secrets
npx wrangler secret put GITHUB_APP_ID          # numeric, e.g. 1234567
npx wrangler secret put GITHUB_APP_CLIENT_ID   # starts with "Iv23…"
npx wrangler secret put GITHUB_APP_PRIVATE_KEY # if not piped earlier
npx wrangler secret put GITHUB_WEBHOOK_SECRET  # from the GitHub App step
npx wrangler secret put CF_ACCESS_TEAM_DOMAIN  # myteam.cloudflareaccess.com
npx wrangler secret put CF_ACCESS_AUD          # AUD tag UUID from Access app
Never set in production

DEV_BYPASS_SECRET belongs only in .dev.vars for local dev. Setting it via wrangler secret put silently disables all Access JWT verification.

// installation

Deploy the Worker

Make sure dashboard dependencies are installed, then deploy. The command applies pending D1 migrations and compiles the SvelteKit dashboard before uploading.

deploy
npm --prefix dashboard install
npm run deploy

# Deployed coverage-tracker triggers
#   coverage-tracker.yourdomain.com (custom domain)

If Bot Fight Mode or Browser Integrity Check is enabled on your zone, add WAF skip rules for the machine-caller routes (this is separate from the Access bypass — Bot Fight Mode fires before Access):

WAF skip rules
CLOUDFLARE_API_TOKEN=<token> ZONE_DOMAIN=yourdomain.com \
  node scripts/setup-waf-rules.mjs
// installation

Install & verify

From the GitHub App's settings page → Install App → choose the account or org → select repos. This fires an installation: created webhook that populates the owners and projects tables.

Confirm the webhook landed:

verify
npx wrangler d1 execute DB --remote \
  --command "SELECT * FROM owners"

npx wrangler d1 execute DB --remote \
  --command "SELECT full_slug, default_branch, badge_enabled FROM projects"

One row per account in owners and one per repo in projects means the install is complete. badge_enabled is 0 by default — opt in per repo below. If a table is empty, check npx wrangler tail; if owners has rows but projects is empty, trigger a manual resync via POST /api/admin/resync.

// usage

Ingest from CI

Add a workflow step that runs after your test suite and posts coverage to /api/ci/coverage using a GitHub Actions OIDC token. There is no static ingest secret: the Worker verifies the token signature and checks the repository claim against your registered projects, so only your repos can push data. Re-running CI for the same commit is a safe no-op.

upload step
coverage-tracker upload ./lcov.info

The reporting Action accepts lcov or cobertura reports from any CI — Jest, Vitest, pytest-cov, go test, JaCoCo, SimpleCov. Trend history is append-only; PR jobs read baselines but never write.

// usage

Status badges

Badge numbers are opt-in per repo. Find the project id, enable it, then paste the snippet into your README. Available metrics: coverage, complexity, duplication.

enable badge
# find the project id
npx wrangler d1 execute DB --remote \
  --command "SELECT id, full_slug FROM projects"

# enable the public badge endpoint
curl -X PATCH …/api/admin/projects/1/badge \
  -H "Cf-Access-Jwt-Assertion: <token>" \
  -d '{"enabled": true}'

Then drop the shields.io endpoint badge into your README:

README.md md
![coverage](https://img.shields.io/endpoint?url=
  https://coverage-tracker.yourdomain.com
  /api/badge/owner/repo/coverage.json)
coverage94%
// usage

Dashboard

The SvelteKit dashboard is compiled by wrangler deploy automatically and served as static assets by the same Worker — there is no separate Pages project. After first deploy, visit https://coverage-tracker.yourdomain.com; Cloudflare Access prompts you to log in with the identity provider you configured. Once authenticated, the dashboard shows all registered repos and their per-metric, per-branch trend charts.

Blank page or 404?

Check the SvelteKit build completed, that assets.directory points to ./dashboard/build, and that run_worker_first: ["/api/*"] is set so non-API paths serve the SPA.

// reference

API reference

Method & pathAuthDescription
POST /api/ci/coverageOIDCPush typed coverage metrics from CI
GET /api/projectsAccessList all registered owners and repos
GET /api/projects/:owner/:repo/metricsAccessTrend data for one repo
GET /api/baseline/:owner/:repoOIDCLatest value on default branch
GET /api/badge/:owner/:repo/:metric.jsonPublicshields.io endpoint (opt-in repos)
POST /api/webhooks/githubHMACGitHub App installation events
POST /api/admin/resyncAccessReconcile projects against GitHub
PATCH /api/admin/projects/:id/badgeAccessToggle badge visibility
GET /api/healthPublicLiveness check
// reference

Ingest payload

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.

POST /api/ci/coverage json
{
  "line_coverage": 82.4,
  "branch_coverage": 79.1,
  "cyclomatic": 4.2,
  "cognitive": 2.1,
  "duplication_pct": 1.8,
  "maintainability": 95.0
}