A pipeline is just a sequence of stages. Designing it well — getting the right things in the right order — is what makes CI/CD feel fast and trustworthy instead of slow and flaky.
A Standard Pipeline Shape
┌────────┐ ┌────────┐ ┌──────┐ ┌──────────┐ ┌─────────┐ ┌────────┐
│ Lint / │→ │ Build │→ │ Unit │→ │ Package │→ │ Deploy │→ │ E2E / │
│ Static │ │ │ │ test │ │ artefact │ │ to dev │ │ smoke │
└────────┘ └────────┘ └──────┘ └──────────┘ └─────────┘ └────────┘
│
▼
Deploy to staging
│
▼
Deploy to prod
(manual approval)
Stage 1: Lint and Static Checks
Run before anything else — they're fast (seconds) and catch obvious issues. Examples:
- Code formatters (Prettier, Black, gofmt)
- Linters (ESLint, RuboCop, Pylint, golangci-lint)
- Type checks (TypeScript, mypy)
- Static security scanners (Semgrep, CodeQL, Bandit)
- Secrets scanners (gitleaks, trufflehog)
Failing here means a developer pushed without running their pre-commit hook. Fix and re-push.
Stage 2: Build
Compile or assemble the application into the artefact you'll deploy. The "build once" principle says: produce one immutable artefact and promote that exact artefact through every environment. Never rebuild for staging or prod separately — environment differences hide there.
Examples of artefacts:
- A Docker image tagged with the commit SHA
- A JAR / WAR file
- A zipped Lambda package
- A static site bundle
- An npm package, Python wheel, or Rust binary
Push the artefact to a registry — ECR, GHCR, Artifactory, Nexus, S3 — so later stages can pull the same byte-for-byte copy.
Stage 3: Tests — The Test Pyramid
| Layer | Count | Speed | What it tests |
|---|---|---|---|
| Unit | Thousands | Milliseconds | One function/class in isolation |
| Integration | Hundreds | Seconds | Modules together — DB, queue, internal API |
| E2E / system | Dozens | Minutes | Full user flows through the deployed app |
Run unit tests in parallel on every push. Run integration tests after the build succeeds. Run E2E tests against a deployed environment, not in the build job — they need a running system.
Stage 4: Security and Quality Gates
- SCA — software composition analysis: scan dependencies for known CVEs (Snyk, Dependabot, Trivy)
- Container scanning — scan the built image for vulnerable base layers
- SAST — static application security testing (CodeQL, Semgrep)
- License compliance — make sure no GPL'd code accidentally landed in your proprietary product
- Coverage thresholds — fail the build if test coverage drops below a target
Treat findings as actionable signals, not noise. If you ignore the scanner today, you'll keep ignoring it tomorrow.
Stage 5: Deploy to Dev / Preview
Every successful build deploys to a development environment automatically. Often each PR also gets its own ephemeral preview environment — Vercel, Netlify, Cloudflare Pages, and Render do this for you; Kubernetes shops use tools like Argo CD with PR-based namespaces.
Reviewers can click a link and exercise the change before merging.
Stage 6: Integration / Smoke Tests on Deployed System
After deploying, run a small, fast suite that validates the system actually came up: a healthcheck, a few key API calls, a single browser flow. Not a full regression suite — just enough to know the deploy didn't smash anything obvious.
If smoke fails, fail the deploy and notify.
Stage 7: Promotion
The same artefact that passed dev gets promoted to staging, then to prod. Promotion is just "redeploy this version to the next environment." Common gating mechanisms:
- Manual approval in the CI tool (a button click)
- A protected branch / tag
- A change-management ticket reference
- Time windows (no Friday deploys, etc.)
Stage 8: Production Deploy
Production deploys benefit from progressive strategies — blue/green, canary, rolling — covered in detail later. The core idea is: don't replace 100% of instances at once. Instead, route a small fraction of traffic to the new version, watch metrics, and roll forward only if signals are healthy.
Speed Matters
A pipeline that takes 45 minutes is a pipeline developers context-switch out of. Aim for under 10 minutes from push to dev deploy. Tactics:
- Cache dependencies — node_modules, pip wheels, Gradle cache, Docker layers
- Parallelize — run linters, types, and tests concurrently
- Shard tests across multiple runners
- Skip unnecessary work — if only docs changed, don't build and test the whole app (path filters)
- Fail fast — fastest checks first, expensive ones later
- Larger runners for build-heavy steps; smaller ones for lint
Pipeline Anti-Patterns
- Rebuilding for each environment — same code, different artefact = subtle bugs
- Snowflake CI server — manually configured, undocumented, terrified to touch
- Tests that pass locally but fail in CI — usually environment leakage; fix once, never trust later
- Long-lived feature branches — defeats the "C" in CI
- "Just rerun the pipeline" culture — flaky tests destroy trust; quarantine and fix them
- Manual steps in the runbook — every manual step is a future incident
A Simple Mental Model
- Lint and type-check (seconds)
- Build the artefact once (a few minutes)
- Run tests against it (parallelised)
- Scan it for CVEs and secrets
- Deploy that exact artefact to dev → staging → prod
- At each environment, validate before moving on
Build once, test thoroughly, promote with confidence. That's CI/CD.