使用 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 中宣布:

  1. npm v12 已正式发布,并成为 latest 版本。

  2. 预计从 2026 年 8 月上旬开始,配置为绕过双因素认证的 Granular Access Token(GAT)将不能再绕过账户、包和组织管理操作的 2FA。

  3. 预计从 2027 年 1 月前后开始,2FA-bypass Token 将不能再直接发布 npm 包。

  4. 自动发布应迁移到 Trusted Publishing(OIDC),或者改用需要人工 2FA 审批的 Staged Publishing。

过去常见的自动发布方式,是在 npm 中创建一个允许发布且绕过 2FA 的 Token,再将它保存为 GitHub Repository Secret:

1
2
3
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

这种方案依赖一个长期凭证。只要 Token 没有过期或被撤销,拿到它的人就可能在允许的权限范围内持续操作。Token 还可能因为日志输出、错误配置、第三方 Action、开发者电脑或仓库 Secret 管理不当而泄漏。

随着 2FA-bypass Token 的直接发布能力被逐步取消,继续围绕长期发布 Token 建设自动发布流程不仅风险更高,也不是可持续方案。

1.2 OIDC 解决了什么问题

OIDC 发布不需要预先生成一个长期 npm 发布 Token。每次 Workflow 运行时:

1
2
3
4
5
6
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。

本文使用:

1
2
3
4
5
Node.js 24
npm 12
ubuntu-latest
.github/workflows/publish.yml
GitHub Environment: npm

2.1 新包需要先完成首次发布

Trusted Publisher 是在已有 npm 包的 Settings 页面中配置的,因此全新的包通常需要先由维护者交互式发布一次:

1
2
npm login
npm publish --access public

首次发布时按照 npm 提示完成 2FA。包创建成功后,再配置 Trusted Publisher,后续版本即可完全交给 GitHub Actions 和 OIDC。

不要为了首次发布而创建长期的 2FA-bypass Token。

2.2 检查 package.json

一个公开作用域包可以使用如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "@your-scope/your-package",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "git+https://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.accesspublic,这样无需每次添加 --access public

  • publishConfig.registry 可以防止包被误发到其他 Registry。

三、在 npm 中配置 Trusted Publisher

3.1 通过 npmjs.com 配置

登录 npmjs.com,进入要发布的包,然后打开:

1
2
3
4
5
6
Packages
→ 选择包
→ Settings
→ Trusted publishing
→ Select your publisher
→ GitHub Actions

填写以下字段:

字段 示例 说明
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
2
npm Trusted Publisher Environment name: npm
GitHub Actions Job environment: npm

如果 npm 中填写了 Environment,但 Workflow 没有声明 environment: npm,发布认证会失败。

3.3 使用 npm CLI 配置

也可以使用 npm trust 命令创建信任关系:

1
2
3
4
5
npm trust github @your-scope/your-package \
--repo OWNER/REPOSITORY \
--file publish.yml \
--env npm \
--allow-publish

先使用 --dry-run 检查参数:

1
2
3
4
5
6
npm trust github @your-scope/your-package \
--repo OWNER/REPOSITORY \
--file publish.yml \
--env npm \
--allow-publish \
--dry-run

这个命令与网页配置的作用相同。它需要当前终端已经登录 npm,并拥有目标包的管理权限。

四、在 GitHub 中配置发布环境

4.1 创建 Environment

打开 GitHub 仓库,进入:

1
2
3
Settings
→ Environments
→ New environment

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
2
3
Settings
→ Secrets and variables
→ Actions

如果旧 Workflow 中存在 NPM_TOKEN

  1. 先保留 Secret,完成一次 OIDC 发布验证。

  2. 确认 Workflow 没有读取该 Secret。

  3. 删除旧 Workflow 中的 NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  4. 最后在 GitHub 和 npm 中撤销不再使用的发布 Token。

不要在尚未验证 OIDC 可用前先撤销旧发布方式,否则可能中断发布。

4.4 保护发布 Tag

仅仅使用 push.tags: ["v*"] 不代表任何人都应该能创建发布 Tag。组织仓库可以通过 GitHub Rulesets 限制谁能创建或更新匹配 v* 的 Tag:

1
2
3
4
5
Settings
→ Rules
→ Rulesets
→ New ruleset
→ New tag ruleset

Tag 保护与 Environment 审批配合使用,可以避免普通写权限成员通过创建 Tag 直接触发生产发布。

五、编写 GitHub Actions Workflow

在项目中创建 .github/workflows/publish.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
name: Publish to npm

on:
push:
tags:
- "v*"
workflow_dispatch:

concurrency:
group: npm-publish
cancel-in-progress: false

jobs:
publish:
name: Publish package
runs-on: ubuntu-latest
environment: npm

permissions:
contents: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
registry-url: "https://registry.npmjs.org/"
package-manager-cache: false

- name: Show runtime versions
run: |
node --version
npm --version

- name: Verify tag matches package version
if: github.event_name == 'push'
shell: bash
run: |
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 dependencies
run: npm ci

- name: Run tests
run: npm test --if-present

- name: Build package
run: npm run build --if-present

- name: Preview package contents
run: npm pack --dry-run

- name: Publish package
run: npm publish

提交 Workflow:

1
2
3
git add .github/workflows/publish.yml
git commit -m "ci: publish npm package with OIDC"
git push

如果通过 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
2
permissions:
id-token: write

根据 GitHub OIDC 参考文档id-token: write 只允许请求 OIDC Token,本身不会直接授予修改仓库或 npm 包的权限。npm 仍然会验证 Token,并根据 Trusted Publisher 配置决定是否允许发布。

