TL;DR
- CI (Continuous Integration): merge small changes frequently and run automated builds & tests on every change to catch problems early.
- CD (Continuous Delivery): every commit is always deployable; deploying to prod is a business decision (often requires approval).
- CD (Continuous Deployment): every green commit ships to production automatically (with safety checks).
- Payoff: fewer integration surprises, faster lead time, safer releases, reproducible builds, and easy rollbacks.
Why CI/CD? (the problem it solves)
Before CI/CD, teams batch big changes on long‑lived branches and discover conflicts late (“integration hell”). Releases are manual, error‑prone, and stressful. CI/CD makes integration continuous and releases routine by automating: build → test → package → scan → deploy → verify.
Key concepts (speed run)
- Pipeline: a set of stages (e.g., lint → build → test → package → scan → deploy).
- Jobs/Runners: units of work executed on ephemeral agents.
- Triggers: push, PR/MR, tag, schedule, or manual approvals.
- Artifacts: build outputs (zips, Docker images) passed between stages. Build once, deploy many.
- Caching: reuse dependencies to keep pipelines fast.
- Environments: dev → staging → prod with the same artifact promoted through.
- Secrets: injected at runtime (vaults, OIDC). Keep them out of the repo.
- Observability: logs, test reports, coverage, release notes, deployment tracking.
CI vs CD vs CD (delivery vs deployment)
- CI: run tests & checks on every change; keep
maingreen. - Continuous Delivery: after CI passes, the artifact is ready for production; deploy step may be manual (approval, change window).
- Continuous Deployment: production deploy is automatic when the pipeline is green (often with progressive delivery like canaries).
A tiny pipeline you can copy (GitHub Actions)
# .github/workflows/ci-cd.yml
name: CI/CD
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
id-token: write # for cloud/OIDC deploys
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Install deps (with cache)
run: npm ci
- name: Lint & test
run: npm run lint && npm test -- --ci
build:
needs: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- name: Build
run: npm run build
- name: Package artifact
run: tar -czf app.tgz dist package.json
- name: Upload artifact
uses: actions/upload-artifact@v4
with: { name: webapp, path: app.tgz }
deploy-staging:
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/download-artifact@v4
with: { name: webapp, path: . }
- name: Deploy
run: |
# example: copy to server or call your deploy script
./scripts/deploy.sh --env=staging app.tgz
deploy-prod:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- uses: actions/download-artifact@v4
with: { name: webapp, path: . }
- name: Manual approval gate
uses: trstringer/manual-approval@v1 # or use environment protection rules
with: { secret: ${{ secrets.APPROVERS }} }
- name: Deploy
run: ./scripts/deploy.sh --env=prod app.tgz
Swap
deploy.shwith your infra of choice (Kubernetes, ECS, Serverless, Railway, Fly.io). Prefer OIDC to authenticate rather than long‑lived cloud keys.
Popular tools (pick one per layer)
- CI servers: GitHub Actions, GitLab CI, Jenkins, CircleCI, Buildkite, Azure DevOps.
- Artifacts & registries: GitHub Packages, GHCR, ECR, GCR, Artifactory, Nexus.
- Deploy/orchestrate: Kubernetes, Helm, Argo CD/Flux (GitOps), ECS/Fargate, Nomad, Cloud Run, App Service, Vercel/Netlify/Fly.
- Secrets: HashiCorp Vault, AWS Secrets Manager, SSM, Doppler, 1Password, environment‑scoped secrets in your CI.
Release strategies (safe delivery)
- Blue/Green: keep two prod stacks; switch traffic once healthy.
- Canary: release to a small % then ramp up while monitoring.
- Rolling: replace pods/instances gradually.
- Feature flags: ship dark; toggle on/off without redeploy.
- DB migrations: prefer forward‑compatible (“expand → migrate → contract”) and practice roll forward.
Test strategy (keep pipelines fast & valuable)
- Test pyramid: many unit, fewer integration, a handful E2E.
- Make tests deterministic; quarantine or fix flaky tests fast.
- Parallelize and cache dependency installs/builds. Aim for < 10 minutes total CI by default.
Good practices checklist
- [ ] Keep
mainalways green; protect with PR checks. - [ ] Build once, promote the same artifact across envs.
- [ ] Infra as Code (Terraform/Pulumi/Helm); review changes like app code.
- [ ] Secrets from a vault/OIDC, not repo.
- [ ] Observability on deploys (logs, metrics, traces, error rate, latency).
- [ ] Rollback plan (and practice it).
- [ ] Use preview environments per PR when feasible.
- [ ] Track DORA metrics: deployment frequency, change lead time, change failure rate, MTTR.
Anti‑patterns to avoid
- Long‑lived branches that diverge from
main. - Building a different artifact for each environment.
- Secrets in code or
.envcommitted to repo. - Slow pipelines with no caching or unnecessary E2E tests on every push.
- “Works on my machine” differences—use containers for build & runtime.
One‑minute adoption plan
- Start with lint + unit tests on PRs.
- Add build + artifact stage; publish to a registry.
- Create a staging deploy on merge to
main. - Add manual prod gate (Delivery) → later move to auto deploy with canaries (Deployment).
- Measure & tighten: flaky tests, pipeline time, rollout safety, rollback speed.