Engineering
CI/CD
Continuous integration and deployment pipelines for frontend applications
CI/CD
Continuous Integration and Continuous Deployment automate testing, building, and deploying applications, ensuring consistent quality and fast delivery.
GitHub Actions
Basic Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type check
run: pnpm type-check
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
build:
runs-on: ubuntu-latest
needs: lint-and-test
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: distDeployment Workflow
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
env:
VITE_API_URL: ${{ secrets.API_URL }}
# Deploy to Vercel
- 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'
# Or deploy to Cloudflare Pages
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: my-project
directory: distE2E Testing Workflow
# .github/workflows/e2e.yml
name: E2E Tests
on:
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps
- name: Build
run: pnpm build
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-reportMatrix Testing
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testGitLab CI
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
NODE_VERSION: "20"
.node-setup:
image: node:${NODE_VERSION}
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- corepack enable
- pnpm install --frozen-lockfile
lint:
extends: .node-setup
stage: test
script:
- pnpm lint
- pnpm type-check
test:
extends: .node-setup
stage: test
script:
- pnpm test -- --coverage
coverage: '/Lines\s+:\s+(\d+.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
extends: .node-setup
stage: build
script:
- pnpm build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy-staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
script:
- echo "Deploying to staging"
only:
- develop
deploy-production:
stage: deploy
environment:
name: production
url: https://example.com
script:
- echo "Deploying to production"
only:
- main
when: manualDeployment Platforms
Vercel
// vercel.json
{
"buildCommand": "pnpm build",
"outputDirectory": "dist",
"framework": "vite",
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
],
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}Cloudflare Pages
# wrangler.toml
name = "my-app"
compatibility_date = "2024-01-01"
[site]
bucket = "./dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200Docker
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# nginx.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /assets {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_types text/plain text/css application/json application/javascript;
}Environment Management
Environment Variables
# GitHub Actions
env:
VITE_API_URL: ${{ vars.API_URL }}
VITE_APP_KEY: ${{ secrets.APP_KEY }}
# Different environments
jobs:
deploy-staging:
environment: staging
env:
VITE_API_URL: https://api.staging.example.com
deploy-production:
environment: production
env:
VITE_API_URL: https://api.example.comFeature Flags
// Feature flag configuration
const features = {
newDashboard: process.env.VITE_FEATURE_NEW_DASHBOARD === 'true',
betaFeatures: process.env.VITE_ENVIRONMENT !== 'production',
};
// Usage
if (features.newDashboard) {
return <NewDashboard />;
}
return <LegacyDashboard />;Monitoring and Rollback
Health Checks
deploy:
steps:
- name: Deploy
run: ./deploy.sh
- name: Health Check
run: |
for i in {1..10}; do
if curl -s https://example.com/health | grep -q "ok"; then
echo "Health check passed"
exit 0
fi
sleep 10
done
echo "Health check failed"
exit 1
- name: Rollback on failure
if: failure()
run: ./rollback.shDeployment Notifications
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Deployment ${{ job.status }}: ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Deployment ${{ job.status }}*\nRepo: ${{ github.repository }}\nBranch: ${{ github.ref_name }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}Best Practices
CI/CD Guidelines
- Run tests and linting on every pull request
- Use caching to speed up builds
- Separate CI and CD workflows
- Use environment-specific configurations
- Implement health checks after deployment
- Set up automatic rollback on failures
- Monitor deployments and notify team
- Keep deployment times under 10 minutes