What is CI/CD?
CI/CD automates the process of testing and deploying your application:
Developer pushes code
│
▼
┌─────────────────┐
│ CI (Continuous │ Build → Lint → Test
│ Integration) │ (Every commit)
└────────┬────────┘
│ (all tests pass)
▼
┌─────────────────┐
│ CD (Continuous │ Deploy to staging/production
│ Deployment) │ (Automatic or on merge to main)
└─────────────────┘
CI/CD Pipeline Stages
1. Code Push ──→ 2. Checkout ──→ 3. Install Dependencies
│
▼
4. Lint Code
│
▼
5. Run Tests
│
▼
6. Build (if needed)
│
▼
7. Deploy ──→ Staging / Production
1. GitHub Actions CI Pipeline
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:7
ports:
- 27017:27017
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint code
run: npm run lint
- name: Run tests
run: npm test
env:
NODE_ENV: test
MONGO_URI: mongodb://localhost:27017/test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-key-for-ci-at-least-32-chars
SESSION_SECRET: test-session-secret-for-ci
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage
path: coverage/
2. Full CI/CD Pipeline with Deployment
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
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: '18'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
env:
NODE_ENV: test
MONGO_URI: mongodb://localhost:27017/test
JWT_SECRET: ${{ secrets.JWT_SECRET }}
SESSION_SECRET: ${{ secrets.SESSION_SECRET }}
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} myapp:latest
- name: Push to registry
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
docker push myapp:${{ github.sha }}
docker push myapp:latest
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /app
docker-compose pull
docker-compose up -d --force-recreate
docker system prune -f
3. Environment-Specific Pipelines
# .github/workflows/staging.yml
name: Deploy to Staging
on:
push:
branches: [develop]
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm test
env:
NODE_ENV: test
MONGO_URI: mongodb://localhost:27017/test
JWT_SECRET: ${{ secrets.JWT_SECRET }}
SESSION_SECRET: ${{ secrets.SESSION_SECRET }}
- name: Deploy to staging
run: |
# Deploy to staging server
echo "Deploying to staging..."
# rsync, SSH, or use a deployment tool
4. Package.json Scripts for CI
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --forceExit --detectOpenHandles --verbose",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch",
"lint": "eslint . --ext .js",
"lint:fix": "eslint . --ext .js --fix",
"format": "prettier --write 'src/**/*.js'",
"ci": "npm run lint && npm test",
"build": "echo 'No build step needed'",
"precommit": "lint-staged"
},
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm run ci"
}
}
}
5. Pre-Commit Hooks (Husky)
npm install --save-dev husky lint-staged
// package.json
{
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
npx husky init
# .husky/pre-commit
npx lint-staged
# .husky/pre-push
npm run ci
6. Environment Secrets in CI
# GitHub Actions Secrets (set in repo settings):
# Settings → Secrets and variables → Actions
# Required secrets:
# - JWT_SECRET: production JWT secret
# - SESSION_SECRET: production session secret
# - MONGO_URI: production MongoDB connection string
# - REDIS_URL: production Redis URL
# - DEPLOY_HOST: SSH host for deployment
# - DEPLOY_USER: SSH username
# - DEPLOY_KEY: SSH private key
# - DOCKER_USERNAME: Docker registry username
# - DOCKER_PASSWORD: Docker registry password
7. Docker-Based CI/CD
# .github/workflows/docker-deploy.yml
name: Docker Build & Deploy
on:
push:
tags:
- 'v*' # Trigger on version tags
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/myapp:latest
${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
8. Deployment Script
#!/bin/bash
# scripts/deploy.sh
set -e
echo "🚀 Starting deployment..."
# Pull latest code
git pull origin main
# Install dependencies
npm ci --only=production
# Run database migrations (if using Prisma/Sequelize)
npx prisma migrate deploy
# Restart application
pm2 reload ecosystem.config.js
echo "✅ Deployment complete!"
9. Health Check After Deployment
# Add this step after deployment
- name: Health check
run: |
for i in {1..10}; do
curl -f http://localhost:3000/health && break
sleep 5
done
- name: Rollback if health check fails
if: failure()
run: |
echo "Health check failed. Rolling back..."
# Rollback logic (e.g., redeploy previous Docker tag)
10. Database Migrations in CI
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# Or for Sequelize:
- name: Run migrations
run: npx sequelize-cli db:migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Complete Production Pipeline
name: Production Pipeline
on:
push:
branches: [main]
concurrency:
group: production
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '18', cache: 'npm' }
- run: npm ci
- run: npm run lint
test:
needs: lint
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: '18', cache: 'npm' }
- run: npm ci
- run: npm test
env:
MONGO_URI: mongodb://localhost:27017/test
JWT_SECRET: ${{ secrets.JWT_SECRET }}
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to registry
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
docker tag myapp:${{ github.sha }} myapp:latest
docker push myapp:${{ github.sha }}
docker push myapp:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /app
docker-compose pull
docker-compose up -d --force-recreate
sleep 10
curl -f http://localhost:3000/health || docker-compose down && exit 1
notify:
needs: deploy
runs-on: ubuntu-latest
if: always()
steps:
- name: Notify on Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Key Takeaways
- CI runs on every commit: lint → test → build
- CD runs on merge to main: deploy to production
- Use GitHub Actions with services (MongoDB, Redis) for integration tests
- Store all secrets in GitHub Secrets - never in code
- Use environment-specific pipelines for staging vs production
- Add pre-commit hooks (Husky) for catching issues early
- Implement health checks after deployment with auto-rollback on failure
- Use concurrency groups to prevent parallel deployments
- Set up notifications (Slack, email) for deployment status
- Use version tags (v1.0.0) for release-based deployment