Trovella Wiki

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

#CheckCommandWhat It Catches
1Formatpnpm format:checkWhitespace, quotes, semicolons, trailing commas
2Lintpnpm turbo lint --affectedImport violations, unused imports, type safety, style rules
3Dependency graphpnpm dep-cruiseCross-layer imports, circular dependencies
4Dead codepnpm lint:dead-codeUnused exports, files, dependencies (Knip)
5Duplicationpnpm lint:duplicationCopy-pasted code blocks (jscpd)
6Typecheckpnpm turbo typecheck --affectedType errors across all packages
7Migratepnpm db:migrateMigration applies cleanly to fresh Postgres
8Testpnpm turbo test --affectedUnit and integration test failures
9Buildpnpm turbo build --affectedNext.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.log is banned; use @repo/logger instead
  • unicorn/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.

On this page