If you write software, you run into the term "CI/CD" practically every day. You run tests with GitHub Actions. You merge into main, and it deploys to production on its own. Setups like these tend to get lumped together under the name CI/CD.

Ask me to define the pieces, though, and things get surprisingly fuzzy. What exactly is CI, and what exactly is CD? Where do Continuous Delivery and Continuous Deployment part ways? I'll admit I started out thinking "CI is automated testing, CD is automated deployment." That isn't far off, but it drifts a little from the original meaning.

Rather than staring at definitions, it's quicker to ask what was clogged. Picture software development as a single run of plumbing, and the history of CI/CD reads as the story of clearing one blockage after another, working downstream.

Integration Hell

In software development of the past, it wasn't unusual to integrate all the changes at the very end. Each developer or team worked for a long stretch, and everything got merged into one at the finish. Picture A, B, and C each working separately and converging right before the deadline.

Each environment may have worked perfectly, yet the moment everything is combined, problems erupt. Code that A changed, B changed too. A change to one feature breaks another. Piles of conflicts, failing tests, and mismatched specs all arrive at once.

And because weeks or months of changes are integrated in one go, tracking down the cause is a real slog. In plumbing terms, it's the grand clog you get from not running the water for months. This is the so-called "Integration Hell." Judging by the name alone, it is not a contraption one wishes to become acquainted with.

That is where the idea of Continuous Integration came in.

Continuous Integration: Integrate Often

CI stands for Continuous Integration. The idea is remarkably simple. If integrating everything at the end is what makes it painful, integrate far more often.

Instead of developing for months and watching it all explode at the end, you integrate a small change and check it. Then you repeat that cycle over and over. Integrate while changes are still small, and when something goes wrong, the blast radius is easy to pin down. Run the water often, and the pipes rarely clog.

Continuous Integration became widely known in the 1990s as a practice of Extreme Programming (XP). In systematizing XP, Kent Beck made frequent integration of changes by developers a key practice. One point is worth nailing down here, though. CI, properly speaking, is not a CI tool.

Running GitHub Actions Is Not CI

These days, hearing "CI" makes most people picture a particular scene. You git push, GitHub Actions kicks off, and Lint, Test, and Build run in order. These are, of course, important mechanisms for achieving CI.

But the real purpose of CI lies elsewhere. It is to integrate code changes frequently and keep confirming that they haven't broken the system. GitHub Actions, Jenkins, GitLab CI, CircleCI: all are merely tools that support this. The practice of CI comes first, and the tools are the means.

So adopting GitHub Actions doesn't necessarily mean you're practicing CI. Say you've spent six months on a giant feature branch and finally open a pull request at the end. Even if GitHub Actions runs tests on that PR, running CI once on a six-month mega-PR and merging it is little more than a gesture. Most of the problems Continuous Integration set out to solve stay right where they were.

Having automated tests isn't enough. Keeping changes small and integrating them continuously is part of what CI is.

After Integration, Releases Get Stuck

CI made the stretch from writing code through building, testing, and integrating far more efficient. Every change runs automated tests, so you can quickly tell whether it looks safe to integrate. Then a different problem shows its face. You can integrate, but getting it into production is hard.

Integrated code doesn't reach users as-is. After development wraps up, there may be QA, writing release procedures, preparing servers, changing configuration, changing the DB, and filing a request with the operations team. Only once all of that is cleared does a production release finally happen.

In other words, what CI sped up was the leg from developer to integrated software. The last leg, from integrated software to production, was left untouched. You can integrate every day, yet production releases happen once every three months. At that rate, the speed at which changes reach users barely improves. The clog has simply moved downstream.

That is where Continuous Delivery came in.

Continuous Delivery: Always Ready to Ship

Continuous Delivery is the next step. CI let code be integrated continuously. So the thought became: keep that code in a state where it can be released to production at any time.

Concretely, you automate the whole process from a commit to a production-ready state. It runs through build, unit tests, integration tests, and acceptance tests, and puts the result on staging. This sequence is called a Deployment Pipeline. For an essay built around plumbing, the name is almost too convenient.

The idea of Continuous Delivery developed at places like ThoughtWorks in the 2000s. It became widely known through Continuous Delivery, the book Jez Humble and David Farley published in 2010. If CI is about always staying integrable, Continuous Delivery is about always staying releasable. That framing makes it easy to keep straight.

