Quality Checks
The 10 sequential checks in the quality job, their execution order, why order matters, and what each check catches.
The quality job runs 10 checks sequentially. The order is deliberate -- each check builds on the guarantees established by earlier checks. Reordering would produce confusing error messages or miss violations entirely.
Check Execution Order
| # | Check | Command | What It Catches |
|---|---|---|---|
| 1 | Format | pnpm format:check | Whitespace, quotes, semicolons, trailing commas |
| 2 | Lint | pnpm turbo lint --affected | Import violations, unused imports, type safety, style rules |
| 3 | Dependency graph | pnpm dep-cruise | Cross-layer imports, circular dependencies |
| 4 | Dead code | pnpm lint:dead-code | Unused exports, files, dependencies (Knip) |
| 5 | Duplication | pnpm lint:duplication | Copy-pasted code blocks (jscpd) |
| 6 | Typecheck | pnpm turbo typecheck --affected | Type errors across all packages |
| 7 | Migrate | pnpm db:migrate | Migration applies cleanly to fresh Postgres |
| 8 | Test | pnpm turbo test --affected | Unit and integration test failures |
| 9 | Build | pnpm turbo build --affected | Next.js build errors (SSR, config) |
The duplication report is always uploaded as an artifact (step 10), even if earlier checks fail.
Why Order Matters
Format before lint
Prettier and ESLint share some formatting concerns (semicolons, quotes). Running Prettier first establishes a consistent baseline so ESLint errors reflect real issues, not formatting disagreements.
Lint before typecheck
ESLint's no-restricted-imports rule catches cross-package boundary violations. These violations would produce confusing TypeScript errors ("cannot find module") if typecheck ran first. By linting first, the developer sees a clear "import X from Y is not allowed" message instead of a cryptic type resolution failure.
Lint before dependency graph
The dep-cruise check validates the architectural layer hierarchy (app > service > infra > core > leaf). ESLint's no-restricted-imports catches the same violations at the file level, while dep-cruise validates the package-level dependency graph. Running lint first means import-level violations are caught before the broader graph check.
Typecheck before migrate
Typecheck validates that migration files and schema definitions are type-correct. Running typecheck first prevents the migration step from failing with runtime errors that would have been caught statically.
Migrate before test
The CI Postgres service container starts empty. pnpm db:migrate applies all migrations, creating the schema that integration tests need. Without this step, any test that queries the database (RLS tests, search tests) would fail with "relation does not exist" errors.
Test before build
Tests are faster than a full Next.js build and catch logic errors. Running tests first gives faster feedback on the most actionable failures. If tests pass but build fails, the issue is typically a Next.js configuration problem, not a code logic error.
Check Details
1. Format Check (Prettier)
pnpm format:check
Runs Prettier in check mode (no writes) across the entire monorepo. Fails if any file differs from Prettier's output. The pre-commit hook runs prettier --write on staged files, so format failures in CI typically mean the pre-commit hook was bypassed.
2. Lint (ESLint via Turborepo)
pnpm turbo lint --affected
Runs ESLint in each package that has changed files (Turborepo's --affected flag). Each package has its own eslint.config.js with package-specific rules. Key rules enforced:
no-restricted-imports-- SDK boundary violations (external SDKs must go through@repo/*wrappers)no-console--console.logis banned; use@repo/loggerinsteadunicorn/expiring-todo-comments-- every TODO must have a date[YYYY-MM-DD]or ticket[TRO-NNN]simple-import-sort-- deterministic import ordering
3. Dependency Graph (dependency-cruiser)
pnpm dep-cruise
Validates the package dependency graph against the layer hierarchy defined in .dependency-cruiserrc.mjs. Catches violations like a @repo/db package importing from @repo/api (core importing from service layer).
4. Dead Code (Knip)
pnpm lint:dead-code
Detects unused exports, files, and dependencies across the monorepo. Knip analyzes the full dependency graph to find code that is never imported by any consumer. This prevents the accumulation of dead code that increases bundle size and maintenance burden.
5. Duplication (jscpd)
pnpm lint:duplication
Scans for copy-pasted code blocks using jscpd. The HTML report is uploaded as a jscpd-report artifact (14-day retention) regardless of job outcome. This makes the report available for review even when the job fails on an earlier step.
6. Typecheck (TypeScript via Turborepo)
pnpm turbo typecheck --affected
Runs tsc --noEmit in each affected package. TypeScript strict mode is enabled across the monorepo (strict: true in the shared tsconfig.json). This catches type errors that ESLint cannot detect.
7. Database Migration
pnpm db:migrate
Applies all migrations to the ephemeral CI Postgres (port 5433). This validates that:
- Migration SQL is syntactically correct
- Migrations apply cleanly to a fresh database (no dependency on manual state)
- The migration journal (
_journal.json) is consistent
This is a separate concern from the migrate-prod job, which runs against production Cloud SQL.
8. Test (Vitest via Turborepo)
pnpm turbo test --affected
Runs Vitest in each affected package. Tests include:
- Unit tests -- pure logic, no external dependencies
- Integration tests -- RLS policy verification, cache operations, search indexing (using service containers)
The --affected flag means only packages with changed files (or packages that depend on changed packages) run their tests.
9. Build (Next.js via Turborepo)
pnpm turbo build --affected
Runs the production build for @repo/web and any affected library packages. This step receives the SENTRY_AUTH_TOKEN secret for source map uploads. Build failures at this stage typically indicate:
- Next.js config issues (middleware, route conflicts)
- SSR-incompatible code (browser-only APIs used at module scope)
- Environment variable mismatches (
NEXT_PUBLIC_*not available)
Failure Behavior
Each check runs as a separate GitHub Actions step. Steps run sequentially, and a failure stops execution -- later steps are skipped. This means:
- A format error prevents lint from running
- A lint error prevents typecheck from running
- A test failure prevents build from running
The one exception is the duplication report upload, which uses if: always() and runs regardless of prior failures.
Local Equivalent
The pnpm ci:check command mirrors checks 1--8 locally. The build step (check 9) is omitted for speed -- it rarely fails when typecheck passes. Use pnpm ci:full to include the build. See Local CI Parity for details.