@baicie/release
v0.4.0
Published
`@baicie/release` 是用于 monorepo 固定版本发版的内部 release 工具包,适合 Zeus、Zeus UI,以及其他需要统一版本、统一 changelog、canary 发版、publish-only 补发的仓库。
Readme
@baicie/release 使用文档
@baicie/release 是用于 monorepo 固定版本发版的内部 release 工具包,适合 Zeus、Zeus UI,以及其他需要统一版本、统一 changelog、canary 发版、publish-only 补发的仓库。
它主要解决以下问题:
- 多包 workspace 固定版本发布;
- 自动更新 package version;
- 自动更新 root
package.jsonversion; - 自动生成统一
CHANGELOG.md; - 支持 changesets fixed group;
- 支持 npm publish dry-run;
- 支持跳过已存在版本;
- 支持 npm provenance;
- 支持 canary 版本;
- 支持发布失败后重新执行
publishOnly; - 支持 CI 中 tag 触发稳定发版;
- 支持 canary 发布后触发下游仓库兼容检查;
- 支持 Expo
app.json的expo.version/expo.android.versionCode同步。
一、核心发版模型
当前推荐采用两种模式:
type ReleaseMode = 'changesets-fixed' | 'workspace-fixed'1. changesets-fixed
适合使用 changesets 管理发版说明的 monorepo。
特点:
- 从
.changeset/config.json读取fixed包组; - 自动生成 synthetic changeset;
- 执行
pnpm changeset version; - 强制 fixed group 内所有包版本一致;
- 统一生成 root
CHANGELOG.md; - 清理 changesets 生成的包级
CHANGELOG.md; - 如果包级
CHANGELOG.md原本存在,会恢复原内容,避免误删历史文件; - 如果包级
CHANGELOG.md原本不存在,生成后会删除。
推荐用于 Zeus 主仓库。
2. workspace-fixed
适合不使用 changesets,只希望直接统一更新所有发布包版本的场景。
特点:
- 直接遍历 publishable packages;
- 统一更新 package version;
- 更新 root
package.jsonversion; - 可通过
afterVersion自定义后处理。
适合简单工具包仓库或不需要 changeset 文档流的项目。
二、安装与脚本接入
在目标仓库中安装:
pnpm add -D @baicie/release在根目录新增 release 配置文件,例如:
// release.config.ts
import { defineReleaseConfig } from '@baicie/release'
export default defineReleaseConfig({
repo: 'baicie/zeus',
repositoryUrl: 'https://github.com/baicie/zeus',
mode: 'changesets-fixed',
packageManager: 'pnpm',
workspace: {
roots: ['packages', 'integrations', 'create', 'devtools'],
ignore: ['@zeus-js/playground'],
publishable(pkg) {
return pkg.name.startsWith('@zeus-js/')
},
},
rootVersionPackage: '@zeus-js/zeus',
changelogFile: 'CHANGELOG.md',
changesets: {
configFile: '.changeset/config.json',
releaseFile: '.changeset/release.md',
requireChangeset: false,
readIgnore: true,
readFixed: true,
cleanupPackageChangelogs: true,
unifiedChangelog: true,
},
publish: {
access: 'public',
provenance: true,
skipExisting: true,
retry: 5,
},
precheck: {
commands: [
['pnpm', 'install', '--frozen-lockfile'],
['pnpm', 'typecheck'],
['pnpm', 'lint'],
['pnpm', 'test'],
['pnpm', 'build'],
],
},
canary: {
enabled: true,
prefix: 'canary',
tag: 'canary',
envName: 'CANARY_VERSION',
includeBranches: ['main', 'feat/**', 'fix/**', 'release/**', 'hotfix/**'],
dispatch: {
tokenEnv: 'ZEUS_UI_DISPATCH_TOKEN',
repository: 'baicie/zeus-ui',
eventType: 'zeus-canary-published',
payload({ version, sha }) {
return {
source: 'baicie/zeus',
version,
sha,
}
},
},
},
})然后在 package.json 中添加脚本:
{
"scripts": {
"release": "baicie-release",
"release:publish": "baicie-release publish",
"release:version": "baicie-release version:packages",
"release:canary": "baicie-release canary"
}
}如果项目当前是通过自定义 CLI 入口加载配置,也可以保持现有封装,只要最终调用 runRelease、runPublish、runCanary 即可。
三、稳定版本发版流程
推荐稳定发版流程:
pnpm release工具会执行:
- 解析目标版本;
- 确认 Git 工作区干净;
- 更新 workspace packages 版本;
- 更新 root
package.json版本; - 更新 lockfile;
- 执行 precheck;
- 生成 release plan;
- 可选执行 npm publish dry-run;
- 提交 release commit;
- 创建 git tag;
- push commit 和 tag;
- 由 CI 根据 tag 执行真实 publish。
四、指定版本发版
直接指定版本:
pnpm release --version 0.1.0发布 beta:
pnpm release --version 0.1.0-beta.0 --tag beta发布 rc:
pnpm release --version 0.1.0-rc.0 --tag rc五、dry-run
执行 dry-run:
pnpm release --version 0.1.0 --drydry-run 会执行大部分本地准备流程,包括:
- version 文件更新;
- changelog 更新;
- lockfile 更新;
- precheck;
- release plan;
- npm publish dry-run。
注意:完整 release dry-run 会改动本地 version、changelog 和 lockfile。执行后需要通过 git diff 检查,并手动还原。
git diff
git checkout -- .六、publishOnly 补发
publishOnly 用于版本文件、tag、构建产物都已经准备好,但 npm publish 失败后重新发布。
例如 CI 中 tag 已经创建,但 publish 因 npm 网络或 provenance 问题失败,可以执行:
pnpm release --publishOnly 0.1.0 --skipBuild建议补发前先 dry-run:
pnpm release --publishOnly 0.1.0 --skipBuild --drypublishOnly 会校验:
- 必须传入 version;
- version 必须是合法 semver;
- 当前 packages 的版本必须与传入 version 一致;
- 非 dry-run 时必须存在
NODE_AUTH_TOKEN或NPM_TOKEN。
如果目标版本已经发布,并且配置了:
publish: {
skipExisting: true,
}则会跳过 npm 上已经存在的包,避免重复发布失败。
七、单独执行 publish
也可以只执行 publish 子命令:
pnpm release:publish --version 0.1.0 --tag latestdry-run:
pnpm release:publish --version 0.1.0 --tag latest --dry关闭 provenance:
pnpm release:publish --version 0.1.0 --no-provenance不跳过已存在版本:
pnpm release:publish --version 0.1.0 --no-skip-existing指定 registry:
pnpm release:publish --version 0.1.0 --registry https://registry.npmjs.org/八、canary 发版
canary 用于每次主仓库更新后发布临时版本,供下游仓库验证兼容性。
执行:
pnpm release:canarycanary 版本格式:
{baseVersion}-canary.{date}.{runNumber}.{runAttempt}.{shortSha}示例:
0.1.0-canary.20260609.123.1.a1b2c3d4canary 流程:
- 校验 canary 是否启用;
- 校验是否在 CI 中运行;
- 校验当前分支是否在
includeBranches中; - 校验 npm token;
- 生成 canary version;
- 写入
GITHUB_ENV; - 更新 packages version;
- 更新 lockfile;
- 执行 precheck;
- 执行 publish dry-run;
- 执行真实 publish;
- 触发下游 repository dispatch。
本地调试可以使用:
pnpm release:canary --force-local注意:本地 force-local 仍然需要 npm token,且可能真实发布 canary 包,谨慎使用。
九、GitHub Actions 稳定发版示例
推荐稳定版本通过 tag 触发。
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 22
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm release --publishOnly ${GITHUB_REF_NAME#v} --skipBuild
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}如果启用 npm provenance,需要:
permissions:
contents: write
id-token: write十、GitHub Actions canary 示例
name: Canary
on:
push:
branches:
- main
- 'feat/**'
- 'fix/**'
- 'release/**'
- 'hotfix/**'
permissions:
contents: read
id-token: write
jobs:
canary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 22
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm release:canary
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
ZEUS_UI_DISPATCH_TOKEN: ${{ secrets.ZEUS_UI_DISPATCH_TOKEN }}十一、下游仓库 repository dispatch 示例
Zeus canary 发布成功后,可以触发 Zeus UI 的兼容检查。
下游仓库新增 workflow:
name: Zeus Canary Compatibility
on:
repository_dispatch:
types:
- zeus-canary-published
jobs:
compat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 22
registry-url: https://registry.npmjs.org/
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install Zeus canary
run: |
pnpm add -D @zeus-js/zeus@${{ github.event.client_payload.version }}
pnpm add -D @zeus-js/compiler@${{ github.event.client_payload.version }}
- run: pnpm typecheck
- run: pnpm test
- run: pnpm buildZEUS_UI_DISPATCH_TOKEN 需要是一个 GitHub PAT,至少需要目标仓库的 repository dispatch 权限。
十二、changesets-fixed 的 changelog 语义
changesets-fixed 模式会生成统一 root changelog:
CHANGELOG.md默认配置:
changesets: {
cleanupPackageChangelogs: true,
unifiedChangelog: true,
}这表示:
- 保留根
CHANGELOG.md; - 不保留 changesets 自动生成的包级
CHANGELOG.md; - 如果某个包原本就有自己的
CHANGELOG.md,发版后会恢复原内容; - 如果某个包原本没有
CHANGELOG.md,发版过程中生成的临时 changelog 会被删除; - 即使
changeset version之后的步骤失败,也会尝试恢复包级 changelog 并删除 synthetic changeset 文件。
这个行为用于避免 changesets fixed group 在 monorepo 中生成大量重复的 package changelog。
十三、配置项说明
repo
GitHub 仓库名。
repo: 'baicie/zeus'repositoryUrl
写入 package.json 的 repository url。
repositoryUrl: 'https://github.com/baicie/zeus'发布前会把 package.json 规范化为:
{
"repository": {
"type": "git",
"url": "https://github.com/baicie/zeus",
"directory": "packages/xxx"
}
}mode
发布模式。
mode: 'changesets-fixed'可选值:
'changesets-fixed'
'workspace-fixed'workspace.roots
需要扫描 package.json 的目录。
workspace: {
roots: ['packages', 'integrations', 'create']
}workspace.ignore
忽略不参与 release 的包名或相对路径。
workspace: {
ignore: ['@zeus-js/playground', 'examples/demo']
}workspace.include
只包含指定包名。
workspace: {
include: ['@zeus-js/zeus', '@zeus-js/compiler']
}workspace.publishable
自定义是否可发布。
workspace: {
publishable(pkg) {
return pkg.name.startsWith('@zeus-js/')
}
}rootVersionPackage
用哪个包的 version 作为共享版本来源。
rootVersionPackage: '@zeus-js/zeus'rootPackageJson
是否更新根 package.json version。
rootPackageJson: 'package.json'关闭:
rootPackageJson: falsechangelogFile
统一 changelog 文件。
changelogFile: 'CHANGELOG.md'关闭:
changelogFile: falsepublish.access
npm access。
publish: {
access: 'public'
}publish.provenance
是否启用 npm provenance。
publish: {
provenance: true
}publish.skipExisting
npm 已存在同版本时是否跳过。
publish: {
skipExisting: true
}推荐保持 true,这样补发时不会因为部分包已经发布而整体失败。
publish.retry
发布重试次数。
publish: {
retry: 5
}precheck.commands
发布前质量门禁。
precheck: {
commands: [
['pnpm', 'typecheck'],
['pnpm', 'lint'],
['pnpm', 'test'],
['pnpm', 'build'],
],
}afterVersion
版本文件更新后的 hook。
afterVersion({ version, config }) {
console.log(version)
}注意:
versionPackages --dry不会执行afterVersion;changesets-fixed中afterVersion会在 package changelog 恢复之后执行;- 如果
afterVersion抛错,release 会失败。
十四、常见命令
正常稳定发版
pnpm release --version 0.1.0beta 发版
pnpm release --version 0.1.0-beta.0 --tag beta只准备版本,不发布
pnpm release --version 0.1.0准备并本地直接发布
pnpm release --version 0.1.0 --publishpublishOnly 补发
pnpm release --publishOnly 0.1.0 --skipBuildpublishOnly dry-run
pnpm release --publishOnly 0.1.0 --skipBuild --drycanary
pnpm release:canary本地调试 canary
pnpm release:canary --force-local十五、发版前检查清单
稳定版本发版前确认:
git status
pnpm install --frozen-lockfile
pnpm typecheck
pnpm lint
pnpm test
pnpm build
pnpm release --version 0.1.0 --dry确认 dry-run 无问题后:
pnpm release --version 0.1.0
git push --follow-tagsCI publish 失败后:
pnpm release --publishOnly 0.1.0 --skipBuild --dry
pnpm release --publishOnly 0.1.0 --skipBuild十六、Zeus 推荐配置
Zeus 主仓库推荐:
import { defineReleaseConfig } from '@baicie/release'
export default defineReleaseConfig({
repo: 'baicie/zeus',
repositoryUrl: 'https://github.com/baicie/zeus',
mode: 'changesets-fixed',
packageManager: 'pnpm',
workspace: {
roots: ['packages', 'integrations', 'create', 'devtools'],
publishable(pkg) {
return pkg.name.startsWith('@zeus-js/')
},
},
rootVersionPackage: '@zeus-js/zeus',
changelogFile: 'CHANGELOG.md',
changesets: {
requireChangeset: false,
readIgnore: true,
readFixed: true,
cleanupPackageChangelogs: true,
unifiedChangelog: true,
},
publish: {
access: 'public',
provenance: true,
skipExisting: true,
retry: 5,
},
precheck: {
commands: [
['pnpm', 'typecheck'],
['pnpm', 'lint'],
['pnpm', 'test'],
['pnpm', 'build'],
],
},
canary: {
enabled: true,
prefix: 'canary',
tag: 'canary',
envName: 'CANARY_VERSION',
includeBranches: ['main', 'feat/**', 'fix/**', 'release/**', 'hotfix/**'],
dispatch: {
tokenEnv: 'ZEUS_UI_DISPATCH_TOKEN',
repository: 'baicie/zeus-ui',
eventType: 'zeus-canary-published',
},
},
})十七、推荐测试
@baicie/release 自身建议至少覆盖:
pnpm --filter @baicie/release test
pnpm --filter @baicie/release typecheck
pnpm --filter @baicie/release build关键测试场景:
- publish 参数解析;
- version dry-run 不执行
afterVersion; - canary branch glob;
- package changelog snapshot;
- package changelog restore;
runChangesetsFixedVersion在changeset version失败时恢复;runChangesetsFixedVersion在后续步骤失败时恢复;- synthetic changeset 文件始终清理。
十八、故障处理
npm 401
通常是 token 错误。
检查:
echo $NODE_AUTH_TOKEN
echo $NPM_TOKENGitHub Actions 中确认:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}npm provenance 失败
确认 workflow 有:
permissions:
id-token: write同时 package repository 信息需要匹配真实 GitHub 仓库。
npm 版本已存在
如果是补发,建议开启:
publish: {
skipExisting: true
}然后执行:
pnpm release --publishOnly 0.1.0 --skipBuildcanary 没有触发下游
检查:
ZEUS_UI_DISPATCH_TOKEN是否存在;- token 是否有目标仓库权限;
canary.dispatch.repository是否正确;- 下游 workflow 是否监听了正确的
repository_dispatch.types。
dry-run 后工作区有改动
这是正常行为。检查后还原:
git diff
git checkout -- .十九、Expo app.json 同步
versionPackages 末尾会调用 runAppJsonSyncFromConfig,把 version 写入 app.json,让 Expo/EAS 构建出来的版本号和 package.json 保持一致。
配置
// release.config.ts
import { defineReleaseConfig } from '@baicie/release'
export default defineReleaseConfig({
repo: 'baicie/clash-helper',
repositoryUrl: 'https://github.com/baicie/clash-helper',
mode: 'workspace-fixed',
workspace: { roots: ['packages'] },
appJson: {
enabled: true,
file: 'app.json',
versionNameStrategy: 'exact',
versionCode: 'auto',
writeIosBuildNumber: false,
},
})字段说明:
| 字段 | 类型 | 默认 | 含义 |
| --------------------- | ------------------------------- | ------------ | -------------------------------------------------------------------------------------------------- |
| enabled | boolean | false | 是否启用同步。false 时整个 sync 流程为 no-op |
| file | string | 'app.json' | app.json 路径,相对 cwd |
| versionNameStrategy | 'exact' \| 'strip-prerelease' | 'exact' | exact 原样写入 0.0.0-beta.2;strip-prerelease 去掉预发布后缀后写入 0.0.0 |
| versionCode | number \| 'auto' \| undefined | undefined | 不传则不动 expo.android.versionCode;传数字直接覆盖;传 'auto' 在当前值基础上 +1(要求已存在) |
| writeIosBuildNumber | boolean | false | 是否把 expo.ios.buildNumber 同步为 versionName |
行为
- 文件不存在时直接跳过,不报错。
dry-run模式下只计算 patch、不写盘、不生成.bak。- 写盘前会把原文件复制为
app.json.bak,失败时还原回.bak。 - 同步失败会抛错进入
release.ts的 try/catch 流程,自动回滚。 versionCode='auto'而当前没有expo.android.versionCode时抛错(避免静默重置为 0)。
与 EAS autoIncrement 配合
如果 eas.json 启用了 autoIncrement: true,EAS 会自动累加 versionCode,和 appJson.versionCode='auto' 重复计数。两种选择:
- 让 EAS 控制 versionCode:
appJson.versionCode不传,只同步expo.version。 - 让 release 工具控制 versionCode:
appJson.versionCode='auto',eas.json里把autoIncrement设为false。
老 release() API
src/release.ts 暴露的 legacy release(options) 也支持同样的 appJson 字段,写入失败会触发整个 release 流程的回滚。
十九、推荐实践
- 稳定版本使用 tag 触发 CI publish;
- 本地只负责生成版本、changelog、commit、tag;
- canary 只在 CI 中真实发布;
publish.skipExisting默认开启;publish.provenance默认开启;publishOnly只用于补发,不用于正常发版;- changesets-fixed 模式下只维护 root
CHANGELOG.md; - 包级
CHANGELOG.md如果原本存在,release 工具不会误删; - 每次修改 release 流程后必须跑
@baicie/release测试; - Zeus 发版前必须先 dry-run。
二十、最小命令速查
# 稳定发版 dry-run
pnpm release --version 0.1.0 --dry
# 稳定发版
pnpm release --version 0.1.0
# beta
pnpm release --version 0.1.0-beta.0 --tag beta
# canary
pnpm release:canary
# canary 本地调试
pnpm release:canary --force-local
# publishOnly 补发 dry-run
pnpm release --publishOnly 0.1.0 --skipBuild --dry
# publishOnly 补发
pnpm release --publishOnly 0.1.0 --skipBuild