背景
最近在给 npm 包做 CI 发版时,突然开始频繁失败,npm 的提示也非常直白:

Classic tokens have been revoked.
Granular tokens are now limited to 90 days and require 2FA by default.
Update your CI/CD workflows to avoid disruption.
一句话总结就是:
老的 npm token 方案,已经不适合 CI 了。
现在就记录一次完整的 CI/CD 改造过程,包含:
- npm 新规则到底改了什么
- CI 为什么会突然发布失败
- 过程中遇到的几个"必踩坑"
- 最终一套可长期运行的 GitHub Actions 发包方案
一、npm 这次到底动了谁的蛋糕?
先说结论:
npm 正在彻底废弃"长期有效 token + npm login"的发布模型。
核心变化有三点。
1. Classic Token 作废
以前最常见的做法是:
yaml
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
这种 Classic Token:
- 长期有效
- 没有明确权限边界
- 一旦泄露,后果很严重
现在 npm 明确表态:
Classic Token 不再推荐,并会逐步失效。
2. Granular Token 默认 90 天 + 2FA
新的 Granular Token:
- 必须开启 2FA
- 默认 90 天过期
- 非常不适合无人值守的 CI
👉 这意味着:
即使你换成 Granular Token,CI 依然不稳定。
3. npm 开始强制推广 provenance(供应链校验)
这是最关键、也是最容易被忽略的一点。
npm 现在会校验:
- 这个包是不是在 GitHub Actions 里发布的
- 发布它的仓库是谁
package.json.repository指向哪里- GitHub OIDC 身份是否匹配
校验失败,直接拒绝发布。
二、为什么"明明登录了",还是发布失败?
我们最初遇到的错误包括:
❌ ENEEDAUTH
vbnet
npm error need auth This command requires you to be logged in
原因很简单:
- CI 里已经不再支持老的登录方式
- token 被判定为过期或 revoked
❌ 404 / E404
kotlin
404 '@scope/package@x.y.z' is not in this registry
这个错误非常迷惑,但本质上是:
npm 拒绝了你的发布请求,并不是包不存在。
❌ 422 Unprocessable Entity(最坑)
vbnet
Error verifying sigstore provenance bundle:
package.json: "repository.url" is ""
expected to match "https://github.com/infinilabs/ci"
这一步,几乎是所有人都会踩的坑。
三、npm provenance 校验在校验什么?
npm 的逻辑是:
我不只要你能发布,我还要知道你是从哪里发布的。
它会做一组严格匹配:
| 校验项 | 来源 |
|---|---|
| GitHub Actions 所在仓库 | CI |
| OIDC 身份 | GitHub |
package.json.repository.url |
包元数据 |
只要有一个不一致:
👉 422,拒绝发布。
四、为什么构建产物里的 package.json 会出问题?
我们的发布流程是:
bash
pnpm run build:web
cd out/search-chat
npm publish
注意重点:
真正 publish 的不是源码目录,而是构建产物目录。
很多项目在 build 后:
package.json是重新生成的repository为空- 或指向源码仓库,而不是 CI 仓库
这在 npm provenance 时代,直接就是死刑。
五、正确的 CI 改造思路
目标很明确:
不用 npm token,不用 npm login,让 CI 自己完成身份认证。
核心方案是:
✅ GitHub Actions + OIDC + npm provenance
六、关键改造点一:npm 账户设置以及包设置
npm 账户设置:

npm 对应包设置:


七、关键改造点二:开启 OIDC 权限
在 workflow 顶部增加:
yaml
permissions:
contents: read
id-token: write
这一步非常关键:
id-token: write是 npm provenance 的前提- 没有它,npm 无法验证 CI 身份
八、关键改造点三:彻底移除 npm token
我们做了三件事:
- 删除
NODE_AUTH_TOKEN - 删除
.npmrc - 不再执行
npm login
CI 里只保留:
npm publish
GitHub Actions 会自动用 OIDC 身份和 npm 通信。
九、关键改造点四:修正构建产物里的 repository
在真正 publish 前,强制修正 package.json:
vbnet
jq '.repository = {
"type": "git",
"url": "https://github.com/infinilabs/ci"
}' package.json > tmp.json && mv tmp.json package.json
注意几个要点:
- URL 必须是当前 CI 所在仓库
- 不能是源码仓库
- 不能为空
- 必须是 https GitHub 地址
十、最终发布流程(核心片段)
bash
pnpm run build:web
cd out/search-chat
# 修正 repository
jq '.repository = {
"type": "git",
"url": "https://github.com/infinilabs/ci"
}' package.json > tmp.json && mv tmp.json package.json
# 发布
npm publish
发布成功时,npm 会输出:
- provenance 已签名
- 已写入 sigstore transparency log
十一、为什么不直接关 provenance?
npm 提供了:
css
npm publish --no-provenance
但这只是一个 临时选项:
- 官方趋势是默认开启
- 未来可能直接强制
- CI 早适配,后面少折腾
十二、这次改造后的收益
最终我们得到的是一套:
- ✅ 无 token
- ✅ 无 2FA 人工参与
- ✅ 不会过期
- ✅ 可审计、可追溯
- ✅ 符合 npm 官方长期路线
真正意义上的 无人值守 CI 发包。
十三、小结
npm 不再相信"你是谁",
它只相信 你从哪里来。
在这个前提下,
OIDC + provenance,不是可选项,而是必选项。