Frontend Engineer

What is CI/CD Pipeline?

You push code to GitHub. Within seconds, a server somewhere pulls your code, installs dependencies, runs your tests, checks for lint errors, and tells you if anything broke. If everything passes, it deploys automatically. That's CI/CD.

It sounds complex, but the core idea is simple: automate the stuff you'd forget to do manually.

What CI and CD actually mean

CI (Continuous Integration) means every time someone pushes code, it gets automatically tested and validated. The goal is to catch bugs early, not the night before a deadline.

CD (Continuous Deployment) means once your code passes all checks, it gets deployed automatically. No manual uploads, no SSH into servers, no "I forgot to deploy the latest version."

Together, they create a loop: write code, push, validate, deploy. Every single time.

Why you should care

Without CI/CD, this is what happens:

  • You push broken code to main and don't realize until someone else pulls it
  • Tests exist but nobody runs them locally because it's annoying
  • Deploying means logging into Vercel/Railway and clicking buttons (or worse, FTPing files)
  • "It works on my machine" becomes the default excuse

With CI/CD, the machine catches what humans forget. It runs tests on every push, in a clean environment, every time. No exceptions.

Setting up your first pipeline with GitHub Actions

GitHub Actions is free for public repos and gives you 2,000 minutes/month on private repos. It's the easiest way to start.

Create a file at .github/workflows/ci.yml in your repo:

name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run build
      - run: npm test --if-present

That's it. Push this file, go to the Actions tab in your GitHub repo, and watch it run.

Here's what each step does:

  • actions/checkout@v4 clones your repo into the runner
  • actions/setup-node@v4 installs Node.js with npm caching (so installs are faster on repeat runs)
  • npm ci installs exact versions from your lockfile (faster and more reliable than npm install)
  • The rest runs your lint, build, and tests

If any step fails, the whole pipeline fails and you get a red X on your commit.

Protecting your main branch

The real power comes when you combine CI with branch protection rules.

Go to your repo Settings, then Branches, then Add rule for main:

  1. Enable "Require status checks to pass before merging"
  2. Select your CI workflow as a required check
  3. Enable "Require a pull request before merging"

Now nobody (including you) can push directly to main. Every change goes through a PR, CI runs automatically, and you can only merge if it passes. Your main branch always has working code.

Auto-deploy with Vercel

If you're using Vercel, you already have CD built in. Every push to main triggers a production deployment. Every PR gets a preview deployment with its own URL.

But you can make it smarter. Only deploy after CI passes by using Vercel's GitHub integration with the "Require CI to pass" setting. This way broken code never reaches production.

For other platforms like Railway or Fly.io, you can add a deploy step to your workflow:

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - run: npx railway up
        env:
          RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}

The needs: build-and-test line means this job only runs after CI passes. The if condition ensures it only deploys from main, not from PRs.

Adding more checks

Once the basic pipeline works, you can layer on more checks:

Type checking if you use TypeScript:

- run: npx tsc --noEmit

Check for unused dependencies with depcheck:

- run: npx depcheck

Caching to speed things up (the setup-node action handles npm cache, but you can also cache your build output):

- uses: actions/cache@v4
  with:
    path: .next/cache
    key: nextjs-${{ hashFiles('**/package-lock.json') }}

Common mistakes

Running npm install instead of npm ci. npm install can modify your lockfile. npm ci installs exactly what's in the lockfile, which is what you want in CI.

Not caching dependencies. Without caching, every run downloads all your node_modules from scratch. The setup-node action with cache: 'npm' fixes this.

Skipping CI with [skip ci] in commit messages. It's tempting, but if you skip CI for "small changes," you'll eventually skip it for the change that breaks everything.

Not running CI on pull requests. If CI only runs on main, you'll find out about failures after merging. Run it on PRs so you catch problems before they hit main.

Start here

If you take one thing from this post: go to your most active project, add the workflow file from above, and push it. Watch it run. See what happens when a test fails. That's all you need to get started.

The GitHub Actions documentation is solid and has examples for pretty much every language and framework. The Vercel deployment docs cover the CD side if you're deploying a Next.js app.