CI/CD for Frontend Teams: From Zero to Production Confidence
How to set up GitHub Actions pipelines that catch bugs before your users do
The most expensive bug is the one your users find. The second most expensive is the one your QA team finds. The cheapest? The one your CI pipeline catches before the code ever leaves the developer's branch.
I set up CI/CD pipelines for every team I work with. Not because it's fun (it is), but because the ROI is absurd. A pipeline that takes one day to set up catches thousands of bugs over the lifetime of a project. Here's exactly how to build one.
The Minimum Viable Pipeline
Every frontend project needs at least this:
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Type check
run: npx tsc --noEmit
- name: Lint
run: npx eslint . --max-warnings 0
- name: Test
run: npm test -- --coverage --watchAll=false
- name: Build
run: npm run build
Four gates: types, lint, test, build. If any fail, the PR can't merge. This alone prevents ~60% of the bugs I see in codebases without CI.
Gate 1: TypeScript Type Checking
tsc --noEmit catches an entire category of bugs that linters miss. Interface mismatches, incorrect function signatures, missing null checks — all caught at compile time.
Pro tip: Use strict mode. Always.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"exactOptionalPropertyTypes": true
}
}
Yes, it's more work upfront. No, you won't regret it.
Gate 2: Linting With Zero Tolerance
--max-warnings 0 is the key flag. Without it, warnings accumulate until your linter output is so noisy that nobody reads it. With it, every warning must be fixed or explicitly disabled (with a comment explaining why).
// eslint.config.js
export default [
{
rules: {
"no-console": "warn",
"no-unused-vars": "error",
"prefer-const": "error",
"react-hooks/exhaustive-deps": "warn",
"react-hooks/rules-of-hooks": "error",
},
},
];
Gate 3: Tests
You don't need 100% coverage. You need tests for the things that would be embarrassing if they broke.
What to test:
- User-facing flows (login, checkout, form submission)
- Business logic (price calculation, permission checks)
- Edge cases (empty states, error states, boundary values)
What not to test:
- Implementation details (component internal state)
- Third-party library behavior
- Static UI (unless it's critical branding)
The Testing Strategy That Works
Unit tests (Vitest): Business logic, utilities, hooks
Integration tests (RTL): Component interactions, form flows
E2E tests (Playwright): Critical user journeys (2-5 tests)
A startup doesn't need 500 tests. It needs 50 well-chosen ones that cover the paths users actually take.
Gate 4: Build Verification
If it doesn't build, it doesn't ship. Simple as that. The build step also catches:
- Missing imports
- Tree-shaking issues
- Environment variable problems
- Asset optimization failures
Preview Deployments
This is the single best investment you can make in your deployment workflow. Every PR gets its own live URL that stakeholders can review.
With Vercel: It's automatic. Push a branch, get a preview URL. Done.
With GitHub Actions + custom hosting:
preview:
if: github.event_name == 'pull_request'
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Deploy Preview
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
- name: Comment PR
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: '🚀 Preview: ' + process.env.PREVIEW_URL
});
Now your designer, PM, or CEO can review changes without pulling code or understanding git. This alone cut code review time in half on teams I've worked with.
Advanced: Bundle Size Monitoring
Track your JavaScript bundle size over time. Alert when it grows unexpectedly.
bundle-analysis:
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Analyze bundle
run: npx next-bundle-analyzer || true
- name: Check size limits
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Advanced: Automated Dependency Updates
# .github/workflows/deps.yml
name: Update Dependencies
on:
schedule:
- cron: "0 9 * * 1" # Every Monday at 9am
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx npm-check-updates -u --target minor
- run: npm install
- run: npm test -- --watchAll=false
- run: npm run build
- name: Create PR
uses: peter-evans/create-pull-request@v6
with:
title: "chore: update dependencies"
body: "Automated dependency update"
branch: deps/weekly-update
Dependencies get updated weekly. If tests pass, the PR is ready to merge. If they fail, you know something needs attention — before it becomes a security vulnerability.
The Pipeline I Set Up For Every Client
- PR opened: Type check → Lint → Test → Build → Preview deploy → Bundle size comment
- PR merged to main: Build → Deploy to production → Smoke test → Slack notification
- Weekly: Dependency update PR → Automated tests → Security audit
Total setup time: one day. Bugs caught per month: dozens. Developer confidence: immeasurable.
The goal isn't a perfect pipeline. The goal is a pipeline that catches the mistakes humans make when they're tired, rushing, or context-switching between three features. Computers don't get tired. Let them do the boring work.