Skip to main content

Production Deployment Project

Build a complete production-ready CI/CD pipeline for a full-stack application using GitHub Actions.

Project Overview

This project demonstrates a comprehensive CI/CD pipeline for a modern full-stack application, showcasing real-world deployment patterns and best practices.

Application Architecture

Project Structure

full-stack-app/
├── frontend/ # React application
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── services/
│ │ └── utils/
│ ├── public/
│ ├── package.json
│ ├── Dockerfile
│ └── nginx.conf
├── backend/ # Node.js API
│ ├── src/
│ │ ├── controllers/
│ │ ├── models/
│ │ ├── routes/
│ │ ├── middleware/
│ │ └── utils/
│ ├── migrations/
│ ├── package.json
│ ├── Dockerfile
│ └── .env.example
├── infrastructure/ # Infrastructure as Code
│ ├── docker-compose.yml
│ ├── kubernetes/
│ │ ├── frontend/
│ │ ├── backend/
│ │ ├── database/
│ │ └── monitoring/
│ └── terraform/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── cd-staging.yml
│ ├── cd-production.yml
│ └── security.yml
├── docs/
└── README.md

Frontend Application (React)

Application Structure

Package Configuration (frontend/package.json)

{
"name": "full-stack-app-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"axios": "^1.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.11.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:coverage": "react-scripts test --coverage --watchAll=false",
"eject": "react-scripts eject",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"type-check": "tsc --noEmit"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^4.9.5"
}
}

Main Application Component (frontend/src/App.tsx)

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import theme from './theme';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import { AuthProvider } from './contexts/AuthContext';

function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AuthProvider>
<Router>
<div className="App">
<Navbar />
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</main>
</div>
</Router>
</AuthProvider>
</ThemeProvider>
);
}

export default App;

API Service (frontend/src/services/api.ts)

import axios from 'axios';

const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001';

const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});

// Request interceptor for authentication
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);

// Response interceptor for error handling
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);

export default api;

Frontend Dockerfile

# Multi-stage build for React application
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Build application
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

# Copy built application
COPY --from=builder /app/build /usr/share/nginx/html

# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Backend Application (Node.js)

Application Structure

Package Configuration (backend/package.json)

{
"name": "full-stack-app-backend",
"version": "1.0.0",
"description": "Backend API for full-stack application",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"migrate": "knex migrate:latest",
"migrate:rollback": "knex migrate:rollback",
"seed": "knex seed:run"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^6.1.5",
"morgan": "^1.10.0",
"dotenv": "^16.0.3",
"knex": "^2.5.1",
"pg": "^8.11.0",
"redis": "^4.6.7",
"jsonwebtoken": "^9.0.0",
"bcryptjs": "^2.4.3",
"joi": "^17.9.1",
"multer": "^1.4.5-lts.1",
"compression": "^1.7.4"
},
"devDependencies": {
"nodemon": "^2.0.22",
"jest": "^29.5.0",
"supertest": "^6.3.3",
"eslint": "^8.42.0"
}
}

Server Configuration (backend/src/server.js)

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
require('dotenv').config();

const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const healthRoutes = require('./routes/health');
const { errorHandler } = require('./middleware/errorHandler');
const { rateLimiter } = require('./middleware/rateLimiter');

const app = express();
const PORT = process.env.PORT || 3001;

// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true
}));

// Logging and compression
app.use(morgan('combined'));
app.use(compression());

// Rate limiting
app.use(rateLimiter);

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/health', healthRoutes);

// Error handling
app.use(errorHandler);

// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV}`);
});

module.exports = app;

Database Configuration (backend/src/config/database.js)

const knex = require('knex');
const { Pool } = require('pg');

const config = {
development: {
client: 'postgresql',
connection: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'fullstack_dev',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password'
},
migrations: {
directory: './migrations'
},
seeds: {
directory: './seeds'
}
},

staging: {
client: 'postgresql',
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: { rejectUnauthorized: false }
},
migrations: {
directory: './migrations'
}
},

production: {
client: 'postgresql',
connection: {
host: process.env.DB_HOST,
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: { rejectUnauthorized: false }
},
migrations: {
directory: './migrations'
},
pool: {
min: 2,
max: 10
}
}
};

const environment = process.env.NODE_ENV || 'development';
const db = knex(config[environment]);

module.exports = db;

Backend Dockerfile

FROM node:18-alpine

WORKDIR /app

# Install system dependencies
RUN apk add --no-cache \
postgresql-client \
curl

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001

# Change ownership
RUN chown -R nodejs:nodejs /app
USER nodejs

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1

EXPOSE 3001

CMD ["npm", "start"]

CI/CD Pipeline Configuration

Continuous Integration Workflow

Main CI Pipeline (.github/workflows/ci.yml)

name: Continuous Integration

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

env:
NODE_VERSION: '18'
DOCKER_REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
# Frontend CI
frontend-ci:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install frontend dependencies
working-directory: ./frontend
run: npm ci

- name: Run frontend linting
working-directory: ./frontend
run: npm run lint

- name: Run frontend type checking
working-directory: ./frontend
run: npm run type-check

- name: Run frontend tests
working-directory: ./frontend
run: npm run test:coverage

- name: Upload frontend coverage
uses: codecov/codecov-action@v3
with:
file: ./frontend/coverage/lcov.info
flags: frontend

- name: Build frontend
working-directory: ./frontend
run: npm run build

- name: Upload frontend build artifacts
uses: actions/upload-artifact@v3
with:
name: frontend-build
path: frontend/build/

# Backend CI
backend-ci:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

redis:
image: redis:6
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: backend/package-lock.json

- name: Install backend dependencies
working-directory: ./backend
run: npm ci

- name: Setup test database
working-directory: ./backend
run: |
npm run migrate
npm run seed
env:
NODE_ENV: test
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres

- name: Run backend linting
working-directory: ./backend
run: npm run lint

- name: Run backend tests
working-directory: ./backend
run: npm run test:coverage
env:
NODE_ENV: test
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: test_db
DB_USER: postgres
DB_PASSWORD: postgres
REDIS_URL: redis://localhost:6379

- name: Upload backend coverage
uses: codecov/codecov-action@v3
with:
file: ./backend/coverage/lcov.info
flags: backend

# Security scanning
security-scan:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'

- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'

# Docker build and test
docker-build:
runs-on: ubuntu-latest
needs: [frontend-ci, backend-ci]

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push frontend image
uses: docker/build-push-action@v4
with:
context: ./frontend
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:${{ github.sha }}
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Build and push backend image
uses: docker/build-push-action@v4
with:
context: ./backend
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:${{ github.sha }}
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Test frontend container
run: |
docker run --rm -d -p 3000:80 --name test-frontend \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:${{ github.sha }}
sleep 10
curl -f http://localhost:3000/health
docker stop test-frontend

- name: Test backend container
run: |
docker run --rm -d -p 3001:3001 --name test-backend \
-e NODE_ENV=test \
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:${{ github.sha }}
sleep 10
curl -f http://localhost:3001/health
docker stop test-backend

Staging Deployment

Staging Deployment Workflow (.github/workflows/cd-staging.yml)

name: Deploy to Staging

on:
push:
branches: [ develop ]

env:
ENVIRONMENT: staging
DOCKER_REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2

- name: Deploy to ECS
run: |
# Update ECS service with new images
aws ecs update-service \
--cluster staging-cluster \
--service frontend-service \
--force-new-deployment

aws ecs update-service \
--cluster staging-cluster \
--service backend-service \
--force-new-deployment

- name: Run database migrations
run: |
# Run database migrations
kubectl run migration-job \
--image=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:${{ github.sha }} \
--restart=Never \
--env="NODE_ENV=staging" \
--command -- npm run migrate

- name: Run smoke tests
run: |
# Wait for deployment to be ready
kubectl wait --for=condition=available --timeout=300s deployment/frontend
kubectl wait --for=condition=available --timeout=300s deployment/backend

# Run smoke tests
npm run test:smoke -- --base-url=https://staging.example.com

- name: Notify team
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()

Production Deployment

Production Deployment Workflow (.github/workflows/cd-production.yml)

name: Deploy to Production

on:
push:
branches: [ main ]

env:
ENVIRONMENT: production
DOCKER_REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2

- name: Blue-Green Deployment
run: |
# Deploy to green environment
aws ecs update-service \
--cluster production-cluster \
--service frontend-green \
--force-new-deployment

aws ecs update-service \
--cluster production-cluster \
--service backend-green \
--force-new-deployment

# Wait for green deployment to be ready
aws ecs wait services-stable \
--cluster production-cluster \
--services frontend-green backend-green

- name: Run production tests
run: |
# Run comprehensive tests on green environment
npm run test:production -- --base-url=https://green.example.com

- name: Switch traffic to green
run: |
# Update load balancer to point to green environment
aws elbv2 modify-target-group-attributes \
--target-group-arn ${{ secrets.PROD_TARGET_GROUP }} \
--attributes Key=stickiness.enabled,Value=true

- name: Run database migrations
run: |
kubectl run migration-job \
--image=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:${{ github.sha }} \
--restart=Never \
--env="NODE_ENV=production" \
--command -- npm run migrate

- name: Health check
run: |
# Verify production deployment
curl -f https://api.example.com/health
curl -f https://app.example.com/health

