Overview
A CI/CD pipeline is the automated sequence that takes a code change from a pull request to a running deployment, build, test, package, and release, without a human manually repeating those steps for every change. The value isn't automation for its own sake; it's making the feedback loop between "I made a change" and "I know whether it broke something" as short and reliable as possible, and making every release a repeatable, auditable process instead of a bespoke manual ritual. Most pipeline design problems come from treating the pipeline as a checklist to satisfy rather than a feedback tool to optimize, a pipeline that's thorough but takes 45 minutes trains developers to stop watching it.
Quick Reference
| Stage | Purpose | Typical trigger |
|---|---|---|
| Build | Compile/bundle the artifact | Every push |
| Test | Unit, integration, and/or e2e checks | Every push |
| Package | Produce a deployable artifact (container image, binary) | On merge to main |
| Deploy (staging) | Automated release to a pre-production environment | On merge to main |
| Deploy (production) | Release to production, often behind a gate | Manual approval or automatic on green staging |
Syntax
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci
- run: npm run lint && npm run typecheck && npm test
- run: npm run buildExamples
# Deploy job that only runs after CI passes on main, gated behind
# a required environment approval configured in repo settings.
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: ./scripts/deploy.shFail fast, in order of cost
Order pipeline steps from cheapest/fastest to most expensive, lint before typecheck before unit tests before integration tests before build. A typo shouldn't wait behind a 10-minute integration suite to get flagged.
Visual Diagram
Common Mistakes
- Running the full test suite serially when most of it is independent, parallelizing test shards is usually the single biggest pipeline-speed win available.
- Treating a green pipeline as proof the deployment will succeed, CI validates the code, not the deployment process itself; deploy failures need their own checks (health checks, smoke tests post-deploy).
- No caching of dependencies between runs, so every pipeline run pays a full
npm install/pip installcost that adds minutes for no new information. - Putting a slow, flaky end-to-end suite directly in the required path for every commit, flaky required checks train developers to ignore failures and re-run blindly.
Performance
- Dependency and build-cache reuse between runs is usually the highest-leverage speed improvement, cold installs dominate pipeline time on most projects.
- Parallelizing independent stages (lint, typecheck, and unit tests can usually run concurrently, not sequentially) directly cuts wall-clock time even though total compute cost stays the same.
- Running only the checks relevant to what changed (path-based triggers, affected-package detection in a monorepo) avoids paying full-pipeline cost for a one-line documentation fix.
Best Practices
- Keep the required-to-merge pipeline fast (minutes, not tens of minutes), move slow, lower-signal checks (full e2e suites, load tests) to a separate, non-blocking or scheduled pipeline.
- Make pipeline configuration live in the same repository as the code it builds, versioned and reviewed the same way.
- Use environment-scoped deployment gates for production, not personal judgment about "is now a good time", codify the constraint (approval required, freeze windows) so it's enforced consistently.
- Fail on the first meaningful problem (lint/typecheck) before spending time on expensive stages (integration tests, build), order stages by cost, not by convention.