Not Necessarily Automated All the Way to Production

This is where it gets a little confusing. With Continuous Delivery, the production release itself may be manual. Tests, builds, and everything needed to reach a releasable state are automated. Even so, the final call, whether to ship to production right now, is made by a human.

In GitHub Actions, configuring an approval on the Production environment is close to this. Someone approves "Deploy Production," and the deploy runs. What's required is that a state where pressing the button releases safely is always maintained. Please don't be pedantic and insist that a single manual step disqualifies it from being CD.

Continuous Deployment Goes One Step Further

Here lies the reason the term CD is so slippery. Two different phrases, Continuous Delivery and Continuous Deployment, share the same two letters.

Continuous Deployment takes Continuous Delivery one step further. It fully automates everything from code through test, build, and deploy to production. For example, CI runs on the pull request, merging into main triggers a build, and the result goes straight to production. There is no human release approval; changes that pass the checks go straight into production.

Put side by side, the difference is quite simple.

Continuous DeliveryContinuous Deployment
Target stateCan ship to production any timeShips to production automatically if nothing is wrong
Tests and buildsAutomatedAutomated
Production release decisionMade by a humanAutomated

The key difference boils down to one thing. Does a human decision sit right before the production release?

A History of Clearing Bottlenecks One by One

Looked at historically, the story becomes quite tidy. The first problem was developing, developing, developing, integrating at the end, and falling into Integration Hell. In response, Continuous Integration spread: change in small pieces, integrate often, check automatically.

Then the next clog appears, just past integration. CI gets you as far as integration, but releasing to production is hard. Continuous Delivery answered by making everything through test, build, and staging up to a releasable state run continuously. And if you can build a pipeline that stable, why not automate all the way to production? That step is Continuous Deployment.

In plumbing terms, every clog you cleared revealed the next one downstream. Put precisely, CI/CD was not a single technology invented out of the blue one day. It is the result of gradually clearing the bottlenecks between changing software and getting it to users.

Where CI Ends and CD Begins

Mapped onto a modern development flow, it might look like this. A developer opens a pull request, and Lint, type checks, unit tests, integration tests, and a build run. That much is the territory of Continuous Integration.

After the merge, an artifact is built, deployed to staging, and put through E2E tests until it's release-ready. That's the territory of Continuous Delivery. Beyond it lies the production deploy. A manual approval right before it means Continuous Delivery; if even that is automated, it's Continuous Deployment.

Real systems, of course, aren't always divided this neatly. Still, look at which parts of the flow from writing code to reaching users are run continuously. That usually tells you where things stand.

What "We've Adopted CI/CD" Actually Means

In the field you often hear "we've adopted CI/CD" (I've said it myself more than once). Yet that phrase alone tells you almost nothing about what's actually going on.

It might just mean tests run on pull requests. It might mean tests, then a merge, a build, a staging deploy, a manual approval, and then production. Or it might be fully automated all the way to production once the merge happens. Every one of these can equally be called "CI/CD."

So in practice, it's quicker to ask a few questions. When does CI run, and what does it test? When are artifacts created? Are deploys to staging and production automatic, and is there an approval before production? Looking at these gives you a far more accurate picture of a project's development flow.

The Goal Isn't Automation

Look up CI/CD and you'll find explanation after explanation about automating tests and automating deployments. Automation matters, of course. But automation itself isn't the goal.

What CI wanted to solve was reducing the risk of integrating changes. What Continuous Delivery wanted to solve was reducing the risk of releasing software.

Release three months of changes at once, and when something breaks, the list of suspects is a mountain. If you frequently run a cycle of testing and deploying small changes, the changes you need to check stay small too. CI/CD, as I see it, is a mechanism for creating a state where you can safely repeat small changes. It is the exact opposite of making big changes once in a while.

That's the story of how CI/CD came about and how its three terms differ. CI integrates changes frequently to catch problems early. Continuous Delivery keeps integrated software ready to ship at any time. Continuous Deployment carries problem-free changes straight through to production.

Understanding CI/CD only as "automating tests and deploys with GitHub Actions" is, frankly, a bit of a waste. Behind it lies the idea of keeping changes small, integrating often, and getting them safely to users. When you look at a setup, look not at the brand of the tools but at the flow from code to users. What matters in plumbing is not the brand of the faucet, but that the water runs freely.