为什么我不再使用 release-it 了
我在 Univer 的相关仓库里用了接近三年的 release-it。标准配置是 release-it@17.1.1,配上 @release-it-plugins/workspaces@4.2.0 和 conventional changelog 插件。它们替我创建新版本号、changelog、commit、tag 和 push,长期用下来并没有出现过任何致命问题。
随着 npm 生态日益猖獗的供应链投毒攻击,我开始把工作重心放在了依赖安全上。
Dependabot 在近几年内,每隔上一阵就会报告 release-it 依赖树里出现了漏洞。作为核心,release-it 通常很快就会得到修复并更新,而我所需要的 @release-it-plugins/workspaces 社区插件却经常跟不上。
release-it 其实修得很快
2026 年 3 月 13 日,undici 6.23.0 同时命中五份安全公告。这个版本是 release-it 19.2.4 的直接运行时依赖。站在业务仓库里看,它则是依赖图里的次级依赖。
| 公告 | 类型 | 当前等级 |
|---|---|---|
| GHSA-f269-vfmq-vjvj | 恶意 WebSocket 长度导致客户端崩溃 | High |
| GHSA-2mjp-6q6p-2qxm | 重复 Content-Length 引起请求走私 | Moderate |
| GHSA-vrm6-8vpv-qv8q | WebSocket 解压炸弹耗尽内存 | High |
| GHSA-v9p9-hfj2-hcw8 | 非法压缩参数触发未捕获异常 | High |
| GHSA-4992-7rv2-5pvq | upgrade 参数 CRLF 注入 | Moderate |
release-it 的响应确实比较及时。与之相关的修复 PR #1285 在第二天就被提了上来,3 月 24 日合入,把 undici 从 6.23.0 升到 7.24.3。大约两小时后,release-it@20.0.0-0 发布。稳定版 20.0.0 在 4 月 15 日发布,距离公告公开 33 天。
这里需要补充说明下前面也提到过的实际风险问题。release-it 实际上只从 undici 引入了 Agent,用于处理 GitLab 的自定义 TLS。它压根没使用 WebSocket,也没有把不可信输入传给 upgrade 或底层的 headers 数组。也就是说,对于 Univer 的开发者和其用户来说,这些漏洞并没有真实影响。
虽然无关痛痒,但暴露出来的核心问题是,release-it 的一些扩展能力来自另一个维护节奏不同的项目。只要安全修复跨了主版本,最慢的 peerDependencies 或者生态插件就会决定整个发布链能不能升级。
当时使用的 @release-it-plugins/workspaces@5.0.3 只接受 release-it ^17 || ^18 || ^19。2026 年 6 月 19 日,插件时隔 364 天发布 6.0.0,peerDependencies 仍然停在 19。即使到了 7 月 31 日,支持 release-it@20 的 issue #159 依旧没有关闭,你无法在不 override 的情况下把 release-it 升到 20。
并且这不是偶发的一次:
| release-it 主版本 | 核心发布时间 | workspaces 首个支持版本 | 等待时间 |
|---|---|---|---|
| 17 | 2023-11-11 | 4.1.0(2024-01-15) | 65 天 |
| 18 | 2025-01-06 | 5.0.0(2025-06-18) | 163 天 |
| 19 | 2025-04-18 | 5.0.0(2025-06-18) | 61 天 |
| 20 | 2026-04-15 | 截至 2026-07-31 仍无 | 107 天以上 |
仓库历史也留下了蛛丝马迹。2025 年,Univer Presets 曾把 release-it 19 降回 17,等 @release-it-plugins/workspaces@5 发布后才重新升级。我维护的另一个仓库在 2026 年 4 月 15 日升到 release-it@20,三天后又回到 19.2.4。当时 lockfile 里的 @release-it-plugin/workspaces@5.0.3 又明确只支持到 19。
插件自己的依赖也有相同的问题。PR #158 记录了 walk-sync 链上的 minimatch 和 brace-expansion 漏洞,相关公告公布后 85 至 122 天,插件才在 6.0.0 中更新生产依赖。用户可以靠刷新 lockfile 或 override 提前修,不必等插件发版,但这也意味着发布工具的依赖卫生最后落到了使用者手里。
与其再维护一个插件,不如推倒重来
我最初考虑过 fork @release-it-plugins/workspaces 插件。问题是,fork 之后我要继续追随 release-it 的插件 API、依赖变化和发布行为,维护面并不会缩小。
于是我写了 Verso。它不是一个更通用的 release-it,只处理我真正用到的发布流程:更新 monorepo 里各个 packages 的共享版本号并推送 tag。仓库里如果混有 Rust 项目,也可以把指定的 Cargo.toml 和 Cargo.lock 一起更新。
安装只需要一个开发依赖:
pnpm add -D @amamo/verso{
"scripts": {
"release": "verso"
}
}大多数 pnpm workspace 甚至不需要写 package glob。Verso 会读取 pnpm-workspace.yaml。需要混合 Cargo 时,再加一份很短的配置:
[version]
cargo_manifest_paths = [
"path/to/foo/Cargo.toml",
"path/to/bar/Cargo.toml",
]
[changelog]
enabled = false
[git]
push = "atomic"这里的 Cargo manifest 路径只是占位。verso doctor --json 会检查配置、工作区版本和 Git upstream,下面是精简后的示意输出:
{
"ok": true,
"checks": [
{
"name": "git upstream",
"status": "pass",
"message": "ready to push to origin/main"
}
],
"packageCount": 4,
"currentVersion": "1.2.3"
}Verso 能做什么
Verso 的主要功能都围绕这条流水线:
- 自动发现 npm、pnpm 和 Yarn workspaces,也支持单包仓库;
- 检查所有发布单元是否使用同一版本,并按需更新
package.json、JSON5 或 YAML manifest; - 更新显式配置的 Cargo manifest 和最近的
Cargo.lock; - 从 conventional commits 生成 Angular 风格 changelog;
- 提供 patch、minor、major、alpha、beta、rc 和自定义 semver 选择;
- 支持
init、doctor --json、--dry-run --json,以及发布步骤之间的 hooks; - 创建发布 commit 和 annotated tag,再用
git push --atomic <remote> <branch> <exact-tag>一次推送。
--dry-run 不会写文件,也不会执行会修改状态的 Git 命令。正式执行时,commit 前出错就恢复文件和原有暂存状态。如果 git push --atomic 失败,本地的 commit 和 tag 会保留下来,处理完问题后可以直接重试。
push 成功以后,npm 包和 GitHub Release 都交给 CI,其中 npm 使用 trusted publishing。本地不需要保存 npm token 或 GitHub token。
我没有做的功能
Verso 并不是任何一个现有发布工具的平替,也有很明确的边界:
- 只支持统一版本,不计算独立 package 的 bump;
- 不改写 workspace 内部依赖范围;
- changelog 只有 Angular conventional preset;
- 不直接 publish registry,也不创建 GitHub Release;
- Cargo manifest 必须显式列出。
这些限制有一天可能会变化,就目前来说,它们是为了让工具更小、更专注、更容易维护。