// "Apply quality gate standards for git hooks, testing, CI/CD, and automation using Lefthook, Vitest, GitHub Actions, and quality enforcement. Use when setting up quality infrastructure, configuring hooks, discussing automation, or reviewing quality practices."
| name | quality-gates |
| description | Apply quality gate standards for git hooks, testing, CI/CD, and automation using Lefthook, Vitest, GitHub Actions, and quality enforcement. Use when setting up quality infrastructure, configuring hooks, discussing automation, or reviewing quality practices. |
Standards and patterns for maintaining code quality through automated gates, hooks, testing, and continuous integration.
Quality gates prevent problems before they reach production. Automated checks provide immediate feedback, enforce standards consistently, and free developers to focus on building features rather than remembering process.
Progressive enforcement:
Why Lefthook over Husky:
# Install via package manager of choice
pnpm add -D lefthook
# or npm install -D lefthook
# or brew install lefthook (global)
# Initialize lefthook
pnpm lefthook install
# This creates .lefthook directory and configures git hooks
Basic Setup (Next.js/TypeScript project):
# lefthook.yml - Place in project root
pre-commit:
parallel: true # Run commands in parallel for speed
commands:
lint:
glob: "*.{js,ts,jsx,tsx}"
run: pnpm eslint --fix {staged_files}
stage_fixed: true # Re-stage files after fixing
format:
glob: "*.{js,ts,jsx,tsx,json,md,css}"
run: pnpm prettier --write {staged_files}
stage_fixed: true
typecheck:
glob: "*.{ts,tsx}"
run: pnpm tsc --noEmit
# Note: Only checks, doesn't fix
pre-push:
commands:
test:
run: pnpm test:ci
# Full test suite with coverage
build:
run: pnpm build
# Ensure production build succeeds
commit-msg:
commands:
commitlint:
run: pnpm commitlint --edit {1}
# Enforce conventional commits
Advanced: Monorepo with Multiple Packages
pre-commit:
parallel: true
commands:
# Package-specific linting
lint-web:
glob: "apps/web/**/*.{ts,tsx}"
run: pnpm --filter web lint --fix {staged_files}
root: apps/web/
stage_fixed: true
lint-api:
glob: "apps/api/**/*.ts"
run: pnpm --filter api lint --fix {staged_files}
root: apps/api/
stage_fixed: true
# Shared package linting
lint-shared:
glob: "packages/**/*.{ts,tsx}"
run: pnpm --filter @repo/* lint --fix {staged_files}
stage_fixed: true
# Global formatting
format:
glob: "**/*.{js,ts,jsx,tsx,json,md}"
run: pnpm prettier --write {staged_files}
stage_fixed: true
pre-push:
commands:
# Run tests for changed packages only
test-changed:
run: pnpm turbo run test --filter=[HEAD^1]
# Type check all packages
typecheck:
run: pnpm turbo run typecheck
# Build all packages
build:
run: pnpm turbo run build
Convex-Specific Hooks
pre-commit:
parallel: true
commands:
# Standard linting/formatting
lint:
glob: "*.{js,ts,jsx,tsx}"
run: pnpm eslint --fix {staged_files}
stage_fixed: true
# Convex function validation
convex-typecheck:
glob: "convex/**/*.ts"
run: pnpm tsc --noEmit --project convex/tsconfig.json
# Convex schema validation (if you have validation scripts)
convex-schema:
glob: "convex/schema.ts"
run: pnpm convex dev --once --run convex/validateSchema.ts
# Only run if schema changed
pre-push:
commands:
test:
run: pnpm test:ci
# Deploy to preview environment for testing
convex-preview:
run: |
pnpm convex deploy --preview-name ci-$(git rev-parse --short HEAD)
echo "Preview deployed to: $(pnpm convex dashboard --preview-name ci-$(git rev-parse --short HEAD))"
# Skip pre-commit hooks (use sparingly!)
git commit --no-verify -m "emergency fix"
# Skip pre-push hooks
git push --no-verify
# Skip specific lefthook command
LEFTHOOK_EXCLUDE=test git push
Use vitest-coverage-report-action for PR comments:
Setup:
- uses: davelosert/vitest-coverage-report-action@v2
permissions:
contents: write
pull-requests: write
with:
file-coverage-mode: changes # Only show changed files
Standards:
Use pr-size-labeler for automatic size labeling:
Setup:
- uses: CodelyTV/pr-size-labeler@v1
with:
xs_max_size: '50'
s_max_size: '150'
m_max_size: '300'
l_max_size: '500'
fail_if_xl: 'true'
message_if_xl: 'PR exceeds 500 lines. Please split into smaller PRs.'
Benefits:
Why Vitest:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './test/setup.ts',
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
exclude: ['**/node_modules/**', '**/dist/**', '**/.next/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/.next/**',
'**/test/**',
'**/*.config.{js,ts}',
'**/*.d.ts',
],
// Don't enforce arbitrary thresholds
// Use coverage to find untested paths, not as success metric
thresholds: {
lines: 60, // Baseline, not target
functions: 60,
branches: 60,
statements: 60,
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:ci": "vitest run --coverage",
"test:watch": "vitest --watch",
"typecheck": "tsc --noEmit",
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,css}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,css}\""
}
}
Why GitHub Actions:
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
jobs:
quality-checks:
name: Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm typecheck
- name: Test
run: pnpm test:ci
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
fail_ci_if_error: false
- name: Build
run: pnpm build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: .next/
retention-days: 7
# .github/workflows/comprehensive-ci.yml
name: Comprehensive CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
# Unit and integration tests across Node versions
test-matrix:
name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm test:ci
- run: pnpm build
# E2E tests with Playwright
e2e:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: pnpm playwright install --with-deps chromium
- name: Build application
run: pnpm build
- name: Run E2E tests
run: pnpm test:e2e
env:
PLAYWRIGHT_BROWSERS_PATH: 0
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
# Security scanning
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Audit dependencies
run: pnpm audit --audit-level=moderate
- name: Check for vulnerabilities
run: pnpm dlx @socketsecurity/cli audit
# .github/workflows/convex-ci.yml
name: Convex CI
on:
pull_request:
branches: [main]
paths:
- 'convex/**'
- 'src/**'
jobs:
convex-validate:
name: Validate Convex Functions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type check Convex functions
run: pnpm tsc --noEmit --project convex/tsconfig.json
- name: Deploy to preview
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
run: |
pnpm convex deploy --preview-name pr-${{ github.event.pull_request.number }}
- name: Run Convex tests
run: pnpm test:convex
- name: Comment preview URL
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 Convex preview deployed: https://dashboard.convex.dev/t/pr-${{ github.event.pull_request.number }}`
})
Configure in GitHub Settings → Branches → Branch protection rules for main:
Required settings:
Quality Checks, Test, Build, E2E TestsOptional but recommended:
Setup:
CODECOV_TOKEN# codecov.yml
coverage:
status:
project:
default:
target: auto # Maintain current coverage
threshold: 5% # Allow 5% decrease
if_ci_failed: error
patch:
default:
target: 70% # New code should be well-tested
if_ci_failed: error
comment:
layout: "reach, diff, flags, files"
behavior: default
require_changes: false
ignore:
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/*.config.ts"
- "**/*.d.ts"
- "**/test/**"
- "**/__tests__/**"
When setting up quality gates for a new project:
❌ Husky: Use Lefthook (faster, language-agnostic, simpler) ❌ Arbitrary coverage targets: Use coverage to find gaps, not as success metric ❌ Testing implementation details: Test behavior, not internals ❌ Heavy mocking: Minimize mocks, prefer real integration tests ❌ Skipping hooks routinely: Fix the problem, don't bypass gates ❌ CI that only tests on main: Test on every PR ❌ No branch protection: Enforce quality before merge ❌ Manual version bumping: Automate with changesets/semantic-release
# 1. Initialize project
pnpm init
# 2. Install quality tools
pnpm add -D \
lefthook \
vitest @vitest/coverage-v8 \
@testing-library/react @testing-library/jest-dom \
eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin \
prettier eslint-config-prettier \
@commitlint/cli @commitlint/config-conventional
# 3. Initialize lefthook
pnpm lefthook install
# 4. Create configurations
# - lefthook.yml (git hooks)
# - vitest.config.ts (testing)
# - .github/workflows/ci.yml (CI/CD)
# - .prettierrc (formatting)
# - .eslintrc.js (linting)
# - commitlint.config.js (commit message validation)
# - codecov.yml (coverage reporting)
# 5. Add scripts to package.json
# (see scripts section above)
# 6. Configure GitHub branch protection
# 7. First commit
git add .
git commit -m "feat: setup quality gates infrastructure"
# Lefthook will run pre-commit hooks automatically
Quality is not a phase—it's built into the process.
Coverage is a diagnostic tool, not a goal. 60% meaningful coverage beats 95% testing implementation details.
Automate everything. Manual processes fail. Automated gates are consistent, fast, and free developers to build.
When agents design quality infrastructure, they should: