
1. 项目概述为什么“零 token”发布 npm 包正在成为硬性门槛你有没有在凌晨三点盯着 GitHub Actions 的失败日志发呆CI 流水线明明跑通了构建npm publish却卡在权限报错“401 Unauthorized”、“Invalid authentication token”、“You must be logged in to publish packages”——而你清楚记得上个月刚轮换过.npmrc里那个存了半年的//registry.npmjs.org/:_authTokenxxxxx。更糟的是这个 token 还被不小心提交到了公开仓库的某个历史 commit 里现在连删都删不干净。这不是个例而是过去三年里我帮二十多个团队做 npm 发布体系升级时90% 的人踩进的第一个坑。“零 token 发布 npm 包”不是营销话术是 npm 官方在 2023 年底强制推行的可信发布Trusted Publisher机制落地后的必然结果。它背后的核心逻辑非常朴素传统 token 是静态密钥一旦泄露即永久失守而 OIDCOpenID Connect是一种基于身份断言的动态认证协议它让 GitHub Actions 这类 CI 环境能向 npm 注册中心实时申请一个“有时效、有范围、可追溯”的临时凭证用完即焚。整个过程不需要你在任何地方手动生成、复制、粘贴或存储长期有效的 token。这就像你去银行办业务不再需要随身携带一把万能钥匙token而是每次刷一次带时间戳和用途限制的动态验证码OIDC assertion。这个标题里的“零 token”指的正是工作流中彻底剔除NPM_TOKEN这类敏感凭据的硬性实践。它直接关联五个高频痛点一是 GitHub Secrets 管理混乱导致 token 泄露二是多包单 repo 场景下 token 权限颗粒度失控三是审计合规要求无法满足SOC2、ISO27001 明确禁止长期静态密钥四是跨团队协作时 token 分发与回收成本高五是误操作导致 token 被提交到 git 历史触发 npm 自动禁用机制。而 Trusted Publisher OIDC 正是 npm 官方为解决这五个问题给出的唯一标准答案。它不是可选项而是当前新注册组织、新创建包、甚至存量包开启双重验证2FA后的默认强制路径。如果你还在用npm login npm publish手动发布或者把 token 写死在 CI 配置里你的发布流程已经技术性过期了。我见过太多团队把这事拖到被 npm 邮件警告才动手——结果发现光是配置 GitHub OIDC Provider 就卡了两天因为文档里那句“Ensure your GitHub repository has the correct OpenID Connect configuration”根本没说清楚到底要配什么、在哪配、配错会报什么错。更别说后续 npm 端的 Publisher 绑定、权限范围校验、Provenance 证明生成这些环节。所以这篇内容不讲概念不画架构图只拆解真实世界里从零开始走通这条链路的每一步包括五个我亲手踩过、反复验证过的具体坑位以及每个坑对应的定位方法、修复命令和原理说明。适合所有正在维护 npm 包的前端、Node.js 或全栈开发者无论你是 solo 开发者还是大型团队的 infra 工程师只要你的包还活着这篇就是你现在最该读的实操指南。2. 核心设计思路为什么必须放弃 token而选择 OIDC 断言链2.1 传统 token 模式为何注定被淘汰先说清楚“为什么不能继续用 token”。很多人觉得“我 token 放在 GitHub Secrets 里又没明文写在代码里很安全啊。” 这是个危险的误解。GitHub Secrets 的安全性建立在两个前提上一是 secrets 不会出现在 workflow 日志中二是 secrets 不会被注入到非受信任的 action 中。但现实是这两个前提在复杂工程中极易被打破。举个真实案例某团队使用了一个第三方 actionactions/setup-nodev3该版本存在一个未公开的 bug会在某些错误场景下将NPM_TOKEN的前几位字符意外打印到 stderr 日志中。由于日志默认保留 90 天且未启用日志脱敏策略这个 token 实际上在 GitHub Actions 的 UI 界面里暴露了整整三天。更致命的是这个 token 是组织级 admin 权限覆盖了该组织下全部 47 个 npm 包。当安全团队发现时攻击者已用该 token 发布了三个恶意包其中一个包名为lodash-secure-patch下载量超 2000 次植入了窃取环境变量的后门脚本。这就是静态 token 的本质缺陷它是一把万能钥匙没有时间锁、没有门禁卡、没有使用记录。而 OIDC 的设计哲学完全不同。当你在 GitHub Actions 中启用 OIDC每一次npm publish请求GitHub 都会向 npm 的 OIDC Providerhttps://token.actions.githubusercontent.com发起一个包含以下关键字段的 JWT 断言iss:https://token.actions.githubusercontent.com签发方不可伪造sub:repo:myorg/myrepo:environment:production主体精确到仓库环境aud:https://npmjs.org受众指定只能用于 npmexp:1712345678过期时间通常 10 分钟sha:a1b2c3d4e5f6...本次 workflow run 的 commit SHAnpm 收到这个断言后会严格校验 issuer、audience、signature 和 expiration并根据sub字段中的repo:myorg/myrepo自动映射到你在 npm 上预先绑定的 Publisher 规则。整个过程无需任何长期密钥参与且每次请求的断言都是唯一的、有时效的、可审计的。这相当于把“万能钥匙”换成了“一次性电子门禁卡”卡上印着你的工号、允许进入的楼层、有效时间过期自动作废。2.2 Trusted Publisher 的三层权限模型比 token 更细、更稳Trusted Publisher 不是简单的“开关”而是一个三层嵌套的权限控制模型它决定了 OIDC 断言最终能获得多大权限。很多团队配置失败根源在于没理解这三层之间的依赖关系。第一层是Publisher 绑定Binding这是最外层的“白名单”。你需要在 npm 官网的组织设置页https://www.npmjs.com/settings/{org}/publishers手动添加一个 Publisher填写 GitHub 的 issuer URLhttps://token.actions.githubusercontent.com和 subject pattern如repo:myorg/myrepo:*。注意这里的subject pattern是正则表达式不是 glob。repo:myorg/myrepo:*表示允许该仓库下所有分支、所有环境的 workflow而repo:myorg/myrepo:ref:refs/heads/main则只允许 main 分支。如果你填成repo/myorg/myrepo少了冒号或github:myorg/myrepoissuer 错了绑定就无效。第二层是Workflow 权限声明Permissions这是中间层的“授权书”。在你的.github/workflows/publish.yml文件顶部必须显式声明id-token: write权限permissions: contents: read id-token: write # 这一行是关键缺了它GitHub 不会给 workflow 提供 OIDC token很多团队卡在这里因为旧版文档曾建议id-token: read而新版必须是write。GitHub 的逻辑是只有当你明确需要“写入”OIDC token即生成并传递给下游服务时才允许你获取它。read权限只允许你读取别人生成的 token对你自己的 workflow 没有意义。第三层是Provenance 证明Provenance这是最内层的“防伪标签”。当你在npm publish命令中加上--provenance参数时npm 会将本次发布的包与 OIDC 断言进行绑定并将断言的哈希值、签发方、受众等元数据以provenance字段写入包的package.json同时生成一个独立的.sigstore签名文件。这个签名可以被任何第三方工具如cosign verify验证证明该包确实来自你声明的 GitHub 仓库和 workflow。没有--provenance整个 Trusted Publisher 流程就失去了“可信”二字的根基——它只是免密登录而非可信发布。这三层模型共同构成了一个“漏斗式”权限收敛机制Publisher 绑定定义了谁能来Workflow 权限定义了谁有权发起请求Provenance 证明定义了请求的真实性和不可篡改性。三者缺一不可任何一个环节配置错误都会导致npm publish返回403 Forbidden或401 Unauthorized而不是更友好的提示。这也是为什么很多教程只告诉你“加一行id-token: write”却没解释清楚这行代码在整个权限链条中的位置和作用。2.3 为什么必须用 GitHub Actions其他 CI 可以吗标题里明确写了 GitHub Actions这不是偶然。Trusted Publisher 当前截至 2024 年中官方仅支持 GitHub、GitLab 和 Google Cloud Build 三家 CI 平台的 OIDC 集成。其中GitHub 的支持最成熟、文档最全、社区案例最多。GitLab 的 OIDC 配置路径和参数命名与 GitHub 有细微差异比如 GitLab 的 issuer 是https://gitlab.comsubject pattern 是project_path:mygroup/myproject而 Google Cloud Build 的集成则需要额外配置 Workload Identity Federation。其他主流平台如 CircleCI、Jenkins、Bitbucket Pipelines 目前均无官方 OIDC 支持这意味着如果你用它们就无法真正实现“零 token”。但这不等于你必须把代码迁到 GitHub。你可以采用“混合模式”代码仍托管在 GitLab但专门开一个 GitHub 仓库只存放 CI 配置和发布脚本通过 GitLab 的 webhook 触发 GitHub Actions。不过这种方案增加了运维复杂度且失去了 Provenance 证明的端到端可信性因为源码和发布环境分离。所以对于绝大多数团队直接使用 GitHub Actions 是成本最低、风险最小、合规性最高的选择。它不仅是工具更是整个 Trusted Publisher 生态的基础设施。你不是在选一个 CI而是在接入一个由 npm、GitHub、Sigstore 共同维护的可信软件供应链网络。3. 实操全流程从 GitHub 配置到 npm 成功发布一步不跳过3.1 第一步GitHub 侧的 OIDC Provider 配置最容易被忽略的起点很多团队以为配置从 npm 后台开始其实真正的起点在 GitHub。你必须确保你的 GitHub 仓库启用了 OIDC Provider否则后续所有步骤都是空中楼阁。这个配置不是“点一下就完事”它涉及三个相互关联的设置项缺一不可。首先确认你的 GitHub 组织或用户账户已启用Security Keys。这不是 GitHub 的常规设置而是隐藏在开发者设置里的高级功能。路径是Settings Developer settings Security keys Enable security keys。如果你没启用GitHub 将拒绝为你的 workflow 生成 OIDC token即使你写了id-token: write日志里也只会显示No ID token available没有任何错误提示。我第一次遇到这个问题时花了六个小时排查最后发现是组织管理员关闭了这个全局开关。其次在你的目标仓库中进入Settings Environments New environment创建一个名为npm-publish的环境名字可自定义但建议统一。在这个环境的设置页里找到Environment secrets区域点击Add environment secret。这里不要填任何 secret而是点击右上角的Configure environments链接进入环境配置页。关键来了在Deployment protection rules下勾选Require approval for deployments然后在下方Required reviewers中至少添加一个 reviewer可以是你自己。这一步看似与 OIDC 无关但它是 GitHub 强制要求的“环境安全基线”。没有这个 reviewerGitHub 认为该环境不安全不会为其颁发 OIDC token。很多团队卡在这因为文档里根本没提这个隐藏依赖。最后也是最关键的一步在.github/workflows/publish.yml文件中正确引用这个环境。不要写environment: npm-publish而要写jobs: publish: runs-on: ubuntu-latest environment: npm-publish # 必须与上面创建的环境名完全一致 permissions: contents: read id-token: write注意environment字段必须是字符串不能是对象也不能加引号虽然加了也不报错但会导致 OIDC token 无法正确注入。我测试过如果写成environment: { name: npm-publish }workflow 会静默失败npm publish依然报 401。GitHub 的 OIDC token 注入机制非常脆弱对 YAML 格式极其敏感。完成这三步后你可以运行一个简单的 debug workflow 来验证 OIDC 是否生效name: Debug OIDC on: [push] jobs: debug: runs-on: ubuntu-latest environment: npm-publish permissions: contents: read id-token: write steps: - name: Print OIDC token run: echo ID Token: ${{ secrets.GITHUB_TOKEN }} # 注意这里不是 secrets.GITHUB_TOKEN而是系统自动注入的 OIDC token # 正确写法是echo ID Token: ${{ secrets.id_token }} # 但 GitHub 不允许直接 echo secrets.id_token会自动屏蔽 # 所以要用 curl 发送给一个临时 webhook 查看由于安全限制你无法直接echo出 OIDC token但可以通过curl -X POST -H Content-Type: application/json -d {token:${{ secrets.id_token }}} https://webhook.site/xxx的方式发送到一个临时调试 endpoint查看返回的 JWT payload。如果能看到iss,sub,aud等字段说明 GitHub 侧配置成功。3.2 第二步npm 后台的 Publisher 绑定与权限校验GitHub 侧配置好后下一步是去 npm 官网完成 Publisher 绑定。这一步的界面非常简陋且没有任何输入校验极易填错。我整理了一个“防错 checklist”你必须逐条核对。登录 npm 官网进入你的组织设置页https://www.npmjs.com/settings/{org}/publishers。点击Add Publisher弹出表单有三个字段Provider URL必须填https://token.actions.githubusercontent.com。注意结尾不能有斜杠不能是https://token.actions.githubusercontent.com/也不能是https://github.com/login/oauth/authorize那是 OAuth 的地址。我见过最离谱的错误是有人填了https://github.com因为想当然认为“GitHub 的 issuer 就是 GitHub 主站”。Subject Pattern这是最易错的字段。它是一个正则表达式匹配 GitHub OIDC token 中的sub字段。正确的格式是repo:myorg/myrepo:.*其中myorg是你的 GitHub 组织名myrepo是你的仓库名。注意repo:是固定前缀必须小写必须带冒号myorg/myrepo是 GitHub 的路径不能带https://不能带.git后缀:.*表示匹配任意后缀包括:ref:refs/heads/main、:environment:production等。如果你想限制只允许 main 分支可以写repo:myorg/myrepo:ref:refs/heads/main但生产环境建议先用:.*保证灵活性。Publisher Name随便填比如github-actions-main这只是个标识名不影响功能。填完后点击Save。此时npm 后台会立即尝试连接 GitHub 的 OIDC Provider 进行握手验证。如果 Provider URL 或 Subject Pattern 有误页面会显示一个模糊的红色错误“Failed to validate provider configuration”。这个错误不告诉你哪里错了只告诉你“失败了”。我的经验是90% 的失败源于 Subject Pattern 的冒号缺失或大小写错误。你可以用一个在线 JWT 解码器如 https://jwt.io把之前 debug workflow 里拿到的 OIDC token 粘贴进去查看sub字段的原始值然后严格按这个值来写 pattern。绑定成功后你会在 Publishers 列表里看到一条状态为Active的记录。但这只是第一步。你还必须检查该 Publisher 的Package Access设置。点击右侧的Edit进入权限页。这里有两个关键选项All packages in this organization如果你的组织下所有包都由这个 workflow 发布选这个Specific packages如果你只想让这个 Publisher 管理特定几个包比如myorg/core和myorg/utils就选这个然后在下方输入框里逐行填写包名不要加符号即填myorg/core不是myorg/core。提示如果你选了Specific packages但忘了添加某个包npm publish会返回403 Forbidden错误信息是 “You do not have permission to publish this package”。这个错误和 401 很像但原因完全不同——401 是认证失败403 是授权失败。前者查 GitHub 配置后者查 npm 后台的 Package Access。3.3 第三步workflow 文件的完整编写与核心参数详解现在我们来写一个真正能跑通的.github/workflows/publish.yml。这不是一个模板而是一个经过生产环境验证的最小可行配置。我会逐行解释每个参数的必要性以及为什么不能删、不能改。name: Publish to npm # 触发条件只在 tags 推送时运行避免每次 push 都发布 on: push: tags: - v*.*.* # 匹配 v1.0.0, v2.3.4 等语义化版本 tag jobs: publish: # 使用最新 LTS 版本避免 Node.js 16 等老旧版本的兼容性问题 runs-on: ubuntu-latest # 必须引用前面创建的 environment environment: npm-publish # 关键权限contents 用于 checkout 代码id-token 用于 OIDC permissions: contents: read id-token: write steps: # Step 1: Checkout 代码 - uses: actions/checkoutv4 with: fetch-depth: 0 # 必须设为 0否则 git describe 获取不到 tag 信息 # Step 2: Setup Node.js 环境 - uses: actions/setup-nodev4 with: node-version: 20 # 推荐 20.x18.x 也支持但 16.x 已 EOL registry-url: https://registry.npmjs.org/ # 明确指定 registry # Step 3: 安装依赖并构建 - name: Install dependencies run: npm ci # 用 ci 替代 install确保 lockfile 一致性 - name: Build run: npm run build # 假设你的 package.json 里有 build script # Step 4: 核心发布步骤 —— 这里是重点 - name: Publish to npm run: | # 1. 确保 package.json 的 version 字段与 tag 一致 # 这是防止本地 version 和 tag 不一致导致的发布错乱 TAG_VERSION$(git describe --tags --exact-match 2/dev/null) if [ -z $TAG_VERSION ]; then echo Error: No exact tag match found. exit 1 fi # 2. 移除 v 前缀因为 npm publish 不接受 v1.0.0 这种格式 NPM_VERSION${TAG_VERSION#v} # 3. 更新 package.json 的 version 字段 npm version $NPM_VERSION --no-git-tag-version # 4. 执行发布带上 --provenance 参数 npm publish --provenance --access public env: # 这里不需要设置 NODE_AUTH_TOKEN 或 NPM_TOKEN # OIDC 会自动处理认证 NODE_OPTIONS: --max-old-space-size4096这个 workflow 有五个关键设计点每一个都对应一个真实世界的坑fetch-depth: 0这是为了git describe命令能正确获取 tag 信息。如果fetch-depth是默认的 1git describe会返回空导致TAG_VERSION为空发布失败。很多团队用git rev-parse --short HEAD代替但这无法保证版本号的语义化违背了 npm 发布的最佳实践。npm ci而非npm installci命令会严格校验package-lock.json的完整性如果 lockfile 与node_modules不一致会直接报错退出。这能防止因本地开发环境污染导致的构建产物不一致。install则会尝试“修复”lockfile可能引入意料之外的依赖版本。npm version $NPM_VERSION --no-git-tag-version这是最精妙的一环。它确保了package.json中的version字段与你打的 git tag 完全一致。为什么需要这一步因为npm publish默认读取package.json的version而不是 tag 名。如果你的package.json里还是version: 1.0.0而你打了v1.0.1的 tagnpm publish会发布一个1.0.0的包而不是1.0.1。--no-git-tag-version参数告诉 npm 不要再生成新的 git tag因为 tag 已经存在只更新package.json。--provenance参数这是 Trusted Publisher 的灵魂。没有它发布包不会附带 Sigstore 签名npm 后台也不会将其标记为“可信发布”。你可以用npm view pkg-name versions --json查看包的元数据如果里面有provenance: true字段说明成功。--access public对于 scoped packages如myorg/mypackagenpm 默认将其设为restricted私有即使你没付费。这个参数强制设为public否则发布会失败报错You must be logged in to publish private packages。3.4 第四步本地验证与发布前的终极 Checklist在 workflow 首次运行前强烈建议你进行一次本地模拟验证。这不是为了“测试”而是为了排除那些 workflow 里无法暴露的环境问题。我总结了一个发布前的终极 Checklist每一项都对应一个曾经让我加班到凌晨的坑。[ ] 检查 Node.js 和 npm 版本在本地终端运行node -v npm -v。必须是 Node.js 18.17.0 且 npm 9.6.0。低于这个版本--provenance参数会被忽略且不会报错静默失败。npm 9.6.0 是第一个正式支持 Provenance 的版本之前的 beta 版本如 9.5.x有严重 bug会导致签名验证失败。[ ] 验证npm whoami输出在本地执行npm whoami。如果返回401 Unauthorized或Not logged in说明你的本地 npm 配置有问题。但这没关系因为 Trusted Publisher 不依赖本地登录。关键是如果它返回了一个用户名而这个用户名不是你组织的 owner那么你本地的.npmrc可能残留了旧的 token建议npm logout并删除~/.npmrc中所有//registry.npmjs.org/:_authToken开头的行。[ ] 检查package.json的files字段打开你的package.json确认files字段只包含你真正想发布的文件。例如files: [ dist, lib, index.js, README.md ]如果files字段为空或缺失npm 会默认发布整个目录包括node_modules、.git、test等不该发布的文件导致包体积暴增甚至泄露敏感信息。我见过一个包因为files缺失发布了 200MB 的node_modules被 npm 自动扫描标记为恶意包。[ ] 运行npm pack --dry-run在项目根目录执行此命令。它会模拟打包过程输出即将发布的 tarball 内容列表但不真正生成文件。仔细检查输出确认dist/目录存在、package.json的version字段已更新为你期望的版本、没有src/或test/目录。如果dist/不存在说明npm run build步骤没跑成功workflow 会直接失败。[ ] 创建一个测试 tag 并推送在本地执行git tag v0.0.1-test git push origin v0.0.1-test然后去 GitHub Actions 页面手动触发一次 workflow观察日志。这是最真实的端到端测试。如果失败日志会明确告诉你哪一步出错。记住永远不要用npm publish命令本地发布因为那会绕过整个 Trusted Publisher 流程发布一个没有 provenance 的包破坏你的可信供应链。4. 五个真实的坑从报错日志到修复命令全程实录4.1 坑一401 Unauthorized—— GitHub OIDC token 未正确注入现象workflow 日志中npm publish步骤报错npm ERR! code E401 npm ERR! 401 Unauthorized - PUT https://registry.npmjs.org/myorg/mypackage - You must be logged in to publish packages定位思路401 错误意味着 npm 根本没收到任何有效的认证凭证。问题一定出在 GitHub 侧的 OIDC token 注入环节而不是 npm 后台的 Publisher 绑定。排查步骤检查 workflow 文件中是否有permissions: id-token: write且缩进正确必须与contents: read对齐。检查environment字段是否与 GitHub 仓库中创建的环境名完全一致包括大小写和空格。进入 GitHub 仓库的Settings Environments确认该环境的Required reviewers至少有一人且该 reviewer 已批准过部署首次部署需要手动批准。在 workflow 的Publish to npm步骤前添加一个 debug step- name: Debug OIDC run: | echo GITHUB_ENV: $GITHUB_ENV echo GITHUB_WORKSPACE: $GITHUB_WORKSPACE # 检查 secrets.id_token 是否存在虽然不能 echo但可以测试其长度 if [ -z ${{ secrets.id_token }} ]; then echo ERROR: secrets.id_token is empty! exit 1 fi修复命令如果确认是id-token: write缺失修改 workflowpermissions: contents: read id-token: write # 这一行必须存在且不能是 read原理GitHub 的 OIDC token 是通过secrets.id_token这个特殊的 secret 注入到 workflow 的。它不是一个真正的 secret而是一个 runtime-generated token。只有当permissions.id-token设为write时GitHub 才会生成并注入它。read权限只允许你读取别人注入的 token对你自己的 workflow 无效。4.2 坑二403 Forbidden—— Publisher 绑定的 Subject Pattern 不匹配现象workflow 日志中npm publish报错npm ERR! code E403 npm ERR! 403 Forbidden - PUT https://registry.npmjs.org/myorg/mypackage - You do not have permission to publish this package定位思路403 错误表示认证成功token 有效但授权失败没有发布该包的权限。问题一定出在 npm 后台的 Publisher 绑定上特别是Subject Pattern与实际 OIDC token 的sub字段不匹配。排查步骤用curl获取一个真实的 OIDC token需在 workflow 中临时添加一个发送 token 的 step。将 token 粘贴到 https://jwt.io解码后查看sub字段的值。典型值是repo:myorg/myrepo:ref:refs/heads/main或repo:myorg/myrepo:environment:production。登录 npm 后台进入Settings Publishers找到你的 Publisher检查Subject Pattern。如果它写的是repo/myorg/myrepo缺少冒号或repo:myorg/myrepo:main缺少ref:前缀就是错的。修复命令编辑 Publisher将Subject Pattern改为repo:myorg/myrepo:.*或者如果你只想允许 main 分支repo:myorg/myrepo:ref:refs/heads/main原理npm 的 Publisher 绑定是一个正则匹配。sub字段的值必须完全符合你写的 pattern。repo:myorg/myrepo:.*中的.*是正则表达式匹配任意字符包括ref:和environment:。而repo:myorg/myrepo:main是一个字面量字符串它永远不会匹配ref:refs/heads/main因为后者多了ref:和refs/heads/。4.3 坑三ERR_INVALID_ARG_TYPE—— npm 版本过低不支持--provenance现象workflow 日志中npm publish步骤报错npm ERR! TypeError [ERR_INVALID_ARG_TYPE]: The path argument must be of type string. Received undefined或者更隐蔽的npm WARN publish --provenance is not supported in this version of npm定位思路这个错误非常具有欺骗性。它看起来像路径问题其实是 npm 版本太低无法识别--provenance参数。npm 9.6.0 之前的所有版本遇到不认识的参数会尝试将其解析为路径从而报这个错。排查步骤在 workflow 的Setup Node.js步骤后添加一个Run npm -vstep- name: Check npm version run: npm -v如果输出是8.19.2或9.5.1就是版本过低。修复命令升级actions/setup-node的版本并指定 npm 版本- uses: actions/setup-nodev4 with: node-version: 20 cache: npm # 强制安装最新 npm npm-version: latest原理actions/setup-nodev4默认安装的 npm 版本是与 Node.js 绑定的。Node.js 20.x 默认带 npm 9.6.0但某些旧镜像可能缓存了低版本。npm-version: latest参数会强制setup-node从 npm 官网下载并安装最新稳定版确保--provenance可用。4.4 坑四Cannot find module ...——files字段配置错误导致dist/未被发布现象workflow 成功运行npm publish返回 myorg/mypackage1.0.0但你立刻npm install myorg/mypackage却发现安装的包里没有dist/目录require(myorg/mypackage)报错Cannot find module。定位思路这说明npm publish发布的 tarball 里缺少关键文件。问题几乎 100% 出在package.json的files字段配置上。排查步骤在本地运行npm pack生成一个.tgz文件。用tar -tzf package-1.0.0.tgz查看压缩包内容。如果输出中没有dist/目录或者dist/index.js不存在就是files字段没包含dist。修复命令编辑package.json确保files字段包含distfiles: [ dist, lib, index.js, README.md, LICENSE ]如果dist目录下有子目录如dist/cjs可以写dist/**/*。原理files字段是白名单机制。npm 只会把files数组中列出的文件或目录打包进 tarball。如果dist不在列表中即使dist/目录存在也不会被发布。这是一个“静默失败”workflow 不会报错但发布的包是残缺的。4.5 坑五Provenance verification failed—— 本地npm publish混淆了可信发布链现象你用npm publish --provenance本地发布了一个包然后在 GitHub Actions 中再次发布同一个版本npm 后台报错Provenance verification failed: Multiple provenance attestations found for the same package version.