actions/checkout 读取仓库代码还需要:

1
2
permissions:
contents: read

将权限放在 publish Job 内,可以避免同一 Workflow 的其他 Job 获得请求 OIDC Token 的能力。

5.2 为什么没有 NODE_AUTH_TOKEN

OIDC Workflow 中不应再写:

1
2
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

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
  1. 让 OIDC Token 包含 npm Environment 身份,以匹配 npm Trusted Publisher。

  2. 应用 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
2
3
npm version patch -m "chore(release): %s"
git push
git push --tags

npm version patch 会:

  1. 更新 package.jsonpackage-lock.json 中的版本。

  2. 创建一个 Git Commit。

  3. 创建形如 v1.0.1 的 Git Tag。

Tag 推送到 GitHub 后,publish.yml 开始运行。如果 Environment 配置了 Required reviewers,Job 会在发布前等待审批。

6.2 验证发布结果

在 GitHub 中检查:

1
2
3
Repository
→ Actions
→ Publish to npm

在命令行中检查:

1
2
npm view @your-scope/your-package version
npm view @your-scope/your-package dist-tags

还可以安装刚发布的版本:

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 除了推动发布认证迁移,还默认收紧了安装阶段的供应链安全策略:

  • 依赖的 preinstallinstallpostinstall 生命周期脚本和隐式 node-gyp 构建默认不再自动执行。

  • Git 依赖默认不允许解析。

  • HTTPS Tarball 等远程 URL 依赖默认不允许解析。

升级 npm v12 后,应在开发环境中审查被阻止的依赖脚本:

1
npm approve-scripts --allow-scripts-pending

根据审查结果批准确实需要执行脚本的依赖,并将生成的 allowScripts 配置提交到 package.json。不要在 CI 中直接允许所有依赖脚本,否则会绕过 npm v12 新增的安全边界。

在正式启用 npm v12 前,应本地运行:

1
2
3
4
npm ci
npm test
npm run build --if-present
npm pack --dry-run

确认原生模块、代码生成工具和构建依赖没有因为 install script 被阻止而失效。

九、项目使用私有依赖时的配置

Trusted Publishing 只负责 npm publishnpm stage publish,不会让 npm ci 自动获得私有包读取权限。

如果项目依赖私有 npm 包,需要创建一个只读 Granular Access Token,并仅在安装步骤中使用:

1
2
3
4
5
6
7
- name: Install dependencies
run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_READ_TOKEN }}

- name: Publish package with OIDC
run: npm publish

注意:

  • 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
2
3
4
Package
→ Settings
→ Publishing access
→ Require two-factor authentication and disallow tokens

该选项会阻止传统 Token 发布,但不会阻止 Trusted Publisher 使用 OIDC。

推荐迁移顺序:

  1. 添加 Trusted Publisher。

  2. 提交不包含 NPM_TOKEN 的 Workflow。

  3. 成功发布一个新版本。

  4. 禁止传统 Token 发布。

  5. 撤销旧的自动发布 Token。

  6. 删除 GitHub 中不再使用的 NPM_TOKEN Secret。

10.2 使用 Staged Publishing

安全要求更高的项目可以让自动化流程只暂存版本:

1
npm stage publish

然后由维护者通过 npmjs.com 或 CLI 完成 2FA 审批,版本才会公开。此时在 Trusted Publisher 中只允许 npm stage publish,不要允许直接 npm publish

10.3 固定第三方 Action

示例为了便于阅读使用:

1
2
uses: actions/checkout@v6
uses: actions/setup-node@v6

对高安全要求的发布链路,可以将 Action 固定到审核过的完整 Commit SHA,并由 Dependabot 或 Renovate 提交升级 PR,避免可变 Tag 被上游修改。

10.4 将测试和发布拆分

复杂项目可以先运行没有 id-token: write 的测试 Job,全部通过后再运行发布 Job。只有最后一个 Job 拥有 OIDC 权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
jobs:
test:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24"
- run: npm ci
- run: npm test

publish:
needs: test
environment: npm
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24"
registry-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

依次检查:

  1. Workflow 是否包含 id-token: write

  2. 是否使用 GitHub-hosted Runner。

  3. Node.js 是否 >= 22.14.0

  4. npm 是否 >= 11.5.1

  5. npm 中的 Organization、Repository 是否与当前仓库一致。

  6. npm 中只填写了 publish.yml,而不是完整路径。

  7. Workflow 文件名的大小写和扩展名是否完全一致。

  8. npm 中配置了 Environment 时,Job 是否声明同名 environment

  9. package.jsonrepository.url 是否指向当前 GitHub 仓库。

  10. npm publish 步骤是否意外传入了一个无效的 NODE_AUTH_TOKEN

11.2 npm whoami 失败

这是预期行为。OIDC 身份交换只在 npm publishnpm 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: readid-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
2
3
4
5
6
7
受保护的版本 Tag
→ GitHub Actions 测试和构建
→ GitHub Environment 审批
→ GitHub 签发短期 OIDC Token
→ npm 验证 Trusted Publisher
→ npm publish
→ 自动生成 Provenance

完成迁移并验证成功后,再禁止传统 Token 发布并撤销旧 Token,才能真正移除长期凭证带来的供应链风险。

真实案例

为了更好的便于读者理解,可以参考本人的开源项目 claude-trace

  • publish.yml:使用 GitHub Actions 和 OIDC 自动发布 npm 包的完整 Workflow。

  • publish-oidc.sh:发布前检查版本、构建产物和 OIDC 环境的辅助脚本。

  • oidc.md:项目中配置、使用及排查 OIDC 发布流程的说明文档。

参考资料