DEV Community

Atlas Whoff
Atlas Whoff

Posted on

CI/CD Pipeline Design: From Push to Production in Under 10 Minutes

What a Good CI/CD Pipeline Actually Does

Not just "runs tests before deploy." A real pipeline:

  1. Validates code quality (lint, types, tests)
  2. Builds artifacts
  3. Deploys to staging automatically
  4. Gates production on approval or scheduled windows
  5. Rolls back automatically on failure

Here's how to build one with GitHub Actions.

The Base Pipeline

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Type check
        run: npx tsc --noEmit

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

Parallel Jobs

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm run lint

  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npx tsc --noEmit

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
    env:
      DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npx prisma migrate deploy
      - run: npm test

  deploy-staging:
    needs: [lint, typecheck, test]  # runs after all pass
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh staging
        env:
          DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
Enter fullscreen mode Exit fullscreen mode

Production Deployment with Approval

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # requires approval in GitHub settings
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh production
        env:
          DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}
Enter fullscreen mode Exit fullscreen mode

In GitHub Settings → Environments → production, you can require reviewers before the job runs.

Caching Dependencies

- uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
Enter fullscreen mode Exit fullscreen mode

Proper caching cuts CI time by 40-60%.

Deploy Script Pattern

#!/bin/bash
# scripts/deploy.sh
set -euo pipefail

ENV=${1:-staging}
echo "Deploying to $ENV..."

# Build
npm run build

# Run migrations before deploying new code
if [ "$ENV" = "production" ]; then
  npx prisma migrate deploy
fi

# Deploy (Vercel example)
vercel deploy --prod --token=$VERCEL_TOKEN

# Smoke test
sleep 10
curl -f https://app.example.com/api/health || {
  echo "Health check failed, rolling back"
  vercel rollback --token=$VERCEL_TOKEN
  exit 1
}

echo "Deploy complete"
Enter fullscreen mode Exit fullscreen mode

Slack Notifications

  notify:
    needs: deploy-production
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Notify Slack
        uses: slackapi/slack-github-action@v1.24.0
        with:
          payload: |
            {
              "text": "${{ needs.deploy-production.result == 'success' && '✅' || '❌' }} Production deploy ${{ needs.deploy-production.result }}",
              "attachments": [{
                "text": "${{ github.event.head_commit.message }}",
                "footer": "by ${{ github.actor }}"
              }]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Enter fullscreen mode Exit fullscreen mode

Secrets Management

# Never hardcode secrets
# Bad:
env:
  API_KEY: sk-abc123

# Good:
env:
  API_KEY: ${{ secrets.API_KEY }}

# Per-environment secrets
# GitHub Settings → Environments → staging/production
# Different values per environment, automatic scoping
Enter fullscreen mode Exit fullscreen mode

The 10-Minute Goal

Pipeline phases and target times:

  • Lint + typecheck: 2 min (parallel)
  • Tests with DB: 3 min
  • Build: 2 min
  • Deploy: 1 min
  • Smoke test: 30s

Anything over 10 minutes is friction that causes engineers to merge without waiting for CI.

Fast pipelines get used. Slow ones get bypassed.


GitHub Actions workflows for test, build, and deploy pre-configured: Whoff Agents AI SaaS Starter Kit includes CI/CD templates ready to drop in.

Top comments (0)