更新日期:2026-08-31
npm 在 2026 年把发布流程从“上传后几乎立即可安装”改成了“先扫描,再变成可安装”。新发布的包会接受恶意软件扫描,正常情况下通常约几分钟,高峰时可能更久;被暂缓或阻止的版本不会按普通包一样对消费者开放。与此同时,安全研究、渗透测试、远程管理等可能被合法使用也可能被滥用的“双用途包”,必须显式声明内容类别并提供披露文件。
这两个变化会直接影响自动发布。旧流水线在 npm publish 返回成功后立刻创建 GitHub Release、更新文档、部署依赖该新版本的应用,很容易出现“发布任务绿色,但消费者暂时安装不到”的空窗。更稳妥的方案是把构建、上传、审核、公开、下游推广拆开:CI 用 OIDC 提交 staged package,维护者下载并核对 tarball,再以 2FA 批准,最后确认 registry 已可解析才推广。
本文基于 npm 当前官方文档给出可落地的发布设计。文中的扫描时长是官方描述的常见经验,不是 SLA;流水线必须允许状态暂缓、扫描阻止和人工申诉,而不是写死“五分钟必定上线”。
发布流程现在多了哪些状态
把一次 npm release 拆成以下状态更容易理解:
- 构建完成:源码测试通过,生成待发布 tarball;
- 提交成功:registry 已接受直接发布或 staged publish 请求;
- 等待审核/扫描:版本尚不能被普通消费者可靠安装;
- 人工批准:仅 staged publishing 需要,维护者用 2FA 确认;
- 公开可解析:npm view 包名@版本 能得到目标版本;
- 下游推广:更新 latest 之外的业务标记、Release、部署和通知。
“提交成功”不再等于“公开可用”。发布脚本应把 registry 可解析作为后续自动化的前置条件,并设置有上限的等待时间。超时应该进入人工处理,不应无限重试或把相同 SemVer 再发布一次。
npm 官方说明,新包扫描通常约 5 分钟,高峰可能超过 15 分钟,但没有承诺固定完成时间。扫描结果可能正常通过,也可能暂缓以便进一步分析,或者被阻止。如果维护者认为阻止有误,可以通过 npm 支持渠道申诉。教程和内部 SLO 都不应把经验值写成保证。
先改发布模型,再改命令
推荐把 pipeline 划成四个独立 job:
| Job | 权限 | 产物/结果 | 失败后的动作 |
|---|---|---|---|
| Build | 只读源码、依赖 | 测试报告、tarball、SBOM | 修代码,不接触发布权限 |
| Stage | OIDC,仅允许 npm stage publish | stage ID | 重试前确认 SemVer 状态 |
| Review | 维护者交互登录 | tarball 审核结论 | reject 或进入申诉 |
| Promote | registry 只读或最小管理权限 | Release、部署、通知 | 不重复发布,延迟推广 |
这比把所有步骤塞进一个拥有长期 token 的 job 更容易审计。构建 job 即使被恶意测试脚本控制,也不应该天然拥有 npm 写权限;stage job 只接受已经通过前序检查的制品;人工审核者看的应该是将要公开的 tarball,而不是仓库中“看起来差不多”的源码目录。
第一步:在本地固定将要发布的内容
发布前先核对版本、入口、文件白名单和 tarball:
node --version
npm --version
npm pkg get name version files main exports bin
npm pack --dry-run
npm test
npm run build --if-present
随后生成真正的 tarball,并记录完整性:
npm pack
sha256sum ./*.tgz
tar -tzf ./*.tgz | sort
重点检查以下内容:
- .env、私钥、云凭据、测试 fixture 中的真实数据是否误入包;
- preinstall、install、postinstall 是否确有必要;
- bin、exports、类型声明和 source map 是否指向包内真实文件;
- 根目录 README、LICENSE、NOTICE、SECURITY、DISCLOSURE 是否按政策包含;
- 编译产物是否来自本次 commit,而不是开发机残留目录;
- 包中的 package.json 名称和版本是否与 release tag 一致。
npm pack --dry-run 很有价值,但它不替代真实 tarball 审核。生命周期脚本、文件生成和 ignore 规则可能让最终内容与开发者脑中的目录不同。
第二步:用 Trusted Publishing 替换长期写 token
npm Trusted Publishing 通过 OIDC 把 npm 包、仓库和特定 workflow 建立信任关系。GitHub Actions、GitLab.com shared runners 和 CircleCI cloud 当前受支持;自托管 runner 当前不受支持。npm CLI 至少需要 11.5.1,Node.js 至少需要 22.14.0。
以 GitHub Actions 为例,npm 包设置中要填写组织或用户、仓库、workflow 文件名以及允许动作。文件名只填 publish.yml 这类名称,不填完整 .github/workflows/ 路径。每个包当前只能配置一个 Trusted Publisher。
如果要把机器权限限制为仅提交 staged package,GitHub workflow 可使用:
name: Stage npm package
on:
push:
tags:
- "v*"
permissions:
id-token: write
contents: read
jobs:
stage:
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 test
- run: npm run build --if-present
- run: npm stage publish
npm 后台的 Allowed actions 只选择 npm stage publish。先验证 OIDC stage 能成功,再将包的 Publishing access 改为“要求 2FA 并禁止 token”,最后撤销不再需要的自动化写 token。顺序不能反过来,否则可能先把唯一可用的发布通道锁死。
id-token: write 是 GitHub OIDC 的关键权限,但不意味着 job 可以访问所有云资源;真正的信任边界还包括 npm 后台登记的仓库、workflow 文件和可选 environment。使用 reusable workflow 时尤其要核对调用者:npm 官方提醒,验证可能使用调用 workflow 的名称,父子 workflow 都要拥有正确的 OIDC 权限。
Trusted Publishing 只为 npm publish 或 npm stage publish 建立短期身份。它不会让 npm whoami 显示 OIDC 身份,也不会自动解决私有依赖安装;后者仍可能需要只读 token,且应与发布权限分离。
第三步:使用 Staged Publishing 加入人工审核
Staged Publishing 要求 npm CLI 11.15+、Node.js 22.14+,包必须已经存在于 npm registry,并且维护者账户启用了 2FA。它目前不能用于第一次发布的全新包。
CI 或本地提交候选版本:
npm stage publish
预发布版本必须显式给出 tag,例如:
npm stage publish --tag next
stage 使用与正常发布相同的 SemVer 唯一性规则。某个版本一旦 staged,就不能把相同版本当成新的普通发布再上传;tag 也是 staged package 的不可变属性。若 tag 选错,应先拒绝本次 stage,再修正并重新提交,不要修改 tarball 后继续沿用原审核结论。
维护者在独立、可信环境查看候选项:
npm stage list your-package
npm stage view STAGE_ID
npm stage download STAGE_ID
将下载的 tarball 与 CI 保存的 hash、版本和文件清单对比,并在隔离目录重新安装与测试。确认后才执行:
npm stage approve STAGE_ID
批准会要求 2FA,并把包发布到 live registry。发现问题则执行:
npm stage reject STAGE_ID
不要把 STAGE_ID 当作秘密,但也不要让聊天机器人或无权限自动化根据 ID 直接做批准决策。OIDC 可以提交 stage,不能替代需要人在场的 list、view、approve、reject 审核身份;这正是流程刻意保留的人机分界。
第四步:正确声明双用途包
双用途软件可能包含网络扫描、凭据测试、远程控制、漏洞验证、流量拦截等功能,同时具备合法的防御、安全研究或运维场景。npm 当前政策要求这类包在 package.json 中声明:
{
"name": "your-security-tool",
"version": "1.4.0",
"contentPolicy": {
"class": "dual-use"
}
}
包根目录还要包含名为 DISCLOSURE 的纯文本文件。它应该让审查者和用户理解:
- 哪些具体功能具有双用途性质;
- 设计上的合法使用场景;
- 使用者应取得何种授权;
- 安全限制、日志、速率控制或默认保护;
- 漏洞与滥用报告渠道。
一个简化模板可以是:
This package includes network discovery and credential validation features.
They are intended for systems you own or are explicitly authorized to test.
Operators are responsible for applicable law, scope approval, and audit logging.
Report vulnerabilities or abuse through the repository security policy.
模板必须按真实功能改写。只写“仅供教育用途”不能替代清晰披露,更不能把实际的数据窃取、隐蔽持久化或未经授权控制包装成合法工具。npm 的恶意软件政策继续适用,dual-use 声明不是免责标志。
声明后的发布权限也更严格:直接发布只允许维护者使用交互式 2FA;Trusted Publishing 的 OIDC 或带 bypass 2FA 能力的 token,只能通过 staged publishing 提交,之后仍需人工审核。声明会延续到后续版本,除非 npm Trust & Safety 审查后调整,因此不要用它临时绕过一次发布问题。
第五步:把扫描等待纳入自动化状态机
无论直接发布还是批准 staged package,都不要立刻假定下游可用。可以用有上限的只读轮询确认目标版本已经能从 registry 解析:
PACKAGE_NAME='your-package'
PACKAGE_VERSION='1.4.0'
for attempt in $(seq 1 20); do
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version \
--registry=https://registry.npmjs.org/ >/dev/null 2>&1; then
echo "Package is available"
exit 0
fi
echo "Waiting for registry availability (${attempt}/20)"
sleep 30
done
echo "Package is still unavailable; stop promotion and review npm status"
exit 1
这段脚本最多等待十分钟,只用于演示有界等待。团队应根据发布频率与变更窗口设置自己的上限;超过上限时停止创建下游 Release、镜像或部署,而不是继续发一个新版本“碰碰运气”。因为扫描高峰可能超过 15 分钟,十分钟超时只表示进入人工队列,不代表扫描失败。
为了避免名称被 shell 注入,真实流水线中的包名和版本应来自已经验证的 package.json,并符合组织的名称/semver规则。输出日志不要包含 token、完整 .npmrc 或 OIDC assertion。
如果发布等待期间需要调整 dist-tag,要先理解 npm 当前行为:官方说明 dist-tag 操作可以在扫描期间进行,但依赖新版本已经公开可用的 deprecate、unpublish 等操作可能需要等待。更简单的生产约定是先以 next/canary 发布和安装验证,确认通过后再更新 latest,而不是把扫描队列当成 tag 编排工具。
第六步:加入消费者视角的验证
registry 可返回版本,只证明元数据可见。发布后至少再做一次空目录消费测试:
test_dir=$(mktemp -d)
cd "$test_dir"
npm init --yes
npm install "your-package@1.4.0" --ignore-scripts
node -e "console.log(require('./node_modules/your-package/package.json').version)"
是否使用 --ignore-scripts 取决于测试目标。第一次可用它安全检查包内容;随后应在隔离容器中按消费者真实 npm 版本和安装脚本策略测试功能。不要在拥有云凭据、SSH agent 或生产网络访问的发布 runner 中直接执行新 tarball 的安装脚本。
验证项应包括:
- 包名、版本、dist-tag 与 Git tag 一致;
- tarball integrity 与审核制品一致;
- README、类型、CommonJS/ESM exports 和 CLI bin 可用;
- provenance 是否符合仓库可见性和 provider 限制;
- 安装脚本在 npm 12 未批准场景下的失败方式已经记录;
- 公开包没有密钥、内部 URL、客户数据或调试产物。
GitHub Actions 或 GitLab CI/CD 通过 Trusted Publishing 发布公开仓库中的公开包时,npm 会自动生成 provenance,不需要再传 --provenance。私有仓库、私有包以及 CircleCI 当前存在官方列出的 provenance 限制,不能把“使用了 OIDC”直接等同于“必有 provenance badge”。
被暂缓或阻止时怎么处理
首先停止推广,不要删除日志或篡改相同版本。保留以下证据:
- commit、tag、workflow run 与 runner 类型;
- npm pack 文件清单、tarball hash 与 SBOM;
- stage ID、包名、版本、提交时间和 registry 返回状态;
- 双用途声明、DISCLOSURE 和安全设计说明;
- 与上一个正常版本的内容差异。
然后排查是否误打包了压缩二进制、混淆代码、下载器、凭据相关测试样本或其他容易触发扫描的内容。确属误报时,按 npm 官方支持/申诉渠道提交精确证据。不要通过改名、拆包或反复发布新版本绕过扫描,这会扩大事件并损害后续审查。
若问题来自 staged 候选包且尚未批准,直接 reject,修复后递增版本再提交。SemVer 已经被 staged 使用时,不要尝试复用同一版本。若问题只影响 dist-tag,可以在确认包已正常可用后修正 tag;如果版本已经公开且存在严重缺陷,根据 npm 的 unpublish 政策判断能否撤回,并优先发布修复版与 deprecate 说明。
推荐的权限设计
一套较强但仍可日常运作的配置是:
- 构建 job 没有 npm 写 token;
- Trusted Publisher 绑定唯一云托管 workflow;
- Allowed actions 仅启用 npm stage publish;
- 包设置要求 2FA 并禁止传统 token;
- CI 负责构建、测试和提交,维护者负责下载核验与 2FA 批准;
- 推广 job 等 registry 实际可解析后才运行;
- 私有依赖只读 token 与发布身份分离,并限制暴露 job;
- 双用途包的功能、默认保护与滥用处理由安全负责人共同审查。
这套设计不能消除维护者账号被接管、恶意源码通过 review、构建系统污染等所有风险,但它显著减少长期写 token 泄漏和无人审核直接发布的风险,并让“谁构建、谁提交、谁批准、谁推广”都留下清晰记录。
发布前检查清单
- Node.js 与 npm 达到 Trusted/Stage Publishing 最低版本;
- 包已存在于 registry,确认可以使用 staged publishing;
- npm pack 的真实 tarball 已完成文件、密钥与入口审查;
- Trusted Publisher 的仓库、workflow、environment 和 allowed actions 精确;
- 已先验证 OIDC,再禁止并撤销传统写 token;
- CI 仅 stage,维护者用 2FA approve;
- 双用途包已声明 contentPolicy.class 并包含纯文本 DISCLOSURE;
- staged tarball 与 CI 保存的 hash 一致;
- 扫描等待使用有界轮询,超时停止推广;
- registry 可解析后完成空目录消费者测试;
- 暂缓、阻止、reject 和申诉均有负责人和证据模板。
npm 的新发布机制要求团队接受一个现实:上传动作只是发布过程的中点。把候选制品先固定、机器权限缩到 stage-only、人工审核绑定 2FA,再以 registry 的真实可用状态驱动下游,才不会在扫描时代留下“流水线成功、用户却装不到”的灰色故障。

