⚠️ 法律与用途声明:本文讨论 CI 构建产物的人工晋升门禁设计,基于开源工具(Conan 2 / GitHub Actions / Artifactory CE),属于经验分享。所有企业名、项目名、内部脚本与字段命名均已脱敏,代码示例中的标识符均为占位。读者须遵守所在地区法律法规。

一、构建过了,先别急着上货架
上一篇(NO.A8)留了个扣子:构建成功不等于可以发布,中间那道人工确认省不掉。这篇就把这件事讲透。
很多团队在 CI 做到"构建自动通过"之后,会很自然地想下一步——既然构建都绿了,那就顺手自动发布呗,还省个人工。这个念头很诱人,但真照做的人,多半挨过揍。
这一篇就聊:为什么构建产物只能算"候选",凭什么要人来看一眼才放它进真源,以及这道人工门禁具体在拦什么。
二、构建成功不等于能上架
"构建成功"这件事,其实只证明了一件事:这套源码在这套环境里,能编出二进制。它没证明的东西有一大堆:
- 这个版本号对不对(是不是忘了 bump 版本,覆盖了已发布的包)
- 依赖是不是齐(本地构建碰巧命中了某个旧缓存,换个干净环境可能就缺)
- 源码归档全不全(recipe 声明了
exports_sources,但实际要归档的文件有没有漏) - 这个包该不该现在发(功能做完没、评审过没、有没有赶在发版窗口)

这些问题里,前几个机器还能查一查(版本号、源码归档),最后那个"该不该发"是彻头彻尾的人的判断——机器不知道你功能做没做完、该不该今天上。把发布全自动,等于把这些判断全交给了一个只会看"编译有没有过"的脚本。出过事的人都懂,这种事的代价,远比省下那一次人工确认要大。
所以我们的姿态是:构建自动、发布谨慎。构建可以全自动(机器干),但发布这一步,必须留一个人工确认。
三、候选区:构建产物先在评审台等着
构建成功的产物,不直接进真源,而是先进一个"候选区"——在我们的体系里,它叫构建登记表(Build Registry)。你可以把它理解成评审台:所有构建产物都在台上摆着,等着人来看。
一个候选产物大概长这样(脱敏后的结构):
<run_key>/
├── result.json # 构建结果:成功/失败、包信息、用了哪个工具链
├── package_refs.json # 这次产出的包引用列表
├── summary.md # 人看的摘要
└── artifacts/
└── package-folder.tar.gz # 包内容归档
而登记表里给每个候选记一条账,关键字段是这样(脱敏 JSON):
{
"run_key": "<run_id>-attempt-<n>",
"target": "<目标>",
"status": "success",
"promote_status": "pending",
"cache_status": "active"
}

注意 promote_status 这个字段,它就是门禁的钥匙。构建一完成,它是 pending(待确认);只有经过人工放行,它才会变成 promoted(已上架)。在变成 promoted 之前,这个包再多份副本散落在缓存里,都不算真源,都不被认作正式发布。
四、门禁三道检查
人工放行不是拍脑袋盖个章,是有一组明确检查的。把这套门禁写成伪代码,大概是这样:
def promote(run_key, target, approver):
record = build_registry.get(run_key, target)
# 门禁1:构建本身必须成功
assert record.status == "success", "构建未通过,拒绝放行"
# 门禁2:必须是待确认状态(防止重复发布)
assert record.promote_status == "pending", "已发布或已取消,别重复操作"
# 门禁3:源码归档必须齐全(保证可追溯)
ensure_recipe_sources(record) # 缺 exports_sources 就在这里兜底补上
# 三道都过 → 才允许上传真源 + 合并加速层
conan_upload_to_source(record.package_ref)
merge_into_cache(record.package_ref, target)
# 最后回写状态,记下是谁放的行
build_registry.update(
run_key, target,
promote_status="promoted",
approver=approver,
)

