全栈手账 订阅 RSS

CI/CD 自动化流水线

CI/CD 落地手册:以 GitHub Actions 为主线,讲解流水线设计原则、工作流编写、前后端示例、多环境发布、制品管理、回滚策略与密钥管理,让「提交代码 → 上线」可重复、可回滚。

读了 19 分钟 更新于 11 月 3 日

一、CI / CD 到底指什么

缩写 全称 含义
CI Continuous Integration 每次提交自动跑构建与测试,尽早发现集成问题
CD Continuous Delivery 自动产出可发布制品,手动点一下上线
CD Continuous Deployment 通过全部检查后自动上线

多数团队落地的是 Continuous Delivery——自动化到「制品就绪」,上线由人决定。

没有 CI/CD 时的典型状态

文本
本地能跑 → 提交 → 部署到服务器 → 线上报错 → 手动回滚 → 排查                ↑ 每次都是手工、每次都可能漏步骤

有了 CI/CD 之后

文本
提交 → 自动 lint/测试/构建 → 制品归档 → 自动/一键部署 → 健康检查 → 失败自动回滚

收益:部署频率提升、单次变更风险下降、回滚从「半小时」变成「一条命令」。


二、流水线设计原则

原则 说明
快速失败 lint 与单测放最前面,几秒内给出反馈,不让人等 10 分钟才知道拼写错了
阶段递进 前一步失败就不进下一步,不浪费资源
可重复 同一次提交,任何人跑出的结果一致(锁定依赖版本)
制品一次构建、多处部署 同一份产物部署到测试/生产,避免「测试环境好的、生产是另一份」
幂等部署 重复执行部署不产生副作用
可回滚 任何一次部署都能在分钟级退回上一版本

典型阶段划分

文本
lint → typecheck → unit test → build → [集成测试] → 制品归档 → 部署 staging → 部署 production ↑ 3 分钟内出结果                                    ↑ 需要审批

三、GitHub Actions 基础

3.1 文件位置与结构

文本
.github/└── workflows/    ├── ci.yml          # 提交时跑检查    └── deploy.yml      # 打 tag 或手动触发部署
yaml
name: CI                          # 工作流名称on:                               # 触发器  push:    branches: [main]  pull_request:    branches: [main]jobs:                             # 任务(默认并行)  test:    runs-on: ubuntu-latest        # 运行环境    steps:                        # 步骤(串行)      - uses: actions/checkout@v4      - run: echo "hello"

3.2 四个核心概念

概念 说明
on 何时触发(push / PR / tag / 定时 / 手动)
jobs 任务集合,默认并行;用 needs 指定依赖
steps 一个 job 内的步骤,串行执行
uses / run 前者引用现成 Action,后者执行 shell 命令

3.3 常用触发器

yaml
on:  push:    branches: [main]    paths:                          # 只在特定路径变更时触发      - 'src/**'      - 'package.json'  pull_request:    branches: [main]  workflow_dispatch:                # 手动触发(页面按钮)    inputs:      environment:        description: '部署环境'        required: true        type: choice        options: [staging, production]  schedule:    - cron: '0 2 * * *'             # 每天 02:00 跑一次(安全扫描等)

3.4 依赖缓存(提速关键)

yaml
- uses: actions/setup-node@v4  with:    node-version: 20    cache: 'npm'                    # 自动缓存 node_modules 依赖# 更细粒度的手动缓存- uses: actions/cache@v4  with:    path: ~/.npm    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}    restore-keys: ${{ runner.os }}-npm-

缓存命中后安装依赖可从 60s 降到 10s 以内。key 用 lock 文件的 hash,依赖变了自动失效。

3.5 矩阵构建(多版本测试)

yaml
jobs:  test:    strategy:      matrix:        node: [18, 20, 22]    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: ${{ matrix.node }}      - run: npm ci && npm test

四、完整流水线示例

4.1 前端项目 CI

yaml
name: Frontend CIon:  push:    branches: [main]  pull_request:jobs:  quality:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: 20          cache: 'npm'      - name: 安装依赖        run: npm ci      - name: 代码规范        run: npm run lint      - name: 类型检查        run: npm run typecheck      - name: 单元测试        run: npm run test -- --coverage  build:    needs: quality                    # 质量门禁通过才构建    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: 20          cache: 'npm'      - run: npm ci      - name: 构建        run: npm run build        env:          VITE_API_BASE: ${{ vars.VITE_API_BASE }}      - name: 上传制品        uses: actions/upload-artifact@v4        with:          name: dist          path: dist/          retention-days: 7

4.2 后端项目 CI + 镜像构建

