lhx-kit 是一个包含 8 个可发布包的 monorepo,从 2026 年 5 月起采用 Changesets + GitHub Actions + npm Trusted Publishing(OIDC) 这一套组合拳做发布。本篇把每一步都讲透,并给出可直接复用的配置和故障排查表。
这套模型的关键在于"两阶段":
为什么分两阶段?因为 monorepo 里误发一个包的代价 ≫ 少按一个按钮的代价。
.changeset/config.json重点字段:
| 字段 | 值 | 为什么 |
|---|---|---|
linked |
8 个 @lhx-kit/* 包列表 |
版本号始终对齐——改了某个包发版时,整组拉到同一个新版本号;但没改的包不会被强制重发到 npm(这点和 fixed 不一样)。2026-05 从 fixed 切到这里,详见下面的迁移记 |
fixed |
[] |
留空。为什么不用 fixed —— 见下方迁移说明 |
access |
public |
scoped 包默认是 private(会上传失败),必须显式声明 public |
baseBranch |
master |
部分团队用 main,按实际写 |
commit |
false |
让 changesets/action 统一处理 commit,不让 CLI 自己提 |
updateInternalDependencies |
patch |
内部 workspace 包升级时,依赖它的包也自动 patch——保证 workspace-range 被正确展开 |
ignore |
docs / examples / rmpa / vmpa | 这些是 monorepo 内的 demo 和文档站,不发包;放进 ignore 就不会被 changeset version 误升 |
changelog |
@changesets/changelog-github |
让 CHANGELOG 自动包含 PR 链接和 commit hash,比默认 changelog 信息更全 |
2026-05 迁移记:从
fixed切到linked切换前:
"fixed": [["@lhx-kit/*"]]——8 个包永远共用一个版本号。 切换后:"linked": [...]+"fixed": []——版本号仍对齐,但只发被改的。为什么换?
fixed模式下每次发版都会把 8 个包全部 bump 到统一新号、全部重新 publish 到 npm,即便其中 7 个没动一行代码。结果就是@lhx-kit/tsconfig这种纯配置包发了一堆"代码完全相同、只有版本号涨"的版本,CHANGELOG 也跟着出现一大串无意义条目。linked保留了"用户安装任意一个、搭配版本天然兼容"的好处,又消除了空 release 噪声。详细的写 changeset 实战和易错点见 Changeset 使用手册。
package.json 的 scriptsversion 脚本里为什么要跟一个 pnpm install --lockfile-only?因为 changeset version 改了 package.json 的版本号后,pnpm-lock.yaml 里的 workspace 交叉引用版本号会过时,必须同步刷一次 lockfile 否则下次 --frozen-lockfile 的 CI 会挂。这是被多个团队踩过的坑,写进 SETUP 文档做成默认。
这是 lhx-kit 当前真实在用的 .github/workflows/release.yaml(删掉了无关注释):
| 设计 | 理由 |
|---|---|
permissions.id-token: write |
必须——OIDC 的地基。没这个 npm publish 会在 OIDC 交换阶段 401 |
fetch-depth: 0 |
changesets 要根据完整 git log 生成准确 CHANGELOG |
| `token: ${{ secrets.REPO_PUSH_TOKEN | |
registry-url |
setup-node 会写 .npmrc 让 npm 知道注册表地址——provenance 签名要用 |
--frozen-lockfile |
确保发布用的依赖树和开发者本地完全一致 |
| 不加 paths 过滤 | Version PR 合并那次 push 只动了 package.json / CHANGELOG.md / 删 .changeset/*.md,任何 paths 过滤都可能漏掉 → 导致"合了 Version PR 但没发布" |
这是本套方案最大的亮点。传统方案用 NPM_TOKEN secret,永远面临:
E404 scope not foundTrusted Publishing 用 GitHub OIDC 代替 token。每次发布时:
对每一个 @lhx-kit/* 包重复:
npmjs.com,打开包页面,例如 https://www.npmjs.com/package/@lhx-kit/cli/access| 字段 | 值 | 易错点 |
|---|---|---|
| Publisher | GitHub Actions | 默认已选 |
| Organization or user | juwenzhang |
填 GitHub 用户名,不是 npm 用户名 |
| Repository | lhx-kit |
只填仓库短名,不带 URL、不带 / |
| Workflow filename | release.yaml |
只填文件名,不带 .github/workflows/ 前缀,扩展名是 .yaml 不是 .yml |
| Environment name | (留空) | 除非 workflow 里用了 environment: 块,否则留空 |
lhx-kit 需要配 8 个包:cli、config、offline、renderer、runtime、skills、tsconfig、vite-plugin。
Trusted Publisher 不能用于"从未发布过"的包。第一次必须用传统方式(本地 npm publish --access public 或临时的 NPM_TOKEN)把 0.0.x 推上去;之后切到 Trusted Publishing。
💡 新包的首次发布流程:先用
lhx-cli add package <name>scaffold 出完整结构(package.json / tsconfig / tsup / 双语 README / LICENSE 一次到位),然后cd packages/<name> && npm publish --access public在 npm 网页配 Trusted Publisher,之后纳入 changesets 自动化。详见 CLI → add package。
另一项同等重要的前置条件:npm CLI 必须 ≥ 11.5.1。
Trusted Publishing 依赖 npm 客户端实现的 OIDC token 交换接口
POST /-/npm/v1/oidc/token/exchange。这是 npm 11.5.1 才引入的端点。低于这个版本,publish 时会出现一个极其误导的错误链:
E404 Not Found'@scope/pkg@x.y.z' is not in this registry看起来像"包不存在"或"权限不对",实际上是旧版 npm 调不动交换端点。
actions/setup-node@v4 只负责装 Node,不主动升级 npm在 Release workflow 的 setup-node 之后、publish 步骤之前加:
跑完一次应该能看到 npm notice 改成 11.x。
发布一次看:
npm notice OIDC token verified 之类字样| 维度 | NPM_TOKEN (Granular/Automation) | Trusted Publishing (OIDC) |
|---|---|---|
| 配置工作量 | 1 次建 token + 1 次存 secret | 每包 1 次配 Trusted Publisher |
| 到期 | 90 天轮换 | 永不过期 |
| 泄漏风险 | token 一旦泄漏可被滥用整个过期窗口 | 无长期 secret;短期凭据仅在单次 publish 有效 |
| 权限错配 | 容易漏勾 scope → E404 scope not found |
字段简单:repo + workflow |
| npm 官方态度 | 2024 Q3 起主动警告不推荐 | 首选推荐 |
| provenance 签名 | 可选(NPM_CONFIG_PROVENANCE=true) |
自动且天然绑定 workflow 身份 |
| 失败调试 | E404 / ENEEDAUTH 错误语焉不详 |
OIDC token exchange failed 直接指明问题域 |
一句话:新项目没理由再用 token。旧项目走兼容路线可以,迁移成本极低(本专栏示例就是从 token 迁 OIDC 的真实过程)。
| 症状 | 真实根因 | 解法 |
|---|---|---|
E404 Not Found - PUT ... scope not found |
① 包从未发布过 ② Granular token 漏勾 @lhx-kit scope |
先手动首发;或去 token 页面重配 Packages and scopes |
ENEEDAUTH |
NPM_TOKEN 丢了或失效 | 旧路线:换 token;新路线:切 OIDC |
OIDC token exchange failed |
Trusted Publisher 配的 workflow filename 与实际不符(常见 .yml vs .yaml)/ repo 名拼错 / environment 填了但 workflow 没定义 |
回 npm 网页改 Trusted Publisher 字段 |
403 Resource not accessible by integration |
workflow 没开 permissions: contents: write / pull-requests: write |
在 release.yaml 顶层加上 |
| Version Packages PR 一直不开 | 没有 pending .changeset/*.md |
pnpm changeset 生成 changeset 并 commit |
Failed to find where HEAD diverged from "master" |
master 上还没有 initial commit | 先 push 一次 initial commit |
| Version PR 合并后 publish 没跑 | workflow 加了 paths 过滤,漏了 package.json / CHANGELOG.md |
去掉 paths,Release workflow 必须对 master 每次 push 都跑 |
| npm 网页搜索框搜不到刚发的 scope | 不是 bug,registry 和搜索索引是两套系统 | 等 6–24h,或给别人直接发 https://www.npmjs.com/package/<name> |
| 包里多了意外文件 | package.json 里没声明 files 字段 |
显式列 ["dist", "README.md", "LICENSE"],别靠 .npmignore |
在合 Version PR 之前用这个清单过一遍:
pnpm -r --filter='./packages/*' build 全部 passpackage.json#files 声明的文件确实被 build 产出(用 npm pack --dry-run 验证)package.json#version——让 changesets 管。人工改最容易出"版本倒退"或"CHANGELOG 不对齐"。CHANGELOG.md——同上。