diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index ac8ed98..66b8e3a 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -1,39 +1,26 @@ + + # Installation Guide This guide walks through deploying coverage-tracker to your own Cloudflare account. -Follow the steps in order — several decisions depend on values produced by earlier steps. +Follow the sections in order — several decisions depend on values produced by earlier steps. --- -## Prerequisites - -- A **Cloudflare account** (free tier is sufficient) -- A **domain managed by Cloudflare** (DNS must be proxied through Cloudflare — see [Domain setup](#1-domain-setup) below) -- 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` - ---- - -## 1. Domain setup - -The Worker must be served from a domain whose DNS is managed by Cloudflare. This is required for both custom domain routing and Cloudflare Access. +## Quick start -If your domain's DNS is hosted elsewhere (e.g. Porkbun, Namecheap, Route 53): +If you already run on Cloudflare, the whole setup is six moves. Each links to its full section below. -1. Go to **Cloudflare dashboard → Websites → Add a domain** -2. Enter your domain → select **Free** plan -3. Cloudflare scans and imports your existing DNS records — review them -4. In your registrar's control panel, replace the nameservers with the two Cloudflare nameservers shown -5. Wait for propagation (usually minutes; Cloudflare emails you when active) +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. -You do **not** need to manually create a DNS record for the Worker subdomain — the deploy step handles that automatically. +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. ---- - -## 2. Clone and install - -```bash +```bash file="clone & install" git clone https://github.com/your-org/coverage-tracker cd coverage-tracker npm install @@ -41,381 +28,227 @@ npm install --- -## 3. Create the D1 database - -`wrangler.json` is already present in the repo. Create your D1 database: - -```bash -npx wrangler d1 create coverage -``` - -Copy the `database_id` from the output and add it to the `d1_databases` entry in `wrangler.json`: - -```jsonc -"d1_databases": [ - { - "binding": "DB", - "database_name": "coverage", - "database_id": "paste-your-id-here", // ← add this line - "migrations_dir": "migrations" - } -] -``` - -> **Note:** The committed `wrangler.json` intentionally omits `database_id` so that the -> [Deploy to Cloudflare](https://deploy.workers.cloudflare.com/?url=https://github.com/CoverageTracker/coverage-tracker) -> button can provision D1 automatically. For manual installs, add the field as shown above. -> The custom domain is attached in the Cloudflare dashboard after first deploy (Step 11) — -> no `routes` entry is needed. - ---- - -## 4. Apply the database migration +## Prerequisites -Apply to your remote D1 database: +- 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`. -```bash -npm run db:migrate:remote -``` +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. --- -## 5. Create the GitHub App - -> **Important:** There are two separate GitHub integrations in this project: -> - A **GitHub App** (this step) — for webhook events and API access -> - A **GitHub OAuth App** (Step 8) — for Cloudflare Access dashboard login -> -> Do not conflate them. Create them separately. - -Go to the account or org where you want to host the app: -- **Personal:** GitHub → Settings → Developer settings → GitHub Apps → **New GitHub App** -- **Org:** GitHub → Your org → Settings → Developer settings → GitHub Apps → **New GitHub App** +## Domain & database -### Required fields +Create your D1 database, then wire its id into `wrangler.json` and apply the schema migration. -| Field | Value | -|-------|-------| -| GitHub App name | Something globally unique, e.g. `your-coverage-tracker` | -| Homepage URL | `https://coverage-tracker.yourdomain.com` | -| Webhook → Active | ✓ checked | -| Webhook URL | `https://coverage-tracker.yourdomain.com/webhooks/github` | -| Webhook secret | Generate with `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` — save this value | - -### Leave these blank / unchecked - -- **Callback URL** — not used (no OAuth user flow) -- **Request user authorization (OAuth) during installation** — leave unchecked -- **Setup URL** — leave blank - -### Permissions (Repository) - -| Permission | Level | -|-----------|-------| -| Metadata | Read-only *(auto-selected)* | -| Checks | Read & write | - -No other permissions are needed. - -### Subscribe to events - -Check both: -- **Installation target** -- **Installation repositories** - -### Where can this app be installed? - -Select **Only on this account** for a private self-hosted instance. - -### After creating - -On the app's settings page, note: -- **App ID** → you'll set this as `GITHUB_APP_ID` -- **Client ID** → you'll set this as `GITHUB_APP_CLIENT_ID` - -Generate a private key: scroll to **Private keys → Generate a private key**. -GitHub downloads a `.pem` file in PKCS#1 format. The Worker requires PKCS#8. - -**Convert the private key** (Node.js, no OpenSSL required): - -```bash -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 > private-key-pkcs8.pem +```bash file="create D1" +npx wrangler d1 create coverage ``` -Or as a one-liner piped directly into wrangler (no file written to disk): +Copy the `database_id` from the output into the `d1_databases` entry of `wrangler.json`: -```bash -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 +```jsonc file="wrangler.json" +{ + // ... + "d1_databases": [ + { + "binding": "DB", + "database_name": "coverage", + "database_id": "paste-your-id-here", // ← add this line + "migrations_dir": "migrations" + } + ], + // ... +} ``` ---- - -## 6. Set Wrangler secrets +Then apply the migration to your remote database: -Set the GitHub App credentials: - -```bash -npx wrangler secret put GITHUB_APP_ID # numeric App ID, 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 above -npx wrangler secret put GITHUB_WEBHOOK_SECRET # the value you generated in step 5 +```bash file="migrate" +npm run db:migrate:remote ``` -The two Cloudflare Access secrets (collect these in Steps 7–8 below, then come back): - -```bash -npx wrangler secret put CF_ACCESS_TEAM_DOMAIN # e.g. myteam.cloudflareaccess.com -npx wrangler secret put CF_ACCESS_AUD # AUD tag UUID from the Access app -``` +> [!NOTE] +> The committed `wrangler.json` intentionally omits `database_id` so the Deploy to Cloudflare button can provision D1 automatically. For manual installs, add the field as shown. The custom domain is attached in the Cloudflare dashboard after first deploy — no `routes` entry is needed. --- -## 7. Set up Cloudflare Zero Trust - -Go to **Cloudflare dashboard → Zero Trust**. - -On first visit, choose a **team name** (e.g. `myteam`). This becomes `myteam.cloudflareaccess.com` — your `CF_ACCESS_TEAM_DOMAIN`. +## GitHub App ---- - -## 8. Create a GitHub OAuth App (for dashboard login) +> [!WARNING] +> **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. -This is separate from the GitHub App created in Step 5. Cloudflare Access uses it to authenticate you when you visit the dashboard. +From the account or org that will host the app, go to **Settings → Developer settings → GitHub Apps → New GitHub App** and fill in: -GitHub → Settings → Developer settings → **OAuth Apps → New OAuth App** + + + + + + + + + +
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:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
-| Field | Value | -|-------|-------| -| Application name | `coverage-tracker-access` (any name) | -| Homepage URL | `https://coverage-tracker.yourdomain.com` | -| Authorization callback URL | `https://myteam.cloudflareaccess.com/cdn-cgi/access/callback` | +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**. -Save the **Client ID** and generate a **Client Secret** — you'll need both in the next step. +### Convert the private key ---- +GitHub downloads a PKCS#1 `.pem`; the Worker requires PKCS#8. Convert it with Node — no OpenSSL needed: -## 9. Add GitHub as an identity provider in Zero Trust - -Zero Trust → **Settings → Authentication → Add new** → select **GitHub** +```bash file="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 +``` -Enter the OAuth App's Client ID and Client Secret from Step 8. +From the app's settings page, note the **App ID** and **Client ID** — you set these as secrets next. --- -## 10. Create the Cloudflare Access application +## Cloudflare Access -Zero Trust → **Access → Applications → Add an application → Self-hosted** +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. -| Field | Value | -|-------|-------| -| App name | `coverage-tracker` | -| Session duration | Your preference (e.g. 24 hours) | -| Identity providers | GitHub (the one added in Step 9) | +You will create **two** Access applications for the same hostname. The Dashboard app redirects visitors through GitHub login and sets a `CF_Authorization` session cookie, which the Worker verifies on subsequent browser API calls; the API Bypass app is more specific (`/api`), so Cloudflare applies it to machine callers — CI OIDC, webhooks, health checks, badges — that cannot complete the browser OAuth flow. -You will create **two** Access applications for the same hostname — one that protects the dashboard and one that lets machine callers reach the API without going through the OAuth flow. + + + + + + +
AppPathPolicy
Dashboard (Allow)blank — whole hostAllow → your email. Copy the AUD tag → CF_ACCESS_AUD
API Bypass/apiBypass → Everyone
-### Application 1 — Dashboard (Allow) +> [!WARNING] +> **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. -This application protects the SvelteKit dashboard. When a user visits the dashboard, Cloudflare redirects them through GitHub login and sets a `CF_Authorization` session cookie. The Worker reads this cookie to verify identity on subsequent browser API calls. - -| Field | Value | -|-------|-------| -| App name | `coverage-tracker` | -| Application domain | `coverage-tracker.yourdomain.com` | -| Path | *(leave blank — protects the entire hostname)* | -| Session duration | Your preference (e.g. 24 hours) | -| Identity providers | GitHub (the one added in Step 9) | +--- -Add a policy: -- **Policy name:** Allow deployer -- **Action:** Allow -- **Rule:** Emails → `your-email@example.com` +## Secrets -After saving, open the application settings and copy the **Application Audience (AUD) Tag** — this is your `CF_ACCESS_AUD`. +Set every value with `wrangler secret put` — secrets are never committed. `wrangler.json` references names only. -### Application 2 — API Bypass +```bash file="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 +``` -The CI runner (OIDC), GitHub webhooks (HMAC), health checks, and public badge endpoints are machine callers that cannot complete the browser OAuth flow. Create a second application that bypasses Access for all `/api` paths. Because the bypass application is more specific than the root application, Cloudflare applies it to all requests under `/api` while the root application continues to protect the dashboard. +> [!WARNING] +> **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. -| Field | Value | -|-------|-------| -| App name | `coverage-tracker-api` | -| Application domain | `coverage-tracker.yourdomain.com` | -| Path | `/api` | -| Identity providers | *(none required)* | +--- -Add a policy: -- **Policy name:** Bypass machine callers -- **Action:** Bypass -- **Rule:** Everyone +## Deploy the Worker -> The Worker enforces its own auth in code for every `/api/*` route: GitHub Actions OIDC for -> `/api/ci/coverage` and `/api/baseline/*`, HMAC for `/api/webhooks/github`, and -> Cloudflare Access JWT (from the session cookie) for `/api/projects/*` and `/api/admin/*`. -> No `/api/*` route is unprotected — the bypass only removes the edge OAuth redirect so -> non-browser clients can reach the Worker. +Make sure dashboard dependencies are installed, then deploy. The command applies pending D1 migrations and compiles the SvelteKit dashboard before uploading. -Now go back and set the two Access secrets if you haven't already: +```bash file="deploy" +npm --prefix dashboard install +npm run deploy -```bash -npx wrangler secret put CF_ACCESS_TEAM_DOMAIN -npx wrangler secret put CF_ACCESS_AUD +# Deployed coverage-tracker triggers +# coverage-tracker.yourdomain.com (custom domain) ``` ---- - -## 11. Deploy the Worker +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): -```bash -npm run deploy +```bash file="WAF skip rules" +CLOUDFLARE_API_TOKEN= ZONE_DOMAIN=yourdomain.com \ + node scripts/setup-waf-rules.mjs ``` -Expected output: - -``` -Deployed coverage-tracker triggers - coverage-tracker.yourdomain.com (custom domain) -``` +--- -> **Note:** `npm run deploy` applies any pending D1 migrations and then compiles the SvelteKit -> dashboard (via the `build.command` in `wrangler.json`) before uploading. Make sure -> dashboard dependencies are installed (`npm --prefix dashboard install`) before running -> this step. +## Install & verify -### Add WAF skip rules (if Bot Fight Mode is enabled) +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. -**This is separate from the Access Bypass application you created in Step 10.** The Access Bypass app removes the OAuth redirect for machine callers. Bot Fight Mode is a zone-level WAF feature that blocks non-browser HTTP clients entirely — it fires before Access and cannot be bypassed by Access policy alone. +Confirm the webhook landed: -If you have **Bot Fight Mode** or **Browser Integrity Check** enabled on your Cloudflare zone, run the provided script to add a WAF skip rule for `/api/ci/coverage` and `/api/webhooks/github`: +```bash file="verify" +npx wrangler d1 execute DB --remote \ + --command "SELECT * FROM owners" -```bash -CLOUDFLARE_API_TOKEN= ZONE_DOMAIN=yourdomain.com \ - node scripts/setup-waf-rules.mjs +npx wrangler d1 execute DB --remote \ + --command "SELECT full_slug, default_branch, badge_enabled FROM projects" ``` -The script is idempotent — safe to re-run. It requires a token with **Zone → WAF → Edit** permission. If Bot Fight Mode is off, skip this step. +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 `owners` is empty, the webhook was not received or failed before reaching the database — check the Worker logs with `npx wrangler tail`. If `owners` has rows but `projects` is empty, the App was likely installed with **All repositories** selected and the payload contained no repo list — trigger a manual resync (the installation id is the number at the end of the app's **Configure** URL): -## 12. Install the GitHub App on your repos - -Go to your GitHub App's settings page → **Install App** → select the account or org → choose the repos you want to track (or all repos). - -This fires an `installation: created` webhook to your Worker, which populates the `owners` and `projects` tables. +```bash file="resync" +curl -X POST https://coverage-tracker.yourdomain.com/api/admin/resync \ + -H "Cf-Access-Jwt-Assertion: " \ + -H "Content-Type: application/json" \ + -d '{"installationId": YOUR_INSTALLATION_ID}' +``` -> **If the webhook returns 500:** Check the Worker logs with `npx wrangler tail`, fix the -> issue, clear the failed delivery from the dedup table, then redeliver: +> [!NOTE] +> **If the webhook returns 500** +> Fix the issue shown by `npx wrangler tail`, clear the failed delivery from the dedup table, then redeliver from GitHub App → **Advanced → Recent Deliveries → Redeliver**: > > ```bash > npx wrangler d1 execute DB --remote \ > --command "DELETE FROM webhook_deliveries WHERE delivery_id = 'THE-DELIVERY-ID'" > ``` -> -> Then go to GitHub App → Advanced → Recent Deliveries → Redeliver. ---- +With both tables populated, the Worker is ready to accept metrics — have CI produce a coverage report and let the reporting Action pick it up. See [Generating coverage reports](https://coveragetracker.dev/docs#generating-coverage-reports) for the per-language commands. -## 13. Verify +--- -Run these two queries to confirm the webhook was received and processed correctly. +## Ingest from CI -**Owners table** — should show the GitHub account or org the App was installed on: +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. -```bash -npx wrangler d1 execute DB --remote --command "SELECT * FROM owners" +```bash file="upload step" +coverage-tracker upload ./lcov.info ``` -Expected output (one row per account): - -``` -┌────┬───────────┬───────────┬──────────────┬────────────┬─────────────────────┐ -│ id │ github_id │ login │ type │ avatar_url │ created_at │ -├────┼───────────┼───────────┼──────────────┼────────────┼─────────────────────┤ -│ 1 │ 128944512 │ YourOrg │ Organization │ https://… │ 2026-06-24 04:09:40 │ -└────┴───────────┴───────────┴──────────────┴────────────┴─────────────────────┘ -``` - -If this table is empty, the webhook was not received or failed before reaching the database. Check the Worker logs with `npx wrangler tail`, then see the troubleshooting note in Step 12. +The reporting Action accepts LCOV, Cobertura XML, JaCoCo XML, or Go's native coverage profile from any CI — Jest, Vitest, pytest-cov, go test, JaCoCo, SimpleCov. See [Generating coverage reports](https://coveragetracker.dev/docs#generating-coverage-reports) for the per-language commands. Trend history is append-only; PR jobs read baselines but never write. --- -**Projects table** — should show one row per registered repo: +## Status badges -```bash -npx wrangler d1 execute DB --remote \ - --command "SELECT full_slug, default_branch, badge_enabled FROM projects" -``` +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`. -Expected output: +```bash file="enable badge" +# find the project id +npx wrangler d1 execute DB --remote \ + --command "SELECT id, full_slug FROM projects" -``` -┌────────────────────────────────────┬────────────────┬───────────────┐ -│ full_slug │ default_branch │ badge_enabled │ -├────────────────────────────────────┼────────────────┼───────────────┤ -│ YourOrg/repo-one │ main │ 0 │ -├────────────────────────────────────┼────────────────┼───────────────┤ -│ YourOrg/repo-two │ main │ 0 │ -└────────────────────────────────────┴────────────────┴───────────────┘ +# enable the public badge endpoint +curl -X PATCH …/api/admin/projects/1/badge \ + -H "Cf-Access-Jwt-Assertion: " \ + -d '{"enabled": true}' ``` -`badge_enabled` is `0` (off) by default for all repos — this is intentional. See **Next steps** for how to opt a repo into the public badge endpoint. +Then drop the shields.io endpoint badge into your README: -If the owners table has a row but projects is empty, the GitHub App was likely installed with **All repositories** selected but the webhook payload contained no repo list. Trigger a manual resync: - -```bash -# Find your installation ID in the owners table output or in: -# GitHub → Your org → Settings → Third-party Access → GitHub Apps → Configure -# The installation ID is the number at the end of the URL. - -curl -X POST https://coverage-tracker.yourdomain.com/api/admin/resync \ - -H "Cf-Access-Jwt-Assertion: " \ - -H "Content-Type: application/json" \ - -d '{"installationId": YOUR_INSTALLATION_ID}' +```md file="README.md" +![coverage](https://img.shields.io/endpoint?url=https://coverage-tracker.yourdomain.com/api/badge/owner/repo/coverage.json) ``` -If both tables have rows, the installation is complete and the Worker is ready to accept metric ingestion from CI. - --- -## 14. Dashboard - -The SvelteKit dashboard in `dashboard/` is compiled by `wrangler deploy` automatically (via the `build.command` in `wrangler.json`) and served as static assets by the same Worker. There is no separate Cloudflare Pages project. - -**After first deploy**, navigate to `https://coverage-tracker.yourdomain.com` — Cloudflare Access will prompt you to log in with the identity provider you configured in Step 9. Once authenticated, the dashboard loads and shows all registered repos. +## Dashboard -If the dashboard returns a blank page or 404, check: -- The SvelteKit build completed without errors (`npm --prefix dashboard run build` locally) -- The `assets.directory` in `wrangler.json` points to `./dashboard/build` -- The `run_worker_first: ["/api/*"]` setting is present (so non-API paths serve the SPA) - -The dashboard is served from the same domain and behind the same Cloudflare Access application you created in Step 10. No separate Pages project or additional Access app is needed. - ---- +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. -## Next steps - -- **Ingest metrics from CI:** Add a workflow to your repos that posts coverage data to `/api/ci/coverage` using a GitHub Actions OIDC token. See the reporting Action docs (`.github/actions/report/`). -- **Enable badges:** Opt individual repos into the public badge endpoint: - ```bash - # Find the project ID first - npx wrangler d1 execute DB --remote --command "SELECT id, full_slug FROM projects" - - # Enable badge for a specific project - curl -X PATCH https://coverage-tracker.yourdomain.com/api/admin/projects/1/badge \ - -H "Cf-Access-Jwt-Assertion: " \ - -H "Content-Type: application/json" \ - -d '{"enabled": true}' - ``` - Then add the badge to your repo's README: - ```markdown - ![Coverage](https://img.shields.io/endpoint?url=https://coverage-tracker.yourdomain.com/api/badge/YourOrg/repo-name/coverage.json) - ``` -- **Resync if repos drift:** If you add/remove repos from the GitHub App installation and the webhook is missed, trigger a manual resync via `POST /api/admin/resync` with `{"installationId": YOUR_INSTALLATION_ID}`. +> [!NOTE] +> **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. diff --git a/docs/generating-coverage-reports.md b/docs/generating-coverage-reports.md new file mode 100644 index 0000000..244375e --- /dev/null +++ b/docs/generating-coverage-reports.md @@ -0,0 +1,101 @@ + + +# Generating coverage reports + +The reporting Action does not run your tests or install coverage tools. Your CI step produces a report file (LCOV, Cobertura XML, JaCoCo XML, or Go's native coverage profile); the Action reads it. This page shows the command for each supported language. Coverage Tracker not deployed yet? Start with the [Installation Guide](https://github.com/CoverageTracker/coverage-tracker/blob/main/docs/INSTALLATION.md). + +Format is detected automatically from file content — you don't set it explicitly: + +- First line `mode: set|count|atomic` → Go coverage profile +- Starts with `TN:` / `SF:` → LCOV +- XML root `` → Cobertura +- XML root `` → JaCoCo + +`coverage-tool` is only required when the format resolves to Cobertura — it's the generator name, used to correct for known differences between Cobertura writers (see the Cobertura quirks table below). + +## Coverage + +| Language | Tool | Format | Command | Default path | +|---|---|---|---|---| +| Go | `go tool cover` | native profile | `go test -coverprofile=coverage.out ./...` | `coverage.out` | +| Python | coverage.py | LCOV | `coverage run -m pytest && coverage lcov -o coverage.lcov` | `coverage.lcov` | +| JS/TS | Istanbul (nyc / vitest / jest) | LCOV | `vitest run --coverage --coverage.reporter=lcov` | `coverage/lcov.info` | +| Rust | cargo-llvm-cov | LCOV | `cargo llvm-cov --lcov --output-path lcov.info` | `lcov.info` | +| C/C++ | gcovr | LCOV | `gcovr --lcov -o coverage.lcov` | `coverage.lcov` | +| C# | coverlet | LCOV | `dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov` | `coverage.info` | +| Java | JaCoCo | JaCoCo XML | `mvn test jacoco:report` (Maven) or `./gradlew jacocoTestReport` (Gradle) | `target/site/jacoco/jacoco.xml` / `build/reports/jacoco/test/jacocoTestReport.xml` | +| Bash | kcov | Cobertura | `kcov --include-path=. coverage/ ./script.sh` | *dynamic — set `coverage-path`* | +| Clojure | Cloverage | LCOV | `lein cloverage --lcov` | `target/coverage/lcov.info` | +| Dart | Flutter test / `coverage` pkg | LCOV | `flutter test --coverage` | `coverage/lcov.info` | +| Elixir | ExCoveralls | LCOV | `mix coveralls.lcov` | `cover/lcov.info` | +| Erlang | covertool | Cobertura | `rebar3 do eunit, cover, covertool generate` | *dynamic — set `coverage-path`* | +| Haskell | hpc + hpc-codecov | LCOV | `cabal test --enable-coverage && hpc-codecov cabal:all -f lcov -o lcov.info` | `lcov.info` | +| Lua | LuaCov + `luacov-reporter-lcov` | LCOV | `luacov -r lcov` | `luacov.report.out` | +| Perl | Devel::Cover + lcov's `perl2lcov` | LCOV | `cover -test && perl2lcov -o coverage.lcov` | `coverage.lcov` | +| PHP | PHPUnit | Cobertura | `XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-cobertura=coverage.xml` | `coverage.xml` | +| Ruby | SimpleCov + `simplecov-lcov` | LCOV | (configure `SimpleCov::Formatter::LcovFormatter` in `spec_helper.rb`) then `rspec` | `coverage/lcov.info` | + +> [!NOTE] +> **Go is parsed directly** +> Go is read from its native coverage profile — no LCOV/Cobertura conversion, so no accuracy loss. + +> [!WARNING] +> **JaCoCo isn't Cobertura** +> Java's JaCoCo XML is a different schema, not a dialect of Cobertura XML. It's parsed with its own module in the reporter, not the Cobertura path. + +## Automatic report discovery + +The "Default path" columns on this page are real probe targets, not just conventions. When `coverage-path` is unset, the Action probes these paths in a fixed order and uses the first hit: + +1. `coverage.out` +2. `coverage/lcov.info` +3. `lcov.info` +4. `coverage.lcov` +5. `coverage.info` +6. `cover/lcov.info` +7. `target/coverage/lcov.info` +8. `target/site/jacoco/jacoco.xml` +9. `build/reports/jacoco/test/jacocoTestReport.xml` +10. `coverage.xml` +11. `luacov.report.out` + +If nothing is found, the Action fails with an error that lists every path it probed. An explicit `coverage-path` input always wins over probing. + +> [!NOTE] +> **kcov and covertool need an explicit path** +> Their default output paths contain dynamic segments (the script name, the app name) — write the file wherever the tool puts it and point `coverage-path` at it. + +## Complexity and duplication (optional) + +Only coverage is required. Complexity and duplication are opt-in: set the path explicitly (`complexity-path` / `duplication-path`), or write the report to the default location below and the Action picks it up automatically. + +| Metric | Tool | Command | Default path | +|---|---|---|---| +| Complexity — Go | gocyclo | `gocyclo -avg ./... > gocyclo.txt` | `gocyclo.txt` | +| Complexity — Python | Radon | `radon cc -j . > radon.json` | `radon.json` | +| Complexity — everything else | [Lizard](https://github.com/terryyin/lizard) | `lizard --xml > lizard-report.xml` | `lizard-report.xml` | +| Duplication — any language | [jscpd](https://github.com/kucherenko/jscpd) | `npx jscpd . --reporters json --output ./jscpd-report` | `jscpd-report/jscpd-report.json` | + +There is no `complexity-tool` input — Radon (JSON), gocyclo (plain text), and Lizard (XML) are recognized by content shape. When probing finds more than one complexity file, precedence is `radon.json` → `gocyclo.txt` → `lizard-report.xml`; an explicit `complexity-path` overrides probing entirely. + +> [!WARNING] +> **Thresholds make reports mandatory** +> If `max-complexity` or `max-duplication` is configured and no report file for that metric is found — neither at the input path nor at a default location — the Action **fails** with an actionable error. A metric is skipped silently only when it is both unconfigured and absent. + +> [!NOTE] +> **Java gets complexity for free** +> JaCoCo's `COMPLEXITY` counter is already in the coverage report — no separate step needed for Java. An explicit `complexity-path` (or a probed complexity file) overrides the JaCoCo-derived value. + +## Cobertura quirks + +Cobertura XML is a shared DTD, not an enforced spec — generators disagree on two things the reporter corrects for based on `coverage-tool`: + +| `coverage-tool` | Trust `branch-rate`? | Notes | +|---|---|---| +| `gocover-cobertura` | No — always `0` | Go's block-based coverage can't map to branches | +| `kcov` | Yes | | +| `covertool` | Yes | | +| `phpunit` | Yes | | +| `gcovr` | Yes | | + +If your `coverage-tool` isn't listed, the reporter treats `branch-rate` as trustworthy by default — open an issue if that's wrong for your generator.