CI/CD 自动化流水线
CI/CD 落地手册:以 GitHub Actions 为主线,讲解流水线设计原则、工作流编写、前后端示例、多环境发布、制品管理、回滚策略与密钥管理,让「提交代码 → 上线」可重复、可回滚。
一、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 或手动触发部署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 常用触发器
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 依赖缓存(提速关键)
- 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 矩阵构建(多版本测试)
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
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: 74.2 后端项目 CI + 镜像构建
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 }}:latest4.3 部署 job(SSH 方式)
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 14.4 发布目录结构(软链切换方案)
/var/www/app/├── releases/│ ├── a1b2c3d/ # 每次部署生成一个目录│ ├── e4f5g6h/│ └── i7j8k9l/ # 最新└── current -> releases/i7j8k9l # 软链指向当前版本优点:回滚只需切换软链,秒级完成;旧版本保留,便于对比排查。
五、多环境发布策略
5.1 用分支/标签映射环境
| 触发 | 环境 | 自动化程度 |
|---|---|---|
push 到 develop |
开发/预览环境 | 全自动 |
push 到 main |
测试(staging) | 全自动 |
打 v* tag |
生产 | 需人工审批 |
deploy-staging: if: github.ref == 'refs/heads/main'deploy-production: if: startsWith(github.ref, 'refs/tags/v') environment: name: production url: https://example.com5.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 写进制品(如构建时注入版本文件),线上可精确追溯是哪个提交。
// 构建时注入版本信息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 |
| 数据库回滚 | 最慢 | 需迁移脚本可逆 |
关键原则
- 数据库变更要向前兼容:先加字段(旧代码不报错)→ 部署新代码 → 再删旧字段。避免「代码回滚了但表结构回不去」。
- 回滚脚本要预先写好并测试,不要等出事现写。
- 回滚后要能知道原因:保留上一版本的日志与制品。
# 软链回滚示例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 密钥专用 | 部署用独立密钥,不复用个人密钥,可随时吊销 |
| 定期轮换 | 人员变动或怀疑泄露时立即换 |
permissions: contents: read packages: write# 只在需要的 job 中提升权限jobs: deploy: permissions: contents: read id-token: write # 使用 OIDC 时进阶:用 OIDC 短期凭证替代长期密钥(云厂商支持),彻底消除静态密钥泄露风险。
九、质量门禁
CI 的核心价值不是「跑通」,而是「拦住不该上线的」:
| 门禁 | 检查什么 | 卡在哪一步 |
|---|---|---|
| Lint | 代码规范 | 最早(秒级) |
| Type check | 类型错误 | 早(十秒级) |
| 单元测试 | 逻辑正确性 | 早 |
| 覆盖率门槛 | 不低于基线 | 早 |
| 依赖审计 | 已知漏洞 | 中 |
| 集成测试 | 跨模块协作 | 中 |
| 构建 | 能否产出产物 | 中 |
| 健康检查 | 部署后是否可用 | 最后的兜底 |
- 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 的价值可以浓缩成一句话:把「部署」从一件需要小心谨慎的事,变成一件随时可做、随时可退的事。
四个落地优先级:
- 先有 CI(lint + test + build),这是收益最快的一步——它能拦住 80% 的低级问题。
- 再做自动化部署 + 健康检查,让上线不再依赖某个人记得所有步骤。
- 加上 releases 软链与回滚脚本,把「回滚」从半小时缩短到几秒。
- 最后优化体验(缓存、矩阵、并行),把流水线时长从 10 分钟压到 3 分钟。
最后提醒:流水线本身也是代码,需要维护。定期检查是否有失效的 Action 版本、是否还在跑无用的步骤——否则半年后它会变成一个没人敢改的黑盒。
完