Skip to content

CI/CD Explained

5-minute read.

The one-line answer

CI (Continuous Integration) - every code change is automatically built and tested.

CD (Continuous Delivery / Deployment) - every change that passes CI is automatically prepared for, or pushed to, production.

Together: every commit is built, tested, and shipped, with no manual handoffs.

Why this exists

The old way: developers wrote code for weeks, "merged" by emailing zips around, then a release engineer spent a day building and deploying. Bugs found 3 weeks after they were introduced. "Works on my machine" rampant.

CI/CD compresses the loop: - Test as soon as code is written β†’ bugs found in minutes - Same build script for every dev β†’ no "release engineer black magic" - Same deploy process every time β†’ boring, predictable releases

The Stripe/Etsy/Google playbook: deploy 50+ times a day, never have an emergency "release weekend."

The pieces

flowchart LR
  D[Developer push] --> CI[CI: build + test + lint + scan]
  CI -- pass --> ART[Artifact registry<br/>container, binary, package]
  CI -- fail --> X[Block merge]
  ART --> CD{Deploy gate}
  CD -- Continuous Delivery<br/>human approval --> PROD1[Production]
  CD -- Continuous Deployment<br/>automatic --> PROD2[Production]

Continuous Integration

Every push triggers: 1. Pull the code 2. Build it (compile, package, build container image) 3. Run tests (unit, integration, sometimes E2E) 4. Run linters, security scanners, code quality checks 5. Report pass/fail

If anything fails, the change is blocked from merging.

Continuous Delivery

Every change that passes CI is: 1. Built into a deployable artifact (container image, binary, etc.) 2. Pushed to a registry 3. Made ready to deploy with one click

Production deploys are a single approval away, but a human still chooses when.

Continuous Deployment

Like Continuous Delivery, but automatic. Every passing change goes to production, no human in the loop. Requires very high confidence in tests.

Most teams do CI + Continuous Delivery. Continuous Deployment is reserved for mature teams or non-critical systems.

A typical pipeline

For a TypeScript web app:

# .github/workflows/ci.yml (GitHub Actions example)
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
      - run: npm run build

Push code β†’ 2 minutes later you know if it's healthy.

For deployment, a separate workflow runs on merges to main:

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t myapp:${{ github.sha }} .
      - run: docker push myregistry/myapp:${{ github.sha }}
      - run: kubectl set image deployment/myapp app=myregistry/myapp:${{ github.sha }}

Deploy strategies

How do you actually push new code without breaking things?

Recreate

Stop old version, start new version. Simple. Causes downtime. OK for non-critical internal tools.

Rolling update

Replace pods/instances one at a time. The default in Kubernetes. No downtime. New code goes live gradually.

Blue/Green

Run two identical environments. Send all traffic to "blue." Deploy to "green." Switch DNS or load balancer. Instant rollback.

Canary

Deploy new version to small % of traffic (5%). Watch metrics. Gradually increase. Roll back at any point if errors spike.

Feature flags

Deploy new code paths but keep them dark. Enable for specific users (beta cohort) before everyone. Decouples deploy from release.

Common tools

Category Examples
CI/CD platform GitHub Actions, GitLab CI, CircleCI, Jenkins, Buildkite
Container builds Docker, Kaniko, Buildkit
Image registries Docker Hub, ECR, ACR, GAR, GitHub Packages
Deploy tools Kubernetes, ArgoCD, Flux, Spinnaker, Octopus, AWS CodeDeploy
IaC apply Terraform Cloud, Atlantis, GitHub Actions
Secret management AWS Secrets Manager, Vault, GitHub Secrets, Doppler
Feature flags LaunchDarkly, GrowthBook, Flagsmith, Unleash

GitHub Actions is now the default for most projects on GitHub. Fast, integrated, generous free tier for open source.

What "good" looks like

For a healthy CI/CD setup:

  • Every push to a PR triggers CI
  • CI completes in <10 minutes (ideally <5)
  • A green PR can be merged and deployed in <30 minutes
  • Deploys happen many times a day, not weekly
  • Rollback is one click or one command, takes <5 minutes
  • No human runs commands in production directly - everything goes through the pipeline

Common pitfalls

  • Slow CI - 30-min CI = developers context-switch and lose momentum. Aim for <10 min.
  • Flaky tests - tests that fail randomly destroy trust. Fix or quarantine immediately.
  • Manual deploy steps - "remember to run the migration script first" = will be forgotten. Automate it.
  • No rollback plan - if you deploy without thinking through rollback, you'll regret it.
  • Secrets in CI logs - mask them, don't echo $TOKEN.

What to look at next