OpenZeppelin Contracts 全自动发布流程解析:Changesets、release-vX.Y 分支与 release-cycle 工作流
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
OpenZeppelin Contracts 是用于安全智能合约开发的 Solidity 库(见 README.md),其版本发布并非人工打包上传,而是由 RELEASING.md 定义的一套完全自动化发布流程:编译、打包、发布全部在干净的 CI 环境(GitHub Actions)中完成。本文以仓库根目录的 RELEASING.md 为骨架,结合 release-cycle.yml 工作流、scripts/release 目录下的全部脚本与 .changeset 配置,逐步拆解 Changesets 变更集管理、release-vX.Y分支模型、候选版(rc)晋升正式版的状态机逻辑,以及 npm 发布与回合并入master的完整链路。读完本文,你将能完整理解该库"一次触发、全链路自动"的发布架构,并可直接复用到自己的开源项目中。
一、总览:为什么需要"全自动发布"
人工发布流程的常见风险是:本机环境差异导致构建产物不一致、漏跑打包步骤、发布版本与源码标签错位。RELEASING.md 明确说明,OpenZeppelin Contracts 的发布流程会:
- 在干净的 CI 环境(GitHub Actions)中执行编译、打包、发布;
- 通过 release-cycle.yml 工作流落地,减少人为错误与不一致;
- 保证发布过程一致且可靠(consistent and reliable)。
也就是说,维护者本地只需要完成日常开发(合并 PR),真正的发版工作全部交给 CI 状态机自动推进。
二、Changesets:CHANGELOG.md 的自动管理机制
2.1 变更集的基本约定
RELEASING.md 指出:每个对代码库相关的改动都必须附带一个 changeset(变更集),Changesets 工具(@changesets/cli)被用于CHANGELOG.md的自动维护。变更集本质上是存放在.changeset/目录下的一组 Markdown 文件,仓库当前就存在这样的实例,例如:
.changeset/brown-jokes-applaud.md.changeset/eip712-drop-string-fallback.md.changeset/erc7739-malformed-contents-descr.md.changeset/governor-prevent-late-quorum-max-deadline.md.changeset/paymaster-guarantor-effective-prefund.md
这些文件名是 Changesets 随机生成的短标识,文件内容描述改动影响(major / minor / patch)与变更说明。配置位于 .changeset/config.json。
2.2 PR 阶段的强制校验
在 PR 层面,仓库用 changeset.yml 工作流强制约束:
- 触发条件:针对
master分支的 PR,事件类型为opened、synchronize、labeled、unlabeled; - 若 PR 带有
ignore-changeset标签则跳过检查; - 检查命令为
npx changeset status --since=origin/master,并用fetch-depth: 0拉取完整历史,以便 Changesets 找到 merge-base 判断该 PR 引入了哪些未记录变更。
这意味着"每个改动带变更集"不是口头约定,而是 CI 强制门禁。
三、分支模型:release-vX.Y 与 rc → final 的晋升
RELEASING.md 定义了清晰的分支模型:
- 发布周期发生在名为
release-vX.Y的发布分支上; - 每个分支先以**发布候选(release candidate,rc)**身份开始,最终被晋升(promote)为正式版;
- 发布分支可通过从
mastercherry-pick 补丁的方式更新,旧版本发布时也可能直接在分支上提交; - 根据分支状态,这些提交会触发"新的 rc"或"补丁版本递增"。
原文档用 mermaid git 图描述完整生命周期,全文复刻如下:
从图中可提炼出三种典型流转:
- 开启新版本:从
master分出release-vX.Y,先发布vX.Y.0-rc.0; - 候选版迭代:
master上的修复(如 "Fix A")被 cherry-pick 到发布分支,产出vX.Y.0-rc.1,最终晋升为vX.Y.0; - 补丁维护:正式版合并回
master后,若旧分支需要修复("Patch B"),同样 cherry-pick 并产出vX.Y.1。
四、核心引擎:release-cycle 工作流的状态机设计
release-cycle.yml 是整套流程的中枢。它的触发方式只有两种:
push到任意release-v*分支;- 手动触发
workflow_dispatch。
工作流通过concurrency按workflow + ref互斥,避免同一分支并发发布。真正的"决策大脑"是statejob 中调用的 state.js:它读取当前分支名、触发事件类型、待处理 changeset 数量、是否处于预发布模式、npm 上是否已发布当前版本、是否已存在回合并 PR 等状态,然后输出 6 个决策标志(见 state.js 中的shouldRun*函数)。
4.1 状态标志与触发条件对照表
| 输出标志 | 含义 | 触发条件(对应shouldRun*逻辑) |
|---|---|---|
start | 开启新 rc | 位于master+ 手动触发 + 非机器人运行 |
promote | 晋升正式版 | 位于release-v*+ 手动触发 + 非机器人运行 |
changesets | 更新发布 PR | release-v*分支上的 push,或机器人触发的workflow_dispatch |
publish | 发布到 npm | release-v*push + 无待处理 changeset + npm 尚未发布该版本 |
merge | 创建回合并 PR | release-v*push + 非预发布 + 已是正式版本号 + 无待处理 changeset + 尚无回合 PR |
is_prerelease(全局变量) | 是否处于预发布模式 | 由@changesets/pre读取的preState.mode === 'pre'决定 |
其中"npm 是否已发布"通过请求https://registry.npmjs.com/<包名>/<版本>判断(见 state.js),这是防止重复发布的关键幂等手段。
4.2 状态机流转示意
工作流文件头部(release-cycle.yml)用 ASCII 图描述了四种状态之间的转换:
D: Manual Dispatch(手动触发) M: Merge release PR(合并发布 PR) C: Commit(推送提交) Development ─D→ RC-Unreleased ─M→ RC-Released ─C→ Final-Unreleased ─M→ Final-Released即:开发分支手动触发进入 RC 未发布态 → 合并发布 PR 后 RC 已发布 → 推提交(晋升)进入 Final 未发布态 → 合并发布 PR 后 Final 已发布。
五、六个 Job 的逐步拆解
5.1 state:状态判定(所有 job 的前置)
statejob 使用actions/github-script执行 state.js,把 5 个标志与is_prerelease通过setOutput暴露给下游,其余 5 个 job 全部needs: state,用if: needs.state.outputs.xxx == 'true'决定是否执行。
5.2 start:创建发布候选分支
当start == true(master上手动触发)时执行 start.sh,关键步骤:
- 运行
npx changeset status --output=...把变更集状态写入临时 JSON(注意:changeset status --output只接受相对路径,所以用realpath --relative-to=.转换); - 防御性断言:
jq '.releases | length'必须等于 1,确保一次只发布一个版本; - 从
newVersion中提取X.Y生成分支名release-vX.Y并git checkout -b; - 执行
npx changeset pre enter rc进入rc 预发布模式,提交 "Start release candidate" 并推送; - 通过 rerun.js 以
workflow_dispatch重新触发工作流,让流程自动滚到下一阶段。
注意pre enter rc会把 rc 前缀写入.changeset/pre.json,这也是后续is_prerelease判定的依据。
5.3 promote:退出预发布,晋升正式版
当promote == true(release-v*分支上手动触发)时执行 exit-prerelease.sh:
npx changeset pre exit rc git add . git commit -m "Exit release candidate" git push origin即调用changeset pre exit rc退出预发布模式,提交后推送,再由 rerun.js 重新触发工作流继续后续发布步骤。只有is_prerelease == 'true'时才执行该脚本(对应 workflow 中的if条件)。
5.4 changesets:生成并更新版本变更 PR
当changesets == true时,工作流:
- 用
fetch-depth: 0检出(确保拿到全部 tag); - 通过 set-changesets-pr-title.js 计算发布 PR 标题;
- 调用
changesets/action@v1,其中version命令指向npm run version; - 环境变量
PRERELEASE传入is_prerelease,决定是否走预发布版本递增。
npm run version实际执行 version.sh,该脚本在changeset version(按变更集更新 CHANGELOG 与版本号)之后,还依次执行三个后处理脚本:
- format-changelog.js:格式化 CHANGELOG.md 排版;
- synchronize-versions.js:同步各相关文件的版本号(含 contracts/package.json);
- update-comment.js:更新 PR 上的版本说明注释;
最后调用oz-docs update-version(来自@openzeppelin/docs-utils依赖)同步文档版本。合并这个 PR 之后,发布分支就进入了"已发布"状态。
5.5 publish:打包并发布 npm
当publish == true时进入npm环境(environment),先由 pack.sh 完成打包与 tag 决策:
cd contracts TARBALL="$(npm pack | tee /dev/stderr | tail -1)"pack.sh 中dist_tag()的决策逻辑非常关键,它根据三种情况选择 npm dist-tag:
| 场景 | dist-tag | 说明 |
|---|---|---|
PRERELEASE == "true"(预发布模式) | next | rc 版本走next标签,不污染latest |
版本号高于 npm 上的latest | dev | 新特性开发版本 |
| 其他(旧版本的补丁) | tmp | 旧分支补丁,临时标签发布后即删除 |
随后:
- 打包产物(tarball)作为 workflow artifact 上传,供下一步完整性校验使用;
- 执行 publish.sh 真正发布:先从 tarball 中解出
package/package.json读取包名与版本,写入.npmrc(刻意用转义的\${NPM_TOKEN}避免 token 落盘),再npm publish "$TARBALL" --tag "$TAG"; - 发布后的清理逻辑:
TAG == tmp:发布后立即删除该临时 tag;TAG == latest:若nexttag 恰好指向刚发布的版本,则一并删除next(避免残留指向正式版的 next 标签);
- 最后由 github-release.js 调用 GitHub API 创建 Release:tag 名为
v${version},正文通过正则解析 CHANGELOG.md 提取对应版本章节(extractSection函数按标题层级截取),并依据PRERELEASE标记是否 prerelease。
5.6 integrity_check:tarball 完整性校验
integrity_checkjobneeds: publish,下载上一步上传的 tarball artifact 后执行 integrity-check.sh(通过TARBALL环境变量传入路径)。这一步是对"CI 中打包并发布"的产物做落地校验:下载回来的产物必须与发布内容一致,防止打包与上传之间的不一致。
5.7 merge:创建回合并 PR
当merge == true(正式版发布且无待处理 changeset、尚未存在回合 PR)时:
- 以
merge/<ref>为名创建临时分支,强制推送到远端(git push -f); - 调用 GitHub API 创建以
master为 base 的 PR,标题为Merge release-vX.Y branch。
该 PR 由人工/自动化审核合并后,master即获得本次发布分支的全部变更,完成整个发布周期闭环。
六、与仓库其他机制的协同
6.1 包结构与发布物裁剪
发布以contracts/子目录为包根(该目录内有独立的 package.json),npm pack的产物只包含库源码。仓库根目录 package.json 中的files字段也做了白名单裁剪:
"files": [ "/contracts/**/*.sol", "!/contracts/mocks/**/*" ]即只发布contracts下的 Solidity 源码、排除 mocks(测试辅助合约不进包);prepack阶段由 prepack.sh 做最终整理。
6.2 升级版(Upgradeable)同步发布
仓库还维护着配套的@openzeppelin/contracts-upgradeable,其发布由独立的 release-upgradeable.yml 工作流驱动,与主包发布形成双轨(本文聚焦主库流程,升级版机制可查阅该工作流与 scripts/upgradeable 目录)。
6.3 发布质量前置门禁
全自动发布之所以"可靠",前提是日常 CI 已经把好质量关:
- checks.yml:编译、测试、lint 等常规检查;
- changeset.yml:强制每个 PR 携带变更集;
- 发布分支上 push 触发的
changesetsjob 会再次核对:若仍有待处理 changeset,publish与merge都会被状态机拦住(hasPendingChangesets为 true),确保只有 CHANGELOG 完整记录的版本才会被发布。
七、发布全流程时间线总结
把六个 job 串起来,一次完整发布的时间线如下:
- 开发合并到
master,PR 均通过 changeset.yml 强制变更集检查; - 维护者在
master上手动触发工作流 →startjob 创建release-vX.Y分支并changeset pre enter rc; - 分支 push 触发
changesetsjob,changesets/action依据npm run version(version.sh)生成版本变更 PR,合并后publishjob 打包(pack.sh 决策nexttag)并发布到 npm; - 维护者在
release-vX.Y上再次手动触发 →promotejob 执行 exit-prerelease.sh 退出预发布,重新触发后changesets生成正式版 PR,publish以latesttag 发布正式版; integrity_check校验 tarball,mergejob 创建回合并 PR,合入后master与正式版同步,流程闭环。
整套架构的核心价值在于:发布决策全部由 state.js 基于真实仓库状态(分支、事件、changeset、npm registry)计算得出,任何一步都可通过 push 或手动触发自动推进,从而在减少人为操作的前提下保证每个版本都经过"变更集记录 → CI 编译测试 → 打包校验 → npm 发布 → 回合并"的完整可审计链路。若要进一步研究,建议从 release-cycle.yml 与 scripts/release/workflow/state.js 两个文件入手,它们分别是流程的"地图"与"大脑"。
【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考