# What a CI/CD Pipeline Actually Does

> CI/CD is the automated path from a commit to production: every change is built and tested automatically, and releases become small, frequent, and safe to roll back.


---

# What a CI/CD Pipeline Actually Does

You keep hearing "the pipeline" - the pipeline is green, the pipeline broke, wait for the pipeline before
you merge. Somewhere out there, every time you push, machines build your code, run your tests, and maybe
ship it to real users, and nobody ever sat you down and explained what's actually happening in there. This
guide is that sit-down. By the end, "CI/CD" stops being office noise and becomes a thing you can reason
about: what each letter means, what the stages do, and why teams trust an automated path from your laptop
to production.

## How to read this

- **Want it to finally make sense?** Read in order. Each phase builds the mental model one piece at a time:
  first CI, then CD, then why the whole thing is worth the trouble.
- **Just need to settle one argument?** Phase 2 has the table that untangles continuous *delivery* from
  continuous *deployment* - the two things everyone mixes up.

## The phases

1. **[CI: Continuous Integration](01-continuous-integration.md)** - every change is automatically built and
   tested the moment you push, so problems surface in minutes instead of at release. The red/green gate.
2. **[CD: Delivery vs Deployment](02-delivery-vs-deployment.md)** - the two meanings of "CD," the stages a
   pipeline runs (build → test → deploy), and the deploy strategies (blue-green, canary, rolling) at a
   gentle level.
