# M6 测试、CI/CD 与部署上线：为什么先立质量门与最小上线路径

做到 `M6` 时，这个项目已经不再只是一个“本地能跑”的练习仓库了。

前面几个里程碑已经逐步把核心能力立起来了：

- `M2` 固定了鉴权与角色边界
- `M3` 固定了内容模型、发布态与公开展示
- `M4` 固定了 AI 工具链的最小边界
- `M5` 固定了对外导出与下载能力

这时候项目会遇到一个非常现实的问题：

如果这些能力只是在本地偶尔能跑通，它真的算一个可持续演进、可复用、可开源教学的项目吗？

答案当然是否定的。

所以 `M6` 解决的不是“再多做一个功能”，而是第一次认真回答三个更基础的问题：

1. 项目有没有稳定的本地质量门禁？
2. 这些门禁有没有被搬到 GitHub 上自动执行？
3. 三端项目有没有一条讲得清楚、跑得通的最小上线路径？

这篇文章就围绕这三个问题，讲清楚为什么 `M6` 要被单独拉出来做，以及它如何把项目从“功能能跑”推进到“可以持续演进”。

## 一、为什么 `M6` 值得单独成为一个里程碑

很多项目做到这里，会本能地继续往前冲功能，觉得测试、CI、部署这些事情“后面再补也行”。但对教程型项目来说，这种顺序其实很危险。

因为当前仓库的目标不是只让作者自己能跑起来，而是让读者也能：

- 拉下来复现
- 顺着里程碑理解设计
- 在自己的分支里继续演进
- 知道项目什么时候算“过门”

如果没有 `M6`，前面那些功能就会缺少一个共同的质量边界。后果通常会是：

- 每个应用各有一套测试命令，根目录没有统一入口
- 本地自测过不过都靠记忆，没人知道 PR 应该跑什么
- GitHub Actions 只是摆设，甚至还在跑旧时代的脚本
- 部署说明零散地散在 README 或对话里，读者不知道三端应该怎么上线

所以 `M6` 真正要做的，是把前五个里程碑已经长出来的功能，第一次收敛成一套“可验证、可复现、可上线”的工程主线。

## 二、`M6` 这轮到底要交付什么

这一轮的交付目标可以概括成一句话：

让读者能看到这个 monorepo 如何从本地统一质量门禁出发，接上 GitHub Actions，最后再形成三端最小部署路径。

这里面有三个关键边界。

### 1. 先统一本地质量门禁

对教学项目来说，CI 永远不应该是第一道门。

第一道门必须是本地自己能稳定执行的一组固定命令，比如：

- `test`
- `typecheck`
- `build`

如果本地都没有稳定入口，后面再接 GitHub Actions 其实只是把混乱自动化而已。

所以 `M6` 的第一步，是先把 `server / web / admin` 的测试、类型检查和构建入口统一到根目录，再把测试策略和质量门禁文档收清楚。

### 2. 再把本地门禁搬到 GitHub

当根级命令稳定之后，第二步才是 GitHub Actions。

这里的关键不是“有没有 CI”，而是：

- CI 跑的是不是和本地同一套门禁
- 触发分支是不是已经切到当前真正使用的 `development / main`
- 它是在复验本地门禁，还是在替代本地自测

`M6` 的选择很明确：CI 是第二道门，它复用本地统一命令，但不代替开发者在本地自测。

### 3. 最后才讲三端部署路径

测试和 CI 稳住之后，才轮到部署文档。

这一轮不追求复杂运维体系，而是先讲清楚三件事：

- `web / admin` 为什么适合放在 `Vercel`
- `server` 为什么继续放在云服务器
- 为什么即使进入上线阶段，也依然要坚持“唯一业务后端是 NestJS”

这一点非常关键，因为部署拓扑不是孤立决策，它其实延续的是整个项目从一开始就坚持的架构边界。

## 三、`M6` 为什么要按这 3 个 issue 来拆

这一轮的拆法，其实是在回答一个很典型的工程问题：

当一个项目已经“能跑”之后，应该先收什么，才能让它进入“能持续演进”的状态？

`M6` 的答案是：

先收本地门禁，再收远端复验，最后收上线路径。

### issue-20：先做测试基建

第一步不是 GitHub Actions，更不是部署，而是先让本地质量门稳定下来。

这一轮真正固定下来的，是：

- 根级统一命令入口
- 各应用测试入口的基本稳定性
- 测试分层和质量门禁文档

这意味着项目第一次有了一条可以被团队、被读者反复复用的“本地自测主线”。

### issue-21：再做 GitHub Actions

本地门禁稳定以后，下一步才是把它搬到 GitHub。

这一步的重点不是花哨的工作流，而是最小一致性：

- 触发分支和当前 Git Flow 对齐
- CI 直接复用根级命令
- 旧时代的动态脚本检测被移除

也就是说，`issue-21` 不是在“新造一套 CI”，而是在保证远端跑的仍然是项目已经确定好的同一套门禁。

### issue-22：最后收部署文档

当本地和远端两道门都成立后，项目才有资格讲“上线”。

这一步主要解决的是：

- 三端部署拓扑如何解释
- 各应用的环境变量和构建入口怎么配置
- `server` 的最小守护与反向代理路径是什么
- 上线之后应该按什么清单去验收

这一步的价值，不是让项目立刻变成成熟运维体系，而是先给出一条足够清楚、足够可复现的最小路径。

## 四、这一轮最重要的设计取舍

### 1. 为什么先统一根级命令，而不是直接写 CI

如果一开始就写 GitHub Actions，而本地命令还是分散的，CI 很快就会变成一个“只有远端知道怎么跑”的黑盒。

这对教程型项目特别不友好，因为读者会失去最重要的一件事：在本地理解每道质量门到底在检查什么。

所以 `M6` 选择先把根级命令立起来，本质上是在先收“本地工程语言”。

### 2. 为什么 CI 只做最小工作流

CI 很容易越做越重，尤其是当我们已经知道后面还有：

- 多环境矩阵
- preview / production 区分
- 分支保护
- 部署流水线

这些东西都很重要，但不适合在 `M6` 第一轮就一起上。

因为当前真正要先讲清楚的是：

- 本地门禁和远端门禁之间的关系
- PR 到底要过哪些检查
- CI 为什么是复验，而不是替代开发者的本地自测

所以 `M6` 刻意只做最小 CI，保护教程节奏。

### 3. 为什么三端部署拓扑仍然坚持“唯一业务后端是 NestJS”

进入部署阶段后，有些项目会开始动摇最初的架构边界，觉得是不是可以顺手把部分逻辑塞到 `Next.js` 一侧，或者让不同前端各自承担一点后端职责。

但这个项目在 `M6` 反而更强调：

- `web / admin` 是前端壳，适合上 `Vercel`
- `server` 才是唯一业务后端，适合独立部署在云服务器

这不是为了“看起来规范”，而是为了让部署拓扑继续和业务边界一致。否则到了线上，读者会开始分不清到底谁才是真正的 API 入口。

### 4. 为什么当前阶段故意不做复杂运维体系

对教程型项目来说，最容易犯的错之一，就是在“第一次上线”阶段就把所有运维话题一起卷进来，比如：

- IaC
- 多环境矩阵
- 监控报警
- CDN / HTTPS 细节
- 更复杂的发布流水线

这些能力都值得写，但如果在 `M6` 第一轮全部压进来，读者就很难看清“最小上线路径”到底是什么。

所以 `M6` 的选择是先给出一条最小、可信、能验证的部署主线，把后续扩展口明确留下来，而不是第一轮就做满。

## 五、关键文件应该怎么读

如果你想顺着代码和文档复盘 `M6`，建议优先看这几组材料。

### 本地质量门禁

- `package.json`
- `turbo.json`
- `docs/20-研发流程/04-测试策略与质量门禁.md`

这里解决的是“本地该跑哪些门，以及这些门怎么统一起来”。

### GitHub Actions

- `.github/workflows/ci.yml`
- `docs/20-研发流程/05-GitHub-Actions-最小-CI-流程.md`

这里解决的是“同一套门禁如何被 GitHub 远端复验”。

### 部署拓扑与上线说明

- `docs/40-部署上线/01-Vercel-与-云服务器-最小部署说明.md`
- `apps/web/lib/env.ts`
- `apps/admin/lib/env.ts`
- `apps/server/src/config/env-paths.ts`

这里解决的是“三端如何在当前架构约束下被最小上线”。

## 六、`M6` 的测试重点是什么

`M6` 的测试重点不是新增业务行为，而是确认工程门禁和上线路径是不是真的站稳了。

### 1. 先锁本地统一命令

这里至少要证明：

- 根级测试命令能运行
- 根级类型检查命令能运行
- 根级构建命令能运行

这一步锁住的是“项目有没有统一的本地质量门”。

### 2. 再锁 GitHub Actions 配置有效性

这一层要证明：

- 工作流 YAML 合法
- 触发分支和当前 Git Flow 一致
- 远端执行的是根级统一命令

这一步锁住的是“CI 是否真的在复用本地主线”。

### 3. 最后锁部署说明与环境变量一致性

部署文档这一层要证明：

- 文档描述和代码里的环境变量读取方式一致
- 三端的构建 / 启动命令与真实脚本一致
- 上线验收清单能覆盖公开站、后台、角色边界、导出与 AI 配置

这一步锁住的是“文档不是想象图，而是能对上现有仓库状态”。

## 七、这一轮最容易踩的坑

### 1. 把 CI 当作替代本地自测的东西

这样团队很快就会形成“先推上去再看 GitHub 报错”的习惯，开发流程会变得越来越被动。

### 2. 保留旧时代脚本和新入口并行

这会让读者和协作者分不清到底哪一套才是真正的质量门。

### 3. 部署文档脱离真实代码状态

这是最常见的问题。文档里写一套，代码里环境变量、端口、构建脚本又是另一套，最后读者根本复现不了。

### 4. 在第一次上线阶段就把运维体系做得过重

这样会让 `M6` 从“最小可上线”变成“高级运维导论”，彻底冲掉当前里程碑的重点。

## 八、为什么 `M6` 完成后，项目才真正达到“基础教程版闭环”

前面的 `M2 ~ M5` 已经逐步把功能边界、内容边界、AI 边界和导出边界收住了。

而 `M6` 做的事情，是把这些边界第一次统一纳入“质量门 + 远端复验 + 最小上线路径”。

这意味着项目终于不再只是：

- 作者本地可以跑
- 读者需要靠对话猜怎么做

而开始变成：

- 有固定门禁
- 有 CI 复验
- 有最小上线说明
- 有可继续演进的工程主线

所以 `M6` 的意义，并不是“把部署文档补完”，而是让这个仓库第一次具备了真正的阶段性闭环。

## 九、如果你要自己复现 `M6`，建议按这个顺序

1. 先统一根级测试、类型检查和构建命令
2. 再把这套命令搬到 GitHub Actions
3. 最后再根据现有应用边界整理三端部署路径

这个顺序的好处是，项目每往前走一步，都在先稳定更基础的一层：

- 本地门
- 远端门
- 上线路径

这样不会一上来就把复杂部署问题压在一个还没有统一门禁的仓库上。

## 十、这篇文章对应的原始材料

如果你想继续往下看每个 issue 的实现细节，可以配合这些开发日志一起阅读：

- `docs/30-开发日志/M6-issue-20-测试基建.md`
- `docs/30-开发日志/M6-issue-21-GitHub-Actions.md`
- `docs/30-开发日志/M6-issue-22-Vercel-与-云服务器部署文档.md`
- `docs/40-教程与博客/M6-测试-CI-CD-与部署上线-教程大纲.md`

这些记录更适合看“每一步具体做了什么”，而本文更希望回答“为什么这一轮必须先立质量门与最小上线路径”。

## 小结

`M6` 做的不是一套完整 DevOps 平台，而是先把项目最关键的工程收口点立起来：

- 本地统一质量门
- GitHub 远端复验
- 三端最小部署路径
- 与现有架构边界一致的上线拓扑

这些边界一旦成立，后面的分支保护、预发环境、更复杂的部署流水线和运维体系，都会更像是在已有工程主线之上继续增强，而不是重新发明一套流程。
