使用 GitHub Actions 和 OIDC 安全发布 npm 包
摘要
本文介绍如何使用 npm Trusted Publishing,在 GitHub Actions 中通过 OpenID Connect(OIDC)发布 npm 包。整个发布过程不需要创建或保存长期有效的 NPM_TOKEN,并可通过指定仓库、Workflow 和 GitHub Environment 限制发布来源。
本文将依次完成以下工作:
-
解释为什么 npm 自动发布应从长期 Token 迁移到 OIDC。
-
介绍 OIDC 和 npm Trusted Publishing 的工作原理。
-
在 npmjs.com 中为包添加 GitHub Actions Trusted Publisher。
-
在 GitHub 中配置 Environment、审批规则和最小权限。
-
编写一个完整、安全且不依赖
NPM_TOKEN的发布 Workflow。 -
介绍私有依赖、首次发布、npm v12、Provenance 和常见错误。
一、为什么要使用 OIDC 发布 npm 包
1.1 npm 正在限制 2FA-bypass Token
GitHub 在 2026 年 7 月 8 日发布的 npm install-time security and GAT bypass2fa deprecation 中宣布:
-
npm v12 已正式发布,并成为
latest版本。 -
预计从 2026 年 8 月上旬开始,配置为绕过双因素认证的 Granular Access Token(GAT)将不能再绕过账户、包和组织管理操作的 2FA。
-
预计从 2027 年 1 月前后开始,2FA-bypass Token 将不能再直接发布 npm 包。
-
自动发布应迁移到 Trusted Publishing(OIDC),或者改用需要人工 2FA 审批的 Staged Publishing。
过去常见的自动发布方式,是在 npm 中创建一个允许发布且绕过 2FA 的 Token,再将它保存为 GitHub Repository Secret:
1 | - run: npm publish |
这种方案依赖一个长期凭证。只要 Token 没有过期或被撤销,拿到它的人就可能在允许的权限范围内持续操作。Token 还可能因为日志输出、错误配置、第三方 Action、开发者电脑或仓库 Secret 管理不当而泄漏。
随着 2FA-bypass Token 的直接发布能力被逐步取消,继续围绕长期发布 Token 建设自动发布流程不仅风险更高,也不是可持续方案。
1.2 OIDC 解决了什么问题
OIDC 发布不需要预先生成一个长期 npm 发布 Token。每次 Workflow 运行时:
1 | GitHub Actions Job |
与长期 Token 相比,OIDC 的主要优势是:
-
没有长期发布密钥:GitHub Secrets 中不再需要保存
NPM_TOKEN。 -
凭证生命周期短:身份令牌按 Job 动态生成,不能作为长期凭证反复使用。
-
身份限制更精确:npm 可以只信任指定 GitHub 用户或组织、仓库、Workflow 文件和 Environment。
-
不需要人工轮换 Token:减少 Token 创建、保存、更新和撤销的维护工作。
-
降低泄漏影响:仓库里没有可直接复制走并长期使用的发布 Token。
-
自动生成 Provenance:满足条件时,npm 会自动发布来源证明,让使用者可以确认包从哪个仓库和 Workflow 构建而来。
需要注意,OIDC 并不是“关闭认证”。它是将认证方式从“持有一个长期秘密”改为“由 GitHub 在运行时证明当前 Workflow 的身份”。
二、前置条件和限制
根据 npm Trusted Publishing 官方文档,使用 GitHub Actions 发布时需要满足以下条件:
-
Node.js
>= 22.14.0。 -
npm CLI
>= 11.5.1。 -
使用 GitHub-hosted Runner,当前不支持 self-hosted Runner。
-
npm 包已经存在,并且当前 npm 用户拥有该包的管理权限。
-
Workflow 文件位于仓库的
.github/workflows/目录。 -
package.json中的repository.url必须与发布来源的 GitHub 仓库准确对应。 -
一个 npm 包同一时间只能配置一个 Trusted Publisher。
本文使用:
1 | Node.js 24 |
2.1 新包需要先完成首次发布
Trusted Publisher 是在已有 npm 包的 Settings 页面中配置的,因此全新的包通常需要先由维护者交互式发布一次:
1 | npm login |
首次发布时按照 npm 提示完成 2FA。包创建成功后,再配置 Trusted Publisher,后续版本即可完全交给 GitHub Actions 和 OIDC。
不要为了首次发布而创建长期的 2FA-bypass Token。
2.2 检查 package.json
一个公开作用域包可以使用如下配置:
1 | { |
需要重点检查:
-
name必须是准备配置 Trusted Publisher 的 npm 包名。 -
version每次发布都必须递增,npm 不允许覆盖已有版本。 -
repository.url中的OWNER/REPOSITORY必须与实际 GitHub 仓库匹配。 -
公开作用域包建议设置
publishConfig.access为public,这样无需每次添加--access public。 -
publishConfig.registry可以防止包被误发到其他 Registry。
三、在 npm 中配置 Trusted Publisher
3.1 通过 npmjs.com 配置
登录 npmjs.com,进入要发布的包,然后打开:
1 | Packages |
填写以下字段:
| 字段 | 示例 | 说明 |
|---|---|---|
| Organization or user | hanqunfeng |
GitHub 仓库所属用户或组织,不包含 @ |
| Repository | my-package |
只填写仓库名,不填写完整 URL |
| Workflow filename | publish.yml |
只填写文件名,不能填写 .github/workflows/publish.yml |
| Environment name | npm |
可选;填写后,GitHub Job 必须使用同名 Environment |
| Allowed actions | npm publish |
允许 Workflow 直接发布 |
所有字段都区分大小写,应与 GitHub 中的值完全一致。
如果希望发布前必须由维护者进行 2FA 审批,可以只允许 npm stage publish。本文介绍自动直接发布,因此选择 npm publish。
保存配置时 npm 不会验证仓库和 Workflow 是否真实存在。字段写错通常要等到第一次发布时才会出现 ENEEDAUTH,因此应在保存前逐项核对。
3.2 Environment 是否必须填写
Environment 是可选项,但生产发布建议配置。填写 npm 后,npm 不仅检查仓库和 Workflow,还会检查 OIDC Token 中的 Environment 身份。
这意味着下面两处必须完全一致:
1 | npm Trusted Publisher Environment name: npm |
如果 npm 中填写了 Environment,但 Workflow 没有声明 environment: npm,发布认证会失败。
3.3 使用 npm CLI 配置
也可以使用 npm trust 命令创建信任关系:
1 | npm trust github @your-scope/your-package \ |
先使用 --dry-run 检查参数:
1 | npm trust github @your-scope/your-package \ |
这个命令与网页配置的作用相同。它需要当前终端已经登录 npm,并拥有目标包的管理权限。
四、在 GitHub 中配置发布环境
4.1 创建 Environment
打开 GitHub 仓库,进入:
1 | Settings |
Environment 名称填写:
1 | npm |
名称应与 npm Trusted Publisher 中的 Environment name 完全一致。
4.2 配置部署保护规则
可以根据仓库安全要求配置:
-
Required reviewers:发布前必须由指定维护者批准。
-
Deployment branches and tags:只允许受保护分支或指定 Tag 部署。
-
Prevent self-review:触发发布的人不能批准自己的发布。
-
Wait timer:批准后等待一段时间再开始发布。
如果使用 v* Tag 触发发布,建议只允许符合版本规则的 Tag,例如:
1 | v* |
GitHub 不同套餐和仓库可见性支持的 Environment 保护能力可能不同,应以仓库 Settings 页面实际提供的选项为准。
4.3 不需要创建 NPM_TOKEN
OIDC 发布不需要在下面的位置添加 npm 发布 Token:
1 | Settings |
如果旧 Workflow 中存在 NPM_TOKEN:
-
先保留 Secret,完成一次 OIDC 发布验证。
-
确认 Workflow 没有读取该 Secret。
-
删除旧 Workflow 中的
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}。 -
最后在 GitHub 和 npm 中撤销不再使用的发布 Token。
不要在尚未验证 OIDC 可用前先撤销旧发布方式,否则可能中断发布。
4.4 保护发布 Tag
仅仅使用 push.tags: ["v*"] 不代表任何人都应该能创建发布 Tag。组织仓库可以通过 GitHub Rulesets 限制谁能创建或更新匹配 v* 的 Tag:
1 | Settings |
Tag 保护与 Environment 审批配合使用,可以避免普通写权限成员通过创建 Tag 直接触发生产发布。
五、编写 GitHub Actions Workflow
在项目中创建 .github/workflows/publish.yml:
1 | name: Publish to npm |
提交 Workflow:
1 | git add .github/workflows/publish.yml |
如果通过 HTTPS 使用 GitHub Personal Access Token(classic)执行 git push,该 Token 除了仓库写入权限外,还必须勾选 workflow scope(Update GitHub Action workflows);否则无法新增或修改 .github/workflows/ 下的 Workflow 文件。
5.1 id-token: write 是关键权限
下面的权限允许当前 Job 向 GitHub OIDC Provider 请求 JWT:
1 | permissions: |
根据 GitHub OIDC 参考文档,id-token: write 只允许请求 OIDC Token,本身不会直接授予修改仓库或 npm 包的权限。npm 仍然会验证 Token,并根据 Trusted Publisher 配置决定是否允许发布。
actions/checkout 读取仓库代码还需要:
1 | permissions: |
将权限放在 publish Job 内,可以避免同一 Workflow 的其他 Job 获得请求 OIDC Token 的能力。
5.2 为什么没有 NODE_AUTH_TOKEN
OIDC Workflow 中不应再写:
1 | env: |
npm CLI 会识别 GitHub Actions OIDC 环境,在执行 npm publish 时完成身份交换。如果仍提供一个具有发布权限的长期 Token,可能会掩盖 Trusted Publisher 配置错误,让 Workflow 看似迁移成功,实际仍然依赖旧 Token。
5.3 为什么关闭发布任务中的依赖缓存
示例使用:
1 | package-manager-cache: false |
发布任务应优先保证构建来源清晰、可重复。缓存可以放在普通 CI 中加速测试,但 npm 官方示例建议发布构建不要使用依赖缓存,以减少旧缓存或受污染缓存进入发布产物的风险。
5.4 为什么使用 Environment
下面的配置有两个作用:
1 | environment: npm |
-
让 OIDC Token 包含
npmEnvironment 身份,以匹配 npm Trusted Publisher。 -
应用 GitHub Environment 中配置的审批、Tag 限制和等待规则。
如果不准备使用 Environment,应同时删除:
-
npm Trusted Publisher 中的 Environment name。
-
Workflow 中的
environment: npm。
5.5 为什么校验 Tag 和 package.json 版本
v1.2.3 Tag 应发布 package.json 中的 1.2.3。如果二者不一致,可能出现:
-
Tag 表示的版本与 npm 实际版本不同。
-
重复发布一个已经存在的版本。
-
Release Notes 与真实包版本错位。
Workflow 在安装依赖前进行校验,可以更早终止错误发布。
六、执行一次发布
6.1 更新版本并创建 Tag
例如发布补丁版本:
1 | npm version patch -m "chore(release): %s" |
npm version patch 会:
-
更新
package.json和package-lock.json中的版本。 -
创建一个 Git Commit。
-
创建形如
v1.0.1的 Git Tag。
Tag 推送到 GitHub 后,publish.yml 开始运行。如果 Environment 配置了 Required reviewers,Job 会在发布前等待审批。
6.2 验证发布结果
在 GitHub 中检查:
1 | Repository |
在命令行中检查:
1 | npm view @your-scope/your-package version |
还可以安装刚发布的版本:
1 | npm install @your-scope/your-package@1.0.1 |
七、Provenance 来源证明
使用 GitHub Actions Trusted Publishing 时,npm 会在满足条件的情况下自动生成 Provenance,不需要添加:
1 | npm publish --provenance |
自动生成需要同时满足:
-
通过 OIDC Trusted Publishing 发布。
-
从公开 GitHub 仓库发布。
-
发布的是公开 npm 包。
Provenance 为包提供可验证的构建来源,包括源代码仓库和执行发布的 Workflow。使用者可以在 npm 包页面查看来源信息,从而降低包被伪造或发布链路不透明的风险。
私有 GitHub 仓库当前不会生成 Provenance,即使最终发布的是公开 npm 包,但 OIDC Trusted Publishing 本身仍然可以使用。
八、npm v12 对发布 Workflow 的影响
npm v12 除了推动发布认证迁移,还默认收紧了安装阶段的供应链安全策略:
-
依赖的
preinstall、install、postinstall生命周期脚本和隐式node-gyp构建默认不再自动执行。 -
Git 依赖默认不允许解析。
-
HTTPS Tarball 等远程 URL 依赖默认不允许解析。
升级 npm v12 后,应在开发环境中审查被阻止的依赖脚本:
1 | npm approve-scripts --allow-scripts-pending |
根据审查结果批准确实需要执行脚本的依赖,并将生成的 allowScripts 配置提交到 package.json。不要在 CI 中直接允许所有依赖脚本,否则会绕过 npm v12 新增的安全边界。
在正式启用 npm v12 前,应本地运行:
1 | npm ci |
确认原生模块、代码生成工具和构建依赖没有因为 install script 被阻止而失效。
九、项目使用私有依赖时的配置
Trusted Publishing 只负责 npm publish 或 npm stage publish,不会让 npm ci 自动获得私有包读取权限。
如果项目依赖私有 npm 包,需要创建一个只读 Granular Access Token,并仅在安装步骤中使用:
1 | - name: Install dependencies |
注意:
-
NPM_READ_TOKEN只授予读取必要私有包的权限。 -
不要为它启用发布权限或 2FA bypass。
-
不要在
npm publish步骤中传递该 Token。 -
项目中的
.npmrc只能引用环境变量,不能写入真实 Token。
例如:
1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} |
十、进一步加固发布流程
10.1 OIDC 验证成功后禁止传统 Token 发布
完成一次 OIDC 发布验证后,进入 npm 包设置:
1 | Package |
该选项会阻止传统 Token 发布,但不会阻止 Trusted Publisher 使用 OIDC。
推荐迁移顺序:
-
添加 Trusted Publisher。
-
提交不包含
NPM_TOKEN的 Workflow。 -
成功发布一个新版本。
-
禁止传统 Token 发布。
-
撤销旧的自动发布 Token。
-
删除 GitHub 中不再使用的
NPM_TOKENSecret。
10.2 使用 Staged Publishing
安全要求更高的项目可以让自动化流程只暂存版本:
1 | npm stage publish |
然后由维护者通过 npmjs.com 或 CLI 完成 2FA 审批,版本才会公开。此时在 Trusted Publisher 中只允许 npm stage publish,不要允许直接 npm publish。
10.3 固定第三方 Action
示例为了便于阅读使用:
1 | uses: actions/checkout@v6 |
对高安全要求的发布链路,可以将 Action 固定到审核过的完整 Commit SHA,并由 Dependabot 或 Renovate 提交升级 PR,避免可变 Tag 被上游修改。
10.4 将测试和发布拆分
复杂项目可以先运行没有 id-token: write 的测试 Job,全部通过后再运行发布 Job。只有最后一个 Job 拥有 OIDC 权限:
1 | jobs: |
如果测试 Job 生成构建 Artifact,发布 Job 下载 Artifact 后应校验来源和内容,避免“测试一份代码、发布另一份代码”。
十一、常见问题排查
11.1 npm ERR! code ENEEDAUTH
依次检查:
-
Workflow 是否包含
id-token: write。 -
是否使用 GitHub-hosted Runner。
-
Node.js 是否
>= 22.14.0。 -
npm 是否
>= 11.5.1。 -
npm 中的 Organization、Repository 是否与当前仓库一致。
-
npm 中只填写了
publish.yml,而不是完整路径。 -
Workflow 文件名的大小写和扩展名是否完全一致。
-
npm 中配置了 Environment 时,Job 是否声明同名
environment。 -
package.json的repository.url是否指向当前 GitHub 仓库。 -
npm publish步骤是否意外传入了一个无效的NODE_AUTH_TOKEN。
11.2 npm whoami 失败
这是预期行为。OIDC 身份交换只在 npm publish 或 npm stage publish 时发生:
1 | npm whoami |
不能用于验证当前 Workflow 的 OIDC 发布身份。
11.3 使用 Reusable Workflow 后认证失败
如果使用 workflow_call,npm 可能校验调用方 Workflow 的文件名,而不是实际包含 npm publish 的被调用 Workflow。父、子 Workflow 还都需要正确传递 id-token: write 权限。
遇到此问题时,应确认 npm Trusted Publisher 中登记的是实际参与 OIDC 身份声明的 Workflow 文件名。对于关键发布流程,直接在已登记的顶层 Workflow 中执行发布通常更容易审计。
11.4 手动触发可以发布,Tag 触发失败
检查:
-
Tag 是否满足
v*。 -
Tag 是否指向包含
publish.yml的提交。 -
Tag 与
package.json版本是否一致。 -
Environment 的 Deployment branches and tags 是否允许该 Tag。
-
Tag Ruleset 是否允许当前用户或自动化创建该 Tag。
11.5 npm 中找不到 Trusted publishing
常见原因包括:
-
包还没有完成首次发布。
-
当前 npm 账号不是包维护者。
-
登录了错误的 npm 账号或组织。
-
当前账号没有修改包设置的权限。
十二、完整迁移检查清单
发布前:
-
>= 22.14.0,npm>= 11.5.1。 -
package.json的包名、版本、Registry 和repository.url正确。 -
contents: read和id-token: write等必要权限。 -
NPM_TOKEN。 -
package.json版本一致。
首次 OIDC 发布成功后:
-
NPM_TOKEN。
总结
npm 对 2FA-bypass Token 的限制意味着,长期发布 Token 不再适合作为 GitHub Actions 自动发布的基础。Trusted Publishing 使用 GitHub OIDC 在运行时证明 Workflow 身份,不需要保存长期发布密钥,并且可以将权限限制到指定仓库、Workflow 和 Environment。
一套推荐的 npm 发布链路是:
1 | 受保护的版本 Tag |
完成迁移并验证成功后,再禁止传统 Token 发布并撤销旧 Token,才能真正移除长期凭证带来的供应链风险。
真实案例
为了更好的便于读者理解,可以参考本人的开源项目 claude-trace
-
publish.yml:使用 GitHub Actions 和 OIDC 自动发布 npm 包的完整 Workflow。
-
publish-oidc.sh:发布前检查版本、构建产物和 OIDC 环境的辅助脚本。
-
oidc.md:项目中配置、使用及排查 OIDC 发布流程的说明文档。