从 EOTP 到 E404:一次 npm 自动发布踩坑全记录

从 EOTP 到 E404:一次 npm 自动发布踩坑全记录(附完整修复路径)

简单需求:GitHub Actions 自动发布 npm 包。

结果踩了 6 个坑,差点放弃。这是一份完整复盘。

背景

给一个老 npm 包加自动发布:git push master → CI 跑 test → npm publish 上 registry。

workflow 一开始长这样(典型的旧式 token 发布):

yaml 复制代码
publish:
  needs: test
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 22.x
        registry-url: https://registry.npmjs.org
    - run: npm ci
    - run: npm publish --access public
      env:
        NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

看着没毛病。push 一下,CI 跑起来。

然后噩梦开始。


坑 1:CI publish 报 EOTP

css 复制代码
npm error code EOTP
npm error This operation requires a one-time password from your authenticator.

含义:npm 账号开了 "Require 2FA for write actions",每次 publish 都要 OTP。CI 没法交互输 OTP。

直觉以为重建一个 token 就行。但实际上:

Token 类型 能 bypass 2FA?
Classic - Publish
Classic - Read-only 不能 publish
Classic - Automation ✅ 唯一可以
Granular Access Token ❌(默认)

只有 Classic Automation Token 能绕开 2FA for writes。

坑 2:Classic Token 入口被 npm 下架了

打开账号 tokens 页面,按 "Generate New Token" ------ 只剩 Granular Access Token 选项。Classic 入口没了。

Granular Token 默认不 bypass 2FA,需要账号 2FA 设置里允许"token bypass 2FA"才行。配置层级又多一道。

------ 这条路堵了,换思路。

坑 3:disable 账号 2FA 后变 E403(死锁!)

那干脆关账号 2FA。CI rerun,错误变了:

vbnet 复制代码
403 Forbidden - Two-factor authentication or granular access token
with bypass 2fa enabled is required to publish packages.

原来这个包被 npm 强制 enroll 进了包级 2FA 政策 (旧包 / 高下载量包会被自动 enroll)。账号 disable 2FA 后,包级仍然要求 2FA 或 bypass-2FA 的 token。

死锁

markdown 复制代码
账号 2FA disabled → 包级要求 2FA → publish 失败
       ↑                              ↓
       └── 但要创建 bypass-2FA token 必须先开账号 2FA ─┘

绕不开。唯一干净出路:Trusted Publishing (OIDC) ------ GitHub Actions 和 npm registry 通过 OIDC 互信,完全不用 token。

坑 4:改 workflow 文件 push 被 GitHub 拒

OIDC 路径要改 workflow:删掉 NODE_AUTH_TOKEN、加 permissions: id-token: write、加 --provenance

本地 commit 完 push:

sql 复制代码
! [remote rejected] master -> master (refusing to allow an OAuth App
  to create or update workflow `.github/workflows/publish.yml`
  without `workflow` scope)

关键认知:账户权限 ≠ OAuth app 权限。

我账户是 owner、什么都能做。但 gh CLI 拿的 OAuth token 只有 repo, gist, read:org, admin:public_key 这几个 scope,没有 workflow 。GitHub 把 workflow 文件单独锁在 workflow scope 后面 ------ 因为 CI workflow 直接控制自动化(能读 secrets、能跑任意命令、能发恶意版本),改一行就能注入后门。

解决方法两条:

  1. gh auth refresh -s workflow -h github.com 给 token 加 scope(要浏览器确认)
  2. 浏览器直接在 GitHub 网页编辑 workflow 文件 + commit

坑 5:SSH key 推到另一个 GitHub 账号

公司机器不想动 OAuth scope,试 SSH push:

vbnet 复制代码
$ git push origin master
ERROR: Permission to <owner>/<repo>.git denied to <another-account>.

机器有两个 SSH key:

  • 默认 id_ed25519 → 对应账号 A(个人 / 工作账号)
  • 另一个 id_ed25519_personal → 对应账号 B(仓库 owner)

