CI/CD Pipeline
Set up robust continuous integration and deployment pipelines for your Refract applications to ensure code quality, automated testing, and reliable deployments.
GitHub Actions Workflow
Complete CI/CD pipeline using GitHub Actions:
- Basic Workflow
- Advanced Workflow
.github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 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 linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Upload test coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
NODE_ENV: production
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
deploy-staging:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- name: Deploy to staging
run: |
# Deploy to your staging environment
npm run deploy:staging
env:
STAGING_API_KEY: ${{ secrets.STAGING_API_KEY }}
deploy-production:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- name: Deploy to production
run: |
# Deploy to your production environment
npm run deploy:production
env:
PRODUCTION_API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
.github/workflows/advanced-ci-cd.yml
name: Advanced CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '18.x'
jobs:
quality-checks:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Needed for SonarCloud
- 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 -- --format=json --output-file=eslint-report.json
continue-on-error: true
- name: Run Prettier check
run: npm run format:check
- name: Run security audit
run: npm audit --audit-level=moderate
- name: Check bundle size
run: npm run build:analyze
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
test-suite:
runs-on: ubuntu-latest
strategy:
matrix:
test-type: [unit, integration, e2e]
steps:
- name: Checkout code
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 ${{ matrix.test-type }} tests
run: npm run test:${{ matrix.test-type }}
env:
CI: true
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results-${{ matrix.test-type }}
path: |
coverage/
test-results/
performance-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
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: Build application
run: npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli@0.12.x
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
- name: Run bundle analysis
run: npm run analyze:bundle
- name: Performance regression test
run: npm run test:performance
build-and-deploy:
needs: [quality-checks, test-suite, performance-tests]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout code
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: Generate version
id: version
run: |
VERSION=$(date +%Y%m%d%H%M%S)-${GITHUB_SHA:0:7}
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build application
run: npm run build
env:
NODE_ENV: production
VITE_VERSION: ${{ steps.version.outputs.version }}
- name: Create deployment package
run: |
tar -czf deployment-${{ steps.version.outputs.version }}.tar.gz dist/
- name: Upload deployment package
uses: actions/upload-artifact@v3
with:
name: deployment-package
path: deployment-${{ steps.version.outputs.version }}.tar.gz
deploy-staging:
needs: build-and-deploy
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Download deployment package
uses: actions/download-artifact@v3
with:
name: deployment-package
- name: Deploy to staging
run: |
# Extract and deploy
tar -xzf deployment-*.tar.gz
# Deploy using your preferred method
aws s3 sync dist/ s3://${{ secrets.STAGING_BUCKET }} --delete
aws cloudfront create-invalidation --distribution-id ${{ secrets.STAGING_DISTRIBUTION_ID }} --paths "/*"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
- name: Run smoke tests
run: |
curl -f https://staging.yourapp.com/health || exit 1
npm run test:smoke -- --baseUrl=https://staging.yourapp.com
deploy-production:
needs: build-and-deploy
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Download deployment package
uses: actions/download-artifact@v3
with:
name: deployment-package
- name: Deploy to production
run: |
# Extract and deploy
tar -xzf deployment-*.tar.gz
# Blue-green deployment
aws s3 sync dist/ s3://${{ secrets.PRODUCTION_BUCKET }}-blue --delete
# Switch traffic after successful deployment
aws cloudfront create-invalidation --distribution-id ${{ secrets.PRODUCTION_DISTRIBUTION_ID }} --paths "/*"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
- name: Run production smoke tests
run: |
curl -f https://yourapp.com/health || exit 1
npm run test:smoke -- --baseUrl=https://yourapp.com
- name: Notify deployment
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-type: application/json' \
--data '{"text":"🚀 Production deployment successful: version ${{ needs.build-and-deploy.outputs.version }}"}'
Package.json Scripts
Configure your package.json with comprehensive scripts:
package.json scripts
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:analyze": "vite build --mode analyze",
"preview": "vite preview",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write src/**/*.{js,jsx,ts,tsx,json,css,md}",
"format:check": "prettier --check src/**/*.{js,jsx,ts,tsx,json,css,md}",
"type-check": "tsc --noEmit",
"test": "vitest",
"test:unit": "vitest run --coverage",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:e2e": "playwright test",
"test:smoke": "playwright test --config playwright.smoke.config.ts",
"test:performance": "lighthouse-ci",
"analyze:bundle": "npx vite-bundle-analyzer dist/stats.json",
"analyze:deps": "npx depcheck",
"deploy:staging": "aws s3 sync dist/ s3://staging-bucket --delete",
"deploy:production": "aws s3 sync dist/ s3://production-bucket --delete",
"security:audit": "npm audit",
"security:fix": "npm audit fix",
"prepare": "husky install"
}
}
Quality Gates Configuration
Set up quality gates to prevent bad code from reaching production:
- ESLint Config
- Lighthouse CI
- SonarCloud
.eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended'
],
rules: {
// Performance rules
'react-hooks/exhaustive-deps': 'error',
'no-unused-vars': 'error',
// Code quality rules
'complexity': ['error', 10],
'max-depth': ['error', 4],
'max-lines-per-function': ['error', 50],
// Security rules
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error'
},
overrides: [
{
files: ['**/*.test.{js,jsx,ts,tsx}'],
rules: {
'max-lines-per-function': 'off'
}
}
]
};
lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000'],
startServerCommand: 'npm run preview',
startServerReadyPattern: 'Local:',
numberOfRuns: 3
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'categories:best-practices': ['error', { minScore: 0.9 }],
'categories:seo': ['error', { minScore: 0.9 }],
// Custom assertions
'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 300 }]
}
},
upload: {
target: 'temporary-public-storage'
}
}
};
sonar-project.properties
sonar.projectKey=your-org_your-refract-app
sonar.organization=your-org
# Source code
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx
sonar.exclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx,**/node_modules/**,**/dist/**
# Coverage
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx
# Quality gates
sonar.qualitygate.wait=true
# Language settings
sonar.javascript.environments=browser,node
Docker Integration
Containerize your application for consistent deployments:
Dockerfile
# Multi-stage build for production
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Build application
RUN npm run build
# Production stage
FROM nginx:alpine AS production
# Copy built application
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:80"
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped
Automated Testing Strategy
Comprehensive testing pipeline:
vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
exclude: [
'node_modules/',
'dist/',
'**/*.test.{js,jsx,ts,tsx}',
'**/*.config.{js,ts}'
]
},
setupFiles: ['./src/test/setup.ts'],
globals: true
}
});
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'test-results/junit.xml' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
webServer: {
command: 'npm run preview',
port: 3000,
reuseExistingServer: !process.env.CI
}
});
Deployment Strategies
Different deployment approaches:
- Blue-Green Deployment
- Canary Deployment
blue-green-deploy.sh
#!/bin/bash
# Blue-Green deployment script
BUCKET_BLUE="your-app-blue"
BUCKET_GREEN="your-app-green"
DISTRIBUTION_ID="your-cloudfront-id"
# Determine current active environment
CURRENT=$(aws s3api get-bucket-tagging --bucket $BUCKET_BLUE --query 'TagSet[?Key==`Environment`].Value' --output text 2>/dev/null || echo "green")
if [ "$CURRENT" = "blue" ]; then
TARGET_BUCKET=$BUCKET_GREEN
TARGET_ENV="green"
else
TARGET_BUCKET=$BUCKET_BLUE
TARGET_ENV="blue"
fi
echo "Deploying to $TARGET_ENV environment..."
# Deploy to target environment
aws s3 sync dist/ s3://$TARGET_BUCKET --delete
# Run smoke tests
curl -f https://$TARGET_ENV.yourapp.com/health || exit 1
# Switch traffic
aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*"
# Tag the new active environment
aws s3api put-bucket-tagging --bucket $TARGET_BUCKET --tagging 'TagSet=[{Key=Environment,Value='$TARGET_ENV'}]'
echo "Deployment to $TARGET_ENV completed successfully!"
canary-deployment.yml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: refract-app
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 10
- pause: {duration: 2m}
- setWeight: 25
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 10m}
- setWeight: 75
- pause: {duration: 10m}
analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: refract-app
trafficRouting:
nginx:
stableService: refract-app-stable
canaryService: refract-app-canary
selector:
matchLabels:
app: refract-app
template:
metadata:
labels:
app: refract-app
spec:
containers:
- name: refract-app
image: your-registry/refract-app:latest
ports:
- containerPort: 80
Monitoring and Alerting
Set up monitoring for your CI/CD pipeline:
monitoring-alerts.yml
# Slack notification for deployment status
- name: Notify deployment status
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
fields: repo,message,commit,author,action,eventName,ref,workflow
if: always()
# PagerDuty integration for critical failures
- name: Trigger PagerDuty alert
if: failure()
run: |
curl -X POST https://events.pagerduty.com/v2/enqueue \
-H 'Content-Type: application/json' \
-d '{
"routing_key": "${{ secrets.PAGERDUTY_ROUTING_KEY }}",
"event_action": "trigger",
"payload": {
"summary": "CI/CD Pipeline Failed",
"source": "GitHub Actions",
"severity": "critical"
}
}'
Best Practices
- Fail fast: Run quick checks (linting, type checking) before expensive operations
- Parallel execution: Run independent jobs in parallel to reduce pipeline time
- Caching: Cache dependencies and build artifacts to speed up subsequent runs
- Security scanning: Include security audits and dependency checks
- Environment parity: Keep staging and production environments as similar as possible
- Rollback strategy: Always have a plan to quickly rollback deployments
- Monitoring: Monitor pipeline performance and success rates
- Documentation: Keep deployment procedures well-documented
Related Topics
- Performance Monitoring - Monitor deployed applications
- Scaling Applications - Scale your deployment infrastructure
- Deploy - Basic deployment setup