🔄 CI/CD Concepts
You've written tests. Now imagine those tests running automatically — every push, every pull request, on a clean machine that doesn't care what's cached on your laptop. That's the heart of CI/CD: a robot teammate that builds, tests, and ships your code so humans don't have to remember to.
Week 12 · Day 5 (Friday: Continuous Integration) · Lecture 1
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Define continuous integration and explain the specific problem it solves
- Distinguish continuous delivery from continuous deployment — the two very different "CD"s
- Trace a change through the canonical lint → test → build → deploy pipeline stages
- Identify the essential components of a pipeline: source control, a CI runner, tests, artifacts, and environments
- Compare deployment strategies (blue-green, canary, rolling) and when each reduces risk
- Recognize why small, frequent, automatically-tested merges beat big-bang integrations
Estimated Time: 60 minutes
Practice: Design a pipeline on paper and sketch the stages your own project would need.
In This Lesson
The "Works on My Machine" Problem
Every developer has said it, usually while staring at a red build: "But it works on my machine!" The trouble is that your machine has your node version, your environment variables, your half-committed files, and a folder full of cached dependencies nobody else has. When five developers each integrate a week of separate work at once, the merge is a minefield — and the bugs only surface when everything collides.
Continuous Integration and Continuous Delivery/Deployment (CI/CD) is the industry's answer. Instead of integrating rarely and manually, you integrate constantly and let automation do the checking. A neutral server pulls each change, installs dependencies from scratch, runs the whole test suite, and tells you within minutes whether the change is safe. If deployment is wired up, that same server can ship the passing code to real users without a human touching a terminal.
The payoff is not just fewer bugs. It's confidence: a green checkmark on a pull request means the change survived a repeatable, honest gauntlet — not just "it seemed fine when I tried it."
What Continuous Integration Really Is
Continuous Integration (CI) is the practice of merging every developer's work into a shared main branch frequently — ideally many times a day — with each merge automatically built and tested. The word doing the heavy lifting is automatically. CI is not "we use GitHub." CI is "every push triggers a build and a test run, and a broken build is treated as a stop-the-line emergency."
📖 The original definition
Martin Fowler, who popularized the term, describes CI as a practice where team members integrate their work frequently, "each integration is verified by an automated build (including test) to detect integration errors as quickly as possible." The two pillars are frequency and automated verification — neither works without the other.
Think of a busy kitchen. In a bad kitchen, cooks each prepare a full dish in isolation and only combine everything at the very end — where clashing flavors and missing ingredients are discovered too late. In a CI kitchen, every component is tasted the moment it's added, so a mistake is caught while it's a single spoonful, not a ruined banquet.
What CI gives you
| Benefit | Why it matters |
|---|---|
| Early bug detection | A failing test on push is 100x cheaper to fix than a bug in production |
| Smaller integrations | Merging daily means each merge is tiny and easy to reason about |
| Always-shippable main | Because main is continuously verified, it stays deployable |
| Fast feedback | Authors learn within minutes, while the code is still fresh in their heads |
| Shared responsibility | A red build is everyone's problem, which builds a quality culture |
Delivery vs. Deployment: The Two CDs
Here's the distinction that trips up almost everyone. "CD" stands for two different things, and the difference is one human click.
Continuous Delivery
Every change that passes the pipeline is automatically prepared and ready to release to production — the artifact is built, tested, and sitting in staging. But the final push to production waits for a human to approve it. You could ship any commit at the press of a button; you choose when.
Continuous Deployment
Goes one step further: there is no manual gate. Every change that passes every stage of the pipeline is automatically released to production. The pipeline itself is trusted enough that a green run means "live for users," full stop.
The mnemonic: Delivery stops at the deli counter waiting for you to order; Deployment sends it straight to the door.
⚠️ Which should you choose?
Continuous deployment demands mature testing, monitoring, and easy rollback — because a bad commit reaches users with no human safety net. Most teams start with continuous delivery (automation up to a manual "go" button) and graduate to full deployment only once they trust the pipeline. Both sit on top of solid CI; neither is possible without it.
The Pipeline Stages
A pipeline is a sequence of automated stages a change must pass, in order, from commit to production. Each stage is a checkpoint: if it fails, the pipeline stops and the change never advances. The canonical ordering runs the fast, cheap checks first so failures surface quickly.
Stage by stage
- Source — a push or pull request triggers everything. The runner checks out the exact commit.
- Lint — static analysis (ESLint, Prettier, type-checking). Fast, catches style and obvious errors first.
- Test — unit tests, then integration tests. This is where correctness is verified.
- Build — compile/bundle the app into a deployable artifact (a build folder, a Docker image).
- Deploy — ship the artifact to staging, run acceptance tests, and (after delivery approval or automatically) to production.
🔑 Build once, deploy many
A golden rule: build the artifact one time, then promote that exact same artifact through staging and into production. If you rebuild for each environment, you're testing one thing and shipping another. Same artifact everywhere = what you tested is what users get.
Anatomy of a Pipeline
Every CI/CD setup, whatever the tool, is assembled from the same building blocks. Recognizing them makes any platform — GitHub Actions, GitLab CI, CircleCI, Jenkins — feel familiar.
| Component | Job | Common tools |
|---|---|---|
| Source control | Stores code, tracks changes, fires triggers | Git on GitHub, GitLab, Bitbucket |
| CI runner | A clean VM that executes the pipeline | GitHub Actions, Jenkins, CircleCI |
| Test framework | Runs the checks that gate the change | Jest, Vitest, Playwright, Cypress |
| Artifact storage | Holds build outputs between stages | Actions artifacts, Docker Hub, npm |
| Environments | Staging and production targets | Netlify, Vercel, AWS, Kubernetes |
| Monitoring | Watches the app after deploy | Sentry, Datadog, Grafana |
A quick sketch of a real Node.js CI job — you'll build these for real in the next lesson, but here's the shape so the vocabulary lands:
# .github/workflows/ci.yml — a first taste
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
verify:
runs-on: ubuntu-latest # a clean, throwaway Ubuntu VM
steps:
- uses: actions/checkout@v4 # 1. get the code
- uses: actions/setup-node@v4 # 2. install Node
with:
node-version: '20'
cache: 'npm' # cache deps to speed up runs
- run: npm ci # 3. install exactly what's locked
- run: npm run lint # 4. lint (fast checks first)
- run: npm test # 5. test
- run: npm run build # 6. build the artifact
Notice how the YAML mirrors the stages we just walked through: check out, set up, install, lint, test, build. The CI server does in a clean room exactly what a careful developer would do by hand — but the same way, every single time.
Deployment Strategies
Once a pipeline can deploy automatically, how it releases matters. Pushing new code to every server at once is fast but risky — one bad build takes everyone down. Smarter strategies trade a little complexity for a lot of safety.
Blue-green
Run two identical production environments, "blue" (live) and "green" (idle). Deploy the new version to green, test it fully, then flip the load balancer so all traffic goes to green. If anything breaks, flip back to blue instantly. Zero-downtime, instant rollback — at the cost of running double the infrastructure during a release.
Canary
Named after the canary in a coal mine. Route a small slice of traffic — say 5% — to the new version while everyone else stays on the old one. Watch the error rates. If the canary stays healthy, gradually shift more traffic; if it struggles, pull it back before most users ever notice.
Rolling
Update instances a few at a time — replace instance 1, wait, replace instance 2, and so on — until the fleet is fully upgraded. No extra infrastructure like blue-green, and no total outage, but for a moment old and new versions serve traffic side by side, so they must stay compatible.
💡 Feature flags: decouple deploy from release
A complementary trick: ship code to production but keep the new feature hidden behind a flag (an if that reads a config value). Now deploying the code and releasing the feature are separate events. You can turn a feature on for 1% of users, run an A/B test, or kill a misbehaving feature instantly — no redeploy required.
Practice & Quiz
🏋️ Exercise 1: Order the stages
Goal: A teammate proposes this pipeline order. It has a costly mistake. Identify what's wrong and rewrite the order.
# Proposed (flawed) pipeline order:
1. Run full end-to-end test suite (12 minutes)
2. Lint the code (5 seconds)
3. Build the application
4. Run unit tests (30 seconds)
5. Deploy to production
💡 Hint
Which stages are cheap and which are expensive? A "fail fast" pipeline runs the checks most likely to fail — and quickest to run — first, so you don't burn 12 minutes only to fail on a missing semicolon.
✅ Solution
# Corrected: cheap + likely-to-fail checks first
1. Lint the code (5 seconds) # catches style/syntax instantly
2. Run unit tests (30 seconds) # fast correctness check
3. Build the application # only build if code is sound
4. Run full end-to-end suite (12 min) # slow, run only on a good build
5. Deploy to production # last, after everything is green
The flaw was running the slowest stage first. A linting error would waste 12 minutes before failing. Ordering by speed gives developers feedback in seconds for the most common mistakes.
🏋️ Exercise 2: Delivery or deployment?
Goal: For each scenario, decide whether it describes continuous delivery or continuous deployment.
- Every passing commit lands in staging; a release manager clicks "Promote" to send it live.
- A merge to main that passes all tests is serving real users eight minutes later, no human involved.
- The pipeline builds and tests every change but a Slack approval is required before production.
✅ Solution
1. Delivery — automation stops at a human "Promote" gate.
2. Deployment — fully automatic all the way to production.
3. Delivery — the required approval means it's not automatic; a human gates the release.
The single deciding question: does a human have to approve the production release? Yes = delivery. No = deployment.
🎯 Quick Quiz
Question 1: What is the defining feature of Continuous Integration?
Question 2: The only difference between continuous delivery and continuous deployment is:
Question 3: Why should a pipeline lint before it runs slow end-to-end tests?
Best Practices & Pitfalls
✅ Do
- Commit small and often — integrate daily so merges stay tiny and conflicts stay trivial
- Treat a red build as a stop-the-line emergency; fix it before starting new work
- Keep the pipeline fast (cache dependencies, parallelize) so feedback stays under ~10 minutes
- Build the artifact once and promote the same one through every environment
- Store pipeline config in the repo (pipeline-as-code) so it's versioned and reviewed like any code
❌ Don't
- Don't let CI stay red "for now" — a normally-broken build trains people to ignore it
- Don't rebuild separately for staging and production; you'd ship something you never tested
- Don't skip tests to "unblock" a deploy — that defeats the entire purpose of the pipeline
- Don't put continuous deployment in place before you have solid monitoring and easy rollback
⚠️ The flaky-test trap
A test that passes and fails randomly without code changes is flaky. Flaky tests are corrosive: they train the team to hit "re-run" and ignore red builds, which eventually hides a real failure. Quarantine flaky tests, fix the root cause (usually timing or shared state), and don't let "just re-run it" become a habit.
Summary
🎉 Key Takeaways
- CI = merge frequently, and automatically build + test every change to catch integration problems early
- Continuous delivery keeps a manual approval before production; continuous deployment removes it — the difference is one human click
- The canonical pipeline runs lint → test → build → deploy, cheapest checks first, to fail fast
- Every pipeline is built from the same parts: source control, a runner, tests, artifacts, environments, monitoring
- Build once, deploy many — promote the exact artifact you tested
- Blue-green, canary, and rolling deploys plus feature flags shrink the blast radius of a bad release
📚 Additional Resources
- GitHub Docs — Understanding GitHub Actions
- Martin Fowler — Continuous Integration (the canonical essay)
- Martin Fowler — Continuous Delivery vs. Deployment
🚀 What's Next?
You now understand what a pipeline is and why it exists. Next, you'll build one for real: the next lesson, GitHub Actions for CI, walks through workflow YAML — triggers, jobs, runners, and steps — and gets your first CI pipeline running on an actual repository.
🔄 The robot teammate awaits
You've got the mental model. Time to teach a machine to build and test your code on every push.