Skip to content

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 code
  • develop — integration branch
  • feature/* — new features
  • release/* — release preparation
  • hotfix/* — 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