Continuous Integration and Continuous Deployment (CI/CD) automate the process of testing and deploying your applications, reducing errors and shipping faster. This comprehensive guide covers setting up production-ready CI/CD pipelines using GitHub Actions and deploying to popular platforms.
Why CI/CD?
- Automated Testing: Catch bugs before they reach production
- Faster Deployments: Ship features multiple times per day
- Consistency: Same process every time, reducing human error
- Rollback Capability: Quickly revert problematic deployments
- Team Collaboration: Enable parallel development without conflicts
- Quality Assurance: Enforce code quality standards automatically
CI/CD Pipeline Stages
A typical CI/CD pipeline includes:
- Code Commit: Developer pushes code to repository
- Build: Compile/bundle the application
- Test: Run unit, integration, and E2E tests
- Quality Checks: Linting, security scans, code coverage
- Deploy to Staging: Deploy to test environment
- Deploy to Production: Deploy to live environment (manual or automatic)
- Monitor: Track errors and performance
GitHub Actions Setup
GitHub Actions provides built-in CI/CD directly in your repository. Create workflows in .github/workflows/
.
Basic CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run type-check
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Build application
run: npm run build
- name: Run E2E tests
run: npm run test:e2e
Advanced CI with Caching
# .github/workflows/ci-advanced.yml
name: CI Advanced
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: 20
jobs:
lint-and-type-check:
name: Lint and Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run TypeScript check
run: npm run type-check
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
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
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run migrations
run: npm run db:migrate
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Build application
run: npm run build
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
Deployment Strategies
1. Deploy to Vercel
# .github/workflows/deploy-vercel.yml
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
- name: Comment PR with preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '✅ Preview deployed to Vercel!'
})
2. Deploy to Firebase
# .github/workflows/deploy-firebase.yml
name: Deploy to Firebase
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
- name: Deploy to Firebase
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}'
channelId: live
projectId: your-project-id
3. Deploy to AWS S3 + CloudFront
# .github/workflows/deploy-aws.yml
name: Deploy to AWS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./out s3://your-bucket-name --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
Environment Management
GitHub Environments
# .github/workflows/multi-env-deploy.yml
name: Multi-Environment Deploy
on:
push:
branches: [main, staging, develop]
jobs:
deploy-staging:
if: github.ref == 'refs/heads/staging'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.yourapp.com
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: npm run deploy:staging
env:
API_URL: ${{ secrets.STAGING_API_URL }}
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://yourapp.com
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: npm run deploy:production
env:
API_URL: ${{ secrets.PRODUCTION_API_URL }}
DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}
Environment Secrets
Store sensitive data in GitHub Secrets (Settings → Secrets and variables → Actions). Never commit secrets to your repository.
Testing Strategies
Unit Tests with Jest
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '/src/$1',
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.tsx',
],
coverageThresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
E2E Tests with Playwright
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('should login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'wrong@example.com');
await page.fill('[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toContainText('Invalid credentials');
});
});
Security Scanning
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run npm audit
run: npm audit --audit-level=moderate
- 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 results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Notifications and Monitoring
Slack Notifications
# Add to any workflow
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "🚨 Deployment failed for ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Deployment Failed*\nRepository: ${{ github.repository }}\nBranch: ${{ github.ref }}\nCommit: ${{ github.sha }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Rollback Strategy
# .github/workflows/rollback.yml
name: Rollback
on:
workflow_dispatch:
inputs:
version:
description: 'Version to rollback to'
required: true
type: string
jobs:
rollback:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.version }}
- name: Deploy previous version
run: |
npm ci
npm run build
npm run deploy:production
- name: Notify team
run: |
echo "Rolled back to version ${{ inputs.version }}"
Performance Monitoring
// Add to your application
// lib/monitoring.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
// Track deployment
fetch('https://api.sentry.io/api/0/organizations/org/releases/', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SENTRY_AUTH_TOKEN}`,
},
body: JSON.stringify({
version: process.env.NEXT_PUBLIC_APP_VERSION,
projects: ['your-project'],
}),
});
Best Practices
- Fast feedback: Keep pipeline runs under 10 minutes
- Fail fast: Run quick tests first (linting, unit tests)
- Cache dependencies: Speed up builds with caching
- Parallel jobs: Run independent jobs concurrently
- Environment parity: Keep staging identical to production
- Blue-green deployment: Zero-downtime deployments
- Feature flags: Decouple deployment from release
- Automated rollbacks: Revert automatically on failures
- Monitor everything: Track deployments, errors, performance
CI/CD Checklist
- Automated testing (unit, integration, E2E)
- Code quality checks (linting, formatting)
- Security scanning
- Dependency vulnerability checks
- Build verification
- Environment-specific configurations
- Deployment automation
- Rollback procedures
- Monitoring and alerting
- Documentation
Conclusion
A well-configured CI/CD pipeline is an investment that pays dividends in developer productivity, code quality, and deployment confidence. Start simple with automated testing, then gradually add deployment automation, security scanning, and monitoring.
Remember: the goal is not perfection but continuous improvement. Your CI/CD pipeline should evolve with your application, adding checks and automations as they become valuable to your team.
Need Help Setting Up CI/CD?
Yonda Solutions specializes in DevOps and CI/CD pipeline setup. We can help you implement automated testing, deployment strategies, and monitoring for your applications. Contact us today to streamline your deployment process.