OpenHands Agent Canvas 的 release-please 自动化发布流程全解:从 Conventional Commits 到 npm 与 GHCR 双通道发布
本篇指南基于 OpenHands 仓库中 Agent Canvas(@openhands/agent-canvas)的官方发布技能文档 .agents/skills/release.md,完整还原该仓库以 release-please 为核心的 trunk-based 自动化发布体系:如何由 Conventional Commit 推导版本号、如何审查并"切割"(cut)一个 release PR、tag 推送后如何同时触发 npm 与 Docker 发布,以及发布失败时的四类典型排障手段。读完后你可以独立完成一次完整的版本发布,并理解仓库内 release-please-config.json、.release-please-manifest.json、.github/release.yml 及各发布 workflow 之间的联动关系。
一、总体机制:release-please 驱动的五步发布链路
Agent Canvas 的发布是 trunk-based(主干开发)且完全自动化 的,核心由组织级共享的可复用 workflow(OpenHands/release-actions 仓库)承载。整条链路可以概括为五个环节:
- PR 合入
main必须遵循 Conventional Commit 标题(feat、fix、perf、docs、chore、build、ci、refactor、style、test、revert)。.github/workflows/pr.yml 会对 PR 标题做 lint,并打上对应的type:标签;squash merge 时 PR 标题即 commit message。该 workflow 特意使用pull_request_target(而非pull_request)以保证 fork 来源的 PR 也能被处理,且故意不传secrets: inherit——因为可复用 workflow 只读 payload 中的标题、不检出 PR 代码,这样做可以让 App token 保持在作用域之外(见该文件头部的安全注释)。 - 每次向
main推送都会触发 release-please(.github/workflows/release.yml),维护一个标题为chore(main): release X.Y.Z的 draft release PR,累积自上一个版本以来合入的所有变更。 - release PR 预先写好全部版本号变更:
package.json、package-lock.json、config/defaults.json中的versions.agentCanvas,以及README.md/README.windows.md中用x-release-please-version注释标注的 Docker 镜像 pin。 - 将 release PR 标记为 "Ready for review" 是显式的"切割发布"信号:.github/workflows/release-ready.yml 监听
ready_for_review事件,向 Slack 频道#proj-agent-canvas发通知,并给 PR 打上release: ready标签。 - 合并 release PR 后,release-please 推送
vX.Y.Ztag(使用组织 release App token)并创建 GitHub Release。同一个 tag 推送随即触发 npm-publish.yml(发布到 npm)与 docker.yml(构建多架构 GHCR 镜像)。
版本号如何推导
下一版本号由上次发布以来合入的 Conventional Commit 类型决定:
| 提交类型 | 版本影响 |
|---|---|
fix |
patch(X.Y.Z → X.Y.(Z+1)) |
feat |
minor(X.(Y+1).0) |
任意类型带 ! 后缀,或提交信息带 BREAKING CHANGE footer |
major((X+1).0.0) |
docs / chore / refactor 等其余类型 |
仅出现在 release notes 中,单独不产生 release PR |
Release notes 由 .github/release.yml 按 type: 标签分组,配置为六个类别:Features(type: feat)、Bug Fixes(type: fix)、Performance(type: perf)、Documentation(type: docs)、Maintenance(type: chore/build/ci/refactor/style/test/revert)、Other Changes(其余标签兜底)。注意配置里 skip-changelog: true,即不维护 CHANGELOG.md,GitHub Releases 本身就是 changelog(仓库中现存的 CHANGELOG.md 只保留了最早的 1.0.0-alpha.2 历史条目)。
版本面(version surface)清单定义在 release-please-config.json 中,当前 .release-please-manifest.json 记录的已发布版本为 1.16.0,与 package.json 的 version 字段及 config/defaults.json 的 versions.agentCanvas 完全一致。
铁律:永远不要手工往 release PR 分支上提交任何东西 —— 该分支由 release-please 独占,且它会在每次 main 有推送时 force-push 该分支。
二、核心配置文件逐项解读
release-please-config.json
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"include-component-in-tag": false,
"include-v-in-tag": true,
"draft-pull-request": true,
"packages": {
".": {
"release-type": "node",
"changelog-type": "github",
"skip-changelog": true,
"extra-files": [
{
"type": "json",
"path": "config/defaults.json",
"jsonpath": "$.versions.agentCanvas"
},
{
"type": "generic",
"path": "helm/agent-canvas/Chart.yaml"
},
"README.md",
"README.windows.md"
]
}
}
}
各字段的作用:
include-v-in-tag: true:tag 带v前缀(如v1.16.0),这正是 npm-publish.yml 中tags: ['v*']触发条件的来源。draft-pull-request: true:版本 bump 以 draft PR 形式存在,"标记 ready" 才成为显式发布信号(下节详述)。release-type: node:按 Node.js 语义(SemVer)从 conventional commits 推导版本号。skip-changelog: true+changelog-type: github:变更说明直接写入 GitHub Release,不生成 changelog 文件。extra-files是关键细节:config/defaults.json走 JSON jsonpath 更新($.versions.agentCanvas)。该文件是全仓库"版本 pin 的单一事实来源",被scripts/dev-safe.mjs、scripts/dev-with-automation.mjs、docker/entrypoint.sh(经生成的 defaults.env)和.github/workflows/docker.yml共同读取——所以它必须和 npm 包版本同步 bump,否则 Docker 安装路径会 pin 到旧版。helm/agent-canvas/Chart.yaml必须用generic(注释驱动)类型。配置文件中的注释解释得很清楚:如果写成裸路径字符串,release-please 会解析为 CompositeUpdater,覆盖 chart 级version并在 js-yaml 重新序列化时剥离文件内所有注释。README.md与README.windows.md是纯文本,靠行内注释# x-release-please-version定位替换。当前仓库中可确认这三处标注:README.md 第 99 行(docker-compose 中的ghcr.io/openhands/agent-canvas:1.16.0)和 README.windows.md 第 15、24 行。
.release-please-manifest.json
{ ".": "1.16.0" }
记录当前已发布版本,release-please 每次运行据此对比、决定是否需要开/更新 release PR。
.github/workflows/release.yml:带"CI 守门"的 release-please 调用方
.github/workflows/release.yml 的触发分支为 main 与 release/**(维护分支,发布作用域限定在该分支),包含两个 job:
await-required-checks:只有当推送的 commit 标题匹配chore(*): release *(即 release PR 的 squash 合并 commit)时才等待——因为只有这次合并会真正打 tag。它轮询test-and-build (ubuntu)这个 check run 直到 success(最长 40 分钟,20 秒一次轮询)。原因是仓库的 "Release Tag" ruleset 要求被 tag 的 commit 上该检查必须为绿,否则 release-please 会与 CI 竞争、tag 推送会被 pre-receive 规则拒绝。非 release commit 的推送直接跳过等待。release-please:needs: await-required-checks,以secrets: inherit调用共享的release-please.yml可复用 workflow。文件注释强调这是必需而非优化:release-please 必须使用组织 App token(RELEASE_APP_ID/RELEASE_APP_PRIVATE_KEY)创建 release,因为只有它触发的 release 才会发出release: published事件;若退化为GITHUB_TOKEN,事件会被抑制,后续发布链路断掉。
三、Step 1:找到 Release PR
gh pr list --state open --label "autorelease: pending"
如果没有任何 release PR 打开,只有两种可能:上次发布以来没有可发布内容(无 feat/fix/breaking 合入),或者 release.yml 在 main 上正在失败:
gh run list --workflow=release.yml --limit=3
四、Step 2:审查 Release PR
打开 release PR,确认两件事:
- 标题中的版本号符合预期。它由合入 commit 的类型计算得出——如果看起来不对,回查上次发布以来各 PR 标题的 conventional 类型。想强制指定版本,向
main合入一个 commit message 带Release-As: X.Y.Zfooter 的提交即可(release-please 官方支持的覆盖机制)。 - 暂存的 bump 覆盖所有版本面:
package.json、package-lock.json、config/defaults.json、README.md、README.windows.md(以及helm/agent-canvas/Chart.yaml),且 release notes 列出了预期变更。
五、Step 3:切割发布(需先与用户确认)
在此停下来,先向用户确认再执行——把 PR 标记 ready 并合并后就会真实发布到 npm 和 GHCR。
gh pr ready <release-pr-number>
这一步触发 release-ready 门禁(release-ready.yml):Slack 通知落到 #proj-agent-canvas 频道,PR 被贴上 release: ready 标签。然后以 squash 方式(与其他 PR 相同)合并该 release PR。注意 SLACK_BOT_TOKEN 是可选的——没有它时门禁仍会打标签,发布照常进行。
六、Step 4:观察发布流水线
合并 release PR 会再次触发 main 上的 release.yml:等待所需检查通过后推送 vX.Y.Z tag、创建 GitHub Release;tag 推送随即点火两条发布流水线。监控命令:
gh run list --workflow=release.yml --limit=3
gh run list --workflow=npm-publish.yml --limit=3
gh run list --workflow=docker.yml --limit=3
从 npm-publish.yml 的实现可以看到 tag 推送后的完整动作:Node 24 环境安装依赖 → 跑全量测试 → 分别构建 app 与 library(npm run build / npm run build:lib,构建期注入 VITE_POSTHOG_API_KEY)→ 把生产遥测默认值烘进打包产物 → npm pack --dry-run 校验包内容 → 校验 package.json 版本与 tag 版本严格一致(不一致即失败,这是"tag 与包版本不符"的防线)→ 解析 dist-tag → 以 OIDC trusted publishing 发布:
npm publish --access public --provenance --tag ${{ dist_tag }}
dist-tag 的解析策略(源码注释中有完整说明):在第一个稳定版发布之前,所有版本都用 latest,保证 npm install @openhands/agent-canvas 始终解析到最新构建;一旦存在稳定版,预发布版本退回各自的 alpha / beta / rc 标签,只有稳定版保留 latest。之所以把 dist-tag 直接写在 publish 命令里而不是事后 npm dist-tag add,是因为 OIDC 的 trusted-publishing token 只覆盖 npm publish 这一次调用,额外的 dist-tag 请求会返回 E401。
docker.yml 同样监听 v* tag,负责构建并发布多架构的 ghcr.io/openhands/agent-canvas 镜像(main 分支与 PR 上则跑构建冒烟)。
七、Step 5:验证发布
# GitHub release
gh release view v<version>
# npm(发布传播约需 2 分钟)
npm view @openhands/agent-canvas@<version>
npm view @openhands/agent-canvas dist-tags # 稳定版应持有 latest
# Docker
docker pull ghcr.io/openhands/agent-canvas:<version>
三点交叉验证:GitHub Release 存在、npm 上可查询到该版本(且 dist-tag 符合预期)、GHCR 镜像可按版本号拉取。
八、Troubleshooting:四类典型故障
1. 合入 main 后没有 release PR 出现
只有 feat、fix 和 breaking 变更会产生 release PR。同时检查 main 上的 release.yml 运行记录——该 workflow 在设计上会故意失败当组织 secrets RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY 不可用时(GITHUB_TOKEN 兜底会打出一个永远不会触发发布流水线的 tag,因此宁可显式失败)。
2. 提议的版本号不对
版本来自上次发布以来的 conventional commit 历史。修复方式向前看:向 main 合入一个带 Release-As: X.Y.Z footer 的 commit 来固定下一个版本。
3. PR 标记 ready 后没有 Slack 消息
SLACK_BOT_TOKEN 按设计是可选的——门禁仍会打上 release: ready 标签,发布流程正常推进,只是没有通知。
4. package.json 版本与 tag 不一致
正常流程下不可能发生:release-please 在 release PR 里 bump package.json,tag 打在合并 commit 上,而 npm-publish.yml 会强制校验两者一致。如果真遇到了,说明有人手工推了 tag——解决办法是删除该 tag,把打 tag 的职责交还给 release-please。
九、关键实践要点总结
- Conventional Commit 标题即发布契约:每个 PR 标题的类型前缀同时决定
type:标签(release notes 分组)和版本号推导,改标题要慎重。 - "标记 ready"是发布闸门:draft PR +
ready_for_review事件监听构成了一个显式的人工确认点,避免了"合入即发布"的误操作;发布前必须走人工审查(版本号 + bump 覆盖面)。 - 版本面必须整体同步:npm 包、
config/defaults.json(Docker/dev 脚本的 pin 源)、Helm Chart、README 镜像 pin 由 release-please 在一个 PR 中原子更新,任何一处遗漏都会造成 npm 与 Docker 安装路径版本漂移。 - 发布链的信任根是组织 App token:
release: published事件驱动后续流程,App token 缺失时 workflow 故意失败而不是静默降级。 - 规则式防线:Release Tag ruleset 要求 tag commit 上
test-and-build (ubuntu)为绿,release.yml的await-required-checksjob 与npm-publish.yml的版本一致性校验共同保证"未经 CI 验证的 commit 打不出 tag、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