Skip to main content
Back to module
Front-End Engineering · Shipping

CI for front-end

Reading50mLesson 1 of 3 in this module

Set up a GitHub Actions workflow that runs lint, typecheck, tests and a build on every pull request.

Why it matters

"It works on my machine" is not a release process. Continuous integration (CI) runs the same checks on a clean machine for every push and pull request, so broken code is caught before it is merged, not after it reaches users. On most front-end teams a pull request cannot be merged until CI is green, and setting up or fixing a pipeline is a normal mid-level task.

What a front-end pipeline checks

A typical pipeline runs these steps, cheapest first, so failures show up quickly:

  1. Install dependencies exactly as locked: npm ci (uses package-lock.json, fails if it is out of date).
  2. Lint: npm run lint (ESLint, including accessibility and React hooks rules).
  3. Type check: npm run typecheck, usually tsc --noEmit.
  4. Unit and integration tests: npm test (Vitest or Jest) in run-once mode, not watch mode.
  5. Build: npm run build, which catches import errors and framework build errors.
  6. Optional, often in separate jobs: end-to-end tests (Playwright), a Lighthouse budget check, bundle size limits, and a preview deploy.

Make sure each of these is a script in package.json, so the commands are the same locally and in CI:

{
  "scripts": {
    "lint": "eslint .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "build": "vite build"
  }
}

GitHub Actions basics

GitHub Actions reads YAML workflow files from .github/workflows/. A workflow has triggers (on), one or more jobs, and each job has steps that run on a fresh virtual machine called a runner. Actions is free for public repositories, and private repositories get a monthly allowance of free minutes on GitHub's hosted runners (see GitHub's billing docs, checked on 2026-10-23).

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

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

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build

What each part does:

  • on: run on every pull request and on pushes to main.
  • permissions: contents: read: give the job's token the least access it needs.
  • concurrency: cancel an older run on the same branch when you push again, saving minutes.
  • actions/checkout: clone the repository onto the runner.
  • actions/setup-node: install the Node version from .nvmrc and cache npm's download cache between runs.
  • Each run step is a shell command. If any exits with a non-zero code, the job fails and the PR shows a red cross.

Making CI useful

  • Required checks. In the repository settings, add a branch protection rule (or ruleset) for main that requires the CI job to pass before merging.
  • Fast feedback. Cache dependencies, cancel superseded runs, and split slow work (e2e) into a separate job that runs in parallel.
  • Same as local. Pin the Node version (.nvmrc or engines) and use npm ci, so CI and laptops agree.
  • Readable failures. One step per check, so the failing step's name tells you what broke.
  • Secrets. Never put tokens in the YAML. Store them as repository secrets and reference ${{ secrets.NAME }}. Pull requests from forks do not receive secrets, which protects you from malicious PRs.
  • Coverage gates (optional): fail the build when test coverage drops below an agreed threshold. Set the bar where it is today and only raise it.

Reading a failed run

Open the PR's Checks tab, select the failed job and expand the red step. Read from the first error, not the last line. Common causes:

  • npm ci fails: package-lock.json is out of sync. Run npm install locally and commit the lock file.
  • Tests pass locally but fail in CI: they depend on your time zone, locale, an environment variable or test order.
  • Build fails on case: macOS file systems are usually case-insensitive and Linux runners are not, so import Button from './button' breaks if the file is Button.tsx.

Common mistakes

  • Running npm install instead of npm ci, so CI quietly uses different versions.
  • Running tests in watch mode, so the job hangs until it times out.
  • Only running CI on main, after the bad code is merged.
  • Letting a flaky test stay red, so people learn to ignore CI.
  • Giving workflows broad token permissions or printing secrets in logs.

Quick recap

  • CI runs install, lint, type check, tests and build on a clean machine for every PR.
  • GitHub Actions workflows live in .github/workflows/ and are made of triggers, jobs and steps.
  • Use npm ci, a pinned Node version, caching, least-privilege permissions and cancelled duplicate runs.
  • Make the check required on main, and keep it fast and trustworthy.

Try it

In the Do tab you add this pipeline to a repository of your own, break it on purpose, and fix it.

Also in this lesson: