Roxabi Boilerplate
Architecture

CI/CD Architecture

Pipeline stages, quality gates, caching strategy, git hooks, and deployment automation in Roxabi Boilerplate.

Overview

Roxabi uses a three-layer quality pipeline to catch issues at progressively broader scopes: local git hooks enforce standards on every commit and push, GitHub Actions CI validates every pull request and branch push, and Vercel handles production deployment automatically on merge to main.

Loading diagram...

This design follows a "fail fast, fail local" philosophy: most issues are caught before code leaves the developer's machine, reducing CI costs and feedback time.


Lefthook Git Hooks

Lefthook manages local git hooks. It installs automatically via the prepare script (lefthook install) when running bun install.

Configuration lives in lefthook.yml at the repository root.

Hook Stages

HookTriggerCommandsParallel
pre-commitBefore commit is createdBiome check + auto-fixYes
commit-msgAfter commit message is writtenCommitlint validationNo
pre-pushBefore push to remoteLint, typecheck, test with coverageYes

pre-commit

Runs Biome on staged files matching *.{js,ts,jsx,tsx,json,jsonc}. The --write flag auto-fixes issues and stage_fixed: true re-stages corrected files so the commit includes the fixes.

pre-commit:
  parallel: true
  commands:
    biome:
      glob: "*.{js,ts,jsx,tsx,json,jsonc}"
      run: bunx biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
      stage_fixed: true

commit-msg

Validates the commit message against Conventional Commits using commitlint. Rejects messages that do not match the <type>(<scope>): <description> format. See Commit Conventions below for the full type list.

commit-msg:
  commands:
    commitlint:
      run: bunx commitlint --edit {1}

pre-push

Runs the full quality suite in parallel before any push reaches the remote. This mirrors what CI will check, catching failures early.

pre-push:
  parallel: true
  commands:
    lint:
      run: CI=true bun run lint
    typecheck:
      run: CI=true bun run typecheck
    test:
      run: CI=true bun run test:coverage

The CI=true environment variable ensures consistent behavior between local and CI environments (e.g., disabling watch mode in Vitest).


GitHub Actions Workflows

Six workflows live in .github/workflows/:

WorkflowFileTriggerPurpose
CIci.ymlPR + push to main/stagingQuality gates for all code changes
Deploy Previewdeploy-preview.ymlManual (workflow_dispatch)On-demand preview deployments to Vercel
PR Titlepr-title.ymlPR opened/editedValidates PR title follows Conventional Commits format
Auto Mergeauto-merge.ymlDependabot PRsAuto-merges Dependabot PRs that pass CI
Neon Cleanupneon-cleanup.ymlPR closed/mergedCleans up Neon database branches for closed/merged PRs
PR Review & Auto-fixpr-review.ymlPR opened/synchronizedRuns Claude Code automated review + auto-fix on every PR

CI Workflow (ci.yml)

The CI workflow runs on every pull request targeting main or staging, and on direct pushes to those branches. It also exposes workflow_call so other workflows can invoke it as a reusable step.

Concurrency control prevents duplicate runs. Each branch gets at most one active CI run; new pushes cancel in-progress runs:

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Job Dependency Graph

Loading diagram...

The first three jobs (lint, typecheck, test) run in parallel. build runs only after all three succeed. e2e runs after build succeeds, but only when relevant paths changed.

Job Details

Lint -- Installs dependencies with bun install --frozen-lockfile, then runs:

  • bun lint -- Biome check across the entire monorepo
  • bun run i18n:check -- Validates translation file completeness
  • bun run license:check -- Scans dependency licenses for compliance
  • bun run env:check -- Validates that .env.example is in sync with expected variables

