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

•11 min read•Subid Das
cicddevopsgithub-actionsgitlab-ciautomation
CI/CD Pipeline Design: From Push to Production in Minutes

A slow CI/CD pipeline kills productivity. Developers wait 30 minutes for feedback. A good pipeline gives feedback in 3 minutes. This guide covers pipeline architecture.

Pipeline Architecture#

# .github/workflows/ci-cd.yml
name: CI/CD

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

jobs:
  # Stage 1: Quick feedback (3 minutes)
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run test
      - run: npm run type-check

  # Stage 2: Build (2 minutes)
  build:
    needs: lint-and-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - run: npm ci
      - run: npm run build
      
      - uses: actions/upload-artifact@v3
        with:
          name: build-artifact
          path: dist/

  # Stage 3: Security scan (parallel, 1 minute)
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm audit --audit-level=moderate
      
      - name: Run SAST scan
        uses: securelabs/sast-scan@v1

  # Stage 4: Deploy to staging (5 minutes)
  deploy-staging:
    needs: [build, security-scan]
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v3
      - uses: actions/download-artifact@v3
        with:
          name: build-artifact
          path: dist/

      - name: Deploy to Vercel Staging
        run: |
          npx vercel deploy --prod \
            --token ${{ secrets.VERCEL_TOKEN }} \
            --scope ${{ secrets.VERCEL_ORG_ID }}

      - name: Smoke tests
        run: npm run test:e2e

  # Stage 5: Manual approval + production deploy
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://myapp.com
    steps:
      - uses: actions/checkout@v3
      - uses: actions/download-artifact@v3
        with:
          name: build-artifact

      - name: Deploy to production
        run: |
          npx vercel deploy --prod \
            --token ${{ secrets.VERCEL_TOKEN }}

      - name: Health check
        run: |
          curl -f https://myapp.com/health || exit 1

      - name: Notify Slack
        if: success()
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
            -d '{"text":"Deployed to production"}'

Total time: ~12 minutes (lint: 3, build: 2, security: 1, staging: 5, production: 1)

Stage 1: Lint & Test (Fast Feedback)#

Run this first. Catches obvious bugs in seconds.

lint-and-test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        cache: npm  # 💡 Cache npm dependencies
    
    - run: npm ci --prefer-offline --no-audit
    - run: npm run lint
    - run: npm run test -- --coverage
    - run: npm run type-check

  # Fail fast on type errors
  continue-on-error: false

Optimization: Use caching to skip dependency downloads.

setup-node:
  with:
    cache: npm  # Saves 30+ seconds per run

Without caching: 45s With caching: 5s

Caching Strategy#

- name: Cache dependencies
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

- name: Cache build
  uses: actions/cache@v3
  with:
    path: .next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-

Cache key includes hash of package-lock.json. Changes to dependencies invalidate cache automatically.

Stage 2: Build (Production Artifact)#

build:
  needs: lint-and-test
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
    
    - run: npm ci
    - run: npm run build
    
    # Upload build as artifact
    - uses: actions/upload-artifact@v3
      with:
        name: build-output
        path: dist/
        retention-days: 7  # Keep 7 days then delete

Artifacts are compressed and cached. Saves recompiling in stage 3.

Stage 3: Parallel Security Scans#

Run these in parallel—they don't depend on each other.

security-scan:
  runs-on: ubuntu-latest
  steps:
    # SAST: Static code analysis
    - uses: actions/checkout@v3
    - uses: securelabs/sast-scan@v1

    # Dependency audit
    - run: npm audit --audit-level=moderate

    # Container scanning (if building Docker images)
    - uses: aquasecurity/trivy-action@master
      with:
        image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}

    # License compliance
    - run: npm ls --all | grep UNLICENSED && exit 1 || true

All run in parallel in same job. One failure stops pipeline.

Stage 4: Deploy to Staging#

deploy-staging:
  needs: [build, security-scan]
  runs-on: ubuntu-latest
  if: github.event_name == 'push'  # Only on push, not PR
  
  steps:
    - uses: actions/download-artifact@v3
      with:
        name: build-output

    - name: Deploy to staging
      run: |
        npm run deploy:staging

    # Smoke tests on staging
    - name: Smoke tests
      run: |
        curl -f https://staging.myapp.com/health || exit 1
        npm run test:e2e -- --baseUrl=https://staging.myapp.com

Tests against real staging environment. Catches integration bugs.

