Delivery Pipeline · CI/CD pipelines

CI/CD pipelines

This page draws an exact line between continuous integration, continuous delivery, and continuous deployment — the single most misused set of terms in the field — then walks the stages a real pipeline runs on every commit, why the pipeline definition itself lives in version control next to the code it builds, and why stage order is a deliberate cost decision, not an arbitrary list. Read it once and you'll be able to say precisely which of the three C's your own team's pipeline actually implements.

☺ Explain it like I'm 10

Picture a factory line with an inspector standing after every station, not just one inspector at the very end. A part that comes out crooked gets caught and pulled off the line one station after it was bent — not three hours later, after it's already been painted, wired, and boxed for shipping. A CI/CD pipeline is that same line applied to code: a linter catches a mistake in seconds, tests catch one in minutes, a security scan catches one before the box is ever sealed. Two factories can run the identical line and still differ at the loading dock: one has a supervisor sign off on every truck before it pulls out (continuous delivery), the other lets any truck that passed every inspection drive itself onto the highway (continuous deployment).

The three C's, precisely

Continuous Integration (CI) means developers merge small changes into a shared trunk frequently — multiple times a day, not once a week off a long-lived branch — and every merge automatically triggers a build and a test run. The practice traces back to Kent Beck's Extreme Programming and was named and popularized by Martin Fowler in the early 2000s. CI's entire promise is narrow: it tells you, within minutes, whether the trunk still builds and still passes its tests. It says nothing about releasing anything to anyone. Frequent merging only works if branches stay short-lived, which is why CI and trunk-based branching strategy are really one practice wearing two names.

Continuous Delivery (CD) extends CI one step further: every change that passes the pipeline produces a release candidate that is automatically verified and proven deployable to production at any time. The pipeline itself builds the confidence; a human still decides when to actually release — often a single click in a deployment tool, but a deliberate, gated decision every time. Jez Humble and David Farley formalized this in their 2010 book Continuous Delivery, and its core claim is strict: if your main branch isn't always in a releasable state, you don't have continuous delivery yet, no matter how much automation you've bolted on.

Continuous Deployment removes that last human gate. Every change that passes every stage of the pipeline ships to production automatically, with no approval step in between. The only thing standing between a merged commit and a production user is the pipeline's own judgment. This is the rule worth memorizing: all continuous deployment is continuous delivery, but not all continuous delivery is continuous deployment. Delivery is the necessary foundation (always releasable); deployment is delivery with the release button wired to the pipeline instead of to a person.

Anatomy of a pipeline: from commit to production

A mature pipeline is a fixed sequence of stages, each one a gate the change must clear before the next stage even starts. A common ordering looks like this, and the order is not incidental — it runs cheapest and fastest checks first:

Notice the same artifact moves through staging and production unmodified — nothing gets rebuilt between environments. Rebuilding at each stage means you're no longer testing the thing you're about to ship; you're testing a cousin of it.

# pipeline.yml — a generic pipeline definition, versioned in the app's own repo
stages:
  - lint
  - test
  - build
  - scan
  - deploy-staging
  - deploy-prod

lint:
  stage: lint
  script: eslint . --max-warnings 0
  timeout: 2m

test:
  stage: test
  script: npm test -- --coverage
  timeout: 8m
  needs: [lint]

build:
  stage: build
  script: docker build -t registry.internal/app:$CI_COMMIT_SHA .
  artifacts: [registry.internal/app:$CI_COMMIT_SHA]
  needs: [test]

scan:
  stage: scan
  script: trivy image registry.internal/app:$CI_COMMIT_SHA --exit-code 1 --severity HIGH,CRITICAL
  needs: [build]

deploy-staging:
  stage: deploy-staging
  script: kubectl apply -f k8s/staging --image=registry.internal/app:$CI_COMMIT_SHA
  environment: staging
  needs: [scan]

smoke-test:
  stage: deploy-staging
  script: curl -sf https://staging.internal/healthz
  needs: [deploy-staging]

deploy-prod:
  stage: deploy-prod
  script: kubectl apply -f k8s/prod --image=registry.internal/app:$CI_COMMIT_SHA
  environment: production
  when: manual        # this one line is the entire continuous-delivery vs. continuous-deployment boundary
  needs: [smoke-test]

Pipeline-as-code

