Continuous Integration
Continuous Integration (CI) is a development practice where developers frequently merge their code changes into a shared repository — often multiple times a day. Each merge triggers an automated build and test suite to detect integration errors as early as possible.
CI is the first line of defense against regressions. A well-tuned CI pipeline catches bugs, style violations, type errors, and performance regressions before they ever reach a reviewer or production environment.
CI is built on a few foundational practices that every team should adopt:
| Principle | Description | Impact |
|---|---|---|
| Frequent Commits | Push small, focused changes daily | Reduces merge conflicts, isolated bugs |
| Automated Build | Every commit triggers a reproducible build | Eliminates "works on my machine" |
| Automated Tests | Unit, integration, and lint checks run automatically | Catches regressions within minutes |
| Fast Feedback | Pipeline should complete in under 10 minutes | Maintains developer flow state |
| Main Branch is Stable | Broken builds block further merges | Enforces quality gating |
Pre-commit hooks catch issues before code is even committed. They run linting, formatting, and type-checking locally, providing instant feedback without consuming CI resources.
| 1 | # Install Husky in your project |
| 2 | npx husky init |
| 3 | |
| 4 | # Add a pre-commit hook to lint staged files |
| 5 | npx husky add .husky/pre-commit "npx lint-staged" |
| 6 | |
| 7 | # .lintstagedrc.json example |
| 8 | { |
| 9 | "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], |
| 10 | "*.{css,scss}": ["stylelint --fix", "prettier --write"], |
| 11 | "*.{json,md,yaml}": ["prettier --write"] |
| 12 | } |
| 13 | |
| 14 | # Add commit-msg hook for conventional commits |
| 15 | npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1" |
info
Branch protection rules enforce CI discipline. They prevent direct pushes to main, require pull requests, mandate status checks, and ensure branches are up to date.
| Rule | Setting | Purpose |
|---|---|---|
| Require PR | Required | Prevents direct pushes to protected branches |
| Required Approvals | 1-2 reviewers | Ensures code review before merge |
| Status Checks | All passing | Blocks merge if CI fails |
| Up-to-Date | Required | Branch must be based on latest main |
| Conversation Resolution | Required | All comments must be resolved before merge |
| 1 | # GitHub branch protection rule (via API or UI) |
| 2 | # Path: Settings > Branches > Add rule |
| 3 | |
| 4 | Branch name pattern: main |
| 5 | Settings: |
| 6 | - Require a pull request before merging |
| 7 | - Require approvals: 1 |
| 8 | - Dismiss stale reviews: true |
| 9 | - Require review from Code Owners: true |
| 10 | - Require status checks before merging |
| 11 | - Require branches to be up-to-date: true |
| 12 | - Status checks: |
| 13 | - "continuous-integration / lint" |
| 14 | - "continuous-integration / test" |
| 15 | - "continuous-integration / build" |
| 16 | - "codecov/project" |
| 17 | - Require conversation resolution: true |
| 18 | - Do not allow bypassing the above settings |
A robust CI pipeline is composed of sequential stages. Each stage filters out defects, so later stages run faster and more predictably.
1. Install Dependencies
Clean install of dependencies using lockfiles. Cache between runs for speed.
| 1 | npm ci # Clean install from package-lock.json |
| 2 | # or |
| 3 | yarn install --frozen-lockfile # Yarn equivalent |
2. Lint & Format
Enforce code style and catch obvious errors before running expensive tests.
| 1 | npm run lint # ESLint or equivalent |
| 2 | npm run format:check # Prettier check (not autofix in CI) |
| 3 | npm run typecheck # TypeScript checking (tsc --noEmit) |
3. Unit Tests
Fast, isolated tests that validate individual functions and components.
| 1 | npm run test:unit # Vitest, Jest, or Mocha |
| 2 | npm run test:coverage # With coverage reporting |
4. Build
Verify the application builds successfully. Catches bundling errors and missing dependencies.
| 1 | npm run build # Next.js, Vite, or Webpack build |
5. Integration & E2E Tests
Slower tests that validate the system as a whole. Run against a build artifact or preview deployment.
| 1 | npm run test:integration # API/integration tests |
| 2 | npm run test:e2e # Playwright or Cypress |
best practice
Choosing the right CI provider depends on your team size, project complexity, and budget. Here is a comparison of the three most popular options:
| Feature | GitHub Actions | Travis CI | CircleCI |
|---|---|---|---|
| Pricing | Free: 2000 min/mo | Free: limited (now paid-only for new users) | Free: 6000 min/mo (1 user) |
| Configuration | YAML in .github/workflows | YAML in .travis.yml | YAML in .circleci/config.yml |
| Matrix Builds | Built-in (strategy.matrix) | Built-in (matrix) | Built-in (matrix) |
| Cache | actions/cache@v3 | cache: directories | save/restore_cache |
| Self-Hosted | Yes (runners) | Yes (Travis CI Enterprise) | Yes (self-hosted runners) |
| MacOS/Windows | Yes | Yes (paid) | Yes (paid) |
| Marketplace | Extensive (20K+ actions) | Limited | Orbs (3rd-party) |
| Best For | GitHub-hosted projects | Open-source, legacy projects | Complex pipelines, large teams |
| 1 | # GitHub Actions — Full CI Pipeline Example |
| 2 | name: CI Pipeline |
| 3 | on: |
| 4 | push: |
| 5 | branches: [main, develop] |
| 6 | pull_request: |
| 7 | branches: [main] |
| 8 | |
| 9 | jobs: |
| 10 | lint: |
| 11 | runs-on: ubuntu-latest |
| 12 | steps: |
| 13 | - uses: actions/checkout@v4 |
| 14 | - uses: actions/setup-node@v4 |
| 15 | with: |
| 16 | node-version: 20 |
| 17 | cache: npm |
| 18 | - run: npm ci |
| 19 | - run: npm run lint |
| 20 | - run: npm run typecheck |
| 21 | |
| 22 | test: |
| 23 | runs-on: ubuntu-latest |
| 24 | steps: |
| 25 | - uses: actions/checkout@v4 |
| 26 | - uses: actions/setup-node@v4 |
| 27 | with: |
| 28 | node-version: 20 |
| 29 | cache: npm |
| 30 | - run: npm ci |
| 31 | - run: npm run test:unit -- --coverage |
| 32 | - uses: codecov/codecov-action@v3 |
| 33 | |
| 34 | build: |
| 35 | runs-on: ubuntu-latest |
| 36 | needs: [lint, test] |
| 37 | steps: |
| 38 | - uses: actions/checkout@v4 |
| 39 | - uses: actions/setup-node@v4 |
| 40 | with: |
| 41 | node-version: 20 |
| 42 | cache: npm |
| 43 | - run: npm ci |
| 44 | - run: npm run build |
| 45 | - uses: actions/upload-artifact@v4 |
| 46 | with: |
| 47 | name: build-output |
| 48 | path: .next/ |
pro tip
Status checks report the result of CI jobs back to the pull request. They appear as green checkmarks, red X's, or yellow dots next to each commit.
Required status checks must pass before a PR can merge. Configure them in your repository settings under Settings > Branches > Branch protection rules.
| Check Name | Status | Required |
|---|---|---|
| continuous-integration/lint | ✓ Passing | Yes |
| continuous-integration/test | ✓ Passing | Yes |
| continuous-integration/build | ✓ Passing | Yes |
| codecov/project | ⚠ Pending | Not required |
warning