Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ on:
branches: [main]
pull_request:

# Least-privilege default token; no job in this workflow writes to the repo.
# Least-privilege default token; the only write anywhere is the coverage-comment
# job's pull-requests:write, scoped to that job (and it never runs for fork PRs).
permissions:
contents: read

Expand Down Expand Up @@ -88,11 +89,74 @@ jobs:
run: pnpm typecheck
- name: Test
if: ${{ !cancelled() }}
run: pnpm test
run: pnpm test -- --coverage
# Coverage ratchet (#93): per-workspace lines/branches may not drop below
# coverage-baseline.json (0.5pp tolerance). The checker self-tests first,
# so the gate is itself gated — same pattern as the docs check below.
- name: Coverage ratchet
if: ${{ !cancelled() }}
run: |
pnpm check:coverage:test
pnpm check:coverage
- name: Upload coverage report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report
retention-days: 14
path: |
coverage-summary.md
apps/*/coverage/coverage-summary.json
packages/*/coverage/coverage-summary.json
# Docs integrity (#102): fail on a dangling docs reference in source or an
# unindexed doc. The checker self-tests first, so the gate is itself gated.
- name: Docs integrity
if: ${{ !cancelled() }}
run: |
pnpm check:docs:test
pnpm check:docs

# Sticky PR comment with the coverage table (#93). Same-repo PRs only: fork
# PRs get a read-only token no matter what `permissions:` says, so for them
# the table lives in the step summary + artifact instead. `always()` so the
# comment still updates when the ratchet fails; continue-on-error so a
# comment hiccup never gates a merge.
coverage-comment:
needs: check
if: >-
always() && github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: write
continue-on-error: true
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage-report
- name: Sticky coverage comment
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ ! -f coverage-summary.md ]; then
echo "coverage-summary.md missing from artifact; nothing to post"
exit 0
fi
marker='<!-- coverage-report -->'
# Two steps, not a pipeline: `gh api | head -1` under the runner's
# default `pipefail` can surface gh's SIGPIPE (exit 141) as failure.
gh api "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" --paginate \
--jq ".[] | select(.body | startswith(\"${marker}\")) | .id" > comment-ids.txt
existing=$(head -1 comment-ids.txt)
if [ -n "$existing" ]; then
gh api --method PATCH "repos/${GH_REPO}/issues/comments/${existing}" \
-F body=@coverage-summary.md > /dev/null
echo "updated comment ${existing}"
else
gh api --method POST "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
-F body=@coverage-summary.md > /dev/null
echo "created comment"
fi
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ build/
.react-router/
.turbo/
coverage/
# rendered by scripts/check-coverage.mjs for the CI artifact/PR comment
/coverage-summary.md

# Cloudflare / wrangler
.wrangler/
Expand Down
11 changes: 8 additions & 3 deletions apps/etl/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
import { sharedCoverage } from '../../vitest.shared';

const here = dirname(fileURLToPath(import.meta.url));

Expand All @@ -29,7 +30,11 @@ export default defineConfig({
'cloudflare:workflows': resolve(here, 'src/test/cloudflare-workflows-stub.ts'),
},
},
// The refresh Workflow test runs the full refresh-slice.sql derive against a real SQLite —
// generous headroom for loaded CI runners, same rationale as packages/db/vitest.config.ts.
test: { testTimeout: 120_000 },
test: {
environment: 'node',
// The refresh Workflow test runs the full refresh-slice.sql derive against a real SQLite —
// generous headroom for loaded CI runners, same rationale as packages/db/vitest.config.ts.
testTimeout: 120_000,
coverage: sharedCoverage(['src/**']),
},
});
2 changes: 1 addition & 1 deletion apps/web/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "./tsconfig.json",
"include": ["vite.config.ts", "vitest.config.ts"],
"include": ["vite.config.ts", "vitest.config.ts", "../../vitest.shared.ts"],
"compilerOptions": {
"composite": true,
"strict": true,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { defineConfig } from 'vitest/config';
import { sharedCoverage } from '../../vitest.shared';

export default defineConfig({
test: {
environment: 'node',
include: ['app/**/*.test.ts', 'app/**/*.test.tsx', 'workers/**/*.test.ts'],
coverage: sharedCoverage(['app/**', 'workers/**']),
},
});
29 changes: 29 additions & 0 deletions coverage-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"tolerance": 0.5,
"workspaces": {
"apps/etl": {
"lines": 74,
"branches": 58.2
},
"apps/web": {
"lines": 89.7,
"branches": 81.8
},
"packages/config": {
"lines": 92.8,
"branches": 72.2
},
"packages/db": {
"lines": 94.2,
"branches": 79
},
"packages/ingest": {
"lines": 85.8,
"branches": 80
},
"packages/shared": {
"lines": 95.4,
"branches": 80
}
}
}
14 changes: 14 additions & 0 deletions docs/review-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@
- Всеки пакет с тестове закача `"test": "vitest run"` в turbo графа — `apps/etl` (`eop.test.ts`)
беше тихо прескачан от `turbo run test`, докато това не се поправи.

## Coverage ratchet

- Локално: `pnpm test -- --coverage && pnpm check:coverage`. CI пуска същото — покритието се
измерва с `@vitest/coverage-v8` през общия preset (`vitest.shared.ts`), по workspace.
- Ratchet правилото: lines% и branches% на всеки workspace не може да падне под комитнатия
`coverage-baseline.json` с повече от 0.5pp (толерансът поглъща шум от малките пакети с 1–2
тестови файла). Спад ⇒ червено CI; PR-ът показва таблицата с делтите (step summary + sticky
коментар за същия-repo PR-и, artifact за форкове).
- Покачване с >1pp ⇒ скриптът подканя `node scripts/check-coverage.mjs --update` — прегледайте и
комитнете новия baseline в същия PR. Умишлен, ревюиран спад се изразява със сваляне на числото
в `coverage-baseline.json`, не с изключване на проверката.
- Нов workspace с тестове се добавя и в `coverage-baseline.json` (и получава `vitest.config.ts`
с `sharedCoverage(...)`); `packages/api-contract` е освободен, докато няма тестове.

## Integrity gate

- Пуска се върху обслужвания D1 след `precompute` в `ship-domain.mjs` и след `runSliceDerive()` в
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"lint": "prettier --check .",
"check:docs": "node scripts/check-docs.mjs",
"check:docs:test": "node --test scripts/check-docs.test.mjs",
"check:coverage": "node scripts/check-coverage.mjs",
"check:coverage:test": "node --test scripts/check-coverage.test.mjs",
"format": "prettier --write .",
"setup": "node scripts/setup.mjs",
"import": "node scripts/import.mjs",
Expand All @@ -26,6 +28,7 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260521.1",
"@vitest/coverage-v8": "^4.1.7",
"@types/node": "^25.9.1",
"prettier": "^3.8.3",
"turbo": "^2.9.14",
Expand Down
9 changes: 9 additions & 0 deletions packages/config/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
import { sharedCoverage } from '../../vitest.shared';

export default defineConfig({
test: {
environment: 'node',
coverage: sharedCoverage(['src/**']),
},
});
7 changes: 6 additions & 1 deletion packages/db/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { defineConfig } from 'vitest/config';
import { sharedCoverage } from '../../vitest.shared';

// The db suite is real-SQLite integration style: tests shell out to the sqlite3 CLI dozens of
// times each, and the heaviest (ship-domain) legitimately runs for around a minute. On loaded CI
// runners individual tests can exceed vitest's 5s default timeout and fail flaky (seen on the
// refresh-slice EOP derivation test), so give the whole suite generous headroom - correctness
// here is asserted by the checks, not by speed.
export default defineConfig({
test: { testTimeout: 120_000 },
test: {
environment: 'node',
testTimeout: 120_000,
coverage: sharedCoverage(['src/**']),
},
});
9 changes: 9 additions & 0 deletions packages/ingest/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
import { sharedCoverage } from '../../vitest.shared';

export default defineConfig({
test: {
environment: 'node',
coverage: sharedCoverage(['src/**']),
},
});
9 changes: 9 additions & 0 deletions packages/shared/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
import { sharedCoverage } from '../../vitest.shared';

export default defineConfig({
test: {
environment: 'node',
coverage: sharedCoverage(['src/**']),
},
});
Loading