诊断命令(很有用,多账号机器必备):

bash 复制代码
ssh -T git@github.com
# Hi account-A! ...

ssh -T -i ~/.ssh/id_ed25519_personal git@github.com
# Hi account-B! ...

临时切 key 推:

bash 复制代码
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_ed25519_personal -o IdentitiesOnly=yes" \
  git push origin master

IdentitiesOnly=yes 关键 ------ 否则 SSH agent 可能把别的 key 也试一遍,最先成功认证的就是用了哪个账号。

关键认知 :SSH 鉴权和 OAuth scope 完全是两套体系。SSH key 直接对应一个 GitHub 账号,账号有写权限就能推任何文件(包括 workflow),不存在 scope 限制。

坑 6:OIDC 配好了还报 E404(最阴的一个)

workflow 改成 OIDC 风格 + npm 端配好 Trusted Publisher(owner / repo / workflow filename / environment 留空)。push,CI 跑:

vbnet 复制代码
npm notice Publishing to https://registry.npmjs.org/ ...
npm notice publish Signed provenance statement with source ...     ← OIDC token ✓
npm notice publish Provenance statement published to transparency log  ← sigstore ✓
npm error code E404
npm error 404 Not Found - PUT https://registry.npmjs.org/<pkg>
npm error 404  '<pkg>@x.y.z' is not in this registry.

包都打好了、provenance 都签了、sigstore 透明日志都记录了,最后 PUT 到 npm 返回"包不存在"???

这个错误信息是 npm 服务端给的误导性映射 ------ 实际是 OIDC token 没生效,npm 用了空 token 鉴权失败,错误码被映射成了 404 + "not in this registry"。

CI log 里有关键线索:

yaml 复制代码
env:
  NODE_AUTH_TOKEN: XXXXX-XXXXX-XXXXX-XXXXX

XXXXX 是 GitHub Actions 显示空 secret 的占位(NPM_TOKEN 被我删了)。

actions/setup-node@v4 配置 registry-url 时会自动写一份 .npmrc

ini 复制代码
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}

但 NODE_AUTH_TOKEN 是空值。

而 npm 10.x 不支持 OIDC trusted publishing :它看到 .npmrc 里有 _authToken 字段就用它(即便是空字符串)publish,根本不去做 OIDC token exchange。npm 服务端收到空 token → "无效"→ 返回 404 假装包不存在。

根因node 22.x 默认 npm 是 10.x,OIDC 不生效 。需要 npm 11.5.1+ ,它才会优先用 OIDC token 而忽略 .npmrc 里的空 token。

最终修复:node 22 → node 24

diff 复制代码
  - uses: actions/setup-node@v4
    with:
-     node-version: 22.x
+     node-version: 24.x
      registry-url: https://registry.npmjs.org

  - run: npm ci
  - run: npm publish --access public --provenance

node 24 自带 npm 11.x,OIDC 自动生效。一行 diff。

为什么不在 workflow 里 npm install -g npm@latest 升级 npm?

试过了。撞上经典的 npm 自我升级 bug:

lua 复制代码
npm error code MODULE_NOT_FOUND
npm error Cannot find module 'promise-retry'

npm 升级自己的过程中,旧 npm 还在跑、新 npm 部分文件已经替换,依赖路径错乱。换 node 24 干净利落,绕开整个雷区。

完整可用 workflow

yaml 复制代码
name: Test & Publish

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x, 20.x, 22.x]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

  publish:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    permissions:
      id-token: write       # ← OIDC 必需
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24.x   # ← npm 11+ 必需
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm publish --access public --provenance

加上 npm 端的 Trusted Publisher 配置(4 个字段:org / repo / workflow filename / environment-留空),就跑通了。