Pipeline-as-code means the pipeline definition above is a plain text file — YAML, a Groovy Jenkinsfile, a JSON workflow — checked into the same repository as the application it builds, versioned, diffed, and reviewed exactly like the code it protects. Real formats you'll meet: GitHub Actions reads .github/workflows/*.yml, GitLab CI reads .gitlab-ci.yml at the repo root, Jenkins reads a Jenkinsfile written in its Groovy pipeline DSL, and Azure Pipelines reads azure-pipelines.yml. Before this idiom became standard, pipelines were configured by hand through a CI server's web UI — a setup with no history, no code review, and no way to know what changed between last Tuesday's green build and today's red one.

Pipeline-as-code fixes that by treating the pipeline the same way you treat infrastructure in infrastructure as code: a pull request changes the pipeline, a reviewer reads the diff, and git blame tells you exactly who added that flaky test-retry flag and when. It also means the pipeline can branch with the code — a feature branch can carry its own temporary pipeline tweak without touching what every other branch runs, and reverting a bad pipeline change is a git revert, not an email to whoever has admin on the CI server.

The fast-feedback principle

Stage order in the pipeline above isn't alphabetical or arbitrary — it's sorted by a ratio of cost to signal. Lint takes seconds and catches a class of mistake that would otherwise waste minutes of test time; a security scan takes longer and runs after the build exists, because it needs an artifact to scan. The underlying rule is fail fast, fail loud, fail early: put the cheapest, quickest checks first so a broken change gets rejected before the pipeline spends ten minutes building and deploying something that was never going to pass anyway. A defect caught by a linter in a code editor costs a keystroke to fix; the same defect caught after a production deploy costs an incident, a rollback, and someone's evening.

"Fail loud" is the other half: a failed stage must stop the pipeline outright and notify the owner immediately — a red build in a dashboard nobody watches, or a failure silently skipped so the pipeline "goes green anyway," defeats the entire point. There's no such thing as a soft failure in a pipeline gate; a stage either passes and the change proceeds, or it fails and the change stops, full stop, with the person who broke it told right away rather than discovered days later in an incident.

⚠ Watch out

"CI/CD" gets said as one word so often that teams stop asking which C's they actually run. A pipeline that lints, tests, and builds on every push but still ships by someone SSHing in and running a deploy script by hand isn't continuous delivery — delivery means the release candidate is verified and proven deployable by the pipeline itself, not eyeballed and pushed manually. And a pipeline that pages someone for a Slack "go" before every production release is continuous delivery, not continuous deployment — no matter how automated everything upstream of that approval is. Getting this wrong in a postmortem or a job description isn't pedantry; it's the difference between "any passing change reaches users within minutes, unattended" and "any passing change is one approved click away."

Where the human gate actually sits

In practice, the boundary between delivery and deployment is one mechanism, not a philosophy. GitLab CI's when: manual on a job (as in the YAML above), GitHub Actions' "required reviewers" on a protected environment, a Jenkins input step, and Argo CD's manual-vs-automated sync policy are all the same idea implemented four different ways: the pipeline pauses at the production-deploy stage and waits for a person, or it doesn't. Removing that one gate — nothing else — is what turns a continuous-delivery pipeline into a continuous-deployment one.

Many teams that deploy continuously still control what users actually see using feature flags (tools like LaunchDarkly or the open-source Unleash), decoupling "deployed" from "released": the new code goes to production automatically on every merge, but stays dark behind a flag until someone flips it on for 1% of traffic, then 100%. That's how deployment frequency can hit multiple times a day — the top-quartile mark tracked by the DORA metrics covered in measuring success — without every merge being an uncontrolled surprise for every user at once.

✓ Checkpoint

1. In one or two sentences, what's the exact difference between continuous delivery and continuous deployment, and which one always implies the other? 2. Put these into pipeline order: security scan, build, deploy to staging, lint, unit test. 3. What does "pipeline-as-code" mean, and name two real file formats that implement it. 4. Under the fast-feedback principle, why does lint run before the security/dependency scan rather than after?

Check your answers
  1. Continuous delivery means every change that passes the pipeline is automatically verified and proven deployable, but a human still decides when to actually release it; continuous deployment removes that human gate and ships every passing change straight to production. Continuous deployment always implies continuous delivery (the codebase is always releasable), but continuous delivery does not imply continuous deployment.
  2. Lint → unit test → build → security scan → deploy to staging.
  3. Pipeline-as-code means the pipeline definition is a plain text file checked into version control alongside the app, reviewed and diffed like any other code. Real formats: GitHub Actions' .github/workflows/*.yml, GitLab's .gitlab-ci.yml, a Jenkins Jenkinsfile, or Azure Pipelines' azure-pipelines.yml.
  4. Because lint is nearly free (seconds) and catches obvious mistakes before the pipeline spends the far more expensive time and compute needed to build an artifact and scan it — failing on a typo before you've built anything wastes the least possible time.