CI/CD Pipeline and Production Readiness Checklist

A CI/CD pipeline automates the two most critical steps in software delivery: verifying that changes do not break anything (CI โ€” Continuous Integration), and deploying changes to production automatically when they pass (CD โ€” Continuous Deployment). For the MERN Blog, this means every push to the main branch runs the Jest and Vitest test suites โ€” and if they pass, triggers a Render and Netlify deployment. In this final lesson you will wire together a GitHub Actions workflow for CI, review the production readiness checklist, and add the security and performance essentials that make the MERN Blog safe for real users.

GitHub Actions CI Workflow

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

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

jobs:
  # โ”€โ”€ Server tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  server:
    name: Server Tests
    runs-on: ubuntu-latest
    services:
      mongodb:
        image: mongo:7
        ports: ['27017:27017']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm', cache-dependency-path: server/package-lock.json }
      - name: Install dependencies
        run: cd server && npm ci
      - name: Run tests
        run: cd server && npm test -- --forceExit
        env:
          NODE_ENV:    test
          MONGODB_URI: mongodb://localhost:27017/blogdb_test
          JWT_SECRET:  test-secret-minimum-32-characters-long

  # โ”€โ”€ Client tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  client:
    name: Client Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm', cache-dependency-path: client/package-lock.json }
      - name: Install dependencies
        run: cd client && npm ci
      - name: Run tests
        run: cd client && npm test -- --run
      - name: Build (verify no build errors)
        run: cd client && npm run build
        env:
          VITE_API_URL: https://placeholder.onrender.com

  # โ”€โ”€ Deploy on main (after both test jobs pass) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: [server, client]    # only runs if both test jobs pass
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - name: Trigger Render deploy
        run: |
          curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
      - name: Trigger Netlify deploy
        run: |
          curl -X POST ${{ secrets.NETLIFY_BUILD_HOOK_URL }}
Note: The deploy job uses needs: [server, client] โ€” it only runs if both test jobs succeed. This is the core of CI/CD: automated tests act as a gate before deployment. A failing test prevents the broken code from reaching production. Store the Render deploy hook URL and Netlify build hook URL as GitHub repository secrets (Settings โ†’ Secrets and variables โ†’ Actions) โ€” never hardcode them in the workflow file.
Tip: Render and Netlify both provide deploy hook URLs โ€” special URLs that trigger a new deployment when called with an HTTP POST. Find them in Render (Service โ†’ Settings โ†’ Deploy hooks) and Netlify (Site settings โ†’ Build and deploy โ†’ Build hooks). These hooks are authenticated by the secret URL itself โ€” keep them private. Add them as GitHub Actions secrets so they are not exposed in the workflow logs.
Warning: GitHub Actions minutes are free for public repositories but limited for private repositories (2,000 minutes/month on the free plan). Each CI run for the MERN Blog takes roughly 3โ€“5 minutes. At 2,000 minutes/month that is about 400โ€“660 CI runs before hitting the limit. To reduce usage: cache npm dependencies (already done with cache: 'npm'), skip unnecessary matrix builds, and only run E2E tests on the main branch.

Production Readiness Checklist

Category Item Status
Security Helmet.js security headers โœ… Add app.use(helmet())
Rate limiting on auth routes โœ… Add express-rate-limit
HTTPS everywhere โœ… Render and Netlify provide HTTPS automatically
Secrets in environment variables โœ… Never in code or .env committed to Git
Performance Gzip compression โœ… Add app.use(compression())
Vite production build (minified) โœ… npm run build
Image CDN (Cloudinary) โœ… Covered in Chapter 23
Reliability MongoDB Atlas with replication โœ… Atlas provides replica set by default
Process crash restart โœ… Render restarts crashed services automatically
Health check endpoint โœ… GET /api/health
Observability Structured error logging โœ… Console logs captured by Render
Error monitoring (Sentry) โš ๏ธ Recommended โ€” see below
Uptime monitoring โš ๏ธ UptimeRobot (free)

Rate Limiting on Auth Routes

npm install express-rate-limit

// server/src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');

// General API rate limiter
const apiLimiter = rateLimit({
  windowMs:         15 * 60 * 1000, // 15 minutes
  max:              100,             // 100 requests per window
  standardHeaders:  true,
  legacyHeaders:    false,
  message:          { success: false, message: 'Too many requests โ€” please try again in 15 minutes' },
});

// Strict limiter for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max:      10,     // only 10 login/register attempts per 15 minutes
  message:  { success: false, message: 'Too many auth attempts โ€” please try again later' },
  skipSuccessfulRequests: true, // do not count successful logins
});

module.exports = { apiLimiter, authLimiter };

// server/index.js โ€” apply limiters
const { apiLimiter, authLimiter } = require('./src/middleware/rateLimiter');
app.use('/api',       apiLimiter);
app.use('/api/auth',  authLimiter); // stricter on auth routes
npm install @sentry/node @sentry/react

// server/index.js โ€” Sentry for Express
const Sentry = require('@sentry/node');
Sentry.init({
  dsn:         process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 0.1, // 10% of requests for performance tracing
});
app.use(Sentry.Handlers.requestHandler());
// ... routes ...
app.use(Sentry.Handlers.errorHandler()); // captures unhandled errors

// client/src/main.jsx โ€” Sentry for React
import * as Sentry from '@sentry/react';
Sentry.init({
  dsn:         import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  tracesSampleRate: 0.1,
});

Common Mistakes

Mistake 1 โ€” No rate limiting on auth routes

โŒ Wrong โ€” unlimited login attempts allowed:

POST /api/auth/login โ€” no limit
# Attacker can try 100,000 passwords per minute โ€” brute-force attack

โœ… Correct โ€” strict rate limiter on auth routes: 10 attempts per 15 minutes.

Mistake 2 โ€” Running CI tests against the production database

โŒ Wrong โ€” CI job uses the production MONGODB_URI:

MONGODB_URI: ${{ secrets.MONGODB_URI }}  # production Atlas URI in CI!
# CI tests create and delete real data โ€” corrupts production database

โœ… Correct โ€” CI uses a separate MongoDB service container or a test-only Atlas connection string.

Mistake 3 โ€” Deploying without running tests first

โŒ Wrong โ€” deploy job runs unconditionally:

jobs:
  deploy:
    steps: [ ... ] # runs even if tests failed!

โœ… Correct โ€” use needs: [server, client] so deploy only runs after all tests pass.

Quick Reference โ€” Deploy Hooks

Service Deploy Hook Location Trigger
Render Service โ†’ Settings โ†’ Deploy hooks โ†’ Add hook curl -X POST $RENDER_HOOK_URL
Netlify Site settings โ†’ Build and deploy โ†’ Build hooks โ†’ Add curl -X POST $NETLIFY_HOOK_URL

🧠 Test Yourself

You push a breaking change to the main branch that causes the server test suite to fail. Your CI/CD pipeline has both test jobs and a deploy job with needs: [server, client]. What happens?