- name: Create release
uses: actions/create-release@v1
with:
tag_name: v${{ github.run_number }}
release_name: Release v${{ github.run_number }}
body: |
## Changes in this Release
- Automated deployment from commit ${{ github.sha }}
- Frontend version: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-frontend:${{ github.sha }}
- Backend version: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}-backend:${{ github.sha }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Notify team
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()

Infrastructure as Code

Kubernetes Configuration

Frontend Deployment (infrastructure/kubernetes/frontend/deployment.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: ghcr.io/your-org/full-stack-app-frontend:latest
ports:
- containerPort: 80
env:
- name: REACT_APP_API_URL
value: "https://api.example.com"
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: production
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80
type: ClusterIP

Backend Deployment (infrastructure/kubernetes/backend/deployment.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: ghcr.io/your-org/full-stack-app-backend:latest
ports:
- containerPort: 3001
env:
- name: NODE_ENV
value: "production"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-secrets
key: host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: password
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: backend-service
namespace: production
spec:
selector:
app: backend
ports:
- port: 3001
targetPort: 3001
type: ClusterIP

Monitoring Integration

Prometheus Configuration

apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
data:
prometheus.yml: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'backend'
static_configs:
- targets: ['backend-service:3001']
metrics_path: /metrics
- job_name: 'frontend'
static_configs:
- targets: ['frontend-service:80']
metrics_path: /metrics

Grafana Dashboard

{
"dashboard": {
"title": "Full Stack Application",
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{route}}"
}
]
},
{
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
}
]
},
{
"title": "Error Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"5..\"}[5m])",
"legendFormat": "5xx errors"
}
]
}
]
}
}

Testing Strategy

Frontend Tests

Component Tests (frontend/src/components/tests/Navbar.test.tsx)

import React from 'react';
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Navbar from '../Navbar';

const renderWithRouter = (component: React.ReactElement) => {
return render(
<BrowserRouter>
{component}
</BrowserRouter>
);
};

describe('Navbar', () => {
test('renders navigation links', () => {
renderWithRouter(<Navbar />);

expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('Dashboard')).toBeInTheDocument();
expect(screen.getByText('Profile')).toBeInTheDocument();
});

test('navigates to correct routes', () => {
renderWithRouter(<Navbar />);

const homeLink = screen.getByText('Home');
expect(homeLink.closest('a')).toHaveAttribute('href', '/');
});
});

Integration Tests (frontend/src/services/tests/api.test.ts)

import api from '../api';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'John Doe', email: '[email protected]' }
])
);
})
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('API Service', () => {
test('fetches users successfully', async () => {
const users = await api.get('/users');

expect(users.data).toHaveLength(1);
expect(users.data[0]).toMatchObject({
id: 1,
name: 'John Doe',
email: '[email protected]'
});
});
});

Backend Tests

Unit Tests (backend/src/controllers/tests/auth.test.js)

const request = require('supertest');
const app = require('../../server');

describe('Auth Controller', () => {
describe('POST /api/auth/login', () => {
test('should login with valid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'password123'
})
.expect(200);

expect(response.body).toHaveProperty('token');
expect(response.body).toHaveProperty('user');
});

test('should reject invalid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'wrongpassword'
})
.expect(401);

expect(response.body).toHaveProperty('error');
});
});
});

Integration Tests (backend/src/routes/tests/users.test.js)

const request = require('supertest');
const app = require('../../server');
const db = require('../../config/database');

describe('Users Routes', () => {
beforeEach(async () => {
await db.migrate.rollback();
await db.migrate.latest();
await db.seed.run();
});

afterAll(async () => {
await db.destroy();
});

test('GET /api/users should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);

expect(response.body).toBeInstanceOf(Array);
expect(response.body.length).toBeGreaterThan(0);
});
});

Key Takeaways

Production-Ready CI/CD

  1. Comprehensive Testing: Unit, integration, and end-to-end tests
  2. Security Scanning: Automated vulnerability detection
  3. Multi-Environment Deployment: Staging and production pipelines
  4. Blue-Green Deployment: Zero-downtime production deployments
  5. Monitoring Integration: Application and infrastructure monitoring

Best Practices Implemented

  • Containerization: Docker images for consistent deployments
  • Infrastructure as Code: Kubernetes configurations
  • Database Migrations: Automated schema updates
  • Health Checks: Application and container health monitoring
  • Rollback Capability: Quick recovery from failed deployments

Monitoring and Observability

  • Application Metrics: Request rates, response times, error rates
  • Infrastructure Metrics: CPU, memory, disk usage
  • Log Aggregation: Centralized logging with ELK stack
  • Alerting: Proactive notification of issues
  • Dashboards: Real-time visibility into system health

Congratulations! You've successfully built a complete production-ready CI/CD pipeline for a full-stack application using GitHub Actions. This project demonstrates real-world deployment patterns and best practices that you can apply to your own projects.


This production deployment project showcases the power of GitHub Actions for building sophisticated CI/CD pipelines that can handle complex, multi-service applications with confidence.