3. **[Why It's Worth It](03-why-its-worth-it.md)** - small frequent releases, fast feedback, rollback
   confidence - and the one plain catch: a pipeline is only as trustworthy as the tests inside it.

> This guide is the *map*. When you want to build a real one, the follow-up
> [Your First Pipeline with GitHub Actions](/guides/your-first-pipeline-github-actions) walks you through an
> actual working pipeline, file by file.


---

# CI: Continuous Integration

Picture the old way, because it explains everything that came after. A team of six all work in their own
corners for two weeks. Then, on release day, everyone tries to combine their work at once. Nothing fits.
Your code assumed a function that someone else renamed. Their feature breaks a test you wrote. Two people
edited the same file in incompatible ways. The team loses days untangling a knot that formed silently over
weeks. People called this **integration hell** - and it was so reliably miserable that an entire practice
grew up to prevent it.

That practice is **Continuous Integration**. The fix is almost insultingly simple to state: instead of
combining everyone's work rarely and painfully, combine it *constantly* - and have a machine check every
combination automatically. Small merges, checked often, never grow into a knot.

## What CI actually is

**What it actually is.** Continuous Integration is the habit of merging your work into the shared branch
frequently - and a *server* that, on every push, automatically builds the code and runs the tests to prove
the combination still works.

**Why people get this wrong.** Most people first meet CI as "the thing that turns red and blocks my PR,"
so they think CI *is* the test-runner. That's only half of it. CI is really the whole discipline of
integrating early and often; the automated build-and-test is the *enforcement mechanism* that makes the
discipline safe. The machine isn't the point - frequent, verified integration is the point, and the machine
is how you trust it.

📝 **Terminology.** *Integrate* here means "merge your branch's changes together with everyone else's into
the shared branch." CI is short for **Continuous Integration**.

**What it does in real life.** You push a branch or open a pull request. A CI server (GitHub Actions,
GitLab CI, Jenkins, CircleCI - the brand varies, the idea doesn't) notices, spins up a clean machine,
checks out your code, builds it, and runs your test suite. A few minutes later it reports back: a green
check if everything passed, a red X if something broke. That result sits right on your pull request.

```mermaid
flowchart TD
  Push[You push] --> Checkout[Fresh machine, checkout code]
  Checkout --> Install[Install dependencies]
  Install --> Build[Build]
  Build --> Test[Run the tests]
  Test --> Green[Green: safe to merge]
  Test --> Red[Red: something broke - fix first]
```

## The red/green gate

**What it actually is.** The single most valuable thing CI gives you is a *gate*: a rule that says a pull
request can't be merged into the shared branch until its checks are green. Green means "built and passed
the tests." Red means "stop."

**What it does in real life.** Here's the moment CI earns its keep - you open a PR and the checks come back
red:

```console
$ git push origin feature/discount-codes
...
remote: Resolving deltas: 100% (8/8), done.

# Over on the pull request, a few minutes later:

  ✗ CI / build-and-test (push)   Failed in 2m 14s

  FAIL  src/pricing.test.js
    ✓ applies a flat discount
    ✗ stacks a percentage discount on a flat one

      expected 18.00, received 20.00

  Tests: 1 failed, 14 passed, 15 total
```

*What just happened:* The CI server pulled your branch onto a clean machine, built it, and ran all 15
tests. Fourteen passed; one failed - your new discount-stacking logic returns the wrong total. You learned
this within a couple of minutes of pushing, on your own change, while the code is still fresh in your head.
Nobody else pulled your bug into their work. The gate held.

Compare that to the old world, where this same bug would have surfaced two weeks later, tangled with five
other people's changes, with nobody sure whose code caused it. *That* is the difference CI makes: it moves
the discovery of problems from "much later, mixed with everything" to "right now, on the one change that
caused it."

💡 **Key point.** The whole value of CI is **fast, isolated feedback.** A failure points at *your* change,
*minutes* after you made it. Cheap to find, cheap to fix.

> ⏭️ The depth of *how* to write the tests CI runs is its own subject - see
> [Testing in CI](/guides/testing-in-ci). This phase is about what CI *does* with them.

## Why "on a clean machine" matters

**The gotcha everyone hits: "but it works on my machine."** You run the tests locally, they pass, you push
 - and CI fails. The reason is almost always that your laptop is *not* clean. Over months it has accumulated
a globally-installed tool, a leftover environment variable, a dependency you installed once and forgot. Your
code quietly leans on something that isn't written down anywhere.

CI runs on a fresh, empty machine every single time. It only has what your project explicitly declares - the
dependencies in your lockfile, the config in your repo. So when CI fails but your laptop passes, CI is
usually *right*: it's telling you the project doesn't actually contain everything it needs to build. A
teammate cloning your repo onto a new computer would hit the same wall.

⚠️ **Gotcha.** "Works on my machine" is not a defense - it's a *symptom*. The clean CI machine is the
trustworthy one. When it disagrees with your laptop, suspect a hidden dependency on your local setup before you
suspect the pipeline.

**Why this saves you later.** Because CI forces every build onto a clean machine, your project stays
genuinely portable. New hires can clone and run it on day one. You can rebuild it on a fresh server in an
emergency. The pipeline quietly guarantees something you'd otherwise only discover at the worst possible
moment.

## Recap

1. CI exists to kill **integration hell**: merge small and often instead of rarely and painfully.
2. **Continuous Integration** is the discipline of integrating frequently, enforced by a server that
   **builds and tests every push** automatically.
3. The **red/green gate** blocks merges until checks pass, so a failure points at *your* change, *minutes*
   after you made it.
4. CI runs on a **clean machine**, which is why "works on my machine" failures are real and worth listening
   to.

Watch it animated: [continuous integration](/explainers/CICD.dc.html)


---

# CD: Delivery vs Deployment

Here's a thing nobody warns you about: "CD" stands for two different things, and people use it
interchangeably as if it were one. That's the source of half the confusion around pipelines. Once you can
tell the two apart, the whole "CD" half of CI/CD snaps into focus - so let's pull them apart carefully,
then walk the stages a pipeline actually runs.

## The two CDs

Both start from the same place: your change is green, CI passed, the code is good. The question CD answers
is *what happens next* - and there are two plain answers.

| | **Continuous Delivery** | **Continuous Deployment** |
|---|---|---|
| What's automated | Everything *up to* production | Everything, *including* production |
| The last step | A **human** clicks "Deploy" | No human - green code **ships itself** |
| The promise | Always *ready* to release, on demand | Always *released*, automatically |
| Who tends to use it | Teams wanting a human gate (regulated, high-stakes, or just cautious) | Teams with deep test trust and fast rollback |

**Continuous Delivery** means your pipeline keeps `main` permanently in a *release-ready* state - built,
tested, packaged, staged - so that shipping to production is a single button press whenever a human decides
the moment is right. The robots do all the toil; a person makes the final call.

**Continuous Deployment** goes one step further: there is no button and no person. Every change that passes
the pipeline goes straight to production, automatically. Merge a green PR in the morning and it can be
serving real users by lunch, untouched by human hands.

💡 **Key point.** Continuous **Delivery** = always *ready* to release (human clicks deploy). Continuous
**Deployment** = always *released* (no human in the loop). Same first letters, one crucial difference: who - 
or whether anyone - pushes the final button.

📝 **Terminology.** Because both shorten to "CD," teams often say "continuous delivery" loosely to mean
either. When it matters, ask the precise question: *"Does a human approve the production release, or is it
automatic?"* That question, not the acronym, tells you which one you're dealing with.

## The stages a pipeline runs

Whichever CD you're doing, the pipeline that gets you there is a **sequence of stages**, each gating the
next. Think of it as an assembly line: a change only advances to the next stage if it cleared the one
before.

```mermaid
flowchart LR
  Commit[commit] --> Build[Build<br/>compile, package]
  Build --> Test[Test<br/>run the suite]
  Test --> Staging[Staging<br/>prod-like rehearsal]
  Staging -->|delivery: human clicks deploy<br/>deployment: automatic| Deploy[Deploy]
  Deploy --> Prod[production]
```

- **Build** - turn source code into the thing you actually run: compile it, bundle it, package it into an
  artifact or container image. If it won't build, the line stops here.
- **Test** - run the automated checks from [Phase 1](01-continuous-integration.md). Red stops the line.
- **Staging** - deploy the built artifact to a *staging* environment: a private copy of production, with
  prod-like data and settings, where the change can be exercised for real before any user sees it.
- **Deploy** - release to production. This is the line that Delivery guards with a human and Deployment
  crosses on its own.

📝 **Terminology.** An **artifact** is the packaged output of the build - a compiled binary, a zip, a
container image - the concrete thing you deploy. **Staging** is a rehearsal environment that mirrors
production as closely as practical, so problems show up there instead of in front of customers.

**What it does in real life.** A pipeline definition (often a YAML file in your repo) lists these stages,
and the CI/CD server runs them in order on every change:

```console
# A pipeline run, summarized:

  ✓ build       packaged image app:9f2a1c7         (1m 02s)
  ✓ test        218 passed                          (3m 41s)
  ✓ staging     deployed to staging.example.com     (48s)
  ⏸ deploy      waiting for manual approval ...
```

*What just happened:* The change cleared build, passed all 218 tests, and went live on the staging
environment automatically. Then it *stopped* and is waiting - this is a continuous **delivery** pipeline,
so the production deploy is paused for a human to approve. In a continuous **deployment** pipeline, that
last line would read `✓ deploy` instead of `⏸`, with no pause at all.

## Deploy strategies, gently

When the deploy stage does run, it has to swap the new version in for the old one *without dropping the
users currently relying on the service*. You don't have to master these - just recognize the names, because
they come up constantly. Each is a different answer to "how do we change the running thing safely?"

```text
  ROLLING      ▢▢▢▢  ──►  ◼▢▢▢  ──►  ◼◼▢▢  ──►  ◼◼◼◼
               replace servers a few at a time; old + new run side by side briefly

  BLUE-GREEN   [blue = live]   spin up a full [green = new] copy,
                               then flip ALL traffic blue ──► green at once
                               (blue stays warm, so flipping back is instant)

  CANARY       send 5% of users to the new version, watch the metrics,
               then 25%, 50%, 100% - back out at the first sign of trouble
```

- **Rolling** - replace the old version with the new a few servers at a time, so the service never fully
  goes down during the swap. Simple and common.
- **Blue-green** - keep two complete environments. One ("blue") serves users while you deploy and warm up
  the other ("green"); then you flip all traffic over at once. If green misbehaves, you flip straight back
  to blue. The trade-off: you're paying for two full environments.
- **Canary** - release the new version to a *small slice* of users first (the "canary"), watch the error
  rates and metrics, and only widen the rollout if it stays healthy. You catch a bad release while it's
  hurting 5% of users instead of 100%. The trade-off: it's slower and needs good monitoring to be worth it.

⚠️ **Gotcha.** None of these strategies make a *bad release* good - they limit the *blast radius* and make
backing out fast. Canary catches a bad deploy early; blue-green lets you flip back instantly; rolling keeps
the lights on during the swap. They buy you a safe, reversible change, not a guaranteed-correct one. The
correctness still comes from the tests.

**Why this saves you later.** When someone in a meeting says "let's canary this one" or "just blue-green it
back," you'll know exactly what they're proposing and what it costs - and you'll understand why the deploy
stage is the careful, deliberate part of the whole pipeline.

## Recap

1. **CD is two things.** Continuous **delivery** = always release-ready, a human clicks deploy. Continuous
   **deployment** = green changes ship to production automatically.
2. A pipeline is a **sequence of stages** - typically **build → test → staging → deploy** - and a change
   only advances if it cleared the stage before.
3. The **deploy** stage is where delivery and deployment differ: delivery pauses for a human; deployment
   crosses the line on its own.
4. **Deploy strategies** (rolling, blue-green, canary) limit the blast radius and make backing out fast - 
   they manage risk, they don't replace tests.


---

# Why It's Worth It

You've now got the mechanics: CI builds and tests every change, CD carries the green ones toward production
through staged steps. Setting all that up is real work - pipeline files, test suites, environments - so
it's fair to ask: *is it actually worth the trouble?* Yes, and the reason comes down to one shift in how
releases feel. Let's make that concrete, then take a clear-eyed look at the one thing that can ruin it.

## Small, frequent releases are safer than big rare ones

This is the counterintuitive heart of the whole practice. It *feels* safer to batch up a month of changes
and release once, carefully, with everyone watching. It is not - it's the opposite.

**Why people get this wrong.** A big release feels safe because it's rare and ceremonial - surely something
done that carefully is low-risk. But a month-long release bundles a hundred changes together. When
something breaks (and something will), you're staring at a hundred suspects at once, in production, under
pressure. The bigger the batch, the harder to find the culprit and the scarier to undo.

**What's actually true.** A pipeline makes releasing cheap and routine, so you do it in small pieces - one
change, or a handful, at a time. When a small release breaks, there's one obvious suspect. You know exactly
what changed, because *almost nothing* changed.

```text
   BIG RARE RELEASE                      SMALL FREQUENT RELEASES
   ┌───────────────────────┐            ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
   │ 100 changes at once    │           │  │ │  │ │  │ │  │ │  │
   └───────────────────────┘            └──┘ └──┘ └──┘ └──┘ └──┘
   breaks → 100 suspects,               breaks → 1 suspect,
   hard to find, scary to undo          obvious cause, trivial to undo
```

💡 **Key point.** Smaller releases aren't just *less* risky - they make risk *legible*. When little
changes, the cause of a problem is obvious and the fix is small. The pipeline is what makes small, frequent
releases cheap enough to actually do.

## Fast feedback closes the loop while you still care

**What it does in real life.** Because the pipeline runs on every push, you find out within minutes whether
a change is sound - not days later when you've moved on and forgotten the details. That tight loop is worth
more than it sounds.

The cost of a bug grows the longer it goes undiscovered. A test failure caught two minutes after you wrote
the code is a quick fix - the reasoning is still in your head. The *same* bug found three weeks later, after
you've context-switched away, means re-learning what you were even trying to do before you can fix it. CI/CD
shrinks that gap to minutes, deliberately. You stay in the loop while you still remember everything.

## Rollback confidence turns deploys from terrifying to routine

**What it actually is.** A *rollback* is returning production to the previous known-good version when a
deploy goes wrong. With a real pipeline, the previous version is already built, tested, and packaged - so
going back is fast and boring rather than a frantic rebuild.

**Why this changes everything.** When rolling back is a quick, practiced move, a bad deploy stops being a
catastrophe and becomes an inconvenience - flip back, breathe, investigate calmly. Teams that *trust* their
rollback ship more boldly and more often, because the cost of being wrong is low. Confidence to deploy comes
from confidence to *un*-deploy.

```console
# Yesterday's release is misbehaving in production. Reverting the merge:

$ git revert 9f2a1c7 --no-edit
[main 4d8e0b2] Revert "Add discount stacking"

$ git push origin main
```

*What just happened:* You created a new commit that undoes the bad change and pushed it. The pipeline picks
it up like any other change - builds, tests, deploys - and production returns to working order through the
*same* automated path that shipped the problem. No special emergency procedure, no hand-editing servers.
The mechanism you trust for shipping is the same one you trust for un-shipping.

🪖 **War story.** The first team I watched adopt CI/CD spent a month terrified of the deploy button, the way
they'd always been. Then a bad change went out, someone reverted it, and the pipeline put prod back
together in under ten minutes - calmly, on a Tuesday afternoon, no heroics. After that, the fear drained out
of the room. They started deploying several times a day. Nothing about the code got braver; they'd learned
the undo button worked.

## The plain catch: a pipeline is only as good as its tests

Now the part that keeps this guide straight: every benefit above rests on one assumption. Every "green check
means safe" promise made here is borrowed against the quality of your tests. A pipeline doesn't *know* your
code works - it knows your *tests passed*. Those are only the same thing if the tests are good.

⚠️ **Gotcha - flaky tests poison the whole pipeline.** A **flaky test** is one that passes and fails
randomly on the *same* code, with nothing actually changed - usually because it depends on timing, ordering,
or some shared state it shouldn't. One flaky test does damage far out of proportion to its size, because it
attacks the one thing the pipeline runs on: *trust in the green check.*

📝 **Terminology.** *Flaky* describes a test whose result isn't determined by the code under test - run it
twice on the identical code and you might get pass, then fail. The opposite is *deterministic*: same input,
same result, every time.

Watch how the poison spreads. A flaky test fails for no real reason, so people learn to shrug and click
"re-run." But once "re-run until it's green" becomes the habit, you've quietly trained the whole team to
*ignore red* - and the day a red check means a **real** bug, it gets the same shrug and the same re-run.
The gate is still standing, but everyone has learned to walk around it. A pipeline whose failures get
ignored isn't protecting anything; it's just slowing everyone down while providing false comfort.

So the real lesson underneath all of CI/CD: **invest in tests you can trust, and treat a flaky test as a
genuine bug - fix it or remove it, quickly.** The pipeline is only ever a faithful messenger. Give it
trustworthy tests and it makes you fast and safe; give it flaky ones and it lies to you with a straight face.

> ⏭️ How to write the trustworthy, deterministic tests this all depends on is its own craft - see
> [Testing in CI](/guides/testing-in-ci).

## Where to go next

You now have the working mental model: CI builds and tests every change behind a red/green gate; CD carries
green changes through staged steps toward production, with a human gate (delivery) or without one
(deployment); and the whole thing pays off in small, safe, reversible releases - as long as the tests
inside it are trustworthy.

The natural next step is to *build one*. The follow-up guide,
[Your First Pipeline with GitHub Actions](/guides/your-first-pipeline-github-actions), takes everything here
and turns it into a real, running pipeline - a single YAML file that builds and tests your project on every
push - so the abstract assembly line becomes something you can watch run on your own repo.

## Recap

1. **Small, frequent releases** beat big rare ones: when little changes, the cause of a break is obvious and
   the undo is small.
2. **Fast feedback** finds problems in minutes, while the code is still fresh - far cheaper than discovering
   them weeks later.
3. **Rollback confidence** turns a bad deploy from a catastrophe into an inconvenience, which makes teams
   ship more boldly.
4. **The catch:** a green check only means "tests passed," so a pipeline is only as trustworthy as its
   tests - and **flaky tests poison it** by training everyone to ignore red. Fix flakiness like the bug it
   is.