yaml
name: Backend CIon:  push:    branches: [main]    tags: ['v*']jobs:  test:    runs-on: ubuntu-latest    services:                          # 测试需要数据库时      mysql:        image: mysql:8        env:          MYSQL_ROOT_PASSWORD: test          MYSQL_DATABASE: testdb        ports: ['3306:3306']        options: >-          --health-cmd="mysqladmin ping -h localhost -ptest"          --health-interval=10s          --health-timeout=5s          --health-retries=5    steps:      - uses: actions/checkout@v4      - uses: actions/setup-java@v4        with:          java-version: '17'          distribution: 'temurin'          cache: 'maven'      - name: 编译与测试        run: mvn -B clean verify        env:          DB_HOST: 127.0.0.1          DB_NAME: testdb          DB_PASSWORD: test  image:    needs: test    if: startsWith(github.ref, 'refs/tags/v')   # 只有打 tag 才构建镜像    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - name: 登录镜像仓库        uses: docker/login-action@v3        with:          registry: ghcr.io          username: ${{ github.actor }}          password: ${{ secrets.GITHUB_TOKEN }}      - name: 构建并推送        uses: docker/build-push-action@v6        with:          context: .          push: true          tags: |            ghcr.io/${{ github.repository }}:${{ github.ref_name }}            ghcr.io/${{ github.repository }}:latest

4.3 部署 job(SSH 方式)

yaml
  deploy:    needs: build    if: github.ref == 'refs/heads/main'    runs-on: ubuntu-latest    environment: production            # 可在 GitHub 上配置审批人    steps:      - name: 下载制品        uses: actions/download-artifact@v4        with:          name: dist          path: dist      - name: 上传到服务器        uses: appleboy/scp-action@v0.1.7        with:          host: ${{ secrets.DEPLOY_HOST }}          username: ${{ secrets.DEPLOY_USER }}          key: ${{ secrets.DEPLOY_KEY }}          source: 'dist/*'          target: /var/www/app/releases/${{ github.sha }}      - name: 切换软链并重载        uses: appleboy/ssh-action@v1        with:          host: ${{ secrets.DEPLOY_HOST }}          username: ${{ secrets.DEPLOY_USER }}          key: ${{ secrets.DEPLOY_KEY }}          script: |            set -e            ln -sfn /var/www/app/releases/${{ github.sha }} /var/www/app/current            systemctl reload nginx || true            # 健康检查            for i in $(seq 1 5); do              if curl -sf http://127.0.0.1/health > /dev/null; then                echo "健康检查通过"; exit 0              fi              sleep 2            done            echo "健康检查失败"; exit 1

4.4 发布目录结构(软链切换方案)

文本
/var/www/app/├── releases/│   ├── a1b2c3d/          # 每次部署生成一个目录│   ├── e4f5g6h/│   └── i7j8k9l/          # 最新└── current -> releases/i7j8k9l    # 软链指向当前版本

优点:回滚只需切换软链,秒级完成;旧版本保留,便于对比排查。


五、多环境发布策略

5.1 用分支/标签映射环境

触发 环境 自动化程度
push 到 develop 开发/预览环境 全自动
push 到 main 测试(staging) 全自动
打 v* tag 生产 需人工审批
yaml
deploy-staging:  if: github.ref == 'refs/heads/main'deploy-production:  if: startsWith(github.ref, 'refs/tags/v')  environment:    name: production    url: https://example.com

5.2 环境专属变量与密钥

GitHub 提供两级配置:

类型 用途 可见性
vars.* 非敏感配置(API 地址、环境名) 日志中可见
secrets.* 密钥(Token、私钥、密码) 日志中自动打码

环境级配置在 Settings → Environments 中设置,可绑定审批人与分支保护。

5.3 灰度与分批发布(能力进阶)

方式 说明
蓝绿部署 新版本起一套完整环境,验证后切流量,回滚即切回
金丝雀 先放 5%–10% 流量,观察指标再全量
按用户分批 内部用户 → 10% → 50% → 全量

小项目不必一步到位,但软链切换 + 健康检查这两件事应尽早做——它们让回滚从「重部署」变成「切链接」。


六、制品管理与版本

6.1 制品是什么

构建产物本身(dist/、jar、镜像),一次构建,多环境复用。

反例:在测试环境构建一次、生产环境再构建一次——两次构建结果可能有细微差异(依赖版本、时间戳),排查问题是灾难。

6.2 版本号规范

采用语义化版本 MAJOR.MINOR.PATCH:

变更类型 版本动作 示例
不兼容的接口变更 MAJOR +1 1.4.2 → 2.0.0
向下兼容的新功能 MINOR +1 1.4.2 → 1.5.0
向下兼容的修复 PATCH +1 1.4.2 → 1.4.3

同时把 commit SHA 写进制品(如构建时注入版本文件),线上可精确追溯是哪个提交。

javascript
// 构建时注入版本信息define: {  __APP_VERSION__: JSON.stringify(process.env.GITHUB_REF_NAME || 'dev'),  __COMMIT_SHA__: JSON.stringify(process.env.GITHUB_SHA?.slice(0, 7) || 'local'),}

