第 23 章 · CI/CD
第 23 章 · CI/CD
23.1 CI/CD 概念
| 概念 | 全称 | 说明 |
|---|---|---|
| CI | Continuous Integration | 持续集成:每次提交自动运行测试 |
| CD | Continuous Delivery | 持续交付:自动构建,手动部署 |
| CD | Continuous Deployment | 持续部署:从提交到部署全自动 |
开发者 → git push → CI 流水线 → 构建镜像 → 部署到服务器
↓
代码检查
↓
单元测试
↓
集成测试
↓
构建产物
↓
部署到环境
23.2 GitHub Actions
基本工作流
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- name: 检出代码
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 lint
- name: 类型检查(如有 TypeScript)
run: npm run typecheck
continue-on-error: true
- name: 运行测试
run: npm test -- --coverage
- name: 上传覆盖率报告
if: matrix.node-version == 22
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
缓存优化
steps:
- uses: actions/checkout@v4
# 方式 1:setup-node 内置缓存
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
# 方式 2:手动缓存
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
23.3 完整 CI/CD 流水线
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# === 阶段 1:测试 ===
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm test
# === 阶段 2:构建并推送 Docker 镜像 ===
build:
needs: test # 测试通过后才执行
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: 登录 GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 提取镜像元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=raw,value=latest
- name: 构建并推送镜像
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
# === 阶段 3:部署到服务器 ===
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- name: 部署到服务器
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/my-app
docker compose pull
docker compose up -d
docker system prune -f
23.4 PR 检查工作流
# .github/workflows/pr-check.yml
name: PR Check
on:
pull_request:
types: [opened, synchronize]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: 'npm' }
- run: npm ci
- run: npm run lint -- --format=json --output-file=eslint-report.json
continue-on-error: true
- name: 代码检查结果
if: always()
uses: github/codeql-action/upload-sarif@v3
- name: 运行测试
run: npm test -- --ci --reporters=default --reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_DIR: ./reports
- name: 测试报告
if: always()
uses: dorny/test-reporter@v1
with:
name: Test Results
path: reports/junit.xml
reporter: jest-junit
23.5 部署策略
部署策略对比
| 策略 | 说明 | 停机时间 | 回滚速度 |
|---|---|---|---|
| 直接替换 | 停旧启新 | 有 | 慢 |
| 滚动更新 | 逐步替换实例 | 无 | 中 |
| 蓝绿部署 | 两套环境切换 | 无 | 快 |
| 金丝雀发布 | 先部署少量实例 | 无 | 快 |
蓝绿部署脚本
- name: 蓝绿部署
run: |
# 获取当前运行的颜色
CURRENT=$(ssh $SERVER "cat /opt/app/color" || echo "blue")
if [ "$CURRENT" = "blue" ]; then
NEW="green"
PORT_NEW=3001
else
NEW="blue"
PORT_NEW=3000
fi
# 部署新版本
ssh $SERVER "
docker pull $IMAGE:${{ github.sha }}
docker run -d --name app-$NEW -p $PORT_NEW:3000 $IMAGE:${{ github.sha }}
sleep 10
# 健康检查
curl -f http://localhost:$PORT_NEW/health
# 切换 Nginx
sed -i 's/localhost:.*/localhost:$PORT_NEW/' /etc/nginx/conf.d/app.conf
nginx -s reload
# 停止旧版本
docker stop app-$CURRENT && docker rm app-$CURRENT
echo $NEW > /opt/app/color
"
23.6 Secrets 管理
# 在 GitHub 仓库设置中添加 Secrets
# Settings → Secrets and variables → Actions
# 使用 Secrets
jobs:
deploy:
steps:
- name: 部署
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
echo "部署中..."
Secrets 使用原则
| 原则 | 说明 |
|---|---|
| 最小权限 | 只授权必要的 Secret |
| 分环境管理 | 生产和测试使用不同的 Secret |
| 定期轮换 | 定期更换密钥和密码 |
| 不要硬编码 | 永远不要在代码中写入密钥 |
23.7 常用 GitHub Actions
| Action | 用途 |
|---|---|
actions/checkout | 检出代码 |
actions/setup-node | 配置 Node.js 环境 |
actions/cache | 缓存依赖 |
docker/build-push-action | 构建并推送 Docker 镜像 |
codecov/codecov-action | 上传测试覆盖率 |
github/codeql-action | 代码安全扫描 |
slackapi/slack-github-action | Slack 通知 |
appleboy/ssh-action | SSH 执行远程命令 |
注意事项
⚠️ CI 必须快速:CI 超过 10 分钟会严重影响开发效率,优化测试并行化和缓存。
⚠️ 测试必须稳定:Flaky Test(偶发失败)会降低团队对 CI 的信任度。
⚠️ 保护主分支:设置 Branch Protection Rules,要求 CI 通过才能合并 PR。
⚠️ Secrets 不要泄露到日志:使用
::add-mask::或环境变量方式使用敏感信息。
业务场景
- 开源项目:PR 自动运行测试,合并后自动发布
- 企业项目:代码审查 → CI 测试 → staging 部署 → 手动确认 → 生产部署
- 多环境管理:dev / staging / production 使用不同的配置和 Secrets
- 自动发布 NPM 包:打 Tag 后自动构建并发布到 npm
扩展阅读
上一章:第 22 章 · Docker 部署 下一章:第 24 章 · 最佳实践 — 代码规范、项目结构和性能建议。