Thanks for helping improve clevis. This document describes how to work in the repo so your changes match what CI expects.
| Path | Role |
|---|---|
apps/ui |
Next.js UI (Node 22, Bun) |
apps/api |
Python API |
apps/worker |
Python worker |
packages/checks |
clevis-checks shared Python library |
docker-compose.yml |
Full-stack Docker Compose (profiles: backend, frontend) |
- Node.js 22 (same as CI)
- Bun (same as CI; used for
apps/uiinstalls/scripts) - Python 3.12 (for API/worker checks)
- Docker (for image build verification)
-
Clone the repository.
-
From the repository root, install tooling and enable Git hooks:
npm ci
The
preparescript registers Husky. After this, commits run local checks (see below). -
Install UI dependencies when working on the frontend:
cd apps/ui bun install --frozen-lockfile
The repo uses .gitattributes so text files are stored with LF. On Windows, let Git handle normalization (core.autocrlf is usually fine with true or input). Shell hook scripts (.husky/*) should stay LF.
After npm ci at the repository root, Git uses Husky (core.hooksPath → .husky/_). Then:
pre-commitruns before the commit is recorded. It runsbun run typecheckinapps/ui. Install UI deps (cd apps/ui && bun install --frozen-lockfile) so this can succeed.commit-msgruns immediately after you supply a message—whether fromgit commit -m "your subject"or from the editor. It runs the same Commitlint rules as CI (Conventional Commits via@commitlint/config-conventional), with--verboseoutput like the workflow.
If Commitlint fails, Git does not create the commit. You will see the errors in your terminal; fix the message and run git commit again (no need to wait for CI to discover the problem).
Hooks not running? Run npm ci (or npm install) once at the repo root. If you only install dependencies under apps/ui, root Husky never runs and commits will not be checked locally.
(This root install is npm, not Bun — it's only for the tiny commitlint/Husky devDependency set at the repository root and is unrelated to the apps/ui Bun migration.)
To bypass hooks in exceptional cases (not recommended for routine work): git commit --no-verify.
Dry-run a message without committing (from repo root, after npm ci):
printf '%s\n' 'feat(ui): example valid message' | npx --no -- commitlint --verboseUse Conventional Commits, for example:
feat(ui): add repo filter to sidebarfix(api): handle rate-limit responseschore: bump worker deps
CI runs Commitlint on the commits in each push or pull request, so messages that do not follow the convention will fail the Commit Messages check.
Bug-fix PRs must include a regression test that exercises the failure scenario described in the linked issue — not just a description or manual test-plan checklist. A fix without a test that would have caught the original bug is not considered complete.
Two established patterns to follow, depending on what you're testing:
- Pure-function unit test — for logic extracted into a small helper (e.g.
apps/ui/lib/repo-segment.ts,apps/ui/lib/token-resolve.ts, or a Python function). Plaindescribe/it(UI) or atest_*function (Python), no rendering or mocking needed. Seeapps/ui/tests/lib/repo-segment.test.tsorapps/ui/tests/lib/token-resolve.test.ts. - Hook/component test with mocked I/O — for behavior that lives in a React hook or component (e.g.
useAuth,AuthGuard) or a Python function that hits the network/DB. On the UI side, userenderHook/renderfrom@testing-library/reactwithvi.stubGlobal("fetch", ...)orvi.mock(...)to control responses — seeapps/ui/tests/lib/auth-context.test.tsxorapps/ui/tests/components/auth-guard.test.tsx. On the Python side, tests run against a real Postgres database inside a transaction/savepoint (seeapps/api/tests/conftest.py) rather than mocking the DB.
Run the full test suites before opening a PR:
# UI, from apps/ui
bun run test
# Python (API + worker + packages/checks), from repo root
pytest -qCI enforces two coverage gates, both self-hosted (no external service):
- Global floor — a regression guard, not a target. Overall coverage can't drop below the last-measured baseline (currently ~85% Python, ~21% UI — the UI number is low because most
app/**page components aren't unit-tested yet, not because the gate is lenient). This just stops things from getting worse. - Diff coverage — the real gate. New or changed lines in your PR must be covered by a test (currently
--fail-under=90), checked againstorigin/mainviadiff-cover. This is what actually enforces the rule above ("bug-fix PRs must include a regression test") — it will fail your PR if you touch a line with no test exercising it, regardless of the file's pre-existing coverage.
Check both locally before opening a PR:
# Python — from repo root, with the local Postgres db running (see `docker compose up db`)
pytest -q --cov=apps/api/src --cov=apps/worker/src --cov=packages/checks/src --cov-report=xml --cov-report=term
diff-cover coverage.xml --compare-branch=origin/main --fail-under=90
# UI — from apps/ui
bun run test:coverage
# diff-cover needs lcov.info's paths to be repo-root-relative (vitest emits them relative
# to apps/ui); rewrite them and run from the repo root:
cd .. && sed -E 's#^SF:(.*)$#SF:apps/ui/\1#' apps/ui/coverage/lcov.info | tr '\134' '/' > apps/ui/coverage/lcov-root-relative.info
diff-cover apps/ui/coverage/lcov-root-relative.info --compare-branch=origin/main --fail-under=90pip install diff-cover if you don't have it (it's in requirements-test.txt already for the Python side).
These mirror .github/workflows/ci.yml.
cd apps/ui
bun install --frozen-lockfile
bun run typecheck
bun run lint
bun run test:coverage
bun run build(bun run check runs typecheck, lint, and build together — but not tests; run those separately. See Coverage above for the diff-coverage check CI also runs.)
python -m pip install --upgrade pip
python -m pip install -r apps/api/requirements.txt
python -m pip install -r apps/worker/requirements.txt
python -m pip install -e packages/checks
python -m pip install -r requirements-test.txt
python -m pytest -q --cov=apps/api/src --cov=apps/worker/src --cov=packages/checks/src --cov-report=xml --cov-report=term --cov-fail-under=85
python -m compileall apps/api/src apps/worker/srcFrom the repository root (both apps/api and apps/worker build from the repo root so their images can install the shared packages/checks dependency; apps/ui has no such dependency and builds from its own directory):
docker build -t clevis-api -f apps/api/Dockerfile .
docker build -t clevis-worker -f apps/worker/Dockerfile .
docker build -t clevis-ui -f apps/ui/Dockerfile apps/uiCI also smoke-tests the API and worker images after building — it runs each image's entrypoint module directly (import src.main / import worker) with dummy env vars, bypassing entrypoint.sh so no live DB is required. This catches a class of bug that a plain docker build can't: an image that builds fine but is missing a runtime dependency (e.g. packages/checks not being installed), which only surfaces once the container actually runs. If you change either Dockerfile, run the equivalent locally before opening a PR:
docker run --rm --entrypoint python \
-e DATABASE_URL=postgresql+psycopg://smoke:smoke@localhost:5432/smoke \
-e JOB_SECRET_KEY=local-smoke-test-key -e AUTH_SECRET=local-smoke-test-secret \
clevis-api -c "import src.main"
docker run --rm --entrypoint python \
-e DATABASE_URL=postgresql://smoke:smoke@localhost:5432/smoke \
-e JOB_SECRET_KEY=local-smoke-test-key \
clevis-worker -c "import worker"Auth-flow critical paths (login, GitHub OAuth error redirect, logout, mid-session 401 handling), the Settings GitHub App install surface, and the Security-scan happy/error paths are covered by Playwright tests in apps/ui/e2e/, run against the full docker-compose stack — the real Docker images, not app code in isolation, so this catches deployment-shaped bugs unit tests can't (it's the same infra that would have caught the worker packages/checks gap). CI runs this as a required check (E2E Tests).
Security-scan and org-connection specs that need a live GitHub org use Playwright route mocks against the API host — CI has no real App installation, but the browser still exercises the full UI → API client → render path.
Run locally, from the repository root:
# ⚠️ Uses whatever DB_USER/DB_PASSWORD is in your .env. If you use dummy/different
# credentials than your normal local dev .env, the Postgres volume gets initialized with
# those on first run — Postgres only sets the password at first init, so switching .env
# credentials afterward will fail with "password authentication failed". If that happens,
# `docker volume rm clevis_pgdata` to force a clean re-init (you'll lose local dev DB data).
docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile backend --profile frontend up --build -d
# Wait for both to respond (compose healthchecks cover db/api; poll ui yourself):
curl http://localhost:8080/healthz
curl http://localhost:3000/login
cd apps/ui
bunx playwright install --with-deps chromium # one-time, or after a Playwright version bump
E2E_BASE_URL=http://localhost:3000 E2E_API_BASE=http://localhost:8080 bun run test:e2eCORS note: the API's CORS_ORIGINS defaults to ["http://localhost:3000"]. If you publish the UI on a different local port (e.g. to avoid clashing with another process already on 3000), add a matching CORS_ORIGINS=["http://localhost:<port>"] to .env and restart the api container — otherwise the browser's fetch calls fail with a generic "Failed to fetch" (a CORS rejection, not a real app or network error, but that's all the browser reports).
Tear down afterward with docker compose -f docker-compose.yml -f docker-compose.ci.yml down (add -v only if you want to wipe the DB volume too).
docker-compose.ci.yml is a CI-only override — the base docker-compose.yml deliberately has no host port bindings (Traefik-only, production security posture), so this override publishes api:8080 and ui:3000 to the runner for Playwright to reach.
- Open a PR against the branch your maintainers use as the integration target (often
main). - Keep changes focused; unrelated drive-by refactors make review harder.
- Ensure all CI jobs pass (commits, UI, Python, Docker build + smoke-test, E2E).
- Bug-fix PRs should include a regression test — see Testing above.
- Do not commit secrets. Use
.envlocally (see.env.exampleand the main README for self-host setup). - If you change ignore rules, remember that a bare
lib/entry in.gitignoreignores everylibdirectory in the tree; root-only patterns like/lib/avoid accidentally excluding app code (for example underapps/ui/lib).
If something in this doc is outdated or unclear, opening an issue or a small doc-fix PR is welcome.