CI/CD — Continuous Integration & Continuous Delivery

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: The automated pipeline that takes a code change from “I just pushed it” to “it’s live in production” — running every check, every test, and every deploy step without a human pressing any button.


In plain English

In the old days of shipping software, getting a change from a developer’s laptop to a real production server meant:

  • A human zipped up the code
  • Uploaded it to a server
  • Ran scripts to install dependencies, compile assets, restart services
  • Spot-checked the result
  • Hoped nothing else broke

Every step was manual. Every step was a chance to forget something. Releases were rare, terrifying events.

CI/CD is the alternative. The acronym breaks into two related ideas:

  • Continuous Integration (CI) — every time you push code, a server automatically checks it: runs the tests, type-checks, lints, builds, scans for vulnerabilities. If anything fails, the team knows immediately, before the code merges.

  • Continuous Delivery / Continuous Deployment (CD) — once code passes CI and merges to the main branch, it automatically deploys to staging (Delivery) or all the way to production (Deployment). The line between Delivery and Deployment is about whether a human approval step remains; the principle is the same.

The result: shipping changes goes from a half-day event to a 90-second pipeline you barely notice. Many teams deploy to production dozens of times per day. Sometimes hundreds.

For solo developers on modern hosts (Vercel, Netlify, Cloudflare), CI/CD is almost free — it’s built into the platform. For larger teams or custom infrastructure, you wire it together explicitly with tools like GitHub Actions, GitLab CI, CircleCI, or Jenkins.


Why it matters

Three concrete things CI/CD buys you:

  1. Bugs found in seconds, not days. A failing test catches a bug 30 seconds after the commit. A human review three days later catches it three days late, after other work has built on top of it.

  2. Deploys become boring. Routine, fast, undramatic. When deploying is boring, you do it more often. When you do it more often, each deploy is smaller — meaning when something does break, you have a tiny diff to investigate, not a week’s worth of changes.

  3. A reproducible record. Every deploy is logged: what code, what tests passed, who triggered it, how long it took. If something breaks tomorrow, you can answer “what changed?” in 30 seconds.

The DORA research (DevOps Research and Assessment) has consistently shown that the four metrics most correlated with high-performing engineering teams are: deployment frequency, lead time for changes, time to restore service, and change failure rate. Strong CI/CD moves all four in the right direction.


A typical modern CI/CD pipeline

For a Next.js webapp pushed to GitHub, deployed via Vercel:

1. Developer pushes a commit to a feature branch
                ↓
2. GitHub triggers GitHub Actions:
   - Install dependencies
   - Run `npm run lint`
   - Run `npm run type-check`
   - Run `npm test` (unit tests)
   - Run `npm run build` (production build)
                ↓
3. GitHub triggers Vercel (via integration):
   - Build the same code in a fresh container
   - Run `next build`
   - Upload result to Vercel's edge
                ↓
4. A Preview URL is published for this branch
                ↓
5. Developer opens a Pull Request
   - GitHub Actions run additional jobs (e.g. Playwright E2E tests against the Preview URL)
   - Reviewers check the Preview URL on real devices
                ↓
6. PR is approved + merged to main
                ↓
7. Vercel auto-deploys main to production
                ↓
8. Vercel notifies you in Slack / email: deploy successful, URL live

Total elapsed time on a small project: ~3-5 minutes. Total human time on the deploy itself: zero.


CI vs CD vs Continuous Deployment — the precise distinctions

People mix these up. The clean breakdown:

TermWhat it means
Continuous IntegrationAutomatically build + test every change. Catches integration bugs early. The “I” stands for integrating with shared code (the main branch).
Continuous DeliveryEvery change that passes CI is automatically built into a deployable artifact AND deployed to a staging-like environment. A human decides when to push to production.
Continuous DeploymentSame as Delivery, but the push to production is also automatic. No human gate.

Note the subtle difference between Delivery and Deployment:

  • Delivery: “Always ready to ship; a human chooses when.”
  • Deployment: “Always shipped; if it passed tests, it’s live.”

For solo developers, Continuous Deployment is the norm: push to main → live in 90 seconds. For large teams or regulated industries, Continuous Delivery + a release manager is common.


What a CI pipeline checks

A typical CI run includes some combination of:

