Anatomy of a CI/CD Pipeline
Almost every CI/CD pipeline you will ever meet — whether it runs on GitHub Actions, GitLab CI, or Jenkins — is built from the same handful of stages in roughly the same order. Once you learn this skeleton, every real pipeline becomes easy to read, because you are just looking for “where is the build part, where is the test part, where is the deploy part?” This page walks through those stages one at a time, explains what each does, and teaches you the single most important rule of pipeline design: fail fast. Think of this as the mental model you carry into the concrete tool pages later.
The stages at a glance
A pipeline is an ordered list of stages (named groups of work) that your code flows through, like stations on a factory assembly line. Each stage runs only if the one before it passed. If any stage fails, the pipeline stops and the rest never run.
Here is the generic shape that almost all pipelines follow:
push code
│
▼
[ Checkout ] → [ Build ] → [ Test ] → [ Scan ] → [ Package ] → [ Deploy ]
clone compile run unit security build a ship to
the repo & install tests & & quality Docker image the server
deps lint checks / artifact
The names differ slightly between tools (GitLab calls them “stages”, GitHub Actions calls them “jobs”, Jenkins calls them “stages” too), but the concept is identical: small, ordered, independent steps that each do one job and hand the result to the next.
Stage 1 — Checkout
The first thing any pipeline must do is get a copy of your code. Checkout means cloning your git repository onto the temporary machine (called a runner or agent) that the CI tool spins up to do the work. This runner is a fresh, throwaway Linux box — usually Ubuntu — that exists only for the length of one pipeline run and is then destroyed.
Conceptually the runner does this for you:
git clone --depth 1 https://github.com/acme/myapp.git
cd myapp
git checkout "$COMMIT_SHA"
The --depth 1 flag means “only fetch the latest commit, not the entire history” — this is called a shallow clone and it makes checkout much faster on big repositories.
When to care: you rarely write the checkout step yourself; every CI tool gives you a one-line built-in for it (actions/checkout on GitHub, the implicit clone on GitLab). You only customise it when you need full history (for example, to calculate a version number from git tags).
Stage 2 — Build
The Build stage turns your source code into something runnable. What “build” means depends on your language:
| Language / stack | Build step | Output |
|---|---|---|
| Node.js | npm ci && npm run build | dist/ folder of JS |
| Java (Maven) | mvn package | a .jar file |
| Go | go build ./... | a single binary |
| Python | pip install -r requirements.txt | installed deps |
| Static site | npm run build | static HTML/CSS |
Notice we use npm ci (clean install), not npm install. npm ci installs the exact versions pinned in package-lock.json and deletes any existing node_modules first, so the build is identical every single time. That repeatability is the whole point of CI.
npm ci
npm run build
Output:
added 412 packages in 9s
> [email protected] build
> vite build
✓ 38 modules transformed.
dist/index.html 0.46 kB
✓ built in 2.31s
Stage 3 — Test
The Test stage runs your automated tests against the freshly built code. This is the stage that gives CI its value — without it, your pipeline is just an automated way to ship bugs faster. Tests usually come in layers, fastest first:
- Lint — checks code style and obvious mistakes (e.g.
eslint,ruff). Milliseconds to run. - Unit tests — test one function or class in isolation. Seconds.
- Integration tests — test how parts work together, often hitting a real database. Minutes.
npm run lint
npm test
Output:
Test Suites: 14 passed, 14 total
Tests: 96 passed, 96 total
Time: 4.81 s
The golden rule: a failing test must fail the whole pipeline (exit code non-zero). Never let a test print a warning and continue — that defeats the purpose.
Stage 4 — Scan
The Scan stage checks for security and quality problems that tests do not catch. This includes:
- Dependency scanning — does any library you use have a known vulnerability? (e.g.
npm audit, Trivy, Dependabot). - SAST (Static Application Security Testing) — scans your own source code for risky patterns like SQL injection.
- Secret scanning — makes sure nobody accidentally committed a password or API key.
npm audit --audit-level=high
trivy fs --severity HIGH,CRITICAL .
Security gotcha: Treat a failed scan as seriously as a failed test. A single leaked API key in your git history is enough for an attacker to take over your infrastructure. Make secret scanning a mandatory, pipeline-failing stage — not an optional warning.
Stage 5 — Package
The Package stage takes your built code and bundles it into the single, immutable thing you will actually deploy — an artifact. The most common artifact today is a Docker image (a self-contained bundle of your app plus everything it needs to run).
docker build -t registry.example.com/myapp:$GIT_SHA .
docker push registry.example.com/myapp:$GIT_SHA
Notice we tag the image with the git commit SHA ($GIT_SHA), not just latest. This means every deploy points to one exact, traceable build — and if something breaks, you can instantly roll back to the previous SHA.
When NOT to package as Docker: for a simple static website or a serverless function, your artifact is just a zip of files. Don’t reach for Docker if a tarball will do.
Stage 6 — Deploy
The final Deploy stage pushes the packaged artifact to a real server. On a single Ubuntu host this is often as simple as connecting over SSH and restarting the service:
ssh [email protected] \
"docker pull registry.example.com/myapp:$GIT_SHA && \
docker compose up -d && \
sudo systemctl reload nginx"
Deploy is usually split into environments: deploy to staging automatically, but require a human’s approval before production. That human approval gate is the difference between Continuous Delivery (ready to ship, click to deploy) and Continuous Deployment (every passing build ships automatically).
The fail-fast principle
The single most important design rule is fail fast: order your stages so the cheapest, fastest checks run first. If linting (1 second) catches a problem, you should never have wasted 10 minutes building a Docker image and spinning up a database for integration tests.
| Stage | Typical time | Runs first because… |
|---|---|---|
| Lint | ~2 s | catches typos instantly |
| Unit tests | ~10 s | cheap, no external services |
| Build | ~1 min | needed before packaging |
| Integration tests | ~3 min | slow, needs a database |
| Scan | ~1 min | important but not blocking early |
| Package + Deploy | ~2 min | only worth doing if all above passed |
By failing fast you give developers feedback in seconds instead of minutes, and you save the runner’s CPU time (which usually costs money) for builds that actually deserve it.
Best Practices
- Keep each stage doing one job. A stage that builds and tests and deploys is hard to debug and impossible to re-run partially.
- Order stages cheapest-first so the fast-failing checks reject bad code before expensive work runs.
- Make every check blocking. A test or scan that only warns is the same as no check at all.
- Tag artifacts with the git SHA, never just
latest, so every deploy is traceable and instantly reversible. - Use
npm ci/ locked dependency installs so the build is byte-for-byte repeatable, not “whatever was newest today”. - Gate production deploys behind a manual approval until you fully trust your test coverage.
- Cache dependencies (e.g.
node_modules, Maven.m2) between runs to keep pipelines fast.