从零搭建 CI/CD 流水线 — GitHub Actions 实战指南
前言
最近我给一个 Spring Boot 4 + Vue 3 的前后端项目补全了测试和 CI/CD 体系。在这个过程中,CI 流水线经历了多次调试才全部通过。这些问题覆盖了环境一致性、平台差异、依赖冲突等几乎所有新手都会遇到的坑。
本文将以这次实战为线索,带你从零搭建一套完整的 CI/CD 流水线。读完你将能够:
- 理解 CI/CD 的概念和必要性
- 为自己的项目写出第一个 GitHub Actions 工作流
- 知道常见的坑和应对方法
一、CI/CD 是什么?为什么需要它?
一句话理解
1 | 你 push 代码 → GitHub Actions 自动执行检查 → 全绿就放心了 |
- CI(Continuous Integration,持续集成):每次提交代码,自动编译、测试、检查代码质量。确保”新代码没有搞坏旧功能”。
- CD(Continuous Delivery / Deployment,持续交付/部署):CI 通过后,自动把代码部署到服务器。
一张图理解 CI/CD 在项目中的位置
PR(Pull Request)触发 CI,CI 不通过则管理员根本不应该看到这个 PR。这就是 “安全检查门” 的含义——机器先替你过滤一遍,人只审绿色代码。
没有 CI 会怎样?
| 问题 | 没有 CI | 有 CI |
|---|---|---|
| 新同事 clone 后跑不起来 | 口头告知”你缺个 xxx 依赖” | CI 直接报错,push 前就能发现 |
| 代码写错了破坏已有功能 | 上线后用户发现 | 测试在合并前就挂了 |
| Windows 能跑 Linux 不能跑 | 上了服务器才炸 | CI 运行在 Linux,第一时间暴露 |
| 没人记得跑 lint | 代码风格越来越乱 | CI 自动跑,不通过不给合并 |
二、动手前的准备
CI 的核心是跑你的测试和构建命令。所以在写 CI 文件之前,先把这些东西准备好:
2.1 单元测试
后端用 Maven:
1 | cd edumind |
前端用 npm:
1 | cd vue-project |
确保这两条命令在你的终端里能跑通之后,再交给 CI。
2.2 依赖清单
对于 Java(Maven 项目),确保 pom.xml 里测试依赖齐全。至少需要:
1 | <dependency> |
对于前端,确保 package-lock.json 已提交到 Git。CI 用的是 npm ci,它严格按照 lockfile 安装依赖——如果 lockfile 没提交或过期,直接挂。
2.3 覆盖率工具(可选但推荐)
在 pom.xml 中加入 JaCoCo 插件:
1 | <plugin> |
跑 ./mvnw verify 后会在 target/site/jacoco/ 生成覆盖率报告。
三、编写你的第一个 CI 工作流
GitHub Actions 的配置文件放在 .github/workflows/ 目录下,只要是 .yml 文件,GitHub 就会自动识别。
3.1 文件结构
1 | 项目根目录 |
3.2 后端测试 Job
从最简单的开始——写一个只跑 Maven 测试的 Job:
1 | name: CI — Quality Gate |
关键点解读:
runs-on: ubuntu-latest— CI 跑在 GitHub 提供的 Linux 虚拟机上。这意味着你的构建环境是 Linux,不是 Windows。这是最常见的坑源。services:— 如果你的集成测试需要数据库/缓存,这里可以声明 GitHub 提供的服务容器,CI 会自动启动它们。chmod +x mvnw— Git 不保留文件的可执行位。在 Windows 上开发时mvnw无所谓权限,但 Linux 上必须chmod +x才能运行。每处./mvnw前都要加。--batch-mode— 让 Maven 不等待交互输入,适合 CI 环境。
3.3 前端测试 Job
1 | frontend-test: |
npm ci 和 npm install 的区别:
| 命令 | 行为 |
|---|---|
npm install |
根据 package.json 安装,可能更新 package-lock.json |
npm ci |
严格按照 package-lock.json 安装,不一致就报错 |
CI 里必须用 npm ci,这能保证你本地锁定的版本和 CI 完全一致。
3.4 Docker 构建 Job(可选)
如果你的项目用 Docker 部署,加一个镜像构建的 sanity check:
1 | docker-build: |
needs: [backend-test, frontend-test] 的作用:Docker 构建需要跑测试时必须的 Maven 编译产物(JAR 包),所以等前两个 Job 通过后再执行。
四、常见的坑及排查思路
上面写完了完整的 CI 文件。但第一次 push 大概率不会直接绿——以下是在实战中遇到的几个典型问题和你需要知道的排查方法。
坑 1:package-lock.json 没提交
1 | npm ERR! Missing: vitest from lock file |
原因:本地 npm install 更新了 lockfile,但 git add 漏了。CI 用的 npm ci 严格校验 lockfile。
排查:git status 看 package-lock.json 是否为 M(modified)。确保它和 package.json 一起提交。
坑 2:文件只存在于本地,没被 Git 跟踪
1 | [ERROR] cannot find symbol: class MineruException |
原因:IDE 里新建的类,从未 git add。本地编译没错是因为 IDE 读取文件系统,CI 只认得 Git 仓库里的东西。
排查:git status 里的 ??(untracked)就是地雷。推送前跑一遍 git status 确认没有漏掉的新文件。
坑 3:Windows 上能运行,Linux 上报 Permission denied
1 | ./mvnw: Permission denied |
原因:Windows 不管文件权限,Linux 执行脚本需要 x 权限。Git 在 Windows 上不保留 Unix 可执行位。
解决:在 CI 配置里,每个使用 ./mvnw 的步骤前加 chmod +x mvnw。
坑 4:依赖版本冲突 —— “静默退化”
1 | # Windows 上 JWT 正常 |
原因:Spring Boot 4 自带 Jackson 3.x,但 jjwt 0.11.5 依赖 Jackson 2.x。两个版本共存时,classpath 加载顺序在不同 OS 上可能不同,导致部分序列化功能静默失效——不报错,但行为不对。
通用排查思路:
- 在 CI 日志中加 debug 输出,对比本地和 CI 的行为差异
- 检查
pom.xml的 dependency tree:./mvnw dependency:tree - 如果是版本冲突,升级到兼容双方的最新版本
坑 5:集成测试依赖真实数据库
1 | Failed to load ApplicationContext |
原因:有的测试用了 @SpringBootTest,它会启动完整 Spring 上下文,要求 PostgreSQL、Redis 等服务都在线。CI 的 services 容器虽然提供了数据库,但某些需要额外配置。
方案:
- 纯单元测试(
@ExtendWith(MockitoExtension.class))— 毫秒级,不依赖外部服务 - 集成测试用 Testcontainers — 自动拉取数据库 Docker 镜像,用完即弃
- GitHub Actions 的
services:字段 — 声明 CI 环境所需的辅助容器
五、CD —— 自动部署
CI 是”检查”,CD 是”送到线上”。下面是一份简单的自动部署模板,在打 v 开头的 tag 时触发:
1 | name: Deploy |
敏感信息管理
注意上面的 ${{ secrets.DEPLOY_HOST }} 等变量——这些不能写在文件里明文暴露。
在 GitHub 仓库的 Settings → Secrets and variables → Actions 中添加:
| Secret 名 | 内容 |
|---|---|
DEPLOY_HOST |
服务器 IP |
DEPLOY_USER |
SSH 用户名 |
DEPLOY_SSH_KEY |
SSH 私钥 |
六、完整流程一览
全部配置就绪后,你的日常工作流是这样的:
1 | 1. 开分支写代码 |
结语
“在我机器上能跑”是软件开发中最昂贵的一句话。
CI/CD 的价值不在于配置文件的语法有多精妙,而在于它把”能跑”的标准从”你的机器”提高到了”任何一台机器”。每一次红叉,都在替你发现一个上线后才会暴露的问题。
如果你现在有一个还没有 CI 的项目——从今天开始,在 .github/workflows/ 下创建第一个 .yml 文件,只写一个最简单的 ./mvnw test。从那一行开始,你就已经在通往生产级的路上。






