系列文章: 前端工程師的 Modern Web 實踐之道 - Day 22
預計閱讀時間: 12 分鐘
難度等級: ⭐⭐⭐⭐☆
在前面的三週中,我們深入探討了現代化的開發工具、核心技術實踐,以及品質與效能最佳化。今天我們將進入第四週的主題:工程化與維護。而 CI/CD (Continuous Integration / Continuous Deployment) 流水線設計,正是現代化工程實踐的基石。
還記得你第一次手動部署前端應用的經歷嗎?在本地執行 npm run build,等待構建完成,然後用 FTP 或 SCP 把檔案上傳到伺服器,接著清除快取、測試功能、發現問題、修復 bug、再次構建上傳...這個過程不僅耗時,更容易出錯。
真實場景中的痛點:
技術發展趨勢:
根據 2024 年 State of DevOps 報告,實施完善 CI/CD 的團隊相比傳統開發模式:
這些數據清楚地說明:CI/CD 不只是自動化工具,而是現代軟體工程的核心競爭力。
讓我們先回顧一個典型的傳統前端部署流程:
# 開發者的典型部署步驟
git pull origin main # 1. 拉取最新程式碼
npm install # 2. 安裝相依套件
npm run lint # 3. 執行代碼檢查
npm run test # 4. 執行測試
npm run build # 5. 構建生產版本
scp -r dist/* user@server:/path # 6. 上傳到伺服器
ssh user@server # 7. 連線到伺服器
cd /path && nginx -s reload # 8. 重啟服務
# 然後手動測試生產環境...
這個流程的問題在於:
CI/CD 的本質是將軟體交付過程標準化、自動化、可視化:
Continuous Integration (持續整合):
Continuous Deployment (持續部署):
一個完整的前端 CI/CD 流水線通常包含以下階段:
程式碼提交 → 程式碼檢查 → 自動化測試 → 構建打包 → 部署預發 → 自動化驗證 → 部署生產 → 健康檢查
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
Git ESLint Jest Webpack Staging E2E Test Production Monitoring
讓我們逐一深入了解每個階段:
這個階段的目標是確保程式碼符合團隊規範:
# .github/workflows/ci.yml - GitHub Actions 範例
name: Code Quality Check
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
code-quality:
runs-on: ubuntu-latest
steps:
- name: Checkout 程式碼
uses: actions/checkout@v4
- name: 設定 Node.js 環境
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 安裝相依套件
run: npm ci
- name: 執行 ESLint 檢查
run: npm run lint
- name: 執行 Prettier 檢查
run: npm run format:check
- name: 執行 TypeScript 型別檢查
run: npm run type-check
- name: 程式碼複雜度分析
run: npx plato -r -d reports src
- name: 上傳分析報告
uses: actions/upload-artifact@v4
if: always()
with:
name: code-quality-report
path: reports/
關鍵設計原則:
npm ci 而非 npm install 確保可重複性if: always() 確保即使前面步驟失敗也能收集報告這是 CI/CD 流水線中最重要的品質閘門:
# 測試階段設定
test:
runs-on: ubuntu-latest
needs: code-quality
strategy:
matrix:
node-version: [18, 20]
browser: [chrome, firefox, safari]
steps:
- uses: actions/checkout@v4
- name: 設定 Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: 安裝相依套件
run: npm ci
- name: 執行單元測試
run: npm run test:unit -- --coverage
- name: 執行整合測試
run: npm run test:integration
- name: 執行 E2E 測試
run: npm run test:e2e
env:
BROWSER: ${{ matrix.browser }}
- name: 上傳測試覆蓋率報告
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
flags: unittests
name: codecov-${{ matrix.node-version }}
- name: 測試結果摘要
if: always()
run: |
echo "## 測試結果 🧪" >> $GITHUB_STEP_SUMMARY
echo "- Node.js: ${{ matrix.node-version }}" >> $GITHUB_STEP_SUMMARY
echo "- Browser: ${{ matrix.browser }}" >> $GITHUB_STEP_SUMMARY
npm run test:summary >> $GITHUB_STEP_SUMMARY
進階測試策略:
--onlyChanged)build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: 設定 Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 安裝相依套件
run: npm ci
- name: 構建生產版本
run: npm run build
env:
NODE_ENV: production
# 從 GitHub Secrets 載入環境變數
VITE_API_URL: ${{ secrets.PRODUCTION_API_URL }}
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
- name: Bundle 大小分析
run: |
npm run build:analyze
echo "## Bundle 分析結果 📦" >> $GITHUB_STEP_SUMMARY
du -sh dist/* >> $GITHUB_STEP_SUMMARY
- name: 檢查 Bundle 大小限制
run: |
MAX_SIZE=500000 # 500KB
ACTUAL_SIZE=$(du -sb dist/assets/*.js | awk '{s+=$1} END {print s}')
if [ $ACTUAL_SIZE -gt $MAX_SIZE ]; then
echo "❌ Bundle 大小超過限制: ${ACTUAL_SIZE} > ${MAX_SIZE}"
exit 1
fi
echo "✅ Bundle 大小符合限制: ${ACTUAL_SIZE} <= ${MAX_SIZE}"
- name: 生成 Source Map
run: npm run build:sourcemap
- name: 上傳構建產物
uses: actions/upload-artifact@v4
with:
name: production-build
path: dist/
retention-days: 30
- name: 上傳 Source Map 到 Sentry
run: |
npm install -g @sentry/cli
sentry-cli releases new "${{ github.sha }}"
sentry-cli releases files "${{ github.sha }}" upload-sourcemaps ./dist
sentry-cli releases finalize "${{ github.sha }}"
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: your-org
SENTRY_PROJECT: your-project
構建階段的最佳實踐:
這裡我們展示一個採用藍綠部署策略的範例:
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: 下載構建產物
uses: actions/download-artifact@v4
with:
name: production-build
path: dist/
- name: 部署到 Vercel (Preview)
id: deploy-preview
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 }}
working-directory: ./dist
scope: your-team
- name: 執行煙霧測試
run: |
npm run test:smoke -- --url=${{ steps.deploy-preview.outputs.preview-url }}
- name: 執行 Lighthouse 效能測試
uses: treosh/lighthouse-ci-action@v10
with:
urls: |
${{ steps.deploy-preview.outputs.preview-url }}
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
- name: 部署到生產環境
if: success()
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'
working-directory: ./dist
- name: 通知 Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
部署結果: ${{ job.status }}
提交: ${{ github.event.head_commit.message }}
作者: ${{ github.event.head_commit.author.name }}
查看: ${{ steps.deploy-preview.outputs.preview-url }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
部署策略選擇:
假設我們要為一個 React + TypeScript + Vite 的電商前端專案建立完整的 CI/CD 流程。
my-ecommerce-frontend/
├── .github/
│ └── workflows/
│ ├── ci.yml # 持續整合
│ ├── cd-staging.yml # 部署到預發環境
│ └── cd-production.yml # 部署到生產環境
├── .husky/ # Git hooks
│ ├── pre-commit
│ └── pre-push
├── scripts/
│ ├── deploy.sh
│ └── health-check.sh
├── src/
├── tests/
├── package.json
├── vite.config.ts
└── lighthouse-budget.json
// package.json
{
"scripts": {
"prepare": "husky install",
"lint": "eslint src --ext .ts,.tsx",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
"type-check": "tsc --noEmit",
"test:unit": "vitest run",
"test:changed": "vitest related --run",
"pre-commit": "lint-staged",
"pre-push": "npm run type-check && npm run test:changed"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🔍 執行 pre-commit 檢查..."
npm run pre-commit
# 檢查是否有未解決的 merge conflicts
if git diff --cached --name-only | xargs grep -l "^<<<<<<< HEAD"; then
echo "❌ 發現未解決的 merge conflicts"
exit 1
fi
echo "✅ Pre-commit 檢查通過"
# .husky/pre-push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🧪 執行 pre-push 檢查..."
# 只測試變更的檔案,加快速度
npm run pre-push
if [ $? -ne 0 ]; then
echo "❌ Pre-push 檢查失敗"
exit 1
fi
echo "✅ Pre-push 檢查通過"
# .github/workflows/ci.yml
name: Continuous Integration
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
# 設定並行任務的取消策略
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20'
CACHE_VERSION: 'v1'
jobs:
# Job 1: 程式碼品質檢查
quality:
name: Code Quality
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 獲取完整歷史以進行差異分析
- name: 設定 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: 快取相依套件
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ env.CACHE_VERSION }}-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ env.CACHE_VERSION }}-${{ runner.os }}-
- name: 安裝相依套件
run: npm ci --prefer-offline --no-audit
- name: Lint 檢查
run: npm run lint -- --max-warnings 0
- name: 格式化檢查
run: npm run format:check
- name: TypeScript 型別檢查
run: npm run type-check
- name: 檢查套件漏洞
run: npm audit --audit-level=high
# Job 2: 自動化測試
test:
name: Test Suite
runs-on: ubuntu-latest
needs: quality
timeout-minutes: 15
strategy:
matrix:
node-version: [18, 20]
fail-fast: false # 一個版本失敗不影響其他版本
steps:
- uses: actions/checkout@v4
- name: 設定 Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: 安裝相依套件
run: npm ci --prefer-offline --no-audit
- name: 執行單元測試
run: npm run test:unit -- --coverage --reporter=verbose
- name: 上傳覆蓋率報告
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
flags: unit-tests-node-${{ matrix.node-version }}
fail_ci_if_error: false
- name: 生成測試報告
if: always()
run: |
echo "## 測試結果 🧪" >> $GITHUB_STEP_SUMMARY
echo "Node.js 版本: ${{ matrix.node-version }}" >> $GITHUB_STEP_SUMMARY
npm run test:summary >> $GITHUB_STEP_SUMMARY
# Job 3: E2E 測試
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: quality
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: 設定 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: 安裝相依套件
run: npm ci --prefer-offline --no-audit
- name: 安裝 Playwright 瀏覽器
run: npx playwright install --with-deps chromium firefox
- name: 構建應用
run: npm run build
env:
NODE_ENV: production
- name: 啟動開發伺服器
run: npm run preview &
- name: 等待伺服器啟動
run: npx wait-on http://localhost:4173 --timeout 60000
- name: 執行 E2E 測試
run: npm run test:e2e
- name: 上傳測試截圖
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-screenshots
path: tests/e2e/screenshots/
retention-days: 7
# Job 4: 構建驗證
build:
name: Build Validation
runs-on: ubuntu-latest
needs: [quality, test]
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: 設定 Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: 安裝相依套件
run: npm ci --prefer-offline --no-audit
- name: 構建生產版本
run: npm run build
env:
NODE_ENV: production
- name: 分析 Bundle 大小
run: |
npm run build:analyze
# 生成 Bundle 大小報告
echo "## Bundle 分析 📦" >> $GITHUB_STEP_SUMMARY
echo "### 主要檔案大小" >> $GITHUB_STEP_SUMMARY
du -sh dist/assets/*.js | sort -rh | head -10 >> $GITHUB_STEP_SUMMARY
- name: 檢查 Bundle 大小限制
run: |
# 設定各類檔案的大小限制
MAX_MAIN_JS=300000 # 300KB
MAX_VENDOR_JS=500000 # 500KB
MAX_TOTAL_JS=1000000 # 1MB
MAIN_SIZE=$(find dist/assets -name "index-*.js" -exec du -b {} + | awk '{s+=$1} END {print s}')
VENDOR_SIZE=$(find dist/assets -name "vendor-*.js" -exec du -b {} + | awk '{s+=$1} END {print s}')
TOTAL_SIZE=$(find dist/assets -name "*.js" -exec du -b {} + | awk '{s+=$1} END {print s}')
echo "Main JS: ${MAIN_SIZE} bytes (限制: ${MAX_MAIN_JS})"
echo "Vendor JS: ${VENDOR_SIZE} bytes (限制: ${MAX_VENDOR_JS})"
echo "Total JS: ${TOTAL_SIZE} bytes (限制: ${MAX_TOTAL_JS})"
FAILED=0
if [ $MAIN_SIZE -gt $MAX_MAIN_JS ]; then
echo "❌ Main bundle 超過大小限制"
FAILED=1
fi
if [ $VENDOR_SIZE -gt $MAX_VENDOR_JS ]; then
echo "❌ Vendor bundle 超過大小限制"
FAILED=1
fi
if [ $TOTAL_SIZE -gt $MAX_TOTAL_JS ]; then
echo "❌ Total bundle 超過大小限制"
FAILED=1
fi
if [ $FAILED -eq 1 ]; then
exit 1
fi
echo "✅ 所有 Bundle 大小符合限制"
- name: 上傳構建產物
uses: actions/upload-artifact@v4
with:
name: production-build-${{ github.sha }}
path: dist/
retention-days: 30
- name: 儲存 Bundle 大小趨勢
if: github.ref == 'refs/heads/main'
run: |
mkdir -p .bundle-stats
echo "${{ github.sha }},$(date +%s),${TOTAL_SIZE}" >> .bundle-stats/history.csv
git add .bundle-stats/history.csv
git commit -m "chore: update bundle size stats [skip ci]"
git push
# Job 5: 安全性掃描
security:
name: Security Scan
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- name: 執行 npm audit
run: npm audit --audit-level=moderate
continue-on-error: true
- name: 執行 Snyk 掃描
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
continue-on-error: true
- name: 掃描敏感資訊
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
# 最終狀態檢查
ci-success:
name: CI Status Check
runs-on: ubuntu-latest
needs: [quality, test, e2e, build, security]
if: always()
steps:
- name: 檢查所有任務狀態
run: |
if [[ "${{ needs.quality.result }}" == "success" && \
"${{ needs.test.result }}" == "success" && \
"${{ needs.e2e.result }}" == "success" && \
"${{ needs.build.result }}" == "success" && \
"${{ needs.security.result }}" == "success" ]]; then
echo "✅ 所有 CI 檢查通過"
exit 0
else
echo "❌ CI 檢查失敗"
exit 1
fi
# .github/workflows/cd-staging.yml
name: Deploy to Staging
on:
push:
branches: [develop]
workflow_dispatch: # 允許手動觸發
jobs:
deploy-staging:
name: Deploy to Staging Environment
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- name: 設定 Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 安裝相依套件
run: npm ci
- name: 構建 Staging 版本
run: npm run build
env:
NODE_ENV: staging
VITE_API_URL: ${{ secrets.STAGING_API_URL }}
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_ENVIRONMENT: staging
- name: 部署到 Staging
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: '--target staging'
working-directory: ./dist
- name: 執行煙霧測試
run: npm run test:smoke -- --url=https://staging.example.com
- name: 通知部署結果
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: |
🚀 Staging 部署 ${{ job.status }}
Environment: Staging
Commit: ${{ github.event.head_commit.message }}
Author: ${{ github.actor }}
URL: https://staging.example.com
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
# .github/workflows/cd-production.yml
name: Deploy to Production
on:
push:
branches: [main]
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: '部署版本'
required: true
default: 'latest'
jobs:
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
# 需要通過 GitHub Environment 審批
environment:
name: production
url: https://www.example.com
steps:
- uses: actions/checkout@v4
- name: 設定 Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 安裝相依套件
run: npm ci
- name: 構建生產版本
run: npm run build
env:
NODE_ENV: production
VITE_API_URL: ${{ secrets.PRODUCTION_API_URL }}
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_ENVIRONMENT: production
- name: 生成版本資訊
run: |
cat > dist/version.json << EOF
{
"version": "${{ github.ref_name }}",
"commit": "${{ github.sha }}",
"buildTime": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"author": "${{ github.actor }}"
}
EOF
- name: 部署到 Production (金絲雀 10%)
id: deploy-canary
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 --canary=10'
working-directory: ./dist
- name: 監控金絲雀部署指標
run: |
echo "⏳ 監控金絲雀部署 10 分鐘..."
sleep 600
# 檢查錯誤率
ERROR_RATE=$(curl -s "${{ secrets.MONITORING_API }}/error-rate?env=production-canary")
if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
echo "❌ 錯誤率過高: ${ERROR_RATE}, 回滾部署"
exit 1
fi
# 檢查效能指標
P95_LATENCY=$(curl -s "${{ secrets.MONITORING_API }}/latency?env=production-canary&percentile=95")
if (( $(echo "$P95_LATENCY > 1000" | bc -l) )); then
echo "❌ P95 延遲過高: ${P95_LATENCY}ms, 回滾部署"
exit 1
fi
echo "✅ 金絲雀部署指標正常"
- name: 逐步擴大部署比例
run: |
# 30%
vercel --prod --canary=30 --token=${{ secrets.VERCEL_TOKEN }}
sleep 300
# 50%
vercel --prod --canary=50 --token=${{ secrets.VERCEL_TOKEN }}
sleep 300
# 100% 全量
vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: 健康檢查
run: |
./scripts/health-check.sh https://www.example.com
- name: 建立 Sentry Release
run: |
npm install -g @sentry/cli
sentry-cli releases new "${{ github.sha }}"
sentry-cli releases set-commits "${{ github.sha }}" --auto
sentry-cli releases files "${{ github.sha }}" upload-sourcemaps ./dist
sentry-cli releases finalize "${{ github.sha }}"
sentry-cli releases deploys "${{ github.sha }}" new -e production
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: your-org
SENTRY_PROJECT: your-project
- name: 通知部署成功
if: success()
uses: 8398a7/action-slack@v3
with:
status: success
text: |
🎉 生產環境部署成功!
Version: ${{ github.ref_name }}
Commit: ${{ github.event.head_commit.message }}
Author: ${{ github.actor }}
URL: https://www.example.com
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
- name: 回滾通知
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: |
⚠️ 生產環境部署失敗,已自動回滾
Version: ${{ github.ref_name }}
請檢查錯誤日誌並修復問題
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
不是每次提交都需要執行完整的測試套件。我們可以根據變更範圍智能選擇測試:
# 智能測試選擇
- name: 檢測變更範圍
id: changes
uses: dorny/paths-filter@v2
with:
filters: |
frontend:
- 'src/**'
- 'package.json'
backend-integration:
- 'src/api/**'
ui-components:
- 'src/components/**'
- name: 執行前端單元測試
if: steps.changes.outputs.frontend == 'true'
run: npm run test:unit
- name: 執行 API 整合測試
if: steps.changes.outputs.backend-integration == 'true'
run: npm run test:api
- name: 執行視覺回歸測試
if: steps.changes.outputs.ui-components == 'true'
run: npm run test:visual
// scripts/post-deployment-check.ts
import axios from 'axios';
import { expect } from '@jest/globals';
interface HealthCheckResult {
service: string;
status: 'healthy' | 'degraded' | 'unhealthy';
latency: number;
details?: any;
}
async function performHealthChecks(baseUrl: string): Promise<HealthCheckResult[]> {
const results: HealthCheckResult[] = [];
// 1. 基本可用性檢查
try {
const start = Date.now();
const response = await axios.get(`${baseUrl}/health`, { timeout: 5000 });
const latency = Date.now() - start;
results.push({
service: 'api-health',
status: response.status === 200 ? 'healthy' : 'unhealthy',
latency,
details: response.data
});
} catch (error) {
results.push({
service: 'api-health',
status: 'unhealthy',
latency: -1,
details: error.message
});
}
// 2. 關鍵 API 端點檢查
const criticalEndpoints = [
'/api/products',
'/api/cart',
'/api/user/profile'
];
for (const endpoint of criticalEndpoints) {
try {
const start = Date.now();
const response = await axios.get(`${baseUrl}${endpoint}`, { timeout: 3000 });
const latency = Date.now() - start;
results.push({
service: endpoint,
status: latency < 1000 ? 'healthy' : 'degraded',
latency
});
} catch (error) {
results.push({
service: endpoint,
status: 'unhealthy',
latency: -1,
details: error.message
});
}
}
// 3. 靜態資源載入檢查
try {
const response = await axios.get(`${baseUrl}/version.json`);
expect(response.data).toHaveProperty('version');
expect(response.data).toHaveProperty('commit');
results.push({
service: 'static-assets',
status: 'healthy',
latency: 0,
details: response.data
});
} catch (error) {
results.push({
service: 'static-assets',
status: 'unhealthy',
latency: -1,
details: error.message
});
}
return results;
}
async function main() {
const targetUrl = process.env.TARGET_URL || 'https://www.example.com';
console.log(`🔍 執行部署後健康檢查: ${targetUrl}`);
const results = await performHealthChecks(targetUrl);
// 生成報告
console.log('\n📊 健康檢查結果:');
console.log('═'.repeat(60));
let hasFailures = false;
for (const result of results) {
const icon = result.status === 'healthy' ? '✅' :
result.status === 'degraded' ? '⚠️' : '❌';
console.log(`${icon} ${result.service.padEnd(30)} ${result.status.padEnd(10)} ${result.latency}ms`);
if (result.status === 'unhealthy') {
hasFailures = true;
if (result.details) {
console.log(` 錯誤: ${result.details}`);
}
}
}
console.log('═'.repeat(60));
if (hasFailures) {
console.error('❌ 健康檢查失敗,建議回滾部署');
process.exit(1);
} else {
console.log('✅ 所有健康檢查通過');
process.exit(0);
}
}
main().catch(error => {
console.error('健康檢查執行失敗:', error);
process.exit(1);
});
// scripts/auto-rollback.ts
import axios from 'axios';
interface DeploymentMetrics {
errorRate: number;
p95Latency: number;
successRate: number;
activeUsers: number;
}
async function fetchMetrics(environment: string): Promise<DeploymentMetrics> {
const monitoringAPI = process.env.MONITORING_API!;
const [errorRate, latency, successRate, activeUsers] = await Promise.all([
axios.get(`${monitoringAPI}/error-rate?env=${environment}`),
axios.get(`${monitoringAPI}/latency?env=${environment}&percentile=95`),
axios.get(`${monitoringAPI}/success-rate?env=${environment}`),
axios.get(`${monitoringAPI}/active-users?env=${environment}`)
]);
return {
errorRate: errorRate.data.value,
p95Latency: latency.data.value,
successRate: successRate.data.value,
activeUsers: activeUsers.data.value
};
}
async function shouldRollback(metrics: DeploymentMetrics): Promise<{
shouldRollback: boolean;
reasons: string[];
}> {
const reasons: string[] = [];
// 閾值設定
const thresholds = {
maxErrorRate: 0.05, // 5% 錯誤率
maxP95Latency: 2000, // 2 秒
minSuccessRate: 0.95, // 95% 成功率
};
if (metrics.errorRate > thresholds.maxErrorRate) {
reasons.push(`錯誤率過高: ${(metrics.errorRate * 100).toFixed(2)}% > ${(thresholds.maxErrorRate * 100).toFixed(2)}%`);
}
if (metrics.p95Latency > thresholds.maxP95Latency) {
reasons.push(`P95 延遲過高: ${metrics.p95Latency}ms > ${thresholds.maxP95Latency}ms`);
}
if (metrics.successRate < thresholds.minSuccessRate) {
reasons.push(`成功率過低: ${(metrics.successRate * 100).toFixed(2)}% < ${(thresholds.minSuccessRate * 100).toFixed(2)}%`);
}
return {
shouldRollback: reasons.length > 0,
reasons
};
}
async function performRollback(previousVersion: string) {
console.log(`🔄 執行回滾到版本: ${previousVersion}`);
// 這裡實作實際的回滾邏輯
// 可能是透過 Vercel API、Kubernetes、或其他部署平台
// 範例:使用 Vercel API 回滾
const vercelToken = process.env.VERCEL_TOKEN!;
const projectId = process.env.VERCEL_PROJECT_ID!;
const response = await axios.post(
`https://api.vercel.com/v9/projects/${projectId}/deployments`,
{
target: 'production',
gitSource: {
ref: previousVersion,
type: 'branch'
}
},
{
headers: {
'Authorization': `Bearer ${vercelToken}`
}
}
);
console.log('✅ 回滾完成:', response.data);
}
async function main() {
const environment = process.env.ENVIRONMENT || 'production';
const monitoringDuration = parseInt(process.env.MONITORING_DURATION || '600000'); // 10 分鐘
const checkInterval = 30000; // 每 30 秒檢查一次
console.log(`🔍 開始監控部署: ${environment}`);
console.log(`監控時長: ${monitoringDuration / 1000} 秒`);
const startTime = Date.now();
while (Date.now() - startTime < monitoringDuration) {
try {
const metrics = await fetchMetrics(environment);
const { shouldRollback, reasons } = await shouldRollback(metrics);
console.log(`\n📊 當前指標 (${new Date().toISOString()}):`);
console.log(` 錯誤率: ${(metrics.errorRate * 100).toFixed(2)}%`);
console.log(` P95 延遲: ${metrics.p95Latency}ms`);
console.log(` 成功率: ${(metrics.successRate * 100).toFixed(2)}%`);
console.log(` 活躍使用者: ${metrics.activeUsers}`);
if (shouldRollback) {
console.error('\n❌ 檢測到異常指標:');
reasons.forEach(reason => console.error(` - ${reason}`));
const previousVersion = process.env.PREVIOUS_VERSION || 'HEAD~1';
await performRollback(previousVersion);
process.exit(1);
}
console.log('✅ 指標正常');
} catch (error) {
console.error('監控過程發生錯誤:', error);
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
console.log('\n🎉 監控期結束,部署穩定');
process.exit(0);
}
main().catch(error => {
console.error('自動回滾程式執行失敗:', error);
process.exit(1);
});
現代化的 CI/CD 應該支援特性開關,實現程式碼部署與功能發布的解耦:
// src/utils/feature-flags.ts
interface FeatureFlags {
enableNewCheckout: boolean;
enableABTest: boolean;
enableBetaFeatures: boolean;
}
class FeatureFlagManager {
private flags: FeatureFlags;
private environment: string;
constructor() {
this.environment = import.meta.env.VITE_ENVIRONMENT || 'production';
this.flags = this.loadFlags();
}
private loadFlags(): FeatureFlags {
// 從遠端服務載入特性開關設定
// 可以整合 LaunchDarkly、Optimizely 等服務
// 本地開發環境預設開啟所有特性
if (this.environment === 'development') {
return {
enableNewCheckout: true,
enableABTest: true,
enableBetaFeatures: true
};
}
// 生產環境從 API 載入
return {
enableNewCheckout: false,
enableABTest: true,
enableBetaFeatures: false
};
}
isEnabled(flag: keyof FeatureFlags): boolean {
return this.flags[flag] ?? false;
}
async refresh() {
this.flags = this.loadFlags();
}
}
export const featureFlags = new FeatureFlagManager();
// 使用範例
export function CheckoutButton() {
const useNewCheckout = featureFlags.isEnabled('enableNewCheckout');
if (useNewCheckout) {
return <NewCheckoutFlow />;
}
return <LegacyCheckoutFlow />;
}
CI/CD 的核心價值: CI/CD 不只是自動化工具,而是確保軟體品質、加速交付流程、降低人為錯誤的系統化工程實踐。完善的 CI/CD 流水線能讓部署頻率提升 200 倍,變更失敗率降低 3 倍。
流水線設計原則: 好的 CI/CD 流水線應該包含程式碼品質檢查、自動化測試、構建最佳化、部署驗證、健康檢查等完整環節。每個環節都是品質閘門,確保只有符合標準的程式碼才能進入生產環境。
實戰技術要點:
技術選型思考: GitHub Actions、GitLab CI、Jenkins、CircleCI 各有優劣,如何根據團隊規模、技術棧、成本預算選擇合適的 CI/CD 平台?
Monorepo 挑戰: 在 Monorepo 架構下,如何設計 CI/CD 流水線?如何實現增量構建和智能化測試選擇?可以研究 Nx、Turborepo 等工具的最佳實踐。
安全性加固: CI/CD 流水線本身也可能成為安全漏洞的入口點。如何防範供應鏈攻擊?如何確保部署流程的安全性?可以考慮引入 Sigstore、SLSA 等供應鏈安全框架。
實作挑戰: 嘗試為你的專案建立一個完整的 CI/CD 流水線: