28 · CI 验证流水线:一次发布前的绿勾究竟证明了什么#
小陈改完导出逻辑,本机测试没有报错,就认为可以发布。小周检出同一份代码后却发现数据库测试根本没跑;修好连接后测试通过,容器又因缺少 Worker 运行依赖启动失败。怎样让这些问题在发布前暴露?
第六阶段沿一次版本发布展开:本章读检查流水线,第 29 章把代码装进独立容器,第 30 章观察运行和退出,第 31 章从真实 HTTP 验收到页面与故障恢复。这里说明仓库已有机制,不表示编写教程就已经执行远端 CI 或生产部署。
1. 从本地约定走到独立检查环境#
先看根目录的 .husky/pre-commit、.husky/commit-msg 和 package.json:前者运行 lint-staged,后者运行 commitlint;暂存文件检查反馈快,但它们可以被跳过,也不会自动替你启动测试数据库。
再打开 ci.yml,按 on → permissions → concurrency → jobs 阅读:
| 配置 | 当前行为 | 不包含的能力 |
|---|---|---|
| push.branches=[main] | 推送 main 时运行 | 任意其他分支 push 都自动运行 |
| pull_request / workflow_dispatch | PR 事件和手动触发可运行 | 本地保存 YAML 就触发远端 |
| contents: read | 工作流申请仓库读取权限 | 自动发布镜像或修改分支规则 |
| concurrency 按 workflow/ref 分组 | 同组新运行取消旧运行 | 不同 ref 的任务全部串行 |
取消旧运行是节省资源,不能把已取消当作已通过。记录结论时要同时记录检查的提交 SHA 和运行结果;PR 工作流默认检出的上下文也应以运行页面显示为准,不能只凭本地 HEAD 推测。
2. quality 的依赖为什么要显式提供#
quality 在 ubuntu-latest 上使用 Node 24,启动 PostgreSQL 17 和 Redis 7 两个 service。其环境变量有 DATABASE_URL、TEST_DATABASE_URL 和 TEST_REDIS_URL。
沿第 27 章的 describe.skipIf 回看:本地没有测试连接时,一些集成测试会跳过;CI 明确传连接是为了真正执行。同一个 pnpm test,执行到的测试范围可能因环境不同而不同。 仍应看实际输出,连接失败是失败,不是跳过也算成功。
| 环境 | PostgreSQL / Redis 地址来自哪里 |
|---|---|
| 本机 Node 连开发 Compose | 宿主机 5433 / 6380 |
| CI quality 的 runner 连 service | 映射到 localhost:5432 / 6379 |
| 部署容器彼此访问 | Compose DNS 的 postgres:5432 / redis:6379 |
这些连接不是相同字符串。CI 中的固定密码和登录密钥只用于一次性测试环境,不是让服务器复用的配置。
3. 顺着步骤读“为什么是这个顺序”#
检出代码与历史 → 安装 pnpm / Node → 锁文件安装
→ 格式 → lint → typecheck → 提交信息检查
→ 测试库迁移 → 测试 → 生产构建安装用 --frozen-lockfile,要求依赖声明与锁文件一致,不在 CI 顺手更新依赖。HUSKY=0 让无人值守安装不依赖本地 Git hook。Client 生成来自项目安装 / 构建脚本,但生成访问代码不等于数据库已建表,所以测试前仍要 db:migrate。
格式、lint、类型、测试、build 分别挡不同问题:类型正确的 SQL 查询可能越权;单元测试通过也可能漏复制静态资源。前面一步失败后,后续正常步骤不会继续执行,因此“quality 失败”还不能说明后面的每一步都被试过。
PR 的 commitlint 使用 BASE_SHA 到 HEAD_SHA 的范围,checkout 配 fetch-depth=0 以获得历史;非 PR 分支检查最新一条提交。它没有在每次 main push 里逐条检查全部历史。提交标题沿用中文 Conventional Commits,例如 fix(export): 修正导出范围判断。
4. 为什么 containers 要再构建一次#
第二个 job 有 needs: quality,只有质量任务成功才进入镜像验收。它是另一个 runner,重新 checkout,不是接着复用上一个 job 的 node_modules 或 .next;当前没有上传 / 下载构建产物的步骤。
按第二个 job 的顺序读:
- 从示例复制
.env.deploy,为数据库、Redis、登录密钥生成各自的一次性随机值。 - Compose 构建 web / worker / migrate,等待依赖健康与迁移完成。
- curl 验证 Web
/api/health,容器内执行 worker-health.ts。 - 运行 deployment-smoke.mjs,从 HTTP 创建数据、等待导出并下载检查内容。
- 失败时输出 worker 与 migrate 的最近日志,最后尝试清理本轮容器和卷。
“源码能构建”与“运行镜像里能找到依赖、启动入口正确”是两份证据。Dockerfile 的 Worker 依赖和 Web 静态资源复制出错,可能只在第二个 job 暴露。
清理使用 if: always(),适合一次性 runner;不能把它理解为进程被强杀或运行平台异常时仍绝对完成。也不要把清卷命令用于已有业务数据的服务。失败日志步骤目前只列 worker、migrate,Web 故障可能需要另查 web 日志。
5. 三个具体故障怎样定位#
故障 A:本地测试全绿,CI 迁移后测试失败。 比较本地输出中的 skipped、测试库迁移、连接端口和实际运行提交。先补齐本地独立库复现,不删 CI 的集成测试来让状态变绿。
故障 B:quality 通过,containers 下载等待超时。 先读失败步骤,再查 Worker 是否启动、当前实例心跳、taskId 对应账本与投递日志。第 31 章拆解 smoke 的轮询条件;这不是“类型检查漏了一项”的同类问题。
故障 C:工作流通过,仍有人能直接合并未检查变更。 工作流只定义如何检查,强制合并前检查还依赖 GitHub 分支规则。仓库中有 YAML 不能证明服务器侧规则已经配置;本教程不把两者混称为保护已启用。
6. 在本机复现时逐项记录#
先运行不要求真实数据库的静态检查与已知单元测试:
pnpm format:check
pnpm lint
pnpm typecheck
pnpm exec vitest run tests/tasks.test.ts tests/worker-logger.test.ts --reporter=verbose再按第 27 章准备独立测试库与 Redis,用显式 TEST_DATABASE_URL / TEST_REDIS_URL 跑测试。最后运行 pnpm build;镜像部分则留到第 29、31 章,在隔离部署中执行。上述命令逐条看输出,不把前一条成功算到尚未运行的下一条上。
教程站检查还需单独运行:
pnpm tutorials:test
pnpm tutorials:build当前工作流没有独立调用这两条命令。format / lint 能覆盖某些教程源文件,不等于生成链接、搜索锚点和答案折叠已经被 CI 验证。
建议记录表中至少有“提交、检查名称、环境、passed / failed / skipped、失败步骤、尚未验证范围”。不硬记总测试数,新增用例后它会变;实际输出比 README 中历史数量更可靠。
拓展阅读一次构建验证能否等于可重复发布?
锁文件固定依赖解析,Action 用固定 SHA 引用,能减少部分变化来源;但基础镜像标签、系统包源与 runner 环境仍有自身生命周期。当前也没有镜像仓库发布、版本签名和远端部署步骤。需要进一步发布保障时,应单独设计这些环节,不能从一个绿勾推导出来。
7. 练习与答案#
练习 A:pnpm test 返回 0,但数据库用例全 skipped,报告怎么写#
动手练习查看参考答案
参考答案:写“已执行的测试通过,数据库集成未执行”,并说明缺少连接。按独立测试库步骤补配置、迁移后重跑;不能把命令退出成功直接记成完整集成通过。
练习 B:为什么第二个 job 不直接运行第一个 job 的 .next#
动手练习查看参考答案
参考答案:当前两个 job 没有产物传递,镜像在独立构建流程里生成。这可以验证 Dockerfile 自身是否把构建与依赖准备齐全,但也意味着两次构建,不能声称发布的就是 quality 那份磁盘产物。
练习 C:怎样证明这次教程修改的搜索链接也可用#
动手练习查看参考答案
参考答案:对本次内容运行 tutorials:test,构建后在浏览器搜索一个新增段落、跳转锚点并打开源码页;记录这个本地验证。业务 CI 没有调用教程构建检查,不能用它代替这份证据。
本章链路:提交触发独立 runner → 显式准备依赖与测试服务 → 静态规则、迁移、测试、构建 → 另一个 job 构建并运行镜像 → HTTP 链路验收 → 记录结果与边界。