CI/CD 是一套让软件的构建、测试和发布自动化的工程实践。
-
CI(Continuous Integration,持续集成)
- 开发者频繁把代码合并到共享仓库。
- 每次提交后,系统自动执行编译、代码检查和测试。
- 目的是尽早发现冲突和缺陷,避免问题积累到发布前。
-
CD 通常有两种含义:
- Continuous Delivery(持续交付):自动完成测试、打包和部署准备,但发布到生产环境通常需要人工确认。
- Continuous Deployment(持续部署):通过所有检查后,代码自动发布到生产环境,不需要人工批准。
典型流程如下:
text
提交代码
↓
代码检查
↓
自动测试
↓
构建应用或 Docker 镜像
↓
部署到测试环境
↓
人工批准(持续交付)或自动发布(持续部署)
↓
生产环境
例如,开发者把代码推送到 GitHub 后,GitHub Actions 可以自动:
- 安装项目依赖。
- 运行单元测试。
- 构建 Docker 镜像。
- 将镜像推送到镜像仓库。
- 把新版本部署到服务器。
常见的 CI/CD 工具有:
- GitHub Actions
- GitLab CI/CD
- Jenkins
- CircleCI
- Azure DevOps
- Argo CD
简单来说,CI 负责持续检查代码是否可靠,CD 负责把可靠的代码安全、快速地交付或部署出去。Docker 经常用于 CI/CD,因为同一个镜像可以贯穿测试和生产环境,减少环境差异。
我之前的理解以为CI检查代码的可靠性是检查业务代码是否有bug
你的理解不算错误,但不够完整。
CI 确实可以帮助检查业务代码是否有 Bug,但关键在于:
CI 本身不会理解业务逻辑,也不会自动判断业务代码是否正确;它只是自动运行开发团队预先配置好的检查。
例如,每次提交代码后,CI 可以自动运行:
- 单元测试:检查函数或模块的业务行为。
- 集成测试:检查数据库、服务和模块之间能否正确协作。
- 端到端测试:模拟用户操作,检查完整业务流程。
- 静态分析:发现类型错误、空指针、未使用变量等问题。
- 代码风格检查:检查代码是否符合项目规范。
- 安全扫描:检查依赖漏洞、密钥泄露或常见安全问题。
- 构建检查:确认代码能否成功编译和打包。
假设电商系统中有这样的折扣规则:
text
订单满 100 元,打 9 折
只有开发者先写好对应测试:
text
输入:订单金额 200 元
期望:实付金额 180 元
CI 才能在代码被修改后自动验证这个规则。如果开发者误把折扣改成了 8 折,测试就会失败,CI 会阻止代码继续合并或部署。
但如果测试本身缺失,或者错误地认为实付金额应该是 160 元,那么 CI 仍然可能显示"全部通过"。因此:
text
CI 通过 ≠ 代码绝对没有 Bug
CI 通过 = 代码通过了当前配置的所有检查
更准确地说,CI 的作用是:
在每次代码变更后,自动、重复、尽早地验证代码是否满足团队已经定义好的质量标准,从而降低引入 Bug 的概率。
所以你原先"CI 检查业务代码是否有 Bug"的理解抓住了一部分核心,只需要补充一句:它通过自动执行测试和其他检查来间接发现 Bug,而不是 CI 自己理解业务并检查 Bug。
除此之外还检查什么?
除了通过测试检查业务逻辑,CI 通常还会检查以下内容。具体检查哪些,取决于团队在 CI 流程中配置了什么。
-
代码能否构建
检查代码能否成功编译、打包或生成可运行产物,例如:
- Java 项目能否生成 JAR 包
- 前端项目能否完成
npm run build - Docker 镜像能否成功构建
-
代码格式和规范
使用 ESLint、Prettier、Checkstyle 等工具检查:
- 缩进和格式是否一致
- 是否存在未使用的变量
- 是否违反团队的编码规则
- 是否出现可疑或不推荐的写法
这类检查主要提升可维护性,不一定代表代码有 Bug。
-
类型和静态分析
在不运行程序的情况下分析代码,例如:
- 参数类型是否正确
- 是否可能访问空值
- 某些执行路径是否缺少返回值
- 是否存在无法到达的代码
- 资源是否可能未关闭
常见工具有 TypeScript、MyPy、SonarQube 等。
-
模块能否正确协作
集成测试会检查:
- 应用能否连接数据库
- API 与业务服务能否正常交互
- 消息队列是否正确收发消息
- 第三方服务的调用方式是否符合约定
-
完整用户流程
端到端测试可能模拟真实操作,例如:
text用户注册 → 登录 → 添加商品 → 提交订单 → 支付它检查的不只是单个函数,而是整条业务链路能否工作。
-
安全问题
CI 可以运行安全扫描工具,检查:
- 依赖库是否存在已知漏洞
- 代码中是否误提交密码、令牌或私钥
- Docker 基础镜像是否包含漏洞
- 代码是否存在 SQL 注入等常见危险模式
- 开源依赖的许可证是否允许项目使用
-
测试质量指标
CI 可以统计:
- 单元测试是否全部通过
- 代码覆盖率是否达到要求
- 是否出现不稳定的测试
- 新增代码是否附带测试
需要注意,覆盖率高不代表没有 Bug;它只表示代码在测试中被执行过。
-
兼容性
同一套代码可能需要在多种环境下测试:
- 不同操作系统
- 不同浏览器
- 不同 Python、Java 或 Node.js 版本
- 不同数据库版本
-
数据库变更
对于包含数据库迁移的项目,可以检查:
- 迁移脚本能否成功执行
- 新版本是否能读取旧数据
- 数据库结构是否与代码匹配
- 回滚方案是否有效
-
部署产物和基础设施配置
CI 还可能验证:
- Docker 镜像能否正常启动
- Kubernetes 配置是否有效
- Terraform 等基础设施配置是否合法
- 构建产物是否完整
- 软件包版本是否正确
可以把 CI 理解成一个自动化"检查站":
text
代码变更
↓
格式与规范检查
↓
类型与静态分析
↓
编译和构建
↓
单元测试
↓
集成及端到端测试
↓
安全与依赖扫描
↓
生成可发布产物
其中最重要的一点仍然是:
CI 没有固定的检查能力。团队把哪些工具和规则接入 CI,它就自动执行哪些检查。
所以,"CI 通过"更准确的含义不是"代码没有问题",而是"这次变更通过了项目目前定义的检查标准"。
具体工程中是如何实现的?
在具体工程里,CI 通常不是一个"自动理解代码"的系统,而是:
把原本需要开发者手动执行的命令,写进一份 CI 配置文件,让 CI 平台在每次提交代码时自动执行。
以 GitHub Actions 为例,完整实践通常分为以下几步。
1. 先让项目本身具备检查命令
CI 只是执行器,所以首先要在项目中准备好可以运行的检查。
例如一个 Node.js 项目可能在 package.json 中定义:
json
{
"scripts": {
"format:check": "prettier --check .",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"build": "vite build"
}
}
开发者本地可以执行:
bash
npm run format:check
npm run lint
npm run typecheck
npm test
npm run build
这些命令在本地能运行后,再交给 CI 自动执行。
其他技术栈也类似:
bash
# Python
ruff check .
mypy .
pytest
# Java
mvn test
mvn package
# Go
go vet ./...
go test ./...
go build ./...
2. 在代码仓库中编写 CI 配置
GitHub Actions 的配置通常放在:
text
.github/workflows/ci.yml
例如:
yaml
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: 下载代码
uses: actions/checkout@v4
- name: 安装 Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: 安装依赖
run: npm ci
- name: 检查代码格式
run: npm run format:check
- name: 运行代码规范检查
run: npm run lint
- name: 检查类型
run: npm run typecheck
- name: 运行测试
run: npm test
- name: 构建项目
run: npm run build
这份配置表达的是:
text
有人创建或更新 Pull Request
↓
GitHub 准备一台临时 Linux 机器
↓
拉取代码
↓
安装 Node.js 和项目依赖
↓
依次运行格式、规范、类型、测试和构建检查
↓
返回成功或失败结果
某一个命令返回非零退出码,整个 CI 就会失败。
3. 在 Pull Request 阶段运行 CI
实际团队一般不会让开发者直接修改主分支,而是使用下面的流程:
text
创建功能分支
↓
编写代码和测试
↓
推送到远程仓库
↓
创建 Pull Request
↓
CI 自动运行
↓
代码评审
↓
CI 通过后合并
例如:
bash
git switch -c feature/order-discount
git add .
git commit -m "Add order discount"
git push -u origin feature/order-discount
推送并创建 Pull Request 后,GitHub Actions 会自动运行。
如果测试失败,Pull Request 页面可能显示:
text
❌ lint
✅ typecheck
❌ test
✅ build
开发者需要查看日志、修复问题并再次推送。新提交会自动触发新一轮 CI。
4. 配置主分支保护
仅仅运行 CI 还不够,因为开发者仍可能忽略失败结果、强行合并。
因此,工程中通常会给 main 分支设置保护规则,例如:
- 禁止直接向
main推送。 - 必须通过 Pull Request 合并。
- CI 必须全部通过。
- 至少需要一名成员完成代码评审。
- 分支落后时必须先合并或变基到最新
main。 - 禁止强制推送。
这样,CI 就从"提供参考"变成了真正的质量门禁:
text
CI 失败 → 不能合并
CI 通过 + 评审通过 → 可以合并
5. 给业务代码编写自动化测试
例如,订单满 100 元打九折:
ts
export function calculatePrice(amount: number): number {
return amount >= 100 ? amount * 0.9 : amount;
}
对应单元测试:
ts
import { describe, expect, it } from "vitest";
import { calculatePrice } from "./calculatePrice";
describe("calculatePrice", () => {
it("applies a 10% discount when amount reaches 100", () => {
expect(calculatePrice(200)).toBe(180);
});
it("does not apply a discount below 100", () => {
expect(calculatePrice(99)).toBe(99);
});
it("applies the discount at the boundary value", () => {
expect(calculatePrice(100)).toBe(90);
});
});
当其他开发者不小心修改了折扣逻辑,CI 执行 npm test 时就可能发现回归。
这里有一个重要的职责划分:
- 开发者和测试人员负责定义正确行为并编写测试。
- 测试框架负责执行测试并判断实际结果是否符合预期。
- CI 平台负责在每次代码变化时自动运行测试框架。
- 分支保护规则负责阻止未通过检查的代码进入主分支。
6. 集成测试中启动外部依赖
如果应用依赖 PostgreSQL、Redis 等服务,CI 可以临时启动这些服务。
例如:
yaml
jobs:
integration-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: app_test
ports:
- 5432:5432
env:
DATABASE_URL: postgresql://test:test@localhost:5432/app_test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run db:migrate
- run: npm run test:integration
CI 会创建一个临时 PostgreSQL 容器,然后:
- 创建测试数据库。
- 执行数据库迁移。
- 启动或调用应用。
- 测试应用与数据库的交互。
- 任务结束后销毁临时环境。
这也是 Docker 在 CI 中很常见的原因:它可以快速创建一致、隔离的测试依赖。
7. 将不同检查拆成并行任务
小项目可以把所有命令放在一个任务里。项目变大后,一般会拆分:
yaml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build
lint、test 和 build 可以同时运行,缩短等待时间,也更容易看出是哪类检查失败。
8. 构建并保存发布产物
CI 通过后,通常还会生成后续部署需要的产物,例如:
- Java JAR 包
- 前端静态文件
- npm、Python 或 NuGet 软件包
- Docker 镜像
- 测试报告和覆盖率报告
例如构建 Docker 镜像:
yaml
- name: 构建 Docker 镜像
run: docker build -t my-app:${{ github.sha }} .
这里使用 Git 提交哈希作为镜像版本:
text
my-app:7fd31ab...
这样能够明确知道某个镜像对应哪次代码提交,方便排查和回滚。
如果需要把镜像推送到 Docker Hub 或云厂商的镜像仓库,则要使用平台提供的密钥管理功能保存用户名、令牌等敏感信息,不能直接写在配置文件里。
9. 从 CI 延伸到 CD
常见实践是:
- Pull Request 阶段只运行 CI 检查。
- 合并到
main后构建正式产物。 - 自动部署到测试环境。
- 运行冒烟测试。
- 人工批准后部署到生产环境。
流程可能是:
text
Pull Request
↓
格式检查 + 静态分析 + 自动测试 + 构建
↓
合并到 main
↓
构建 Docker 镜像
↓
推送镜像仓库
↓
部署测试环境
↓
冒烟测试
↓
人工审批
↓
部署生产环境
生产部署还经常配合:
- 滚动发布
- 蓝绿发布
- 金丝雀发布
- 健康检查
- 失败自动回滚
- 日志、指标和告警监控
10. 实际项目通常分层执行
为了兼顾速度和覆盖范围,团队不会在每次提交时都执行所有昂贵测试。常见分层是:
| 触发时机 | 常见检查 |
|---|---|
| 本地提交前 | 格式化、快速静态检查 |
| Pull Request | Lint、类型检查、单元测试、构建 |
| 合并到主分支 | 集成测试、构建镜像、部署测试环境 |
| 发布前 | 端到端测试、安全扫描、冒烟测试 |
| 定时任务 | 完整回归、依赖漏洞扫描、性能测试 |
这样可以让开发者尽快得到反馈,同时保留较完整的质量检查。
归根结底,CI 工程实践可以概括为四部分:
text
项目提供可自动执行的检查命令
+
仓库提供 CI 流程配置
+
代码平台根据事件触发流程
+
分支规则强制要求检查通过
因此,真正建立 CI,不只是"安装一个 Jenkins 或启用 GitHub Actions",而是把团队对代码质量的要求转化成一系列可以自动运行、结果明确、失败后能够阻止合并的检查。