Caveman CLI 发布流程全解:@caveman-ai/cli 的双轨版本线、签名校验与 npm 发布顺序
本文基于 packages/cli/PUBLISHING.md 详解 Caveman CLI 的完整发布规范:cli-v* 与 bin-v* 双发布列车(release train)如何分工、二进制工件如何用密钥签名并做本地校验、以及发布顺序为何是"承重墙"(load-bearing)。读完你可以掌握:从 BINARY_RELEASE 打钉(pin)到 npm 发布再到跨平台冒烟测试的全链路操作,以及 tarball 内容边界、npm --prefix 陷阱等源码级细节。
发布前提与包名约定
仓库要求创始人(founder)签核后方可发布,且必须从经过审查的仓库检出(reviewed repository checkout)发布,绝不允许从一个解包的 tarball 发布——这是防止"改完再发"污染构建链的第一道闸。
包名与入口约定(见 package.json):
- 以
@caveman-ai/cli发布,并始终带npm publish --access public; - 永远不要用裸名
caveman发布——npm 上的该名字属于无关项目; bin字段同时安装两个可执行入口:caveman和cave,两者都指向同一入口dist/index.js;- 包的运行环境要求
node >= 22.13(engines.node),且dependencies为空对象——这是一个零运行时依赖的 JS CLI。
双发布列车:cli-v* 与 bin-v*
Caveman 的产物分为两种,走两条互不干扰的版本线:
| Tag | 负责内容 |
|---|---|
cli-v* |
手动发布本零运行时依赖 JS CLI 到 npm |
bin-v* |
通过 release-binaries.yml 发布签名的 Go 伴生二进制 |
两条线的关键点:
- CLI 的 semver 不隐含二进制版本。当前仓库中,packages/cli/BINARY_RELEASE 的内容是
bin-v1.1.4,而 package.json 的 CLI 版本是1.3.1——两者独立演进。这个文件独立钉住(independently pin)CLI 构建所消费的二进制发布版本。 - Caveman Cloud 的服务端镜像从 Caveman Cloud 仓库发布,不在本仓库范围内。
- 二进制工作流把编译产物和签名清单发布到本仓库的 Releases(
https://github.com/JuliusBrussee/caveman/releases/download前缀)。
二进制钉住是如何进入 CLI 代码的
钉住不是"文档约定",而是被编译进 CLI 产物的。构建脚本 scripts/gen-binaries.mjs 会:
- 读取 packages/cli/BINARY_RELEASE,用正则
^bin-v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$校验其必须是bin-vsemver tag,否则直接抛错; - 读取 packages/cli/BINARY_SIGNING_PUBKEY.pub,校验其为换行终止的单 PEM 公钥块;
- 生成
src/binaries.generated.ts,导出三个常量:BINARY_RELEASE、BINARY_RELEASE_BASE_DEFAULT(releases download 前缀)、BINARY_SIGNING_PUBKEY(公钥全文,以字符串常量形式进入 JS 包)。
该脚本还支持 --check 模式:若生成的常量与已提交的 binaries.generated.ts 不一致,报错"binary release constants are stale"——即 BINARY_RELEASE 改钉后必须重新生成并提交,CI 可以借此拦截过期常量。
签名与校验:密钥体系全链路
仓库环境 binary-release 只需要一个创始人控制的 secret:CAVEMAN_BINARY_SIGNING_PRIVATE_KEY_PEM,它必须与仓库中提交的公钥(BINARY_SIGNING_PUBKEY.pub)匹配。上传使用工作流自身的 GITHUB_TOKEN 并配 job 级 contents: write 权限,不存在跨仓库 PAT。Windows 工件使用 win32/{arm64,amd64} 目录命名,且以 .exe 文件名安装。
这条信任链在代码中是闭环的:
发布端(scripts/sign-binary-checksums.mjs):
- 从环境变量读取私钥,先做公钥归一化比对:
normalizePublicKey(privateKeyPEM)与提交的公钥做 SPKI PEM 规范化比较,不一致则拒绝签名——防止配错密钥; - 生成 sigstore 格式的签名 bundle(mediaType 为
application/vnd.dev.sigstore.bundle.v0.3+json,含 SHA-256 消息摘要与 RSA 签名),写入checksums.txt.keysig,文件权限0o600; - 写盘前先对生成的 bundle 本地自验,自验失败即中止。
消费端(verifyChecksumSignatureBundle,同文件):校验 mediaType、摘要算法 SHA2_256、digest 相等,最后用公钥 verify("sha256", ...) 验证签名。
用户端:caveman setup --install 用编译进 CLI 的公钥校验 checksum manifest 签名,再对流式下载的每个工件逐个验证 SHA-256,之后才执行原子安装。这个本地检查覆盖了"公钥签名的 manifest 链",即 manifest 被公钥签名、工件哈希在 manifest 内、工件内容对得上 manifest。
release-binaries.yml 还叠加了发布侧的防篡改闸门:
- 触发条件为
bin-v*tag,job 绑定binary-release环境(人工审批); - 第一步用
gh api确认 tag 必须是 annotated 且 GitHub 验证verification.verified == true,且 tag 目标是origin/main的祖先; - 强制
packages/cli/BINARY_RELEASE与当前 tag 名严格相等(tr -d '\r\n'后比较),保证"钉住的版本就是正在发布的版本"; - 构建后要求完整的 36 工件矩阵:
build-release-binaries.mjs --list的输出必须与dist/binaries/下实际文件逐一相等; - 发布前检查同名 Release 不存在,拒绝覆盖已有 Release。
发布顺序:为什么资产必须先于 npm 包存在
PUBLISHING.md 明确写道:"Ordering is load-bearing: assets must exist before npm package naming them."(顺序是承重的:资产必须先于引用它们的 npm 包存在。)完整顺序如下:
1. 钉住即将发布的二进制 tag
把 packages/cli/BINARY_RELEASE 设置为即将切出的二进制 tag,运行 CLI 测试并提交生成的常量(即 binaries.generated.ts)。同时要求最新一次 profile-contract Actions 运行是绿的——该工作流(profiles.yml)会拿真实压缩能力去验证随包发布的 agent profiles(校验 profile 注册表、要求生成物与 PR diff 一致、跑 fake-harness 矩阵)。
2. 在本仓库切出二进制 tag
annotated、签名、位于 main 上。release-binaries.yml 在 binary-release 环境审批通过后完成构建、签名、发布到本仓库 Releases。
3. 匿名 HTTP 验证
在没有 GITHUB_TOKEN、GH_TOKEN、~/.netrc 或未认证 gh 的环境下,验证全部 36 个二进制文件和 2 个 manifest 文件均返回匿名 HTTP 200。这一步模拟的正是终端用户的真实下载路径——任何登录态可见但匿名不可见的资产都会在此暴露。
4. 版本号 bump、切 CLI tag、发布 npm
node agents/compile.mjs
pnpm --dir packages/cli test
node agents/probe-installed.mjs --all --json
(cd packages/cli && npm pack --dry-run)
(cd packages/cli && npm publish --access public)
其中 probe-installed --all 是发布机证明(release-machine proof):缺失、损坏或版本漂移的二进制都会使闸门失败;CI 会独立安装每个钉住的 profile,因此本地全局安装无法掩盖 profile 漂移。
5. 干净机器冒烟测试
在 macOS ARM、Linux x64、Windows x64 三平台上,钉死到确切的 npm 版本号执行:安装 CLI → caveman setup --install → caveman --version → caveman wrap --off → 一次压缩加字节级精确的取回(byte-exact retrieval)→ 卸载且不损伤用户配置。
失败后的回退策略:冒烟失败则对该 npm 版本执行 deprecate、回滚 quickstart 安装文案,调查清楚后再谈下一次发布。永远不要覆盖已发布的版本或删除普通二进制 Release;只有在存在活跃安全风险时才移除资产。
包内容边界:tarball 里有什么、不能有什么
npm tarball 保持"纯 JS":只包含 dist/、README.md、LICENSE 和包元数据(对应 package.json 的 files 字段:["dist", "README.md", "LICENSE"])。
关键设计决策:
- 不存在
postinstall网络拉取。用户显式运行caveman setup --install来获取 Go 伴生二进制——因为包管理器的脚本策略(ignore-scripts等)可能会静默拦截运行时安装脚本,显式命令则不受此影响。 - Go 二进制本身不进 npm 包,而是通过 Releases 资产 + 签名 manifest + 本地 SHA-256 校验的通道交付。
发布前检查清单与 tarball 审查
node agents/compile.mjs
pnpm --dir packages/cli test
node agents/probe-installed.mjs --all --json
(cd packages/cli && npm pack --dry-run)
打包后人工审查 tarball,它必须包含:编译进 dist/index.js 的二进制发布常量(BINARY_RELEASE、BINARY_RELEASE_BASE_DEFAULT、BINARY_SIGNING_PUBKEY,由 gen-binaries.mjs 生成);它不得包含:私钥、源码树、Go 二进制、任何凭据。
一个真实的 npm 陷阱
PUBLISHING.md 特别警告:不要使用 npm --prefix packages/cli pack 的写法。npm 11 在 pack/publish 场景下可能忽略 --prefix,转而打包仓库根目录的 package——结果就是把错误的包发出去。所以正确姿势是子 shell 内执行 (cd packages/cli && npm pack ...),与上面命令块中的写法一致。
小结:这套发布规范的三个设计支柱
- 双轨解耦:
cli-v*(npm,手动)与bin-v*(Releases,自动化工作流)各自带版本,靠BINARY_RELEASE单文件钉住,且钉住值被正则校验并编译进 CLI 二进制常量; - 端到端可验证的信任链:创始人私钥 → 工作流环境审批 → 签名 manifest(sigstore bundle)→ 编译进 CLI 的公钥 → 用户端
setup --install的 manifest 签名校验 + 逐工件 SHA-256;每一环都有对应的代码实现(sign-binary-checksums.mjs、gen-binaries.mjs、release-binaries.yml); - 不可逆发布纪律:匿名 200 验证先于 npm 发布、三平台钉版本冒烟、失败只 deprecate 不覆盖、tag 必须 annotated 且 GitHub 签名验证通过——共同构成"发出去的东西必须可追责"的底线。
对维护者而言,这篇规范配合 packages/cli/tests 下的 release-binaries.test.mjs、binary-installer-platform.test.mjs 等安装器测试,覆盖了从 tag 切出到用户落盘的每个关键断言点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00