发布
一个 Cloud Agent 发布由 release.json 和 recipe.tar.gz 两个文件组成。清单(manifest)里写明了:对应哪一个源 commit;每个自有镜像在镜像仓库里的摘要,以及各自支持的平台;沙箱的运行时目录;数据库修订号;部署协议;还有已经记录的兼容性检查结果。recipe 里是安装这些镜像要用的脚本和配置模板,有了它就不需要源码仓库。
下游发行版在自己评审过的锁定文件里,固定这份清单原始字节的 SHA-256;清单再反过来固定 recipe 归档和各个镜像。
文件 |
用途 |
|---|---|
对外发布的清单 schema,使用方用它来校验。 |
|
根据构建记录生成一份完整的清单。 |
|
把明确列出的 recipe 文件打包成一个可复现的归档。 |
|
校验发布配置,并生成绑定到镜像摘要的部署输入。 |
生成器检查什么,又故意不检查什么
生成器要求六个产物一个不少,摘要互不相同,平台必须正好是下面这样:
角色 |
变体 |
平台 |
|---|---|---|
|
— |
|
|
|
|
|
|
|
|
— |
|
|
— |
|
|
— |
|
镜像引用必须用本组织镜像仓库下的摘要。schema 会限制每种角色和每种 Web 变体各有几个,生成器写出清单之前会先用这个 schema 校验一遍。生成器还会检查每个运行时的 default 是否在它的 versions 里;这层关系没法用对外发布的 JSON Schema 表达,所以使用方需要自己另外检查。
版本字段都直接取自各自的来源:deploy_protocol 取自 deploy/DEPLOY_PROTOCOL,database_revision 取自 Alembic 的脚本目录,runtime_catalog 取自沙箱镜像的标签。不要再另外手工维护一份运行时版本列表。
生成器不会去查镜像仓库。下游的解析器必须自己确认:每个摘要都真实存在,带有声明的那些平台,并且每个平台的镜像上都有预期的 org.opencontainers.image.revision 和 io.hybridinference.artifact.role 标签。清单本身校验通过,并不能证明这些都成立。
生成发布
发布工作流会在三种情况下运行:推送到 dev、CI 成功完成,以及对一个 CI 已通过的 dev commit 手动触发。推送触发时可以和 CI 同时构建,但要等这个 commit 上最近一次推送触发的 CI 成功,才会真正发布。之后某次 CI 重跑成功,也可以再触发发布。
工作流的步骤:
确认这个 commit 在
dev上;如果已经有对应的发布,就不去动它。把
SOURCE_COMMIT设为完整的 commit SHA,构建每个产物并按摘要推送。然后把这个摘要拉回来,对拉回的镜像做冒烟测试,记下它的摘要、平台和标签。把六个构建单元的记录汇总起来,构建 recipe,写出清单。
对这几个候选文件本身,跑一遍控制平面的集成验收。
等 CI 成功后,创建一个标签为
agent-<full-sha>的草稿发布,上传两个文件,再把发布设为可见。上传失败的话,草稿会被删掉。
如果 CI 失败或被取消,已经构建好的镜像会留在镜像仓库里,但不会有对应的清单发布出来。发布工作流不会被 pull request 触发,只跑已经合并的代码。它只负责发布产物,不会部署到任何具体的环境。
如果要在仓库根目录手动生成,先把六个构建单元的 JSON 文件放进 cells/。把 SOURCE_COMMIT 设为它们完整的 40 位 SHA,把 PUBLISHER_RUN_URL 设为实际执行发布的那次 Actions 运行的 URL,然后用和工作流相同的命令生成:
mkdir -p release
uv run python -m deploy.release.publish_inputs assemble \
--cells cells \
--source-commit "${SOURCE_COMMIT:?Set the build commit}" \
--output release/inputs.json
uv run python deploy/release/build_recipe.py \
--output release/recipe.tar.gz --report
RECIPE_SHA256=$(sha256sum release/recipe.tar.gz | cut -d' ' -f1)
uv run python -m deploy.release.release_manifest \
--inputs release/inputs.json \
--recipe-url "https://github.com/HarvardMadSys/hybridInference-cloud-agent/releases/download/agent-${SOURCE_COMMIT}/recipe.tar.gz" \
--recipe-sha256 "${RECIPE_SHA256}" \
--publisher-run-url "${PUBLISHER_RUN_URL:?Set the publisher Actions run URL}" \
--output release/release.json
inputs.json 里有 source_commit、artifacts 和 runtime_labels。runtime_labels 是用 docker image inspect 读出来的沙箱镜像标签,和主机给 attempt 打标记时读到的内容一致。只有全部检查都通过后,清单才会一次性写出,不会出现写了一半的文件。
光能构建还不够,还得能启动
smoke.py 会把每个产物启动起来,并检查它的身份标签。CI 和发布用的是同一套代码;发布时检查的是从镜像仓库拉回来的镜像。
产物 |
冒烟测试检查的内容 |
|---|---|
控制平面 |
用 |
Web,两个变体都测 |
在选定的挂载前缀下能打开 |
Runner |
能导入自己的模块,并且带有 Docker CLI 和 |
沙箱 |
每个 Agent CLI 报告的版本,和镜像标签里声明的一致。 |
隧道 |
能运行入口程序;对端连不上时,会明确地报错退出。 |
测试还会检查 org.opencontainers.image.revision、io.hybridinference.artifact.role,以及 Web 变体的 io.hybridinference.artifact.base_path。Dockerfile 里修订号的默认值是 unknown,发布时必须传入源 commit。这些检查只能说明镜像能正常启动,说明不了任务能跑成功、和真实网关能对接,也说明不了执行主机上的 Kata 已经就绪。
控制平面集成验收
acceptance.py 会在一个既没有应用源码、也没有 Dockerfile 的目录里,用解压出来的 recipe 安装候选版本。它以产物模式运行真实的控制平面部署脚本,拉取候选镜像的摘要,再通过 accept_artifacts.py 拿正在运行的容器和清单逐一比对。
验收能证明的 |
仍需在实际环境中验收的 |
|---|---|
不需要源码仓库、也不构建镜像就能安装 |
执行主机、Kata 和真实任务 |
能从镜像仓库拉取镜像,运行中容器的摘要与清单一致 |
其他 CPU 架构 |
在真实的 Postgres 上完成迁移和身份命名空间检查 |
网关契约和浏览器登录 |
API 和 MCP 监听器运行正常;Web 变体的标签正确 |
用户在控制台里看到的实际行为 |
配置里的镜像相互冲突时,在拉取之前就拒绝 |
由下游解析器对镜像仓库做独立校验 |
按确切 commit 判断的主机就绪栅栏,在这里本来就会超时,因为这次验收根本没有执行主机。测试断言的是:前面所有阶段都通过后,部署确实走到了这道栅栏。它不会绕过这项检查,也不会放松它。
正常部署要等过了栅栏才启动 web,所以验收时会用生成的 Compose 文件,加上 --no-build --pull never,手动把它启动起来。比对容器时,用的是镜像仓库的 RepoDigests 而不是镜像 ID,看的是角色标签而不是镜像仓库路径。工作流最终发布的清单和 recipe,与通过这次验收的文件逐字节相同。
镜像仓库的目录布局不属于这份契约
镜像是什么角色,看它的角色标签。仓库名和方便人看的标签只是存放上的约定;部署时认的是清单里的摘要。
包 |
内容 |
|---|---|
|
控制平面、两个 Web 变体、runner 和隧道 |
|
沙箱;因为里面打包了第三方 CLI,单独设置了可见性 |
冒烟测试通过后,每个摘要会打上 control-<sha>、web-agents-<sha>、sandbox-<sha> 这样的标签。这些标签只是方便人查看镜像,部署时不靠它们来选镜像。
buildcache-sandbox-2026-w42 这类标签是发布工作流的层缓存,不是镜像。只有这个工作流会写入它们,每个 ISO 周换一个新的。输入没变的层,在相邻两次发布之间摘要也不变,所以正在运行上一个发布的主机,只需要拉取有变化的那些层。
调整镜像仓库布局时,要确认复制过去的镜像是否保留了原来的索引摘要。重新创建索引或转换格式都可能让摘要改变,这时就要更新锁定文件并经过评审。在回滚窗口结束之前,旧地址要一直可用。
和设计文档不一样的地方
最初的发布设计已经是历史文档了。实际实现和它不同的地方有:
部署协议是 v4。 设计里写的是 v2,现在用的是
shared-live-exact-sha-v4。v3 引入了 schema baseline 的重写;v4 引入了单一沙箱网络,去掉了基于 Squid 的egress产物,并要求每台 runner 主机都配置AGENT_EGRESS_CHAT_NETWORK_POOL和AGENT_EGRESS_DNS_SERVER。v3 的部署脚本装不了 v4 的发布,回滚也不能跨过这条协议边界。五个 Dockerfile 产出六个镜像。 Web 的 Dockerfile 要构建两次,因为挂载前缀是编译进前端包里的。最初设计里的第七个产物,就是现在已经退役的 egress 镜像。
运行时标签沿用原来的命名空间。 运行时标签仍然叫
org.freeinference.runtime.*。要改名,需要单独做一次新旧名字同时读取的迁移。
部署 recipe
替换服务之前,先拿下游的锁定文件核对清单,拿清单核对 recipe 的 SHA-256,再解析并核对各个镜像的摘要。把 recipe.tar.gz 解压到一个发布目录里,会得到一个 recipe/ 目录,里面有部署辅助脚本、配置示例,以及不含 build: 的 Compose 模板。
两台机器都需要 Bash、带 Compose v2 的 Docker,以及 Python 3.12 或更新版本(recipe 的辅助脚本要用)。执行主机还需要事先配置好 Kata;部署过程不会帮你安装沙箱运行时。在每台机器上安装辅助脚本的依赖:
python3.12 -m venv /opt/cloud-agent/deployer
/opt/cloud-agent/deployer/bin/python -m pip install \
-r /opt/cloud-agent/release/recipe/deploy/release/requirements.txt
DEPLOY_ROOT 在不同的发布之间不要变。它下面的 .env、.deployed-sha 和 .shared-live-ready 描述的是这套长期存在的部署,而每次解压出来的 recipe 可以放在别的地方。全新安装时,创建这个根目录,参照对应 recipe 里控制平面或主机的 .env.example 填好 .env。已有的安装,则保留原来的配置、Compose 项目名、主机注册根目录和 Docker 数据目录。生成的 Compose 文件放在 DEPLOY_ROOT 下,这样相对路径的挂载还能指向原来的数据;edge 代理的配置则来自所选的 recipe。
在控制平面配置里,用 AGENT_WEB_VARIANT=root 或 agents 选择 Web 变体。不设置的话,就看 AGENT_WEB_BASE_PATH:为空或没设置时用 root,设为 /agents 时用 agents。
在控制平面机器上设置产物模式的环境变量:
export AGENT_DEPLOY_MODE=artifacts
export AGENT_RELEASE_MANIFEST=/opt/cloud-agent/release/release.json
export AGENT_DEPLOY_PYTHON=/opt/cloud-agent/deployer/bin/python
export DEPLOY_ROOT=/srv/cloud-agent-control
在执行主机上设置同一个发布和同一个解释器,但用它自己的根目录:
export AGENT_DEPLOY_MODE=artifacts
export AGENT_RELEASE_MANIFEST=/opt/cloud-agent/release/release.json
export AGENT_DEPLOY_PYTHON=/opt/cloud-agent/deployer/bin/python
export DEPLOY_ROOT=/srv/cloud-agent-host
commit 和自有镜像的引用都由清单提供。切换到产物模式时,要把以前覆盖镜像和 commit 的设置删掉;最终生效的 Compose 值只要有冲突,在拉取镜像或替换服务之前就会被拒绝。默认的 source 模式仍然可以用于基于源码仓库的部署。
按设计规定的顺序部署两台机器
用阶段(phase)来协调两台机器。在每台机器的 shell 里设好上面的环境变量,然后按顺序执行:
两边各自做准备。这两条命令可以在各自的机器上同时跑,不会替换任何正在服务的服务。
# Control-plane machine bash /opt/cloud-agent/release/recipe/deploy/control-plane/deploy.sh --phase prepare # Execution host bash /opt/cloud-agent/release/recipe/deploy/host/deploy.sh --phase prepare
在控制平面机器上准备数据库:执行迁移,并检查身份命名空间。如果是从零开始的机器,还会启动主机就绪探测要用到的 API 和 MCP 监听器;已有的监听器保持不动。
bash /opt/cloud-agent/release/recipe/deploy/control-plane/deploy.sh --phase prepare-database
激活执行主机:排空并替换 runner,检查新 runner,记录这次部署,并发布这台主机的新代次。
bash /opt/cloud-agent/release/recipe/deploy/host/deploy.sh --phase activate
激活控制平面:必须先通过就绪栅栏,对外的服务才会切换过去。
bash /opt/cloud-agent/release/recipe/deploy/control-plane/deploy.sh --phase activate
不加 --phase 会把这一边的所有阶段一口气跑完。要协调两台机器上线,就得按阶段执行;整段脚本各跑一次,是没办法保证上面这个顺序的。每个阶段都可以重试。数据库准备阶段会一直掌管它自己启动的监听器;激活阶段会把上次失败时停掉的监听器重新启动,好让它们能收到就绪栅栏需要的心跳。
如果另一台机器失败了、决定放弃这次激活,就在控制平面机器上,把只为这次从零部署而启动的监听器清理掉:
bash /opt/cloud-agent/release/recipe/deploy/control-plane/deploy.sh --phase abandon
之前就在服务的监听器不受影响。镜像拉取和主机的 Kata 检查都在排空 runner 之前完成。迁移、身份命名空间核对、按确切 commit 的就绪判断、反向 sandbox manager 探测,以及延后发布的主机就绪状态,都按原定的部署顺序进行。只要就绪检查失败,新的控制平面服务就不会被激活。
脚本测试会在解压出来的 recipe 上运行,期间记录所有 Docker 调用并注入故障;如果环境里有真实的 Compose 环境变量解析器,也会用上。但这些测试证明不了某台具体的 Linux 主机、真实运行的 Kata,或者镜像仓库里的某个发布已经就绪;这些要在实际切换环境时逐一确认。
下游部署仍要自己负责的事
上游的发布工作流不会部署到任何具体的环境。使用它的发行版必须:
在评审过的锁定文件里固定清单的原始字节,并核对 schema、recipe 哈希、镜像仓库摘要、镜像标签和平台;
检查协议是否兼容,并查看清单里的
verified_upgrade_from和verified_rollback_to记录;列表为空,说明这条升级或回滚路径没有验证过;在实际的主机上执行准备阶段、Kata 检查和环境验收;
环境配置、部署记录和恢复材料都放在这个上游仓库之外。
之前验证过的清单、recipe、评审过的哈希和按摘要寻址的镜像,要和长期使用的部署根目录分开保存。产物模式下,主机上线时只拿到一份清单,没法自动选用更早的发布。请按备份与恢复手册检查数据库和部署协议是否兼容,然后用上一个验证过的 recipe,走同样的分阶段就绪流程。注意,上一个发布并不一定兼容已经装上的、更新的迁移。