完整避坑清单

  1. 不要轻易 disable 账号 2FA ------ 会被包级 2FA 政策反咬,进死锁
  2. Trusted Publishing (OIDC) 是 2026 年最优解 ------ 无 token、有 provenance、符合 npm 包级 2FA 政策
  3. node 22.x 默认 npm 10.x,不支持 OIDC ------ 用 node 24.x(自带 npm 11.x)
  4. 不要在 CI 里 npm install -g npm@latest ------ 撞 promise-retry MODULE_NOT_FOUND
  5. 改 workflow 文件需要 workflow OAuth scope ------ gh CLI 默认没有;要么 refresh,要么走浏览器编辑,要么走 SSH
  6. 多账号机器 ------ ssh -T git@github.com 看 key 对应哪个账号;切 key 用 GIT_SSH_COMMAND + IdentitiesOnly=yes
  7. E404 "is not in this registry" ------ 在 OIDC 场景下基本等于 OIDC 没生效,先怀疑 npm 版本
  8. .npmrc 里有 _authToken 会旁路 OIDC ------ setup-node 设置 registry-url 时会自动写一份,npm 11+ 才会忽略它走 OIDC

三层认证体系总结

这次踩坑最大的收获是搞清楚 GitHub 上三套互不相通的认证:

通道 鉴权对象 受 scope 限制? 用场景
账户 + 浏览器 你账户本人 否,账户全权 网页操作
OAuth app (gh CLI / PAT) 账户代理,token 上有 scope 命令行 / API
SSH key 直接关联账号,无 scope 概念 git push

任何一个走不通,换另一个试。


希望这份记录能让下一个走同样路径的开发者少踩几个坑。完整修复链路就 8 行 yaml diff + npm 网页配一下 Trusted Publisher,但搞清楚每个坑的根因花了我两个小时。

相关推荐
YJlio5 天前
OpenClaw v2026.4.8 更新解析:扩展加载修复、通道配置优化、Slack 代理支持与升级避坑
gateway·自动化运维·版本更新·ai agent·openclaw·slack·插件兼容
YJlio5 天前
OpenClaw v2026.4.20 版本更新了哪些内容?深度解析
人工智能·开源项目·自动化运维·版本更新·ai agent·openclaw·kimi k2.6
YJlio5 天前
OpenClaw v2026.4.21 版本更新了哪些内容?图像生成、安全权限、插件修复与日志回退深度解析
人工智能·开源项目·自动化运维·版本更新·ai agent·openclaw·gpt-image-2
YJlio5 天前
OpenClaw v2026.4.23 更新了哪些内容?图像生成、鉴权路由、媒体持久化与排障修复深度解析
人工智能·开源项目·自动化运维·版本更新·ai agent·openclaw·gpt-image-2
YJlio6 天前
OpenClaw v2026.4.9 更新解析:Memory Dreaming、Control UI、安全修复、插件依赖与升级避坑
gateway·memory·自动化运维·版本更新·ai agent·openclaw·dreaming
YJlio6 天前
OpenClaw v2026.4.11 更新解析:Dreaming 导入、结构化 WebChat、视频生成增强、Ollama 缓存与升级避坑
自动化运维·视频生成·版本更新·ai agent·openclaw·dreaming·memory-wiki
YJlio6 天前
OpenClaw v2026.4.5 更新解析:视频/音乐生成、ComfyUI 工作流、多语言控制台、Memory Dreaming 与升级避坑
memory·自动化运维·comfyui·视频生成·版本更新·ai agent·openclaw
YJlio7 天前
OpenClaw v2026.3.23-2 更新解析:Qwen 接入、Knot 主题、插件稳定性、升级验证与避坑清单
自动化运维·qwen·版本更新·ai agent·插件系统·openclaw·clawhub
YJlio7 天前
OpenClaw v2026.3.28 更新解析:Qwen 认证迁移、xAI Responses API、MiniMax 图像生成、插件审批与升级避坑
自动化运维·qwen·版本更新·ai agent·插件系统·xai·openclaw