CheckWhat it doesCatches
Installnpm ci (clean install from lockfile)Lockfile drift, missing dependencies
Linteslint, prettier --checkStyle and basic mistakes
Type-checktsc --noEmitType errors
Unit testsvitest, jest, mochaLogic bugs in pure functions
Integration testsAPI-level tests against fake or real servicesWiring bugs between modules
E2E testsPlaywright, Cypress in a real browserUI flows, real-world regressions
Buildnpm run buildBuild-time errors, broken imports
Security scansnpm audit, Snyk, DependabotKnown vulnerabilities in dependencies
Secrets scangitleaks, GitHub Secret ScanningAccidentally committed API keys
Bundle-size budgettools like size-limitPerformance regressions
Visual regressionChromatic, PercyPixel-level UI changes
A11y checksaxe-core, Pa11yAccessibility regressions

Most projects run a subset. The discipline is “make the cheapest, most useful checks fast, and run them on every commit; run slower checks on PR merge only.”


The tools — by category

CI/CD platforms

ToolNotes
GitHub ActionsMost popular for open-source and small/medium teams. Free for public repos, generous free tier for private. YAML in .github/workflows/.
GitLab CINative to GitLab. Conceptually similar; .gitlab-ci.yml.
CircleCIOlder, popular at scale. Good Docker support.
BuildkiteHybrid model: cloud control plane, your own runners. Liked by big teams.
JenkinsThe grandparent. Self-hosted, infinitely flexible, painful to operate. Mostly legacy in 2026.
TeamCity / Azure DevOps / Bitbucket PipelinesVendor-tied alternatives.

Built-in CI/CD in hosting platforms

Most modern hosts include a CI/CD layer:

HostWhat it builds in
VercelAuto-builds on push, runs build, deploys, posts preview URL.
NetlifySame shape. Build hooks for custom triggers.
Cloudflare PagesSame shape; Wrangler for CLI control.
Fly.iofly deploy from a GitHub Action.
Render / RailwayAuto-deploy on push.

For most solo projects, hosting platform’s built-in CI/CD + GitHub Actions for tests is the right combo. The platform handles the deploy; GitHub Actions handles the tests and other checks before the merge.


A concrete example: a GitHub Actions workflow

A real, small .github/workflows/ci.yml for a Next.js project:

name: CI
 
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
 
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - run: npm ci
 
      - name: Lint
        run: npm run lint
 
      - name: Type-check
        run: npx tsc --noEmit
 
      - name: Test
        run: npm test
 
      - name: Build
        run: npm run build
        env:
          NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}

What this does:

  • Triggers on PRs targeting main and on every push to main
  • Spins up an Ubuntu container, installs Node 20, caches npm
  • Runs lint + type-check + tests + build
  • Uses GitHub Secrets for the build’s env vars (never paste real secrets into YAML)

If any step fails, the run goes red, GitHub shows the failure on the PR, and the merge is blocked (if you’ve configured branch protection).


Branch protection — making CI mandatory

CI is only useful if it actually blocks bad code from landing. In GitHub, this is configured via Branch Protection Rules on main:

  • Require status checks to pass before merging (pick which Actions jobs)
  • Require pull request reviews (1+ approvers)
  • Require branches to be up to date before merging
  • Disallow direct pushes to main

With these in place, the only path to main is via a PR with green CI. The pipeline becomes a real gate, not an advisory signal.

For solo developers, branch protection still has value — it prevents you from accidentally pushing broken code to production at 11pm.


Deployment strategies — once code is built, how to roll it out

When the pipeline reaches “deploy to production,” there are several strategies for how to actually release:

All-at-once (recreate)

Replace the old version with the new one in a single switch. Simplest. What Vercel does by default. Brief moment of unavailability is masked by the CDN serving cached pages.

Rolling deploy

Update servers gradually — 10% at a time, then 25%, etc. Old and new versions coexist briefly. Used for traditional server fleets.

Blue-green deploy

Two complete environments (blue and green). Deploy new version to the inactive one. When ready, flip a switch and 100% of traffic moves to it. Rollback = flip back.

Canary deploy

Route a small fraction of traffic (1-5%) to the new version. Monitor for errors. Gradually increase. If something looks bad, route 100% back to the old version.

Feature flags

Decouple deployment from release. Deploy code that’s behind a flag (off by default). Turn the flag on for a small percentage of users to test. Roll out gradually without redeploying.

For most solo / small projects on Vercel-style platforms: all-at-once with instant rollback is the right default. The platform’s CDN + cache layer plus the rollback button make recovery time tiny (seconds), so the value of more complex strategies is limited.


Pipelines as code

