强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 23 章 · CI/CD

第 23 章 · CI/CD

23.1 CI/CD 概念

概念全称说明
CIContinuous Integration持续集成:每次提交自动运行测试
CDContinuous Delivery持续交付:自动构建,手动部署
CDContinuous 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-actionSlack 通知
appleboy/ssh-actionSSH 执行远程命令

注意事项

⚠️ CI 必须快速:CI 超过 10 分钟会严重影响开发效率,优化测试并行化和缓存。

⚠️ 测试必须稳定:Flaky Test(偶发失败)会降低团队对 CI 的信任度。

⚠️ 保护主分支:设置 Branch Protection Rules,要求 CI 通过才能合并 PR。

⚠️ Secrets 不要泄露到日志:使用 ::add-mask:: 或环境变量方式使用敏感信息。

业务场景

  1. 开源项目:PR 自动运行测试,合并后自动发布
  2. 企业项目:代码审查 → CI 测试 → staging 部署 → 手动确认 → 生产部署
  3. 多环境管理:dev / staging / production 使用不同的配置和 Secrets
  4. 自动发布 NPM 包:打 Tag 后自动构建并发布到 npm

扩展阅读


上一章第 22 章 · Docker 部署 下一章第 24 章 · 最佳实践 — 代码规范、项目结构和性能建议。