使用 GitHub Actions 和 OIDC 安全发布 npm 包
摘要本文介绍如何使用 npm Trusted Publishing在 GitHub Actions 中通过 OpenID ConnectOIDC发布 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 TokenGitHub 在 2026 年 7 月 8 日发布的 npm install-time security and GAT bypass2fa deprecation 中宣布npm v12 已正式发布并成为latest版本。预计从 2026 年 8 月上旬开始配置为绕过双因素认证的 Granular Access TokenGAT将不能再绕过账户、包和组织管理操作的 2FA。预计从 2027 年 1 月前后开始2FA-bypass Token 将不能再直接发布 npm 包。自动发布应迁移到 Trusted PublishingOIDC或者改用需要人工 2FA 审批的 Staged Publishing。过去常见的自动发布方式是在 npm 中创建一个允许发布且绕过 2FA 的 Token再将它保存为 GitHub Repository Secret-run:npm publishenv:NODE_AUTH_TOKEN:${{secrets.NPM_TOKEN}}这种方案依赖一个长期凭证。只要 Token 没有过期或被撤销拿到它的人就可能在允许的权限范围内持续操作。Token 还可能因为日志输出、错误配置、第三方 Action、开发者电脑或仓库 Secret 管理不当而泄漏。随着 2FA-bypass Token 的直接发布能力被逐步取消继续围绕长期发布 Token 建设自动发布流程不仅风险更高也不是可持续方案。1.2 OIDC 解决了什么问题OIDC 发布不需要预先生成一个长期 npm 发布 Token。每次 Workflow 运行时GitHub Actions Job → 向 GitHub OIDC Provider 请求短期 JWT → npm 校验 JWT 的签名和身份声明 → npm 核对仓库、Workflow、Environment 等可信条件 → 本次 npm publish 获得短期发布权限 → 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。本文使用Node.js 24 npm 12 ubuntu-latest .github/workflows/publish.yml GitHub Environment: npm2.1 新包需要先完成首次发布Trusted Publisher 是在已有 npm 包的 Settings 页面中配置的因此全新的包通常需要先由维护者交互式发布一次npmloginnpmpublish--accesspublic首次发布时按照 npm 提示完成 2FA。包创建成功后再配置 Trusted Publisher后续版本即可完全交给 GitHub Actions 和 OIDC。不要为了首次发布而创建长期的 2FA-bypass Token。2.2 检查 package.json一个公开作用域包可以使用如下配置{name:your-scope/your-package,version:1.0.0,repository:{type:git,url:githttps://github.com/OWNER/REPOSITORY.git},publishConfig:{access:public,registry:https://registry.npmjs.org/}}需要重点检查name必须是准备配置 Trusted Publisher 的 npm 包名。version每次发布都必须递增npm 不允许覆盖已有版本。repository.url中的OWNER/REPOSITORY必须与实际 GitHub 仓库匹配。公开作用域包建议设置publishConfig.access为public这样无需每次添加--access public。publishConfig.registry可以防止包被误发到其他 Registry。三、在 npm 中配置 Trusted Publisher3.1 通过 npmjs.com 配置登录 npmjs.com进入要发布的包然后打开Packages → 选择包 → Settings → Trusted publishing → Select your publisher → GitHub Actions填写以下字段字段示例说明Organization or userhanqunfengGitHub 仓库所属用户或组织不包含Repositorymy-package只填写仓库名不填写完整 URLWorkflow filenamepublish.yml只填写文件名不能填写.github/workflows/publish.ymlEnvironment namenpm可选填写后GitHub Job 必须使用同名 EnvironmentAllowed actionsnpm publish允许 Workflow 直接发布所有字段都区分大小写应与 GitHub 中的值完全一致。如果希望发布前必须由维护者进行 2FA 审批可以只允许npm stage publish。本文介绍自动直接发布因此选择npm publish。保存配置时 npm 不会验证仓库和 Workflow 是否真实存在。字段写错通常要等到第一次发布时才会出现ENEEDAUTH因此应在保存前逐项核对。3.2 Environment 是否必须填写Environment 是可选项但生产发布建议配置。填写npm后npm 不仅检查仓库和 Workflow还会检查 OIDC Token 中的 Environment 身份。这意味着下面两处必须完全一致npm Trusted Publisher Environment name: npm GitHub Actions Job environment: npm如果 npm 中填写了 Environment但 Workflow 没有声明environment: npm发布认证会失败。3.3 使用 npm CLI 配置也可以使用npm trust命令创建信任关系npmtrust github your-scope/your-package\--repoOWNER/REPOSITORY\--filepublish.yml\--envnpm\--allow-publish先使用--dry-run检查参数npmtrust github your-scope/your-package\--repoOWNER/REPOSITORY\--filepublish.yml\--envnpm\--allow-publish\--dry-run这个命令与网页配置的作用相同。它需要当前终端已经登录 npm并拥有目标包的管理权限。四、在 GitHub 中配置发布环境4.1 创建 Environment打开 GitHub 仓库进入Settings → Environments → New environmentEnvironment 名称填写npm名称应与 npm Trusted Publisher 中的Environment name完全一致。4.2 配置部署保护规则可以根据仓库安全要求配置Required reviewers发布前必须由指定维护者批准。Deployment branches and tags只允许受保护分支或指定 Tag 部署。Prevent self-review触发发布的人不能批准自己的发布。Wait timer批准后等待一段时间再开始发布。如果使用v*Tag 触发发布建议只允许符合版本规则的 Tag例如v*GitHub 不同套餐和仓库可见性支持的 Environment 保护能力可能不同应以仓库 Settings 页面实际提供的选项为准。4.3 不需要创建 NPM_TOKENOIDC 发布不需要在下面的位置添加 npm 发布 TokenSettings → Secrets and variables → Actions如果旧 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*的 TagSettings → Rules → Rulesets → New ruleset → New tag rulesetTag 保护与 Environment 审批配合使用可以避免普通写权限成员通过创建 Tag 直接触发生产发布。五、编写 GitHub Actions Workflow在项目中创建.github/workflows/publish.ymlname:Publish to npmon:push:tags:-v*workflow_dispatch:concurrency:group:npm-publishcancel-in-progress:falsejobs:publish:name:Publish packageruns-on:ubuntu-latestenvironment:npmpermissions:contents:readid-token:writesteps:-name:Checkout repositoryuses:actions/checkoutv6-name:Setup Node.jsuses:actions/setup-nodev6with:node-version:24registry-url:https://registry.npmjs.org/package-manager-cache:false-name:Show runtime versionsrun:|node --version npm --version-name:Verify tag matches package versionif:github.event_name pushshell:bashrun:|PACKAGE_VERSION$(node -p require(./package.json).version) if [[ ${GITHUB_REF_NAME} ! v${PACKAGE_VERSION} ]]; then echo Tag ${GITHUB_REF_NAME} does not match package version v${PACKAGE_VERSION} exit 1 fi-name:Install dependenciesrun:npm ci-name:Run testsrun:npm test--if-present-name:Build packagerun:npm run build--if-present-name:Preview package contentsrun:npm pack--dry-run-name:Publish packagerun:npm publish提交 Workflowgitadd.github/workflows/publish.ymlgitcommit-mci: publish npm package with OIDCgitpush如果通过 HTTPS 使用 GitHub Personal Access Tokenclassic执行git push该 Token 除了仓库写入权限外还必须勾选workflowscopeUpdate GitHub Action workflows否则无法新增或修改.github/workflows/下的 Workflow 文件。5.1 id-token: write 是关键权限下面的权限允许当前 Job 向 GitHub OIDC Provider 请求 JWTpermissions:id-token:write根据 GitHub OIDC 参考文档id-token: write只允许请求 OIDC Token本身不会直接授予修改仓库或 npm 包的权限。npm 仍然会验证 Token并根据 Trusted Publisher 配置决定是否允许发布。actions/checkout读取仓库代码还需要permissions:contents:read将权限放在publishJob 内可以避免同一 Workflow 的其他 Job 获得请求 OIDC Token 的能力。5.2 为什么没有 NODE_AUTH_TOKENOIDC Workflow 中不应再写env:NODE_AUTH_TOKEN:${{secrets.NPM_TOKEN}}npm CLI 会识别 GitHub Actions OIDC 环境在执行npm publish时完成身份交换。如果仍提供一个具有发布权限的长期 Token可能会掩盖 Trusted Publisher 配置错误让 Workflow 看似迁移成功实际仍然依赖旧 Token。5.3 为什么关闭发布任务中的依赖缓存示例使用package-manager-cache:false发布任务应优先保证构建来源清晰、可重复。缓存可以放在普通 CI 中加速测试但 npm 官方示例建议发布构建不要使用依赖缓存以减少旧缓存或受污染缓存进入发布产物的风险。5.4 为什么使用 Environment下面的配置有两个作用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.3Tag 应发布package.json中的1.2.3。如果二者不一致可能出现Tag 表示的版本与 npm 实际版本不同。重复发布一个已经存在的版本。Release Notes 与真实包版本错位。Workflow 在安装依赖前进行校验可以更早终止错误发布。六、执行一次发布6.1 更新版本并创建 Tag例如发布补丁版本npmversion patch-mchore(release): %sgitpushgitpush--tagsnpm version patch会更新package.json和package-lock.json中的版本。创建一个 Git Commit。创建形如v1.0.1的 Git Tag。Tag 推送到 GitHub 后publish.yml开始运行。如果 Environment 配置了 Required reviewersJob 会在发布前等待审批。6.2 验证发布结果在 GitHub 中检查Repository → Actions → Publish to npm在命令行中检查npmview your-scope/your-package versionnpmview your-scope/your-package dist-tags还可以安装刚发布的版本npminstallyour-scope/your-package1.0.1七、Provenance 来源证明使用 GitHub Actions Trusted Publishing 时npm 会在满足条件的情况下自动生成 Provenance不需要添加npmpublish--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 后应在开发环境中审查被阻止的依赖脚本npmapprove-scripts --allow-scripts-pending根据审查结果批准确实需要执行脚本的依赖并将生成的allowScripts配置提交到package.json。不要在 CI 中直接允许所有依赖脚本否则会绕过 npm v12 新增的安全边界。在正式启用 npm v12 前应本地运行npmcinpmtestnpmrun build --if-presentnpmpack --dry-run确认原生模块、代码生成工具和构建依赖没有因为 install script 被阻止而失效。九、项目使用私有依赖时的配置Trusted Publishing 只负责npm publish或npm stage publish不会让npm ci自动获得私有包读取权限。如果项目依赖私有 npm 包需要创建一个只读 Granular Access Token并仅在安装步骤中使用-name:Install dependenciesrun:npm cienv:NODE_AUTH_TOKEN:${{secrets.NPM_READ_TOKEN}}-name:Publish package with OIDCrun:npm publish注意NPM_READ_TOKEN只授予读取必要私有包的权限。不要为它启用发布权限或 2FA bypass。不要在npm publish步骤中传递该 Token。项目中的.npmrc只能引用环境变量不能写入真实 Token。例如//registry.npmjs.org/:_authToken${NODE_AUTH_TOKEN}十、进一步加固发布流程10.1 OIDC 验证成功后禁止传统 Token 发布完成一次 OIDC 发布验证后进入 npm 包设置Package → Settings → Publishing access → Require two-factor authentication and disallow tokens该选项会阻止传统 Token 发布但不会阻止 Trusted Publisher 使用 OIDC。推荐迁移顺序添加 Trusted Publisher。提交不包含NPM_TOKEN的 Workflow。成功发布一个新版本。禁止传统 Token 发布。撤销旧的自动发布 Token。删除 GitHub 中不再使用的NPM_TOKENSecret。10.2 使用 Staged Publishing安全要求更高的项目可以让自动化流程只暂存版本npmstage publish然后由维护者通过 npmjs.com 或 CLI 完成 2FA 审批版本才会公开。此时在 Trusted Publisher 中只允许npm stage publish不要允许直接npm publish。10.3 固定第三方 Action示例为了便于阅读使用uses:actions/checkoutv6uses:actions/setup-nodev6对高安全要求的发布链路可以将 Action 固定到审核过的完整 Commit SHA并由 Dependabot 或 Renovate 提交升级 PR避免可变 Tag 被上游修改。10.4 将测试和发布拆分复杂项目可以先运行没有id-token: write的测试 Job全部通过后再运行发布 Job。只有最后一个 Job 拥有 OIDC 权限jobs:test:permissions:contents:readruns-on:ubuntu-lateststeps:-uses:actions/checkoutv6-uses:actions/setup-nodev6with:node-version:24-run:npm ci-run:npm testpublish:needs:testenvironment:npmpermissions:contents:readid-token:writeruns-on:ubuntu-lateststeps:-uses:actions/checkoutv6-uses:actions/setup-nodev6with:node-version:24registry-url:https://registry.npmjs.org/package-manager-cache:false-run:npm ci-run:npm run build--if-present-run:npm publish如果测试 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时发生npmwhoami不能用于验证当前 Workflow 的 OIDC 发布身份。11.3 使用 Reusable Workflow 后认证失败如果使用workflow_callnpm 可能校验调用方 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 账号或组织。当前账号没有修改包设置的权限。十二、完整迁移检查清单发布前npm 包已经存在。Node.js 22.14.0npm 11.5.1。package.json的包名、版本、Registry 和repository.url正确。npm Trusted Publisher 的仓库、Workflow 和 Environment 配置正确。GitHub 中已创建同名 Environment。Workflow 使用 GitHub-hosted Runner。发布 Job 只有contents: read和id-token: write等必要权限。Workflow 的发布步骤没有使用NPM_TOKEN。Tag 与package.json版本一致。npm v12 所需的依赖 install script 已经完成审查。首次 OIDC 发布成功后在 npm 包页面确认新版本和 Provenance。将 Publishing access 改为要求 2FA 并禁止传统 Token。撤销旧 npm 发布 Token。删除 GitHub 中不再使用的NPM_TOKEN。保留私有依赖所需的只读 Token并限制其作用范围。定期审计 Trusted Publisher、Environment 审批人和 Tag Ruleset。总结npm 对 2FA-bypass Token 的限制意味着长期发布 Token 不再适合作为 GitHub Actions 自动发布的基础。Trusted Publishing 使用 GitHub OIDC 在运行时证明 Workflow 身份不需要保存长期发布密钥并且可以将权限限制到指定仓库、Workflow 和 Environment。一套推荐的 npm 发布链路是受保护的版本 Tag → GitHub Actions 测试和构建 → GitHub Environment 审批 → GitHub 签发短期 OIDC Token → npm 验证 Trusted Publisher → npm publish → 自动生成 Provenance完成迁移并验证成功后再禁止传统 Token 发布并撤销旧 Token才能真正移除长期凭证带来的供应链风险。真实案例为了更好的便于读者理解可以参考本人的开源项目 claude-tracepublish.yml使用 GitHub Actions 和 OIDC 自动发布 npm 包的完整 Workflow。publish-oidc.sh发布前检查版本、构建产物和 OIDC 环境的辅助脚本。oidc.md项目中配置、使用及排查 OIDC 发布流程的说明文档。参考资料npm install-time security and GAT bypass2fa deprecationTrusted publishing for npm packagesUsing private packages in a CI/CD workflowGenerating provenance statementsGitHub Actions OpenID ConnectOpenID Connect reference