你已经有一个产出 wheel 的构建任务,也有一个应该接收它的私有仓库。错误做法是让第二个任务从某个分支重新构建,然后把结果称作同一个发布版本。要移动的是那个已经测试过的文件,用摘要给它命名,并把上传凭据留在内部。下文的流程跟踪一个构件,从公共 CI 运行器走到内部发布者。

在源码所在之处构建

在已经拥有源码、工具链和测试套件的任务里执行构建。私有仓库并不要求这个任务能访问它。把构建前端安装到项目已经在用的环境里,然后如果项目同时发布两种分发包,就两种都产出。

python -m build --sdist --wheel --outdir dist
python -m twine check dist/*

输出目录要保持小且可丢弃。值得继续往后传的只有下一个任务要发布的文件以及它们的散列值。其余一切,包括虚拟环境和缓存,都可以从记录下来的源码修订重新构建。精简的构件也更容易审查,因为其中每个文件都是别人可以检查的文件。

记录是什么产出了它们:从文件名得到的包名和版本、源码修订,以及创建它们的 workflow 运行。仅凭版本字符串无法识别文件,因为同一版本可以从不同提交构建两次。把修订写在构件说明里文件名旁边,并把这条记录当作交接的一部分。

搬运字节,而不是重新构建的指令

第二次构建就是第二个产品。时间戳、打包进来的依赖和工具版本都可能改变字节,所以重新构建需要自己的测试证据和自己的审查。正确的做法是晋级那些被审查过的文件。

把分发包上传到 CI 平台提供的构件存储,并让内部任务引用那个具体的构件,而不是分支名。

sha256sum dist/*.whl dist/*.tar.gz
# keep the digests beside the run id in the artifact description

如果你的平台支持签名的来源信息,就让它为该构件出具证明,之后核对预期的仓库、workflow 和源码修订。GitHub 的证明文档描述了这种形态的声明。有效的签名是来源证据,既不是漏洞扫描,也不是授权决定。

上传前先在内部核验

内部任务的第一步是检查收到的文件就是被批准的那个文件。要把摘要与构建任务留下的记录比较,而不是与根据到达之物重新算出的值比较。

sha256sum -c digests.txt
python -m twine check dist/*

然后读元数据。包名、版本和声明的依赖应当与你批准的审查一致。一个名字看起来正确但依赖钉版已经改变的 wheel,是贴着同一标签的另一个包。如果元数据变了,就把该构件视为未获批准,并查清是哪次构建产出了它。

如果摘要不匹配,就停下来。不要上传,也不要就地重新构建来修这个文件;那会抹掉传输途中发生了变化这一证据。不匹配意味着构件存储、下载步骤或记录有错,而这三者中的任何一个都应当在发布版本之前给出解释。

处理版本重复的回应

私有仓库通常拒绝覆盖已存在的版本,而这种拒绝是特性。当上传因为版本已存在而失败时,查看仓库中该版本的记录,并把它保存的文件与你打算发布的摘要做比较。

版本已存在可能意味着先前的一次上传成功了而响应丢失了,也可能意味着有人在同一个版本号下发布了不同的内容。

不要删除该版本,也不要为了让错误消失而编出一个新版本号。与发布负责人一起判断已存储的内容是否可以接受,然后记录结论。如果内容不同,你面对的是版本管理问题,而不是流水线问题。

把上传凭据留在单侧

只有内部发布者需要仓库凭据,而且只在执行上传的这段时间里需要。把令牌限定到目标仓库和上传操作,存放在内部系统的密钥库中,并让它完全不出现在构建任务里。如果构建任务能够发布,那么任何能改动构建的人都能发布。

还要限定哪些内部 workflow 可以启动发布者。需要审批的手动触发是发布版本的合理默认做法;向任意分支推送则不是。

完成的发布应当留下三样东西:分发包、它们连同源码修订的摘要,以及显示同一版本的仓库记录。

相关文章