三道检查各管一件事:
- 构建成功:这是底线,没编过的东西没资格谈发布。
- 待确认状态:防止手滑把同一个包 promote 两遍,或者把已经取消的构建又翻出来发。状态机锁死,一个包只能被放行一次。
- 源码归档齐全:A8 讲过真源要带 recipe 和源码。这道检查负责确保源码真的归档了——有些项目 recipe 里忘了声明
exports_sources,门禁会在 promote 阶段兜底补上,绝不让一个"查不到源码"的包溜进真源。
这三道过了,才有资格谈放行。
五、签字放行:人工触发,指名道姓
门禁检查通过后,放行动作本身也是人工触发的——它不是一个自动跑的 job,而是一个必须由人手动发起的动作。伪代码意义上的触发长这样:
# promote 必须人工触发,而且要点名是哪次构建
on:
workflow_dispatch:
inputs:
run_id: { required: true, description: "要发布哪次构建" }
target: { required: true, description: "发布到哪个目标" }

这个设计的关键不在技术,在姿态:系统不会自己发,必须有人主动触发。这个人就是签字的人——他点了那个按钮,就等于在放行单上签了名,后面出了问题,登记表里的 approver 字段记得清清楚楚是谁放的行。
放行之后,包才正式上传到真源(Artifactory),同时合并进加速层缓存(NO.A8 讲过的 promote 派发)。到这一步,一个构建产物才真正从"候选"变成了"正式发布的制品"。
六、为什么是人工,不是自动
有人会问:门禁里那三道检查,不都是程序能判的吗?既然程序能判,为什么还要人点一下?
因为那三道检查是必要条件,不是充分条件。它们能拦住"明显有问题的发布",但拦不住"技术上没问题、但这时候不该发"的情况——比如功能其实还没评审完、比如临近发版节点不想动、比如这次改动有风险想再压一压。这些判断,机器做不了,只有经手的人知道。

所以人工门禁的本质不是"机器不行才让人来",而是把"该不该现在发"这个判断,明确留在人的手里。机器负责把能自动检查的都查了(三道门禁),把剩下的、需要人判断的,明确地交还给人。这不是效率上的妥协,这是责任上的清醒——发布是一件有后果的事,有后果的事,就该有个明确的人来承担。
七、几条攒下来的判断
构建自动,发布谨慎。构建可以全自动,但发布必须留人工。这一条是整套门禁的基调。
候选不等于真源。构建产物先进候选区(promote_status=pending),没放行前再多副本都不算正式发布。
门禁检查的是必要条件。三道检查(成功/状态/源码)拦住明显问题,但"该不该发"还得人判断。别指望机器把发布决定也做了。
放行要留名。谁点的 promote,登记表里记着。出问题能追溯到人,权限和责任才对得上。
源码归档是可追溯的底线。门禁里专门查 exports_sources,没声明的兜底补上,绝不让查不到源码的包进真源。
这套门禁立住之后,最踏实的感觉是:真源里每一份包,都是有人看过、点过头、签过字的。它可能不是最省事的发布方式,但它是最不会半夜被叫起来擦屁股的方式。省事的方法,代价往往在后面;麻烦一次的人工确认,换来的是真源长期的可信。
到这里,制品层(NO.A5 到 NO.A9)就串完了:从安家(A5)、配钥匙(A7)、分清原件复印件(A8)、到这道人工放行门(A9)。下一篇,我们看看怎么把这些构建和发布的状态变得可观测:构建登记表 Build Registry——怎么让每一次构建、每一份缓存的来龙去脉,都看得见、查得着。

参考链接:
[1] Conan 2:上传包与发布 https://docs.conan.io/2/reference/commands/upload.html
[2] Conan 2:exports_sources 与源码归档 https://docs.conan.io/2/reference/conanfile/attributes.html
[3] GitHub Actions:workflow_dispatch(人工触发) https://docs.github.com/en/actions/how-tos/manage-workflow-runs/dispatch-a-workflow
[4] JFrog Artifactory CE(制品真源) https://jfrog.com/open-source/
[5] 状态机与幂等发布(防止重复 promote 的思路) https://martinfowler.com/eaaDev/EventSourcing.html
[6] Conan 2:recipe revision 与可追溯性 https://docs.conan.io/2/reference/commands/list.html