Stage 5: Manual Approval + Production#

deploy-production:
  needs: deploy-staging
  runs-on: ubuntu-latest
  if: github.ref == 'refs/heads/main'
  
  # 🔐 Require manual approval
  environment:
    name: production
    url: https://myapp.com
    
  steps:
    - uses: actions/download-artifact@v3
    
    - name: Deploy
      run: npm run deploy:prod

    # Verify deployment
    - name: Health check
      run: |
        for i in {1..5}; do
          if curl -f https://myapp.com/health; then
            echo "Health check passed"
            exit 0
          fi
          echo "Attempt $i failed, retrying..."
          sleep 10
        done
        exit 1

    # Notify
    - name: Slack notification
      if: success()
      run: |
        curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
          -H 'Content-Type: application/json' \
          -d '{
            "text": "✅ Deployed to production",
            "blocks": [{
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text": "*Production Deployment Successful*\nCommit: ${{ github.sha }}"
              }
            }]
          }'

GitHub UI prompts for approval before production:

⏳ Waiting for approval
Reviewers can approve this deployment

Conditional Execution#

if: github.event_name == 'push'          # Only on push
if: github.ref == 'refs/heads/main'      # Only main branch
if: github.actor != 'dependabot'         # Skip bots
if: success()                             # Only if previous succeeded
if: failure()                             # Only if previous failed
if: always()                              # Always run

Matrix Testing (Multiple Versions)#

Test against multiple Node/OS versions:

test:
  runs-on: ${{ matrix.os }}
  strategy:
    matrix:
      node: [16, 18, 20]
      os: [ubuntu-latest, macos-latest, windows-latest]
  
  steps:
    - uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node }}
    - run: npm test

Runs 9 jobs in parallel (3 nodes × 3 OS). Finds version incompatibilities early.

Failure Handling#

# Notify on failure
- name: Notify failure
  if: failure()
  run: |
    curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
      -d '{"text":"❌ Pipeline failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'

# Retry failing jobs
- uses: nick-invision/retry@v2
  with:
    timeout_minutes: 10
    max_attempts: 3
    command: npm run deploy

# Continue despite failures
- run: npm run test || true  # Non-blocking test
  continue-on-error: true

Performance Benchmarks#

StageTimeOptimization
Lint & test3mCache dependencies
Build2mCache build artifacts
Security1mRun in parallel
Staging5mKeep alive between runs
Production1mMinimize deployment script
Total12mAll running with minimal waste

Checklist#

  • Lint/test stage runs first (fast feedback)
  • Build artifacts cached and reused
  • Security scans run in parallel
  • Staging deploy is gated on lint + security
  • Production deploy requires manual approval
  • Health checks verify deployment
  • Slack/email notifications on success/failure
  • Cache keys include dependency hashes
  • Artifact retention limited (don't store forever)
  • Matrix testing for multiple versions

Conclusion#

A well-designed pipeline:

  • Gives feedback in <5 minutes
  • Prevents bad code from reaching production
  • Enables frequent, safe deploys
  • Reduces manual work

Start simple. Add parallelization and caching. Measure times at each stage. Optimize the slowest bottleneck.

Your team's velocity depends on it.


Pipeline taking longer to run than it takes to write the fix? Book a 15-minute call.

FAQ

What's the fastest way to speed up a slow CI/CD pipeline?

Cache dependencies and parallelize independent stages (lint, test, security scan) before anything else — most slow pipelines are slow because nothing runs concurrently, not because any single step is inherently expensive.

Should tests run before or after the build stage?

Before. Fail fast on lint/unit tests in seconds, before spending minutes building an artifact for code that was already broken.

How do I add a production deploy without risking downtime?

Gate it behind a manual approval step after staging validation, and pair it with a zero-downtime deploy strategy (rolling or blue-green) — approval prevents bad changes, the deploy strategy prevents bad releases from causing outages.

About the author

Subid Dasis a Forward Deployed Engineer & Systems Architect building high-reliability cloud platforms, AI automations, and custom business software. Explore more technical articles on the blog, or get in touch if you're working on something similar.

Recommended Reading

Continue exploring production engineering guides, systems architecture, and cloud patterns.

Let's
build.

Tell me what’s slowing your business down. We’ll work out what to build next.

Tell me about your project

A few sentences are enough to start. No technical brief needed.

Service, budget & timeline (optional) +
I need help with
Budget (optional)
Timeline (optional)
LET'S CONNECT