CI/CD Intro

Table of Contents


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

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):

  • main - production-ready code
  • develop - integration branch
  • feature/* - new features
  • release/* - release preparation
  • hotfix/* - emergency fixes from main

Trunk-Based (modern, simpler):

  • 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

Posted · Updated