From edf32901af9563b87eb6235e83c65f74968942dd Mon Sep 17 00:00:00 2001 From: Torgny Bjers Date: Wed, 1 Jul 2026 23:29:03 -0400 Subject: [PATCH] =?UTF-8?q?Add=20generating-coverage-reports=20docs=20page?= =?UTF-8?q?=20and=20.svx=20=E2=86=92=20.md=20export=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Plan B of the consumer-generated-reports migration: - New docs section "Generating coverage reports" (usage group): per-language report commands, format auto-detection incl. Go's native profile, probe order for default paths, fail-vs-skip threshold rule, Cobertura quirks - scripts/export-docs.ts renders configured .svx files into GitHub markdown for the coverage-tracker repo: docs/generating-coverage-reports.md and docs/INSTALLATION.md (composed from the quick-start/installation/usage sections) — this repo is now the origin of truth for both - .github/workflows/export-docs.yml opens/updates a docs-sync PR against CoverageTracker/coverage-tracker on pushes to main touching .svx sources - Installation .svx sections enriched with downstream-only detail (webhook secret generation, CF_Authorization explanation, webhook-500 troubleshooting, resync example) and corrected wrangler.jsonc → .json - remark-tables plugin styles GFM pipe tables with the existing .deftable look; cross-links added between the installation guide and the new page - README documents the pipeline and COVERAGE_TRACKER_SYNC_TOKEN PAT setup Co-Authored-By: Claude Fable 5 --- .github/workflows/export-docs.yml | 57 ++++ CLAUDE.md | 10 +- README.md | 42 +++ ...mer-generated-reports-migration-plan-v3.md | 313 ++++++++++++++++++ package.json | 1 + scripts/export-docs.test.ts | 141 ++++++++ scripts/export-docs.ts | 155 +++++++++ src/lib/docs-content/05-domain-database.svx | 8 +- src/lib/docs-content/06-github-app.svx | 2 +- src/lib/docs-content/07-cloudflare-access.svx | 2 +- src/lib/docs-content/10-verify.svx | 21 +- src/lib/docs-content/11-ingest.svx | 2 +- .../12-generating-coverage-reports.svx | 99 ++++++ .../{12-badges.svx => 13-badges.svx} | 6 +- .../{13-dashboard.svx => 14-dashboard.svx} | 0 .../docs-content/{14-api.svx => 15-api.svx} | 0 ...gest-payload.svx => 16-ingest-payload.svx} | 0 src/lib/docs-content/remark-plugins.test.ts | 20 ++ src/lib/docs-content/remark-tables.js | 17 + svelte.config.js | 3 +- vitest.config.ts | 2 +- 21 files changed, 887 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/export-docs.yml create mode 100644 docs/plans/consumer-generated-reports-migration-plan-v3.md create mode 100644 scripts/export-docs.test.ts create mode 100644 scripts/export-docs.ts create mode 100644 src/lib/docs-content/12-generating-coverage-reports.svx rename src/lib/docs-content/{12-badges.svx => 13-badges.svx} (81%) rename src/lib/docs-content/{13-dashboard.svx => 14-dashboard.svx} (100%) rename src/lib/docs-content/{14-api.svx => 15-api.svx} (100%) rename src/lib/docs-content/{15-ingest-payload.svx => 16-ingest-payload.svx} (100%) create mode 100644 src/lib/docs-content/remark-tables.js diff --git a/.github/workflows/export-docs.yml b/.github/workflows/export-docs.yml new file mode 100644 index 0000000..5453707 --- /dev/null +++ b/.github/workflows/export-docs.yml @@ -0,0 +1,57 @@ +# Docs export pipeline: renders the configured .svx files into plain markdown +# and opens (or updates) a PR against CoverageTracker/coverage-tracker. +# +# This repo is the origin of truth for shared docs content — the generated +# files downstream (docs/generating-coverage-reports.md, docs/INSTALLATION.md) +# must never be edited directly. See README.md § Docs export pipeline for the +# COVERAGE_TRACKER_SYNC_TOKEN setup. +name: Export docs + +on: + push: + branches: [main] + paths: + - 'src/lib/docs-content/*.svx' + - 'scripts/export-docs.ts' + - '.github/workflows/export-docs.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + export: + runs-on: ubuntu-latest + steps: + - name: Checkout coveragetracker.dev + uses: actions/checkout@v4 + + - name: Checkout coverage-tracker + uses: actions/checkout@v4 + with: + repository: CoverageTracker/coverage-tracker + token: ${{ secrets.COVERAGE_TRACKER_SYNC_TOKEN }} + path: target-repo + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Export docs + run: node --experimental-strip-types scripts/export-docs.ts target-repo + + # Fixed branch name so repeated runs update the same PR; skips cleanly + # (no commit, no PR) when the export produced no diff. + - name: Open or update sync PR + uses: peter-evans/create-pull-request@v7 + with: + path: target-repo + token: ${{ secrets.COVERAGE_TRACKER_SYNC_TOKEN }} + branch: docs-sync + commit-message: 'docs: sync generated docs from coveragetracker.dev' + title: 'docs: sync generated docs from coveragetracker.dev' + body: | + Automated docs sync from [coveragetracker.dev](https://github.com/CoverageTracker/coveragetracker.dev) (`${{ github.sha }}`). + + The files in this PR are generated from `src/lib/docs-content/*.svx` — do not edit them here; edit the `.svx` sources instead. diff --git a/CLAUDE.md b/CLAUDE.md index e8f2cc5..6272174 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,9 +10,17 @@ pnpm build # production build → .svelte-kit/cloudflare pnpm preview # preview production build locally pnpm check # svelte-kit sync + svelte-check (type checking) pnpm deploy # build + wrangler pages deploy +pnpm test # vitest (remark plugins + docs exporter) +pnpm export-docs # render .svx → downstream markdown ``` -There are no tests in this repo. +## Docs are exported downstream + +`src/lib/docs-content/*.svx` is the origin of truth for shared docs. `scripts/export-docs.ts` +renders configured files into `docs/generating-coverage-reports.md` and `docs/INSTALLATION.md` +in the coverage-tracker repo (PR sync via `.github/workflows/export-docs.yml`). Never edit +those generated files downstream. Content must stay dual-renderable: GFM tables and callouts +work on GitHub too; wrap site-only markup in `` … ``. ## Stack diff --git a/README.md b/README.md index 52dac44..cff78c1 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,48 @@ static/ logo-mark.svg ``` +## Docs export pipeline + +This repo is the **origin of truth for shared docs content**. The `.svx` files under +`src/lib/docs-content/` are rendered into plain GitHub markdown and synced to the +[`coverage-tracker`](https://github.com/CoverageTracker/coverage-tracker) repo via PR: + +- `12-generating-coverage-reports.svx` → `docs/generating-coverage-reports.md` +- the quick-start / installation / usage sections → `docs/INSTALLATION.md` + +**Never edit those two files in the coverage-tracker repo** — they carry a `GENERATED` +header and any direct edit is overwritten by the next sync. Edit the `.svx` sources here +instead. + +The sync runs from `.github/workflows/export-docs.yml` on every push to `main` that touches +a `.svx` file or the exporter, and opens/updates a PR on the fixed `docs-sync` branch +(re-runs update the same PR; no diff → no PR). The target list is an explicit config in +`scripts/export-docs.ts` — no globbing; adding a new synced doc is a deliberate config change. + +Run it locally against a sibling checkout: + +```bash +pnpm export-docs ../coverage-tracker +``` + +### Provisioning the sync token + +The workflow authenticates with a fine-grained PAT stored as the repo secret +`COVERAGE_TRACKER_SYNC_TOKEN`. The product GitHub App's permissions are deliberately +**not** widened for this docs plumbing. To (re)provision the token — after expiry, a new +fork, or a maintainer change: + +1. GitHub → **Settings → Developer settings → Personal access tokens → Fine-grained + tokens → Generate new token** +2. Resource owner: the **CoverageTracker** org; Repository access: **Only select + repositories** → `coverage-tracker` +3. Repository permissions: **Contents → Read and write**, **Pull requests → Read and + write**; everything else: No access +4. Set an expiration and note it somewhere you'll see it — when the token lapses, the + workflow fails with an auth error on the checkout or PR step +5. Save the token in **this repo** as the Actions secret `COVERAGE_TRACKER_SYNC_TOKEN` + (Settings → Secrets and variables → Actions) + ## Theming The store writes `data-theme` and `data-mode` onto ``; `app.css` maps each combination to diff --git a/docs/plans/consumer-generated-reports-migration-plan-v3.md b/docs/plans/consumer-generated-reports-migration-plan-v3.md new file mode 100644 index 0000000..bfa2d94 --- /dev/null +++ b/docs/plans/consumer-generated-reports-migration-plan-v3.md @@ -0,0 +1,313 @@ +# Migration plan: consumer-generated reports (v2) + +Moves coverage, complexity, and duplication from "the Action runs the tool" to +"the consumer runs the tool, the Action reads the file." LCOV is accepted +wherever a language's tooling produces it; Cobertura and JaCoCo are supported, +never required; Go is parsed directly from its native `go tool cover` profile. +Complexity and duplication are optional end-to-end — **unless a threshold is +configured for them, in which case a missing report is a hard failure** +(see Phase 1, "Threshold vs. missing report"). + +`collect.sh` is retired entirely, not renamed — see Plan A, Phase 2. + +Design goal: this project replaces Codecov / Code Climate / Coveralls for its +consumers. Minimize adoption barriers — zero-config where possible, explicit +config always available. + +--- + +## Plan A — `coverage-tracker` repo + +### Phase 1 — Design decisions (all resolved — do not revisit) + +- [x] **Coverage formats: four, not three.** LCOV, Cobertura XML, JaCoCo XML, + and the **Go native coverage profile** (`go test -coverprofile`). Go is + never converted to LCOV/Cobertura. +- [x] **Ingested metric semantics** (fixed contract with the Worker — + `POST /api/ci/coverage`, schema in `src/routes/ci.ts`): + | Ingest field | Definition | Source per format | + |---|---|---| + | `line_coverage` (required) | Line coverage % | LCOV `LF`/`LH`; Cobertura `line-rate`; JaCoCo `LINE` counter; Go profile statement coverage (equivalent for Go) | + | `branch_coverage` (optional) | Branch coverage % | LCOV `BRF`/`BRH` (if present); Cobertura `branch-rate` **subject to quirks table**; JaCoCo `BRANCH` counter; Go native: never emitted | + | `cyclomatic` (optional) | Average cyclomatic complexity per function/method | Radon (mean over blocks); gocyclo `-avg` output; Lizard XML average; JaCoCo `COMPLEXITY` counter ÷ `METHOD` counter | + | `duplication_pct` (optional) | Duplicated-lines % | jscpd JSON `statistics.total.percentage` | + `cognitive` and `maintainability` exist in the Worker schema but are + **out of scope for the Action in this migration** — do not emit them. + **No Worker changes are required**; the existing contract is satisfied. +- [x] **`coverage-path` is optional with default-path probing.** If unset, + probe the documented per-language default paths (table in + `docs/generating-coverage-reports.md`) in a fixed order and use the + first hit. If nothing is found, **fail** with an error that lists every + path probed and links to the docs page. This is the zero-config path + for Codecov-style adoption; explicit `coverage-path` always wins. +- [x] **Complexity tools: Radon (Python), gocyclo (Go), Lizard (fallback for + everything else, including C/C++).** No further per-language research. +- [x] **Native complexity tool beats Lizard.** When probing default paths and + multiple complexity files exist, precedence is: `radon.json` → + `gocyclo.txt` → `lizard-report.xml`. An explicit `complexity-path` + input overrides probing entirely. +- [x] **JaCoCo complexity is free.** When the coverage format is JaCoCo, the + parser also emits `cyclomatic` from the `COMPLEXITY`/`METHOD` counters. + Precedence: an explicit `complexity-path` (or a probed complexity file) + **overrides** the JaCoCo-derived value. +- [x] **Duplication: jscpd only**, cross-language. The Action **no longer + auto-installs jscpd** — consumers run it themselves. This is a breaking + change; see Phase 6. +- [x] **No `complexity-tool` input needed.** Radon (JSON), gocyclo (plain + text), and Lizard (XML) are distinguishable by content shape alone. +- [x] **Threshold vs. missing report:** if a threshold input is configured + for a metric (`max-complexity`, `max-duplication`) and no report file + for that metric is found (input path or default probe), the Action + **fails** with an actionable error. Silent skip applies only when the + metric is both unconfigured and absent. +- [x] **Cobertura fallback UX unchanged:** unrecognized/omitted + `coverage-tool` for Cobertura → warn, default to trusting the data. +- [x] **XML parsing: `fast-xml-parser`** (Cobertura, JaCoCo, Lizard). Add as + a bundled dependency; do not introduce a second XML library. +- [x] **Version bump confirmed** — breaking change, major version. +- [x] **Dogfood self-test is back in scope** (previously deferred): vitest's + `lcov` reporter emits `coverage/lcov.info`, which the new architecture + consumes directly — the self-test exercises the most common consumer + path (LCOV) end-to-end. See Phase 4. + +### Phase 2 — Eliminate `collect.sh`; move fully into the TypeScript action + +No shell script. `action.yml`'s `runs:` block changes from a bash-invoking +composite to a plain Node action: + +```yaml +runs: + using: 'node20' + main: 'dist/index.js' +``` + +This also removes the old "invoke `collect.sh` via `bash`" workaround. + +- [ ] Delete `collect.sh` and all inline Python parsers +- [ ] New entrypoint `src/index.ts` does, in order: + 1. Resolve coverage file: `coverage-path` input if set, else probe the + documented default paths in a fixed, documented order; **fail with the + probed-path list** if nothing is found + 2. Resolve `complexity-path` (input, else probe `radon.json` → + `gocyclo.txt` → `lizard-report.xml`) — optional; **fail if + `max-complexity` is set and nothing is found**, else skip silently + 3. Resolve `duplication-path` (input, else probe + `jscpd-report/jscpd-report.json`) — optional; **fail if + `max-duplication` is set and nothing is found**, else skip silently + 4. Sniff coverage format from content: `mode: set|count|atomic` first + line → **Go profile**; `TN:`/`SF:` → LCOV; XML root `` → + Cobertura; XML root `` → JaCoCo. Parse; apply Cobertura quirks + table when applicable + 5. If format is JaCoCo, derive `cyclomatic` from its counters (overridden + by step-2 result if one exists) + 6. If a complexity file was found, sniff shape (JSON → Radon, XML → + Lizard, else → gocyclo) and parse + 7. If a duplication file was found, parse jscpd JSON + 8. Threshold checks → Check Run → ingest POST (existing OIDC / branch / + baseline flow unchanged) + +#### Default path probes + +Coverage (in probe order — first hit wins): + +| Path | Produced by | +|---|---| +| `coverage.out` | `go tool cover` | +| `coverage/lcov.info` | Istanbul/vitest/jest, SimpleCov, Dart/Flutter | +| `lcov.info` | cargo-llvm-cov, hpc-codecov | +| `coverage.lcov` | coverage.py, gcovr, perl2lcov | +| `coverage.info` | coverlet | +| `cover/lcov.info` | ExCoveralls | +| `target/coverage/lcov.info` | Cloverage | +| `target/site/jacoco/jacoco.xml` | JaCoCo (Maven) | +| `build/reports/jacoco/test/jacocoTestReport.xml` | JaCoCo (Gradle) | +| `coverage.xml` | PHPUnit (Cobertura) | +| `luacov.report.out` | LuaCov lcov reporter | + +(kcov and covertool default paths contain glob/dynamic segments — those +consumers set `coverage-path` explicitly; document this.) + +Complexity and duplication: + +| Signal | Tool | Default path | +|---|---|---| +| Complexity | Radon | `radon.json` | +| Complexity | gocyclo | `gocyclo.txt` | +| Complexity | Lizard | `lizard-report.xml` | +| Duplication | jscpd | `jscpd-report/jscpd-report.json` | + +Document all of the above as the convention consumers write to for +auto-detection. + +### Phase 3 — TypeScript modules + +Entrypoint strategy: **keep `src/run.ts` and its exported helpers** +(`parseThreshold`, `buildSummary`, `formatValue`, `formatDelta`, +`thresholdConfigured`, `ThresholdResult`) — the 52 existing tests stay green +untouched. `src/index.ts` is the new entrypoint; it imports run.ts's helpers +for threshold/summary/Check-Run logic and replaces the collect.sh invocation +with the parser pipeline. Strip the collect.sh spawn path (and its +`require.main` guard trigger) out of run.ts; run.ts becomes a pure helper +module. + +- [ ] `src/index.ts` — new entrypoint (orchestration per Phase 2) +- [ ] `src/format.ts` — coverage format sniffer (4 formats incl. Go profile) +- [ ] `src/lcov.ts` +- [ ] `src/goprofile.ts` — native `go tool cover` profile parser +- [ ] `src/cobertura.ts` + quirks table (uses fast-xml-parser) +- [ ] `src/jacoco.ts` (already written) — extend to emit `cyclomatic` from + `COMPLEXITY`/`METHOD` counters +- [ ] `src/complexity/radon.ts`, `src/complexity/gocyclo.ts`, + `src/complexity/lizard.ts`, `src/complexity/detect.ts` (shape sniffer) +- [ ] `src/duplication.ts` (jscpd JSON) +- [ ] `src/paths.ts` — input-or-default-probe resolution for all three + report kinds, incl. probe-order tables above and the + fail-vs-skip rule from Phase 1 +- [ ] `src/run.ts` — remove collect.sh invocation; helpers only +- [ ] Add `fast-xml-parser` to dependencies +- [ ] Update `action.yml` inputs: `coverage-path` (optional, probed), + `coverage-tool` (conditional, Cobertura only), `complexity-path` + (optional), `duplication-path` (optional); remove anything tied to + auto-run behavior +- [ ] Rebuild: commit `dist/index.js`; **delete `dist/run.js`**; update any + reference to the old bundle path + +### Phase 4 — Tests + +- [ ] Layer 1 (vitest): keep the existing 52 run.ts tests as-is; add unit + tests per new module, fixture-per-format (LCOV, Go profile, Cobertura + per quirks entry, JaCoCo incl. complexity derivation, Radon, gocyclo, + Lizard, jscpd); add tests for probe order/precedence and the + threshold-configured-but-missing failure +- [ ] Layer 2 (`test/collect-parsers.sh`): retire entirely; fold remaining + fixture checks into the vitest suite +- [ ] Re-enable `.github/workflows/action-test.yml` (dogfood): run vitest + with the `lcov` coverage reporter, then invoke the local Action with + no `coverage-path` — the probe finds `coverage/lcov.info`, closing the + zero-config LCOV loop end-to-end. Keep the push-main / feature-branch / + PR threshold matrix from the previous self-test +- [ ] Verify (no code change expected): Worker `POST /api/ci/coverage` + accepts payloads with only `line_coverage` — complexity/duplication + fields optional per existing zod schema + +### Phase 5 — Markdown documentation + +- [ ] `docs/generating-coverage-reports.md` — **generated file, do not edit + directly.** Canonical source is `generating-coverage-reports.svx` in + the coveragetracker.dev repo (see Plan B, Phase 2a). Author these + changes there; the sync pipeline PRs them into this repo: + - Clarify that the "Default path" columns are **real probe targets** + (auto-detected when `-path` inputs are unset), and document the + coverage probe order + - Note kcov/covertool require explicit `coverage-path` (dynamic paths) + - Complexity + duplication sections already present — add the + fail-if-threshold-configured-but-missing rule + - If the sync pipeline isn't live yet when Plan A reaches this phase, + make the edits in the `.svx` and copy the exported output over + manually once — never fork the content +- [ ] `.github/actions/report/README.md` — inputs table: `coverage-path` now + optional (probed), `complexity-path` / `duplication-path` optional with + probe fallback; document fail-vs-skip semantics +- [ ] `docs/PROGRESS.md` — new phase entry; mark superseded auto-run entries + as superseded rather than deleting history +- [ ] `docs/INSTALLATION.md` — update the CI example under "Next steps": + explicit test/coverage step, then the Action with zero config + (complexity/duplication shown as optional additions) +- [ ] Root `README.md` — update quick-start snippet (zero-config example) + +### Phase 6 — Release + +- [ ] Major version tag +- [ ] `CHANGELOG.md`: before/after workflow example; **explicitly call out**: + (a) the Action no longer runs tests or coverage tools, (b) jscpd is no + longer auto-installed — duplication silently disappears for consumers + who relied on it unless they add a jscpd step or set + `max-duplication` (which now fails loudly when the report is missing), + (c) `dist/run.js` → `dist/index.js` + +--- + +## Plan B — `coveragetracker.dev` repo + +Sidebar and content are both auto-generated from `.svx` frontmatter — no +Svelte component or manual navigation wiring needed. This repo is the +**origin of truth for shared docs content**; the coverage-tracker repo's +markdown copy is generated from here (Phase 2a). + +### Phase 1 — Frontmatter schema + +- [x] Confirmed: `id`, `kicker`, `title`, `group`. No `order`/`description`. +- [x] Confirmed: sidebar auto-generated from frontmatter. +- [x] Confirm `kicker`/`group` values against the site's existing taxonomy — + resolved: no `guides` group needed; the page uses the existing + `usage` kicker/group and slots after "Ingest from CI" + +### Phase 2 — Content + +- [x] `generating-coverage-reports.svx` drafted; full 17-language table +- [x] **Sourcing decision: coveragetracker.dev is the origin of truth.** + The `.svx` is canonical; `docs/generating-coverage-reports.md` in the + coverage-tracker repo is a generated artifact, synced via PR + (Phase 2a). Direction matters: `.svx → .md` is a trivial emission + (frontmatter `title` → H1, body verbatim — GFM alerts and tables + render natively on GitHub, heading levels already align), whereas the + reverse would need H1-stripping and anchor-rewriting heuristics. +- [x] Author the Plan A Phase 5 doc changes in the `.svx`: probe-order + table, probe semantics, fail-vs-skip rule, kcov/covertool note + (`src/lib/docs-content/12-generating-coverage-reports.svx`) + +### Phase 2a — Docs export pipeline (`.svx → .md` PR sync) + +- [x] `scripts/export-docs.ts` (TypeScript, run with the repo's existing + Node toolchain): for each configured `.svx`, parse frontmatter, emit + `# {title}` followed by the body verbatim, prefixed with an HTML + comment header: `` + (extended: titled callouts are rewritten to GitHub-renderable alerts, + `` blocks are stripped, and multi-source targets + compose several `.svx` into one doc — used for `docs/INSTALLATION.md`, + which is now also a generated artifact per later scope decision) +- [x] Config is an explicit list of `{ svx, targetPath }` pairs (two entries + today: `12-generating-coverage-reports.svx` → + `docs/generating-coverage-reports.md`, and the quick-start / + installation / usage sections → `docs/INSTALLATION.md`) — no globbing, + additions are deliberate +- [x] Workflow `.github/workflows/export-docs.yml`: trigger on push to main + with a path filter on the configured `.svx` files; run the export; + open/update a PR against `CoverageTracker/coverage-tracker` via + `peter-evans/create-pull-request` (fixed branch name, e.g. + `docs-sync`, so repeated runs update one PR); skip cleanly when + there's no diff +- [x] Auth: fine-grained PAT scoped to the coverage-tracker repo only + (Contents + Pull requests: write), stored as a repo secret + (`COVERAGE_TRACKER_SYNC_TOKEN`). Do **not** widen the product GitHub + App's permissions for docs plumbing. **Manual step remaining: generate + the PAT and save the secret** — workflow and docs are in place. +- [x] Update `README.md` in this repo with a "Docs export pipeline" section + documenting the PAT setup so the sync can be re-provisioned (token + expiry, new fork, new maintainer): + - GitHub → Settings → Developer settings → Personal access tokens → + Fine-grained tokens → Generate new token + - Resource owner: the `CoverageTracker` org; Repository access: + Only select repositories → `coverage-tracker` + - Repository permissions: Contents → Read and write, Pull requests → + Read and write; everything else: No access + - Set an expiration and note it — the workflow fails with an auth + error when the token lapses + - Save as repo secret `COVERAGE_TRACKER_SYNC_TOKEN` (Settings → + Secrets and variables → Actions) + - Briefly explain what the pipeline does (`.svx` → `.md` PR sync) and + that `docs/generating-coverage-reports.md` downstream must never be + edited directly +- [x] Ordering: land this pipeline **before or alongside** Plan A Phase 5 so + those doc edits flow through it; if Plan A gets there first, do one + manual export and note it in the PR description — pipeline landed + first; Plan A Phase 5 doc content flows through it + +### Phase 3 — Cross-linking + +- [x] Link `docs/INSTALLATION.md` (coverage-tracker repo) and the + coveragetracker.dev page to each other — both directions use absolute + URLs so the links work on the site and on GitHub; the INSTALLATION.md + side lands via the sync PR (10-verify and 11-ingest link to the page, + the page links back to the GitHub INSTALLATION.md) diff --git a/package.json b/package.json index ca55ddb..64efeaf 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "deploy": "npm run build && wrangler pages deploy .svelte-kit/cloudflare", + "export-docs": "node --experimental-strip-types scripts/export-docs.ts", "test": "vitest run" }, "devDependencies": { diff --git a/scripts/export-docs.test.ts b/scripts/export-docs.test.ts new file mode 100644 index 0000000..cef142d --- /dev/null +++ b/scripts/export-docs.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect } from 'vitest'; +import { + parseSvx, + transformCallouts, + promoteHeadings, + stripSiteOnly, + renderTarget, + targets +} from './export-docs.ts'; + +// --------------------------------------------------------------------------- +// parseSvx +// --------------------------------------------------------------------------- + +describe('parseSvx', () => { + it('extracts the frontmatter title and the body', () => { + const { title, body } = parseSvx('---\nid: x\ntitle: Hello world\ngroup: usage\n---\n\nBody text.\n'); + expect(title).toBe('Hello world'); + expect(body).toBe('Body text.'); + }); + + it('handles CRLF line endings', () => { + const { title, body } = parseSvx('---\r\ntitle: T\r\n---\r\nBody.\r\n'); + expect(title).toBe('T'); + expect(body).toBe('Body.'); + }); + + it('throws on missing frontmatter', () => { + expect(() => parseSvx('no frontmatter')).toThrow(/frontmatter/); + }); + + it('throws when frontmatter has no title', () => { + expect(() => parseSvx('---\nid: x\n---\nBody.')).toThrow(/title/); + }); +}); + +// --------------------------------------------------------------------------- +// transformCallouts +// --------------------------------------------------------------------------- + +describe('transformCallouts', () => { + it('moves an inline callout title to a bold line so GitHub renders the alert', () => { + expect(transformCallouts('> [!WARNING] Two integrations\n> Body.')).toBe( + '> [!WARNING]\n> **Two integrations**\n> Body.' + ); + }); + + it('drops the title when it just repeats the marker kind', () => { + expect(transformCallouts('> [!NOTE] Note\n> Body.')).toBe('> [!NOTE]\n> Body.'); + }); + + it('leaves bare markers untouched', () => { + expect(transformCallouts('> [!NOTE]\n> Body.')).toBe('> [!NOTE]\n> Body.'); + }); + + it('does not touch code inside a blockquote', () => { + const block = '> [!NOTE]\n> ```bash\n> echo hi\n> ```'; + expect(transformCallouts(block)).toBe(block); + }); +}); + +// --------------------------------------------------------------------------- +// stripSiteOnly +// --------------------------------------------------------------------------- + +describe('stripSiteOnly', () => { + it('removes site-only blocks including their markers', () => { + const body = 'Before.\n\n\n94%\n'; + expect(stripSiteOnly(body)).toBe('Before.'); + }); + + it('leaves bodies without markers untouched', () => { + expect(stripSiteOnly('Just text.')).toBe('Just text.'); + }); +}); + +// --------------------------------------------------------------------------- +// promoteHeadings +// --------------------------------------------------------------------------- + +describe('promoteHeadings', () => { + it('promotes headings one level', () => { + expect(promoteHeadings('### Coverage\n\n#### Sub')).toBe('## Coverage\n\n### Sub'); + }); + + it('skips lines inside fenced code blocks', () => { + const body = '### Head\n\n```bash\n# a comment\n## not a heading\n```\n\n### Tail'; + expect(promoteHeadings(body)).toBe('## Head\n\n```bash\n# a comment\n## not a heading\n```\n\n## Tail'); + }); +}); + +// --------------------------------------------------------------------------- +// renderTarget +// --------------------------------------------------------------------------- + +const sources: Record = { + 'one.svx': '---\ntitle: Page one\n---\n\nIntro.\n\n### Details\n\n> [!NOTE] Heads up\n> Careful.', + 'two.svx': '---\ntitle: Page two\n---\n\nMore.' +}; +const read = (name: string) => sources[name]; + +describe('renderTarget', () => { + it('renders a single-source target with H1 from frontmatter and promoted headings', () => { + const out = renderTarget({ targetPath: 'docs/one.md', sources: ['one.svx'] }, read); + expect(out).toContain(''); + expect(out).toContain('# Page one'); + expect(out).toContain('## Details'); + expect(out).toContain('> [!NOTE]\n> **Heads up**'); + expect(out.endsWith('\n')).toBe(true); + }); + + it('renders a multi-source target with configured H1 and H2 sections', () => { + const out = renderTarget( + { targetPath: 'docs/all.md', title: 'Guide', intro: 'Read in order.', sources: ['one.svx', 'two.svx'] }, + read + ); + expect(out).toContain('# Guide'); + expect(out).toContain('Read in order.'); + expect(out).toContain('## Page one'); + expect(out).toContain('## Page two'); + // body headings keep their level in multi-source targets + expect(out).toContain('### Details'); + expect(out).toContain('one.svx, two.svx'); + }); +}); + +// --------------------------------------------------------------------------- +// config sanity — every configured source must exist on disk +// --------------------------------------------------------------------------- + +describe('targets config', () => { + it('references only .svx files that exist', async () => { + const { existsSync } = await import('node:fs'); + const { join } = await import('node:path'); + for (const target of targets) { + for (const source of target.sources) { + expect(existsSync(join('src/lib/docs-content', source)), source).toBe(true); + } + } + }); +}); diff --git a/scripts/export-docs.ts b/scripts/export-docs.ts new file mode 100644 index 0000000..30140bf --- /dev/null +++ b/scripts/export-docs.ts @@ -0,0 +1,155 @@ +/** + * Docs export pipeline: renders configured .svx files from src/lib/docs-content + * into plain GitHub markdown inside a checkout of CoverageTracker/coverage-tracker. + * + * Usage: node --experimental-strip-types scripts/export-docs.ts + * + * The generated files carry a GENERATED header and must never be edited in the + * downstream repo — this repo is the origin of truth for docs content. + */ +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'lib', 'docs-content'); + +export type ExportTarget = { + /** Path of the generated file, relative to the coverage-tracker repo root. */ + targetPath: string; + /** Source .svx filenames in src/lib/docs-content, in emission order. */ + sources: string[]; + /** + * H1 for multi-source targets; each source becomes an H2 section. + * Omit for single-source targets — the frontmatter title becomes the H1 + * and body headings are promoted one level (### → ##). + */ + title?: string; + /** Optional paragraph emitted between the H1 and the first section. */ + intro?: string; +}; + +// Explicit list — no globbing; additions are deliberate. +export const targets: ExportTarget[] = [ + { + targetPath: 'docs/generating-coverage-reports.md', + sources: ['12-generating-coverage-reports.svx'] + }, + { + targetPath: 'docs/INSTALLATION.md', + title: 'Installation Guide', + intro: + 'This guide walks through deploying coverage-tracker to your own Cloudflare account.\n' + + 'Follow the sections in order — several decisions depend on values produced by earlier steps.', + sources: [ + '03-quick-start.svx', + '04-prerequisites.svx', + '05-domain-database.svx', + '06-github-app.svx', + '07-cloudflare-access.svx', + '08-secrets.svx', + '09-deploy.svx', + '10-verify.svx', + '11-ingest.svx', + '13-badges.svx', + '14-dashboard.svx' + ] + } +]; + +export function parseSvx(raw: string): { title: string; body: string } { + const text = raw.replace(/\r\n/g, '\n'); + const match = /^---\n([\s\S]*?)\n---\n/.exec(text); + if (!match) throw new Error('missing frontmatter'); + const title = /^title:\s*(.+)$/m.exec(match[1])?.[1]?.trim(); + if (!title) throw new Error('missing title in frontmatter'); + return { title, body: text.slice(match[0].length).trim() }; +} + +/** + * GitHub only renders `> [!NOTE]` as an alert when the marker is alone on its + * line; the site's callout syntax allows an inline title after the marker. + * Move the title to a bold line of its own, dropping it when it just repeats + * the marker (e.g. "> [!NOTE] Note"). + */ +export function transformCallouts(body: string): string { + return body + .split('\n') + .flatMap((line) => { + const m = /^(\s*)> \[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]+(\S.*)$/.exec(line); + if (!m) return [line]; + const [, indent, kind, title] = m; + const marker = `${indent}> [!${kind}]`; + if (title.trim().toLowerCase() === kind.toLowerCase()) return [marker]; + return [marker, `${indent}> **${title.trim()}**`]; + }) + .join('\n'); +} + +/** + * Removes blocks wrapped in `` … `` — + * decorative markup that only makes sense with the site's CSS. + */ +export function stripSiteOnly(body: string): string { + return body.replace(/[ \t]*[\s\S]*?[ \t]*\n?/g, '').trimEnd(); +} + +/** Promote headings one level (### → ##), skipping fenced code blocks. */ +export function promoteHeadings(body: string): string { + let inFence = false; + return body + .split('\n') + .map((line) => { + if (/^(```|~~~)/.test(line)) inFence = !inFence; + if (inFence) return line; + return line.replace(/^##(#*) /, '#$1 '); + }) + .join('\n'); +} + +export function renderTarget(target: ExportTarget, readSource: (name: string) => string): string { + const docs = target.sources.map((name) => { + const { title, body } = parseSvx(readSource(name)); + return { name, title, body: transformCallouts(stripSiteOnly(body)) }; + }); + + const header = ``; + + let out: string; + if (target.title === undefined) { + out = `${header}\n\n# ${docs[0].title}\n\n${promoteHeadings(docs[0].body)}`; + } else { + const sections = docs.map((d) => `## ${d.title}\n\n${d.body}`); + const head = [header, `# ${target.title}`, target.intro].filter(Boolean).join('\n\n'); + out = [head, ...sections].join('\n\n---\n\n'); + } + return `${out.trimEnd()}\n`; +} + +function main(): void { + const repoRoot = process.argv[2]; + if (!repoRoot || !existsSync(repoRoot)) { + console.error('usage: node --experimental-strip-types scripts/export-docs.ts '); + process.exit(1); + } + + for (const target of targets) { + const rendered = renderTarget(target, (name) => readFileSync(join(CONTENT_DIR, name), 'utf8')); + const outPath = join(repoRoot, target.targetPath); + const previous = existsSync(outPath) ? readFileSync(outPath, 'utf8') : null; + if (previous === rendered) { + console.log(`unchanged ${target.targetPath}`); + continue; + } + mkdirSync(dirname(outPath), { recursive: true }); + writeFileSync(outPath, rendered); + console.log(`written ${target.targetPath}`); + } +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main(); +} diff --git a/src/lib/docs-content/05-domain-database.svx b/src/lib/docs-content/05-domain-database.svx index 08b91f1..56b7038 100644 --- a/src/lib/docs-content/05-domain-database.svx +++ b/src/lib/docs-content/05-domain-database.svx @@ -5,15 +5,15 @@ title: Domain & database group: installation --- -Create your D1 database, then wire its id into `wrangler.jsonc` and apply the schema migration. +Create your D1 database, then wire its id into `wrangler.json` and apply the schema migration. ```bash file="create D1" npx wrangler d1 create coverage ``` -Copy the `database_id` from the output into the `d1_databases` entry of `wrangler.jsonc`: +Copy the `database_id` from the output into the `d1_databases` entry of `wrangler.json`: -```jsonc file="wrangler.jsonc" +```jsonc file="wrangler.json" { // ... "d1_databases": [ @@ -35,4 +35,4 @@ npm run db:migrate:remote ``` > [!NOTE] 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. +> 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. diff --git a/src/lib/docs-content/06-github-app.svx b/src/lib/docs-content/06-github-app.svx index 2c826c4..a091374 100644 --- a/src/lib/docs-content/06-github-app.svx +++ b/src/lib/docs-content/06-github-app.svx @@ -17,7 +17,7 @@ From the account or org that will host the app, go to **Settings → Developer s Homepage URLhttps://coverage-tracker.yourdomain.com Webhook → Activechecked Webhook URL…/webhooks/github - Webhook secretGenerate 32 random bytes — save this value + Webhook secretGenerate 32 random bytes — save this value:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" diff --git a/src/lib/docs-content/07-cloudflare-access.svx b/src/lib/docs-content/07-cloudflare-access.svx index ee6662c..1b0b640 100644 --- a/src/lib/docs-content/07-cloudflare-access.svx +++ b/src/lib/docs-content/07-cloudflare-access.svx @@ -7,7 +7,7 @@ group: installation 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: +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. diff --git a/src/lib/docs-content/10-verify.svx b/src/lib/docs-content/10-verify.svx index f51ee2a..15a5e8e 100644 --- a/src/lib/docs-content/10-verify.svx +++ b/src/lib/docs-content/10-verify.svx @@ -17,4 +17,23 @@ 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`. +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): + +```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}' +``` + +> [!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'" +> ``` + +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. diff --git a/src/lib/docs-content/11-ingest.svx b/src/lib/docs-content/11-ingest.svx index dd120f3..0beac7e 100644 --- a/src/lib/docs-content/11-ingest.svx +++ b/src/lib/docs-content/11-ingest.svx @@ -11,4 +11,4 @@ Add a workflow step that runs after your test suite and posts coverage to `/api/ 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. +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. diff --git a/src/lib/docs-content/12-generating-coverage-reports.svx b/src/lib/docs-content/12-generating-coverage-reports.svx new file mode 100644 index 0000000..5a35af3 --- /dev/null +++ b/src/lib/docs-content/12-generating-coverage-reports.svx @@ -0,0 +1,99 @@ +--- +id: generating-coverage-reports +kicker: usage +title: Generating coverage reports +group: usage +--- + +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. diff --git a/src/lib/docs-content/12-badges.svx b/src/lib/docs-content/13-badges.svx similarity index 81% rename from src/lib/docs-content/12-badges.svx rename to src/lib/docs-content/13-badges.svx index d510b67..ef60fc8 100644 --- a/src/lib/docs-content/12-badges.svx +++ b/src/lib/docs-content/13-badges.svx @@ -21,9 +21,9 @@ curl -X PATCH …/api/admin/projects/1/badge \ Then drop the shields.io endpoint badge into your README: ```md file="README.md" -![coverage](https://img.shields.io/endpoint?url= - https://coverage-tracker.yourdomain.com - /api/badge/owner/repo/coverage.json) +![coverage](https://img.shields.io/endpoint?url=https://coverage-tracker.yourdomain.com/api/badge/owner/repo/coverage.json) ``` + coverage94% + diff --git a/src/lib/docs-content/13-dashboard.svx b/src/lib/docs-content/14-dashboard.svx similarity index 100% rename from src/lib/docs-content/13-dashboard.svx rename to src/lib/docs-content/14-dashboard.svx diff --git a/src/lib/docs-content/14-api.svx b/src/lib/docs-content/15-api.svx similarity index 100% rename from src/lib/docs-content/14-api.svx rename to src/lib/docs-content/15-api.svx diff --git a/src/lib/docs-content/15-ingest-payload.svx b/src/lib/docs-content/16-ingest-payload.svx similarity index 100% rename from src/lib/docs-content/15-ingest-payload.svx rename to src/lib/docs-content/16-ingest-payload.svx diff --git a/src/lib/docs-content/remark-plugins.test.ts b/src/lib/docs-content/remark-plugins.test.ts index ba1b281..54a32db 100644 --- a/src/lib/docs-content/remark-plugins.test.ts +++ b/src/lib/docs-content/remark-plugins.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { compile } from 'mdsvex'; import { remarkCallouts } from './remark-callouts.js'; import { remarkCodeBlocks } from './remark-code-blocks.js'; +import { remarkTables } from './remark-tables.js'; // --------------------------------------------------------------------------- // Helpers @@ -134,3 +135,22 @@ describe('remarkCodeBlocks', () => { expect(code).toMatch(/code=\{"line1\\nline2"\}/); }); }); + +// --------------------------------------------------------------------------- +// remarkTables +// --------------------------------------------------------------------------- + +describe('remarkTables', () => { + const table = '| Language | Tool |\n|---|---|\n| Go | `go tool cover` |'; + + it('renders a GFM pipe table as an HTML table', async () => { + const code = await compiled(table, [remarkTables]); + expect(code).toMatch(/
AppPathPolicy
Go<\/td>/); + }); + + it('tags pipe tables with the deftable class', async () => { + const code = await compiled(table, [remarkTables]); + expect(code).toMatch(/
/); + }); +}); diff --git a/src/lib/docs-content/remark-tables.js b/src/lib/docs-content/remark-tables.js new file mode 100644 index 0000000..663a051 --- /dev/null +++ b/src/lib/docs-content/remark-tables.js @@ -0,0 +1,17 @@ +// @ts-check +import { visit } from 'unist-util-visit'; + +/** + * Tags markdown pipe tables with the site's `deftable` class so they pick up + * the same styling as the hand-written HTML tables in other sections. + * + * @returns {(tree: import('mdast').Root) => void} + */ +export function remarkTables() { + return (tree) => { + visit(tree, 'table', (node) => { + const data = /** @type {any} */ (node.data ??= {}); + data.hProperties = { ...data.hProperties, className: 'deftable' }; + }); + }; +} diff --git a/svelte.config.js b/svelte.config.js index 417a321..5a1b2c5 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -4,6 +4,7 @@ import { mdsvex } from 'mdsvex'; import { resolve } from 'path'; import { remarkCallouts } from './src/lib/docs-content/remark-callouts.js'; import { remarkCodeBlocks } from './src/lib/docs-content/remark-code-blocks.js'; +import { remarkTables } from './src/lib/docs-content/remark-tables.js'; /** * Post-mdsvex preprocessor: injects Callout/CodeBlock imports into .svx files. @@ -45,7 +46,7 @@ const config = { mdsvex({ extensions: ['.svx'], layout: resolve('./src/lib/docs-content/mdsvex-layout.svelte'), - remarkPlugins: [remarkCallouts, remarkCodeBlocks] + remarkPlugins: [remarkCallouts, remarkCodeBlocks, remarkTables] }), injectDocComponents() ], diff --git a/vitest.config.ts b/vitest.config.ts index c7cb422..ee3ec28 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['src/lib/docs-content/**/*.test.ts'] + include: ['src/lib/docs-content/**/*.test.ts', 'scripts/**/*.test.ts'] } });