Type Check -- Generates code artifacts (codegen for Paraglide i18n and TanStack Router route tree), then runs bun typecheck (TurboRepo delegates to each workspace's tsc --noEmit).

Schema Drift Detection -- As part of the type check job, CI runs bun run db:generate with a dummy DATABASE_URL and checks git diff on the apps/api/drizzle/ directory. If uncommitted migration files appear, CI fails with a clear error message directing the developer to generate and commit the migration locally. This catches cases where a schema change was made in code but the corresponding Drizzle migration was not generated.

Test -- Generates code artifacts, then runs bun test:coverage (Vitest with V8 coverage). On pull requests, posts a coverage summary comment using the vitest-coverage-report-action.

Build -- Runs bun run build (TurboRepo builds all workspaces respecting dependency order). This validates that production artifacts compile successfully.

E2E Tests -- Runs Playwright against the built web app. Conditional execution keeps CI fast for backend-only changes:

ConditionRuns E2E?
Push to main or stagingAlways
PR with e2e labelAlways
PR changes apps/web/src/**, apps/web/e2e/**, packages/ui/src/**, playwright.config.ts, ci.yml, or turbo.jsoncYes
PR with only backend/docs changesSkipped

E2E test failures upload the Playwright HTML report as a GitHub artifact (retained 7 days).

E2E Matrix Strategy

E2E tests use a 2-shard strategy across three browser engines to maximise coverage without blocking CI:

JobDescription
e2e-matrixRuns sharded E2E tests across 2 shards × 3 browsers (chromium, firefox, webkit)
merge-reportsAggregates sharded Playwright results into a single HTML report

The shard split reduces wall-clock time for larger E2E suites. The merge-reports job always runs after all matrix shards complete, collecting results into one artifact.

Deploy Preview Workflow (deploy-preview.yml)

A manually-triggered workflow for on-demand preview deployments. Dispatched from the GitHub Actions tab with a branch selection and target choice: web, api, both, or cleanup.

Key behaviors:

  • Web preview: Deploys directly with vercel deploy (Vercel handles the build remotely)
  • API preview: Creates an isolated Neon database branch (preview/{branch}), builds the API with that branch's DATABASE_URL, deploys to Vercel with the branch-specific database URL injected as an environment variable
  • Cleanup: Deletes the Neon database branch for the selected Git branch
  • Preview URLs appear in the workflow run summary

For operational details on triggering previews and managing database branches, see the Deployment Guide.


Biome Configuration

Biome handles both linting and formatting in a single tool. Configuration lives in biome.json at the repository root.

Formatter Settings

SettingValue
Indent styleSpaces
Indent width2
Line width100
Quote styleSingle
SemicolonsAs needed
Trailing commasES5

Linter Rules

Biome runs the recommended rule set with project-specific overrides:

CategoryRuleSeverity
CorrectnessnoUnusedImportsError
CorrectnessnoUnusedVariablesError
StyleuseConstError
StyleuseImportTypeError (off for apps/api/ -- NestJS needs runtime imports for DI)
ComplexitynoExcessiveCognitiveComplexityWarn
SuspiciousnoExplicitAnyWarn
SecuritynoDangerouslySetInnerHtmlWarn

Integration Points

Biome runs at three levels:

  1. Claude Code hooks (.claude/settings.json): Auto-formats on every Edit/Write tool use for .ts, .tsx, .js, .jsx, .json files
  2. Lefthook pre-commit: Checks and auto-fixes staged files before each commit
  3. GitHub Actions CI: bun lint runs biome check . across the full monorepo

File Exclusions

Build artifacts, generated code, and third-party files are excluded from Biome processing:

node_modules, dist, build, .next, .turbo, coverage, .output,
.nitro, .content-collections, routeTree.gen.ts, src/paraglide,
project.inlang, *.css, playwright-report, test-results,
reports/licenses.json, .vercel

Commitlint and Conventional Commits

Commit messages are validated by commitlint using the @commitlint/config-conventional preset, extended with project-specific rules in commitlint.config.cjs.

Allowed Types

TypeDescription
featNew feature
fixBug fix
refactorCode restructuring, no behavior change
docsDocumentation only
styleFormatting, no code change
testAdding or updating tests
choreMaintenance tasks
perfPerformance improvement
ciCI/CD changes
buildBuild system changes
revertRevert a previous commit

Constraints

  • Header max length: 100 characters (more permissive than the conventional default of 72)
  • Format: <type>(<scope>): <description> -- scope is optional but recommended

Commitlint runs as a Lefthook commit-msg hook. Messages that fail validation are rejected before the commit is created. See Contributing for full conventions including scope guidelines and breaking change format.


TurboRepo Caching Strategy

TurboRepo accelerates the monorepo by caching task outputs and skipping unchanged work. Configuration lives in turbo.jsonc (root) with app-specific extensions in apps/api/turbo.json.

Task Graph

Loading diagram...

Key task configurations:

TaskInputsOutputsCache
build$TURBO_DEFAULT$ minus test/story filesdist/, .output/, .vercel/output/, .next/, build/Yes
codegenDefaultsrc/routeTree.gen.ts, src/paraglide/**Yes
typecheck$TURBO_DEFAULT$ minus test filesNone (type-check is side-effect only)Yes
lintDefaultNoneYes
testsrc/**/*.ts, src/**/*.tsx, vitest.config.tscoverage/**Yes
dev----No (persistent)
clean----No

Environment Variable Strategy

TurboRepo uses two mechanisms for environment variables, and the distinction is critical for cache correctness:

env (declared in root turbo.jsonc) -- Variables whose values affect build output. Changing any of these invalidates the Turbo cache for the affected task:

  • API_URL, APP_URL, VITE_* (build task and codegen)
  • CI, VITEST (test task)

passThroughEnv (declared in apps/api/turbo.json) -- Runtime-only secrets that must be available during the build process but whose values should not affect the cache key. This prevents unnecessary cache invalidation when secrets rotate:

// apps/api/turbo.json
{
  "extends": ["//"],
  "tasks": {
    "build": {
      "passThroughEnv": [
        "DATABASE_URL",
        "BETTER_AUTH_SECRET",
        "BETTER_AUTH_URL",
        "CORS_ORIGIN",
        "KV_REST_API_URL",
        "KV_REST_API_TOKEN",
        // ... ~25 more runtime variables
      ]
    }
  }
}

Why this matters: If DATABASE_URL were declared in env instead of passThroughEnv, every secret rotation would invalidate the build cache -- forcing a full rebuild even though the application code is identical. passThroughEnv passes the variable through to the build process without including it in the cache hash.

Cache in CI

GitHub Actions caches the .turbo directory using actions/cache with SHA-based keys:

- uses: actions/cache@v5
  with:
    path: .turbo
    key: turbo-build-${{ runner.os }}-${{ github.sha }}
    restore-keys: |
      turbo-build-${{ runner.os }}-

Each CI job (typecheck, test, build) maintains its own Turbo cache namespace to avoid cross-contamination. The restore-keys fallback means new commits can reuse the most recent cache from the same OS, even if the exact SHA differs.

Remote cache is also configured. The CI workflow passes TURBO_TOKEN and TURBO_TEAM environment variables to TurboRepo, enabling Vercel Remote Cache for cross-run and cross-developer cache sharing.

Dependency Caching

Bun dependencies are cached separately from Turbo:

- uses: actions/cache@v5
  with:
    path: node_modules
    key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
    restore-keys: |
      bun-${{ runner.os }}-

The cache key is derived from bun.lock, so it invalidates only when dependencies change. Combined with --frozen-lockfile, this ensures deterministic installs across CI runs.


Vercel Deployment Pipeline

Vercel is the single deployment platform for both applications. Production deploys happen automatically; previews are manual.

Production Flow

Only merges to main trigger production deployments. Both apps/web and apps/api are separate Vercel projects pointing to the same GitHub repository.

Loading diagram...

Duplicate Deploy Prevention

Two layers prevent unnecessary builds:

LayerMechanismPurpose
PrimarypreviewDeploymentsDisabled: true (Vercel project setting)Prevents Vercel from even attempting preview deployments
SecondaryignoreCommand in vercel.jsonSkips builds on non-main branches and uses turbo-ignore to detect affected packages

The ignoreCommand logic:

[ "$VERCEL_GIT_COMMIT_REF" != "main" ] || npx turbo-ignore @repo/web

This short-circuits: if the branch is not main, the build is skipped immediately (exit 0). If it is main, turbo-ignore checks whether the package was affected by the commit -- skipping the build if nothing relevant changed.

Build Configuration

Build settings are version-controlled in per-app vercel.json files:

AppInstallBuildSpecial
Webbun installturbo run buildNitro outputs to .vercel/output/ (Build Output API)
APIbun installbun run db:migrate && turbo run buildMigrations run before build; schema is always ≥ code version

Branch Protection Rules

Branch protection enforces the pipeline at the GitHub level, preventing merges that bypass quality gates.

main Branch

  • Required pull request reviews (minimum 1 approval)
  • Stale approvals dismissed on new commits
  • Required status checks (CI must pass)
  • Require conversation resolution before merge
  • No direct pushes
  • Only hotfix/* branches and staging merges allowed

staging Branch

  • Required status checks (CI must pass)
  • No direct pushes

These rules integrate with the CI workflow: the lint, typecheck, test, and build jobs are configured as required status checks, so a failing CI run blocks the merge button.

ci-success Aggregator Job

The CI workflow includes a ci-success job that acts as a single required status check for branch protection. Rather than requiring each individual job (lint, typecheck, test, build) as a separate status check in GitHub settings, branch protection requires only ci-success. This job only passes when all preceding jobs succeed, giving a single stable check name that aggregates the full CI result. This simplifies branch protection configuration and avoids stale check entries when jobs are renamed or added.


Claude Code Hooks

In addition to git hooks, Claude Code has its own hook system configured in .claude/settings.json:

PostToolUse -- After every Edit or Write operation on .ts, .tsx, .js, .jsx, or .json files, Biome auto-formats the file. This keeps code consistent without requiring manual formatting.

PreToolUse -- Before every Edit or Write, a security check script (.claude/hooks/security-check.js) scans for dangerous patterns in the proposed changes.

These hooks operate independently of git hooks and run during interactive development sessions with Claude Code.


Pipeline Summary

The complete quality pipeline from developer keystroke to production deployment:

StageToolWhat It ChecksBlocks
Editor saveClaude Code hooksFormatting, security patternsWrite operation
git commitLefthook pre-commitBiome lint + format on staged filesCommit creation
git commitLefthook commit-msgConventional Commits formatCommit creation
git pushLefthook pre-pushLint, typecheck, test + coveragePush to remote
PR / pushGitHub Actions CILint, typecheck, test, build, schema drift, E2EPR merge
Merge to mainVercelBuild, migrate, deployProduction deploy (rollback available)

We use cookies to improve your experience. You can accept all, reject all, or customize your preferences.