10.09 持续集成与制品:让 IM 变更留下可复核证据
返回第十卷 · 测试前置:10.02 · Git 前置:10.03 · 评审前置:10.06
10.09 持续集成与制品:让 IM 变更留下可复核证据
DeepTutor 初稿经技术与教学审阅后的静态课程。工作流、测试结果、构建产物和提交号均为纸上设计;本次没有运行 Go、GitHub Actions、IM 服务、构建或站点。学习者以后在自己的隔离环境执行时,须单独保存真实记录。
一、一次本地通过的改动,为什么提交后仍可能坏
假设学习者为教学 IM 的 POST /v1/conversations/{id}/messages 提交一项待评审变更:把单条正文上限从 09.02 当前合同的 6 字节提高到 9 字节。学习者在本机修改校验函数,运行一项本地测试,看到绿色结果并发出评审请求。另一位同伴拉取提交后却发现:测试数据文件没有纳入提交;或者他的 Go 工具链版本不同;或者只有一条测试覆盖了 ASCII 的 9 个字符,没有覆盖 3 个汉字恰好 9 字节的边界。“我的机器通过”是某次本地实验的证据,尚不是这个提交在独立环境可重复验证的证据。
10.01 教我们先从需求得到独立预期;10.02 把预期变成测试;10.03 让代码和文件归属到一个提交;10.06 让评审聚焦差异与风险。持续集成(CI)把这些步骤接到同一个受控提交:当提交或评审事件发生时,在明确的环境里获取这份代码,运行约定检查,并把结果关联回该提交。它帮助团队及早发现合并风险;CI 绿色也只说明这些检查在这次环境里通过,不自动证明产品满足全部需求。
| 名称 | 本章最小含义 | 本例能回答 | 不能自动回答 |
|---|---|---|---|
| CI | 对选定提交自动重复一组检查 | m-a 上限测试与构建在指定环境是否通过 | 所有真实设备都收到消息 |
| 构建 | 把选定源码和依赖加工为程序等输出 | 目标环境能否生成教学 CLI/服务二进制 | 程序业务行为完全正确 |
| 制品 | 保存、标识并交接一次运行的输出 | 这个二进制或报告来自哪个提交 | 该产物已部署或可安全发布 |
| 持续交付 | 在验证后安排可控的发布候选和推广 | 后续怎样决定上线 | 本章的 PR 检查本身不负责发布 |
本章仍以虚构 u-a/u-b/c-a/m-a 讲业务。当前课程只有本地工具与静态服务合同,没有运行中的 P2 HTTP/WS 或 CI 服务。这里讨论学习者未来可实施的最小流程,不把纸上结果说成仓库已有的流水线。
二、先认识事件、工作流、任务、步骤与 Runner
在 GitHub Actions 这个公开例子里,事件是触发条件,如创建或更新评审请求、向指定分支推送;工作流是保存在仓库中的自动化定义;工作流含一个或多个 job(任务),每个任务在自己的 runner(执行环境) 中运行若干按序排列的 step(步骤)。同一任务的步骤可顺序使用前一步生成的工作区文件;不同任务的环境通常彼此隔离,需要显式交接产物。不能因为两个任务写在同一个 YAML 文件里,就以为它们共用磁盘。
评审请求或受控推送
→ 选定提交 SHA
→ checkout 对应代码
→ 准备指定 Go 工具链
→ 检查源码/运行约定测试
→ 为指定目标构建
→ 保存测试报告、构建制品及身份记录
→ 把成功/失败反馈给该提交的评审这里的 SHA 是代码身份,不是测试质量证明。评审请求上的代码与主分支当前代码也可能不同,检查页须明确究竟验证了哪一个提交或合并候选。若后续有人推送新提交,旧提交的绿色状态不能直接沿用。任务间若要让 build 在 test 成功后才运行,需要建立明确依赖;若要复用生成的文件,需要上传、下载或在同一任务内继续运行,而不是依赖 runner 的偶然状态。
先在纸上画一张表:事件是什么、输入提交是什么、每个任务需要什么、失败时谁收到反馈。此时不必背诵 YAML。平台语法和 action 版本会更新;实际落地时再对照所选平台官方文档。
三、把业务合同分解成有解释力的检查
我们从 10.01 的规则开始,而不是先列一堆命令。拟议 R9 版本定义正文长度使用 UTF-8 字节数;变更获批后,恰好 9 字节可受理,10 字节须在内存追加前被拒绝。独立预期可以写成:"abc" 为 3 字节合法;"中文甲" 为 9 字节在拟议版本合法;"中文甲a" 为 10 字节拒绝;非法请求不应改变内存消息列表。当前 09.02 合同仍为 6 字节,不能把这份纸上 PR 的预期当作已生效接口。若需求另规定空内容、重复 ID、未授权主体,也要分别写合同,不能凭 CI 绿色自己补出含义。
| 检查层 | 本例的输入与预期 | 失败能说明什么 | 通过仍缺什么 |
|---|---|---|---|
| 代码格式与基础静态检查 | 源码符合约定,基本错误能被工具发现 | 某类可自动发现的问题仍在提交里 | 9 字节业务规则可能完全没测到 |
| 单元测试 | 校验函数对 9/10 字节、空值的独立预期 | 规则实现或边界行为与预期不一致 | handler、JSON 与存储接线未必正确 |
| 组件/接口测试 | 假设的 handler 将 10 字节请求拒绝,内存列表不变 | 解析到应用层的合同可能被破坏 | 真正跨网络、真实客户端行为未知 |
| 构建 | 约定目标的包可产出二进制 | 包、依赖、工具链或目标平台不匹配 | 二进制执行和业务正确性仍未证明 |
Go 初学者可把未来个人验证命令记作 go test ./... 与 go build ./...:前者运行当前模块可选包的测试,后者构建所选包;真实项目还需明确工作目录、具体目标包、环境和输出路径。若目标是保存一个可下载二进制,必须指定明确的 -o 输出文件及主包,不能把 go build ./... 的退出成功直接当成“某路径已有制品”。本章不执行这些命令,也不写“已通过”。
当 9 字节测试失败时,报告至少应写出:提交 SHA、任务和步骤、Go 版本、命令、失败用例与期望/实际、测试数据版本。重跑偶然变绿只说明第二次环境里的结果改变;先比较两个运行的输入,再判定是代码修复、数据变化、依赖下载或不稳定测试。把重复失败和“同一代码偶发失败”分开,才能有可靠评审。
四、独立环境、依赖与缓存怎样影响证据
Go 模块的 go.mod 描述模块路径、Go 版本约束及依赖选择;go.sum 保存下载的模块内容和相关 go.mod 的校验值。两者应随课程项目的实际依赖状态管理。go.sum 不是锁住整个构建环境的万能文件:Go 工具链版本、操作系统、CPU 架构、构建参数、CGO、生成文件与环境变量都可能改变输出。要谈“同样输入重建同样制品”,先列出哪些输入被固定;仅有相同 Git SHA 不足以作字节相同的保证。
CI 的 runner 应视作一次新的、可审计的环境:从选定提交取得代码,准备指定工具链,安装所需依赖,再运行检查。教学数据只使用 u-a/u-b/c-a/m-a 等虚构身份,不能依赖真实 IM 账号、线上令牌或某位开发者主目录里的文件。网络和外部服务会失败;若本章的最小测试本来不需要它们,就别把它们变成隐形前置。
缓存为节省时间,可保存模块下载或构建中间结果;它并不是本章的验证结果。缓存命中后仍需运行检查,缓存失效或删除后也应能从声明过的依赖重新完成。若“有缓存就绿、清缓存就失败”,应先调查未声明输入、下载失败或生成步骤遗漏,不能把缓存内容当作项目必需的唯一来源。缓存键、恢复范围与读写权限也属于信任边界;评审请求携带的代码和缓存写入权限要按平台文档审查。
| 纸上情景 | 先查证据 | 当前能说什么 |
|---|---|---|
本机通过,独立 runner 找不到 testdata/messages.json | 提交树、工作目录、读取路径 | 可能遗漏文件或依赖了本机路径 |
| 热缓存通过,冷缓存失败 | 模块下载、生成步骤、完整日志 | 缓存掩盖了某个未声明或不可用的输入 |
| Linux 构建通过,目标平台运行失败 | GOOS/GOARCH、CGO、运行环境 | 这次 Linux 构建不代表目标平台已验证 |
对来自外部的评审请求,先以最小权限运行测试:只读代码所需的权限;不要给不受信代码发布凭据。第三方工作流组件在真实配置中要审查来源并固定到经过审阅的提交版本。若使用高权限事件处理外部评审,不能同时检出并执行不受信变更且暴露写令牌或秘密。这里的安全规则是保护 CI 自身的输入和凭据,与应用的消息权限是不同层。
五、测试数据与失败反馈的最小契约
重复运行测试需要可重建的数据。本题的基线可以只有四个记录:虚构会话 c-a、参与者 u-a/u-b、一条 m-a、一条边界消息正文。每个用例自行建立内存状态;时间和排序若影响预期,就显式提供可控时钟或固定序列。不能让第二次运行依赖第一次留下的文件,也不能用真正的用户聊天记录来验证一个 9 字节规则。
失败反馈应把代码失败与基础设施失败分开:断言期望与实际不一致通常是规则/测试的检查失败;模块下载超时是依赖获取或网络故障;runner 启动失败是环境故障;外部服务不可用则需判断是否本来就应该纳入本阶段检查。分类不等于自动归因,仍要保留日志和输入。尤其不应为了让 PR 显绿,把关键测试设为“失败但允许继续”;真正非阻断的实验性检查要有清楚标注和期限。
同一提交:首次 9 字节用例失败 → 重跑成功
结论:证据互相冲突;不能直接写“代码已修复”。
下一步:比较两次的测试数据、工具链、环境、缓存和失败日志;
能稳定复现后再修改代码或隔离不稳定依赖。本章的 CI 只为教学 P2 的内存受理提供检查设计。它不能用一个单元测试证明未来数据库崩溃恢复、B 设备送达或多设备同步;那些场景需要后续阶段的真实实现和分层证据。若业务合同尚未实现,要明确写“尚无可运行的接口测试”,而不是伪造通过记录。
六、制品与缓存分开:输出要有身份
假想未来将教学 CLI 的一个二进制留作评审附件,它是制品。制品应记录从何而来:代码提交 SHA、工作流运行身份、Go 工具链、GOOS/GOARCH、构建命令或参数、输入依赖状态、产物文件名和文件 SHA-256。摘要让两个文件可比较并发现变化,并不证明源代码无恶意、测试完备或发布获批准。
纸上构建记录
source_commit = <完整提交 SHA>
workflow_run = <本次运行标识>
toolchain = <明确的 Go 版本>
target = linux/amd64,CGO 设置另记
command = go build -o out/imhistory <明确的主包路径>
artifact = out/imhistory
sha256 = <构建后计算的摘要;本章不填假值>若 test 与 build 是两个不同任务,它们不是自动共享的目录。要么在一个任务里按顺序完成并输出;要么通过平台提供的上传/下载机制传递报告或二进制,并记录来自哪个运行。缓存用于以后更快地重新取得依赖;制品用于交给人或后续阶段查看这一次输出。缓存删除不应改变业务真值,制品丢失则会让本次构建的可审阅输出不完整。
“可重现构建”也须定义范围:同源码、相同依赖、相同工具链和目标参数下,构建是否产出相同内容?若嵌入构建时间、使用不固定的生成文件或本地 CGO 库,字节可能不同。先列环境与输入,再验证是否相同;不因两个二进制的摘要不同就立即断言源码被篡改,也不因摘要相同就断言客户端业务正确。
七、给 P2 内存版写一份可审阅的最小 CI 设计
将同一业务变更贯穿流程:需求从 6 字节改到 9 字节,A 提交 m-a,合法请求只承诺当前进程内存受理;超限请求应在追加前拒绝。评审者应能从一个提交看到需求、测试、检查和产物之间的连线。
| 阶段 | 输入 | 纸上检查 | 失败反馈 | 产出/尚未覆盖 |
|---|---|---|---|---|
| 评审请求 | 某提交的代码、go.mod/go.sum、虚构测试数据 | 检出确切代码身份和工具链 | 无法取代码/依赖,标环境失败 | 输入清单,不发布 |
| 合同测试 | 9/10 字节、无效输入、失败后内存列表 | 单元测试与可实现时的接口测试 | 用例名、期望/实际、提交 SHA | 仅证明已运行检查覆盖的内存合同 |
| 构建 | 测试通过的同一提交 | 指定主包、目标和输出路径 | 构建错误、版本/参数 | 带身份的候选文件;不自动部署 |
| 评审与合并 | 需求说明、差异、检查记录 | 10.06 的评审问题与受控合并 | 新提交要重新确认检查 | 合并决策,不等于设备送达 |
一个最小伪配置可以这样读,而不是照抄为可执行 YAML:
on: review_request, trusted_push
permission: read_source_only
job verify(commit): checkout(commit); setup_go(fixed_version); test(module); build(explicit_target)
output: status(commit, test_evidence); artifact(commit, toolchain, target, sha256)
publish: none实际平台语法、事件名、权限键与第三方 action 版本需在学习者实现时对照官方文档。特别是来自外部的代码不能在有发布密钥的高权限任务里直接执行。S3 以后再加持久化与权限集成检查,S4 再加并发、性质与负载验证,S5 再加重试/设备确认链的证据;这些后续检查须由对应实现和业务合同支撑。
八、从失败变式练习,再决定下一步
面对每个变式,先写“已知、未知、下一证据”:依赖下载失败但代码未执行;只测试 9 个 ASCII 字符、漏了 3 个汉字;使用漂移的工具链或 action 标签;热缓存掩盖漏提交的生成文件;二进制摘要与所报提交不匹配;日志意外打印令牌;外部 PR 被授予发布权限。把失败归入具体阶段,收集对应运行记录,再决定是否重跑、修正测试、固定输入或停止传播制品。不要以一页绿色状态代替完整业务验收。
分层练习:先答,再展开反馈
1. CI 的输入至少要能指向什么?
指向被检查的确切提交和声明过的工具链、依赖与数据。
2. 本地一次测试通过能直接证明 PR 提交通过吗?
不能;本地文件、工具链或数据可能与提交/独立环境不同。
3. 工作流、job、step 的包含关系是什么?
事件触发工作流;工作流含任务;任务在 runner 中按序运行步骤。
4. 同一工作流里的两个 job 自动共用磁盘吗?
通常不共用,需明确上传下载或重建输入。
5. 为什么绿色状态必须连到提交 SHA?
新提交可能改变代码;旧检查不能证明新内容。
6. `go test ./...` 通过证明 B 设备收到消息了吗?
不能,只证明所选包中的测试在该环境完成;设备确认另需真实链路证据。
7. 9 个 ASCII 字符与 3 个常见汉字在本题为什么都要测?
本题上限按 UTF-8 字节数;两者可都是 9 字节,字符数不同。
8. 第 10 字节的请求被拒绝后,还要检查什么状态?
内存消息列表不应因失败而改变。
9. `go build ./...` 成功便说明已有指定路径的二进制吗?
不能。要指定主包与 -o 输出路径,并确认该次构建的文件。
10. `go.mod` 与 `go.sum` 各解决什么问题?
前者描述模块及版本要求;后者保存依赖内容的校验值。两者都不固定整个运行环境。
11. 相同 Git SHA 为什么不保证产物字节相同?
工具链、目标、CGO、构建参数或生成输入可能不同。
12. 缓存命中等于测试通过吗?
不等于。缓存只是省时,检查仍须执行。
13. 冷缓存失败、热缓存通过应先查什么?
依赖下载、遗漏的生成步骤或未声明的本地输入。
14. 为什么不能用真实聊天记录作本章测试数据?
它会引入隐私、权限与不可重建状态;虚构受控数据已足够验证规则。
15. 同一提交首次失败、重跑变绿能说修好了代码吗?
不能。代码未变,须比较环境、数据和日志以定位不稳定来源。
16. 测试报告与模块缓存,哪一个是这次运行的证据制品?
报告可作为制品;模块缓存是可删除的加速输入。
17. 文件 SHA-256 相同证明业务功能正确吗?
不能。摘要只能比较文件内容,功能仍需对应测试和验收。
18. 跨 job 交接二进制要记住哪两个动作?
显式上传与下载,并保留运行及提交身份。
19. 外部 PR 为什么不应拿发布密钥运行改动后的脚本?
提交代码可能是不受信输入;高权限凭据会被执行的代码读取或滥用。
20. PR 构建出文件就算发布了吗?
不算。构建与上传候选制品不等于批准和部署。
21. 只有单元测试时,接口合同的状态怎样写?
写“尚无可运行接口测试”;不能虚构其通过记录。
22. P2 CI 绿色,哪些 IM 业务结果仍未知?
持久化、设备送达、多设备同步和真实客户端体验等尚无对应检查与观测。
来源与下一步
- GitHub Actions 基础概念与工作流语法:事件、工作流、任务、步骤和环境。
- GitHub:工作流制品与依赖缓存:跨任务交接与缓存的不同职责。
- GitHub:安全使用与受信边界:最小权限与外部评审代码。
- Go 模块参考与go 命令:依赖校验、测试与构建命令的语义。
本章完成后,S2 已能把需求、请求链、观测与最小自动验证连起来。下一步按学习路线进入06.01 关系模型与业务身份,补 S3 的数据/存储与可信业务前置;10.07 的并发/属性验证及 10.08 的依赖质量工具在 S4 深化,10.10 的发布管理留到 S6。