A key cultural shift in modern CI/CD: the pipeline configuration lives IN the repo, as files (.github/workflows/*.yml, .gitlab-ci.yml, etc.), not as a UI configuration on a separate dashboard.

Why this matters:

  • Reviewed alongside code changes. Touching the pipeline goes through PR review.
  • Versioned. You can see when and why the pipeline changed.
  • Reproducible. Cloning the repo gives you the whole pipeline, not a dashboard you have to recreate.
  • Diff-able. Two branches with different pipelines can be compared with git diff.

The old way was clicking through a UI on Jenkins. The new way is committing YAML. Always prefer the new way.


Common gotchas

  • Secrets in plaintext YAML are a disaster. Never env: AWS_KEY: "abc" directly. Use the platform’s secret store (GitHub Secrets, GitLab CI Variables) and reference them as ${{ secrets.AWS_KEY }}. Secret values are masked in logs automatically — but only if you used the secret mechanism.

  • CI environment is not production. Tests passing in CI ≠ “the deployed app will work.” Always run a real production build (npm run build) in CI to catch build-time issues, and a smoke test against a deployed preview when possible.

  • Cache misses kill build speed. Setup steps (npm ci, dependency installs) often dominate CI time. Use the platform’s caching (e.g. actions/setup-node@v4 with cache: 'npm'). Cache invalidation is hard but the speedup is huge.

  • Long CI times kill morale. A PR that takes 12 minutes to validate breaks flow state. Aim for under 5 minutes total. Split slow jobs to run in parallel. Pull out E2E tests to run only on merge, not every push.

  • Flaky tests undermine the whole system. A test that fails 5% of the time will fail every 20 runs. People learn to “retry until green,” and real failures get ignored. Fix or quarantine flaky tests aggressively.

  • Self-hosted runners are a security concern. If you run your own GitHub Actions runners (vs GitHub-hosted), they can be compromised by malicious PRs from forks. Restrict who can trigger workflows; use ephemeral runners; never trust PR-supplied inputs.

  • pull_request_target is dangerous. This GitHub Actions trigger runs with secrets accessible, on the BASE repo’s code. If you accidentally check out PR-supplied code under this trigger, attackers can exfiltrate secrets. Use pull_request for tests on PRs.

  • Test data leaks into production builds. Hard-coding NODE_ENV=test paths, console.logs from tests, or test-only env vars into production builds happens often. Use clean separation between test and production environments.

  • Long-running builds time out. GitHub Actions has a 6-hour limit per job; most platforms have similar. Builds that approach this are doing too much; split into multiple jobs.

  • Race conditions in deploys. If two PRs merge simultaneously, two deploys may race. Whichever finishes second wins. Most hosts queue deploys per project, but if you’re orchestrating your own, beware.

  • Deploy steps that aren’t idempotent. Running database migrations from the deploy script is fine — IF the migration is idempotent and recoverable. A migration that half-runs and fails can leave the database in a weird state. See migrations.

  • Forgetting to set GitHub Secrets for the new environment. Add a new env var to .env.example, set it in Vercel — but forget to set it as a GitHub Action secret. CI builds fail because the secret isn’t available. Mirror env-var changes across both places.

  • The Pareto principle of CI failures. ~80% of CI failures are flaky tests, lockfile drift, secret-not-set, or “I forgot to commit a file.” Train your reflex: when a build fails, those four are 80% of the answer.

  • Notifications get ignored. Slack-bombing the team for every CI run desensitizes everyone within a week. Only notify on red builds, and on the production deploy itself. Successful CI on a PR doesn’t need to ping anyone.

  • Build matrix explosion. Testing across Node 18, 20, 22 × Ubuntu, macOS, Windows × multiple package managers = 27 jobs per PR. Useful for libraries; massive overkill for an internal webapp. Scope CI to what actually matters.

  • Caching across PRs vs caching across branches. GitHub Actions cache has subtle scope rules. A cache made on main is usable from PR branches; the reverse isn’t always true. Read the docs once before debugging surprising cache misses.

  • CI for the docs that’s separate from CI for the code. Some teams have separate pipelines for content updates vs code updates. Reasonable if it speeds up content workflows, but easy to leave one path under-tested.

  • The “it works on CI but not locally” trap. Usually a missing env var locally, or a difference in OS / Node version. Use nvm, document Node version in package.json engines, and add a “setup” script that mirrors what CI does.

  • Cost creeps up at scale. Free tiers cover early projects, but heavy CI usage on private repos can climb. Cache aggressively; trim unused jobs; pay attention to monthly billing.


See also


Sources