CI/CD Intro¶
CI/CD is the automation that catches your stupid mistakes before they hit production — and if you're not using it , you're one bad merge away from a weekend firefight Continuous Integration means every commit gets tested automatically. Continuous Delivery means every passing build can go to production with a button push. Continuous Deployment means every passing build goes to production automatically
Why CI/CD Matters — The Horror Story¶
The classic: You're deploying manually on a Friday afternoon. You SSH into prod , pull latest , restart the service. Everything looks fine. You close your laptop. Saturday morning your phone blows up — the app is down. You forgot to run migrations. You don't have a rollback plan. You spend your weekend in a debug session. Don't be that person
CI/CD prevents: * Forgetting to run migrations before starting the new code * Deploying code that breaks lint or type checks * Missing environment variables in production config * Architecture mismatches (your M1 Mac builds for arm64 , prod runs on x86) * Deploying from a dirty working tree (uncommitted changes) * No rollback strategy when shit hits the fan
CI Pipeline Stages — The Standard Flow¶
flowchart LR
Lint --> Test --> Build --> Scan --> Deploy Stage 1 — Lint: * ESLint , Prettier , TypeScript checks * Catches syntax errors , style violations , type mismatches * Fastest stage — fails early , saves time
Stage 2 — Test: * Unit tests , integration tests * Runs in isolated environment (Docker container) * Parallel test execution when possible
Stage 3 — Build: * Compiles TypeScript , bundles frontend , builds Docker image * Produces deployable artifact * Tagged with commit SHA for traceability
Stage 4 — Security Scan: * Dependency audit (npm audit , Snyk) * Container image scan (Trivy) * SAST analysis (CodeQL , SonarQube)
Stage 5 — Deploy: * Push to staging/production * Run database migrations * Health check verification after deployment
Branch Strategies — GitFlow vs Trunk-Based¶
GitFlow (classic, complex):
gitGraph
commit id: "main init"
branch develop
commit
branch feature/auth
commit
commit
checkout develop
merge feature/auth
commit
checkout main
merge develop tag "v1.0"
branch feature/payments
commit
commit
checkout develop
merge feature/payments
checkout main
merge develop tag "v1.1" main— production-ready codedevelop— integration branchfeature/*— new featuresrelease/*— release preparationhotfix/*— emergency fixes from main
Trunk-Based (modern, simpler):
gitGraph
commit
commit
branch feature/search
commit
commit
checkout main
merge feature/search
branch feature/payments
commit
commit
commit
checkout main
merge feature/payments
branch feature/notifications
commit
commit
checkout main
merge feature/notifications
commit - Short-lived feature branches (hours , not days)
- Frequent merges to main (multiple times daily)
- Feature flags instead of long-lived branches
- Preferred by teams doing CI/CD properly
Which one to choose: * GitFlow if you do monthly releases and need hotfix support * Trunk-based if you deploy multiple times daily and have feature flags * Most teams over-engineer this — start simple , add complexity when needed
CI Environment — What You're Actually Running On¶
Your CI runs on a clean VM every time — no persistent state , no cached node_modules unless you explicitly set it up
# GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest # Fresh VM every time
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Cache node_modules between runs
- run: npm ci
- run: npm test
Key concepts: * Ephemeral runners — fresh VM for every job , no interference between runs * Caching — dependencies , build output , Docker layers persist between runs * Artifacts — build output saved for later stages (deploy , release) * Secrets — environment variables injected at runtime , never logged
A Minimal CI Pipeline That Actually Works¶
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
quality:
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
- run: npm run typecheck
- run: npm test
build:
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
This pipeline catches lint errors , type errors , test failures , and build failures before anything reaches production. It's minimal , it's fast , and it blocks bad merges
CI/CD Anti-Patterns — Don't Do This Shit¶
Running tests after merge: That defeats the entire point of CI. You want to catch failures before they reach main
Long-running pipelines: If your pipeline takes 30+ minutes , devs will work around it and merge without waiting Parallelize stages , cache aggressively , split into smaller jobs
Deploying from a branch that hasn't been tested: Each deploy should trace back to a specific commit that passed all tests Never deploy from a dirty working tree or a branch with unmerged changes
Hardcoded secrets in pipeline config: Use secrets management — GitHub Secrets , Doppler , Vault Anyone with repo access can read the pipeline YAML
next → devops_06_github_actions.md