文章

CI/CD流程深度解析:从手动部署到全自动流水线

CI/CD 是从代码提交到生产部署的自动化管道:CI 保证每次变更自动构建测试,CD 保证通过验证的产物可靠发布。 本质是化发布日地狱为无数次安全小发布,面试常考流水线阶段设计、缓存策略与部署回滚机制。

CI/CD流程深度解析:从手动部署到全自动流水线

一句话概括

CI/CD 是一套从”代码提交”到”生产部署”的自动化管道——持续集成(CI)确保每次变更都能自动构建测试,持续交付/部署(CD)确保通过验证的产物能可靠地发布到目标环境。本质上是把”发布日地狱”拆解为无数次安全的小发布。

核心知识点

1. CI/CD 三阶段模型

阶段做什么人工介入典型时长
CI(持续集成)Lint → Test → Build无3-15 分钟
持续交付制品管理 → 自动部署到测试环境审批是否发布10-30 分钟
持续部署全自动发布到生产无取决于策略

核心洞察:软件交付的风险往往不来自代码本身,而来自”最后一次集成”的不可预测性。把大爆炸式发布拆散为高频小发布,是 CI/CD 的灵魂。

2. GitHub Actions 核心模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '20'

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '${{ env.NODE_VERSION }}', cache: 'npm' }
      - run: npm ci
      - run: npx eslint src/ --max-warnings 0

  test:
    needs: lint                          # 依赖 lint 完成后才执行
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '${{ env.NODE_VERSION }}', cache: 'npm' }
      - run: npm ci
      - run: npx vitest run --coverage
      - name: Check coverage threshold
        run: |
          npx istanbul check-coverage --statements 80 --branches 75 \
            --functions 80 --lines 80 coverage/coverage-final.json

  build:
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '${{ env.NODE_VERSION }}', cache: 'npm' }
      - run: npm ci
      - run: npm run build
        env:
          NODE_ENV: production

needs 依赖形成流水线:lint → test → build,前一步失败则后续不执行,避免浪费资源。

3. 缓存策略:让 CI 快 10 倍

1
2
3
4
5
6
7
8
9
10
11
12
13
- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      node_modules
    key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
    restore-keys: npm-${{ runner.os }}-

- uses: actions/cache@v4
  with:
    path: node_modules/.cache
    key: build-cache-${{ runner.os }}-${{ github.sha }}
    restore-keys: build-cache-${{ runner.os }}-

命中逻辑:hashFiles('package-lock.json') 不变 → key 不变 → 直接命中缓存(~5 秒)。未命中时用 restore-keys 前缀匹配最近的可用缓存。一个 200+ 依赖的项目,缓存命中后 CI 从 8 分钟降到 1.5 分钟。

4. 前端部署缓存策略

前端资源的缓存策略是面试高频题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 带哈希的文件(内容变更 = 文件名变更)—— 永久缓存
- run: |
    aws s3 sync dist/assets/ s3://bucket/assets/ \
      --cache-control "public, max-age=31536000, immutable"

# index.html —— 完全不缓存
- run: |
    aws s3 cp dist/index.html s3://bucket/index.html \
      --cache-control "no-cache, no-store, must-revalidate"

# Service Worker —— 不缓存
- run: |
    aws s3 cp dist/sw.js s3://bucket/sw.js \
      --cache-control "no-cache"

原理:main.a1b2c3d.js 中的 a1b2c3d 是内容哈希,内容变了哈希就变 → 新文件名 → 浏览器自动下载新文件。immutable 告诉浏览器”此 URL 的内容永不改变,不要发 304 验证请求”。index.html 不缓存是因为它引用了带哈希的资源,必须确保用户拿到最新版本。

5. Monorepo 智能流水线

不希望在每次提交时构建和测试所有包——只检测变更文件所属的包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.changed.outputs.packages }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - id: changed
        run: |
          CHANGED=$(git diff --name-only origin/main...HEAD | \
            grep -oE 'packages/[^/]+' | sort -u | \
            jq -R -s -c 'split("\n")[:-1]')
          echo "packages=$CHANGED" >> $GITHUB_OUTPUT

  build:
    needs: [detect-changes]
    if: needs.detect-changes.outputs.packages != '[]'
    strategy:
      matrix:
        package: ${{ fromJSON(needs.detect-changes.outputs.packages) }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npx turbo run build --filter=${{ matrix.package }}

核心价值:30 个包的 Monorepo 只改了 2 个包 → CI 只构建这 2 个包,而非全量 30 个。

其实你每天都在用

  • 每次 git push 后 GitHub Actions 自动跑 CI:.github/workflows/ 目录下的 YAML 就是触发条件
  • PR 页面的绿色✅或红色❌:那是 CI 的 lint/test/build 结果,红色表示不能合并
  • npm install 在 CI 中用 npm ci 而非 npm install:npm ci 严格按 lock 文件安装、删除 node_modules 重来,保证 CI 环境确定性
  • 生产环境回滚只需要改 symlink:current → v1.0.0 切换为 current → v1.0.1,切回来就是 current → v1.0.0——原子操作,秒级回滚
  • GitHub Pages 的自动部署:push 到 main(或 gh-pages 分支),GitHub Actions 自动构建并发布到 <username>.github.io

常见误解(FAQ)

❌ 误区 1:「CI 和 CD 是一回事」

CI 关注开发阶段的反馈循环(代码能不能编译通过、测试能不能跑通),CD 关注发布阶段的自动化(能不能自动上线)。CI 失败意味着”当前代码不可用”,CD 失败意味着”无法上线”。两者解决的问题不同。

❌ 误区 2:「CD 的环境变量把 .env 也传上去就行」

.env 文件通常包含开发环境变量,不适合生产。正确做法是用 CI 平台的 Secrets + Environment 机制:$ { { secrets.PRODUCTION_API_KEY } },按环境隔离(environment: staging vs environment: production),密钥加密存储且不会出现在日志中。

❌ 误区 3:「部署就是 ftp 上传」

前端部署不是”把文件丢到服务器上”,而是一个包含 CDN 缓存策略、版本化目录、健康检查、回滚机制的完整流程。aws s3 sync 只是其中一步,前面有构建验证,后面有 cloudfront create-invalidation 刷新缓存。

❌ 误区 4:「CI 失败就一定是 CI 配置有问题」

很多时候 CI 失败的原因是你的代码确实是错的(测试挂了、类型不对、lint 不合格),而不是 CI 配错了。先排查代码质量,再排查 CI 配置。另外,确保本地环境和 CI 环境一致(npm ci 而非 npm install,锁定 Node 版本)。

一句话总结

CI/CD 的终极目标是让部署变得「无聊」——每次发布都走相同的流水线、产生相同的结果、不需要任何”紧急操作”。越无聊越可靠,越可靠越敢频繁发布。

本文由作者按照 CC BY 4.0 进行授权