七、回滚策略

方案 速度 前提
软链切换(保留 release 目录) 秒级 部署方式采用 releases + current
重新部署上一版本制品 分钟级 制品已归档
镜像回滚(重新拉旧 tag) 分钟级 镜像保留了历史 tag
数据库回滚 最慢 需迁移脚本可逆

关键原则

  1. 数据库变更要向前兼容:先加字段(旧代码不报错)→ 部署新代码 → 再删旧字段。避免「代码回滚了但表结构回不去」。
  2. 回滚脚本要预先写好并测试,不要等出事现写。
  3. 回滚后要能知道原因:保留上一版本的日志与制品。
bash
# 软链回滚示例PREV=$(ls -1t /var/www/app/releases | sed -n 2p)ln -sfn /var/www/app/releases/$PREV /var/www/app/currentsystemctl reload nginx

八、密钥与权限管理

规则 说明
密钥只存 Secrets 不写进代码、不写进 workflow 文件
日志自动打码 GitHub 会对 Secrets 值自动替换为 ***;但自己 echo 拼接时仍可能泄露
最小权限 GITHUB_TOKEN 按需声明 permissions
SSH 密钥专用 部署用独立密钥,不复用个人密钥,可随时吊销
定期轮换 人员变动或怀疑泄露时立即换
yaml
permissions:  contents: read  packages: write
yaml
# 只在需要的 job 中提升权限jobs:  deploy:    permissions:      contents: read      id-token: write        # 使用 OIDC 时

进阶:用 OIDC 短期凭证替代长期密钥(云厂商支持),彻底消除静态密钥泄露风险。


九、质量门禁

CI 的核心价值不是「跑通」,而是「拦住不该上线的」:

门禁 检查什么 卡在哪一步
Lint 代码规范 最早(秒级)
Type check 类型错误 早(十秒级)
单元测试 逻辑正确性 早
覆盖率门槛 不低于基线 早
依赖审计 已知漏洞 中
集成测试 跨模块协作 中
构建 能否产出产物 中
健康检查 部署后是否可用 最后的兜底
yaml
- name: 覆盖率检查  run: npx coverage-check --min 70

覆盖率门槛设在当前值,只允升不允许降——比一步到位设 80% 更容易推行。


十、常见问题

现象 原因 处理
本地能跑 CI 挂 依赖版本不一致 CI 用 npm ci,提交 lock 文件
构建产物不同 本地/CI Node 版本不同 用 .nvmrc 或固定 node-version
部署后白屏 静态资源路径或 base 配置问题 检查 base 与部署路径
密钥出现在日志里 手动 echo 拼接了 secret 改用 ::add-mask:: 或避免打印
CI 很慢 未缓存依赖 配置 cache: 'npm'
部署成功但功能异常 只检查了进程启动,没做业务验证 健康检查要校验真实接口,不只是端口存活
频繁超时 镜像源慢 换加速源,或把依赖预构建进自定义镜像
数据库迁移失败导致无法回滚 迁移不可逆 采用向前兼容的迁移策略

十一、检查清单

流水线设计

  • lint / typecheck / test 在构建之前
  • job 之间用 needs 明确依赖
  • 失败即中断,不继续后续阶段
  • 依赖缓存已配置(key 用 lock 文件 hash)
  • 制品一次构建、多环境复用

部署

  • 发布目录采用 releases + current 软链
  • 部署后执行健康检查(校验真实接口)
  • 健康检查失败自动回滚
  • 回滚脚本已提前写好并演练过
  • 数据库变更向前兼容

安全

  • 密钥全部走 Secrets,无硬编码
  • GITHUB_TOKEN 按最小权限声明
  • 部署用专用 SSH 密钥(可吊销)
  • 生产部署配置了人工审批(environment)
  • 日志中无密钥泄露

可追溯

  • 制品内注入 commit SHA 与版本号
  • 每次部署有记录(谁、何时、哪个版本)
  • 旧版本制品保留一段时间(便于回滚)

十二、总结

CI/CD 的价值可以浓缩成一句话:把「部署」从一件需要小心谨慎的事,变成一件随时可做、随时可退的事。

四个落地优先级:

  1. 先有 CI(lint + test + build),这是收益最快的一步——它能拦住 80% 的低级问题。
  2. 再做自动化部署 + 健康检查,让上线不再依赖某个人记得所有步骤。
  3. 加上 releases 软链与回滚脚本,把「回滚」从半小时缩短到几秒。
  4. 最后优化体验(缓存、矩阵、并行),把流水线时长从 10 分钟压到 3 分钟。

最后提醒:流水线本身也是代码,需要维护。定期检查是否有失效的 Action 版本、是否还在跑无用的步骤——否则半年后它会变成一个没人敢改的黑盒。


完

前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。