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@v4clones your repo into the runneractions/setup-node@v4installs Node.js with npm caching (so installs are faster on repeat runs)npm ciinstalls exact versions from your lockfile (faster and more reliable thannpm 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:
- Enable "Require status checks to pass before merging"
- Select your CI workflow as a required check
- 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.