元数据卡
- 前置知识:第4章(测试);会用 Git 分支
- 预计时间:45 分钟
- 核心难度:入门
- 完成标志:能搭建一条包含测试、构建、部署的 CI/CD 流水线
你的进度
你重构完零件,本地测试全过,优雅地装进了成品箱。
第二天工匠之都的主仓库发现——你的零件和另一个学徒的零件尺寸冲突,两件东西装在同一个机器里卡死了。谁都不知道,直到交付那天才暴露。
你想起了从数据堡垒来的师兄说过:“我们在那边每件作品交付前都自动跑全量检验,没通过的不让入库。”你在工匠之都的流水线前站定:需要一条自动化的交付流水线。 你的任务
你写代码的环境(IDE、本地数据库、回旋余地)和线上环境(延迟、负载、网络抖动)之间存在一条鸿沟。你需要一座桥——在代码合并到主分支之前,自动完成测试、构建、部署验证。这座桥就是 CI/CD。这一章搭桥。
本章分层
- 必读:GitHub Actions 流水线、Docker 构建、分支策略、三环境
- 选读:自动化测试套件集成
- 进阶:自托管 runner、蓝绿部署
本章不会要求你掌握
- Kubernetes 编排
- 监控告警系统
破局 · 溯源
你在分支上改了一个文件,提交,然后去接水。回来看到队友在群里发:"谁把测试搞挂了?"你一看,是你的代码有一个隐藏的类型错误——编辑器没报、本地测试通过了因为环境不同、但 CI 服务器上的 JDK 版本差异让代码抛了 ClassCastException。
"要是我 push 之前能自动跑一遍测试就好了。"
是的。这就是 CI(持续集成)的起点。
第一层:CI——每次 merge 前自动验证
持续集成的核心规则:每次 push 到共享分支,自动运行完整的构建、测试、静态检查。 任一项失败,阻止合并。
用 GitHub Actions 搭建你的第一条流水线。在你的仓库中创建 .github/workflows/ci.yml:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Cache Gradle
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
- name: Build and test
run: ./gradlew build
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/testdb
- name: Static analysis
run: ./gradlew check
- name: Test report
if: always()
uses: dorny/test-reporter@v1
with:
name: JUnit Tests
path: '**/build/test-results/test/*.xml'
reporter: java-junit这个工作流做了四件事:
- 启动一个 PostgreSQL 容器作为测试数据库
- 编译和运行所有测试
- 运行静态检查(Checkstyle、PMD、Error Prone)
- 发布测试报告到 PR 页面
你把 PR 发出去后,GitHub 自动跑这个流程。失败了?仓库配置了 "Require status checks to pass before merging"——你不能合入一个有红叉的分支。
第二层:CD——每次合并后自动部署
持续部署(CD)是 CI 的延伸:测试通过后,自动把构建产物(JAR、Docker 镜像)部署到目标环境。
第一步:Docker 化你的应用。
# Dockerfile
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY build/libs/tournament-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build
run: ./gradlew bootJar
- name: Build Docker image
run: |
docker build -t tournament-api:${{ github.sha }} .
docker tag tournament-api:${{ github.sha }} \
ghcr.io/${{ github.repository }}:latest
- name: Push to registry
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}:latest
- name: Deploy to staging
run: |
# 假设你的服务器在某个环境变量指向的地址
ssh deploy@${{ secrets.DEPLOY_HOST }} "
docker pull ghcr.io/${{ github.repository }}:latest &&
docker-compose up -d --no-deps api
"第三层:分支策略——代码流动的轨道
CI/CD 的基础是明确的分支策略。没有策略的 Git 仓库就像没有红绿灯的路口。
最常用的两种模式:
Git Flow(适合大团队/定期发布):
main ──┤ release-1.0 ├─── tag v1.0
|
develop ──┤ feature-X ├───┤ feature-Y ├───main:只接受 release 分支的合并develop:日常开发主干feature/*:新功能分支release/*:准备发布hotfix/*:紧急修复
GitHub Flow(适合小团队/持续发布):
main ────────┤ feature-X (merged) ├──────
|
feature-X branchmain永远是可部署状态- 从
main开功能分支 - 功能完成后发 PR,测试通过后合入 main
- 合入后自动部署
GitHub Flow 更简单,适合你的擂台系统——单个服务、小团队。
# 你的日常工作流
git checkout -b feat/score-board
# 写代码...
git add .
git commit -m "feat: add score board endpoint"
git push origin feat/score-board
# 在 GitHub 上创建 PR
# PR 自动触发 CI → 测试通过 → 队友 review → 合入 main → 自动部署第四层:三环境——开发、预发、生产
你注意到一个很不优雅的场景:本地建表 SQL 叫 V1__init.sql,线上跑了 V2__add_index.sql,你在本地建表时数据库 schema 和线上不一致——你测了"报名成功",但线上就是报 500。
解决方案:开发(dev)、预发(staging)、生产(production)三套环境,通过流水线逐步晋升。
开发者本地
│ push
v
开发环境 (dev)
│ merge to main
v
预发环境 (staging) ← 生产环境镜像,外部不可见
│ smoke test 通过
v
生产环境 (production)三环境的黄金规则:
- 环境一致:预发环境和生产环境的配置(除 key/secret 外)尽量一致——同版本 JDK、同操作系统、同数据库版本。
- 数据隔离:开发环境可以用伪造数据,预发环境用脱敏的生产数据子集。
- 晋升唯一:只有经过了 CI/CD 流水线的某个构建产物,才能从一个环境晋升到下一个环境。
# application-dev.yml
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/tournament_dev
# application-staging.yml
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://staging-db:5432/tournament_staging
# application-prod.yml
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://prod-db:5432/tournament_prod第五层:基础设施即代码
你的队友在服务器上手动改了 Nginx 配置。三周后,服务器崩溃了,新搭一台——没人记得那个 Nginx 配置是什么。
基础设施即代码(IaC)的意思是:用代码描述你的基础设施,和你的应用代码一样受版本控制。
# ch06/terraform/main.tf
resource "aws_instance" "api" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
user_data = <<-EOF
#!/bin/bash
docker pull ghcr.io/your-org/tournament-api:latest
docker run -d -p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://${aws_db_instance.main.endpoint}/tournament \
ghcr.io/your-org/tournament-api:latest
EOF
tags = {
Name = "tournament-api"
}
}
resource "aws_db_instance" "main" {
engine = "postgres"
engine_version = "16"
instance_class = "db.t3.micro"
db_name = "tournament"
username = "admin"
password = var.db_password
}不必要用 Terraform——docker-compose.yml、Dockerfile、nginx.conf 这些配置文件都受版本控制,也算 IaC。核心原则:人不碰服务器。
常见陷阱
陷阱一:CI 跑 30 分钟。 你的测试很好,但每次 push 都要等半小时——团队开始绕过 CI 直接合并。解决方案:把测试拆成"快速检查"(5 分钟内的单元测试,每次 push 跑)和"完整套件"(定时或手动触发)。
# 快速检查(每次 push 跑)
name: Quick Check
on: push
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- run: ./gradlew test --tests "*.unit.*"
# 完整套件(nightly 或由 reviewer 触发)
name: Full Suite
on: workflow_dispatch
jobs:
full-test:
runs-on: ubuntu-latest
steps:
- run: ./gradlew test integrationTest e2eTest陷阱二:凭据暴露。 数据库密码、API key 写死在代码里 push 到了 GitHub。补救:用 GitHub Secrets 或 Vault。
# 坏
- run: ./gradlew build -DDB_PASSWORD=supersecret123
# 好
- run: ./gradlew build
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}陷阱三:"在我的机器上能跑"。 CI 环境和本地环境不一致——一个跑 Windows,一个跑 Linux;JDK 版本差一个点。用 Docker 统一环境,或者在 CI 上跑和线上同样的容器。
陷阱四:CD 部署到生产却没有回滚方案。 自动部署后出现了问题,你需要能快速回滚。至少做到的:保留前一个构建产物(Docker tag),部署脚本包含回滚命令。
# 回滚
- name: Rollback
if: failure()
run: |
docker pull ghcr.io/${{ github.repository }}:${{ github.event.inputs.previous_sha }}
./deploy.sh ${{ github.event.inputs.previous_sha }}通关挑战
- 热身:为你当前的项目创建一个
.github/workflows/ci.yml——只需编译、跑测试、生成报告。 - 挑战:把项目 Docker 化,写 Dockerfile 和 .dockerignore,验证本地
docker build && docker run能正常工作。 - 观察:找一个你常用的开源项目(Spring Boot、React),看看它的
.github/workflows/目录——它用了哪些 CI 步骤?测试跑在哪个操作系统上?
旅人笔记
CI/CD 把"写代码"到"上生产"之间的每一步自动化——push 触发测试、Merge 触发构建、Tag 触发部署——你不再是一个发版焦虑的程序员,而是一个通过流水线传递构建产物的工匠。
下一站预告
Part 1 到此结束。你掌握了软件工程基础——从构造到测试到重构到部署。但还有一个更大的领域等着你:当系统的规模继续膨胀,同一类问题反复出现时,有没有模板可以套?下一部分开始讲设计模式。首先,第7章——设计原则总复习。