npm 包发布加固 是做什么的?
目标
将 npm 包从“本地能跑”升级为“诚实、可检查、安全可发布”。优先进行真实验证,而非仅凭感觉:检查包 tarball 内容、运行时契约、CI、GitHub 设置以及依赖更新卫生。
从这里开始
- 确认确切的仓库:
pwd
git status --short --branch
git remote -v
- 检查包体表面:
cat package.json
ls -la
find .github -maxdepth 3 -type f -print 2>/dev/null
- 根据锁文件检测包管理器:
package-lock.json-> npmpnpm-lock.yaml-> pnpmyarn.lock-> yarn
默认使用下方 npm 命令,必要时根据检测到的包管理器进行转换。
包合约
确保 package.json 真实准确:
name:发布目标是有意且可用的version:当前预期的发布版本description、keywords:足以支持注册表搜索license:存在且与LICENSE文件一致repository、homepage、bugs:指向公共仓库。如果公共仓库 URL 尚未最终确定,将其标记为显式的发布前待办事项,而非默默省略type、exports、main、bin:与用户将要导入或运行的内容匹配files:限制 tarball 仅包含可发布工件engines.node:包实际支持的最低运行时版本prepublishOnly或等效脚本:防止发布过时的构建输出
对于 CLI 包,确认 bin 目标已构建、在相关情况下可执行,并通过用户将运行的命令进行冒烟测试。
运行时最低版本规则
Node 引擎是一种承诺,而非装饰。
- 如果
engines.node指定了>=20,CI 必须测试 Node 20。 - 保持
@types/node与最低支持运行时位于同一主版本线上。 - 优先将
@types/node精确固定到该运行时线上的最新补丁版本。 - 除非同时提升
engines.node,否则不要合并@types/node高于运行时最低版本的主版本升级。 engines.node是建议性的:npm 只发出警告,除非用户设置了engine-strict,否则不会硬性阻止安装。因此,在最低版本上运行 CI 才是真正强制履行承诺的方式。
原因:Node 类型仅在开发中使用,但它们会改变 TypeScript 允许的行为。@types/node@26 可能会让代码编译通过,同时使用的 API 在 Node 20 的用户那里却会失败。
验证命令
对于 npm 项目:
npm ci
npm run build --if-present
npm test --if-present
npm audit --omit=dev
npm pack --dry-run
对于 pnpm/yarn,使用等效的 install/build/test/audit/pack 命令。如果审计支持不同,请如实说明,而不要假装支持。
对于 CLI,还需至少运行一次外部冒烟测试:
node dist/path/to/cli.js --help
npm pack --dry-run
如果可行,将打包后的 .tgz 安装到临时目录,并完全按照用户的方式运行二进制文件。
Tarball 审查
npm pack --dry-run 是注册表真相的来源。检查内容:
- 包含构建输出
- 包含
README.md和LICENSE - 排除仅用于源代码的垃圾文件、临时文件、测试文件、私有文档、测试夹具、本地数据库、密钥
- 包大小合理
- CLI 的
bin路径出现在 tarball 中
如果缺少 files 字段,请添加它。不要依赖 .gitignore 作为发布策略。
GitHub 基础配置
添加或验证:
.github/workflows/ci.yml.github/dependabot.yml.github/CODEOWNERS.github/pull_request_template.mdSECURITY.md
对 GitHub 拥有的操作使用当前支持的主版本。如果 Actions 警告某操作运行时已弃用,请在 CI 通过后更新该操作的主版本。
CI 通常应包括:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@CURRENT_MAJOR
- uses: actions/setup-node@CURRENT_MAJOR
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build --if-present
- run: npm test --if-present
将 node-version 调整为包的最低支持运行时版本,并将每个 @CURRENT_MAJOR 替换为操作当前的 major 标签(例如 actions/checkout@v4)——固定到真正的 major,绝不要提交占位符。
Dependabot 基础配置
同时配置包和 GitHub Actions 更新:
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: deps
include: scope
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: deps
include: scope
当 Dependabot 在配置生效后立即打开 PR 时,将此视为配置已生效的证据。对 PR 进行分类;不要盲目合并所有通过检查的改动。
Dependabot PR 分类
使用以下经验法则:
- 运行时依赖的补丁/次版本:如果检查通过且发布说明平淡,则合并。
- 运行时依赖的主版本:检查发布说明并对包的实际行为进行冒烟测试。
- 开发工具的主版本:如果构建/测试/打包通过且不改变运行时契约,则合并。
- 高于运行时最低版本的
@types/node:关闭或替换为固定到运行时最低版本。 - GitHub Actions 的主版本:如果 CI 通过且移除了弃用警告,则合并。
如果两个 PR 在某个小配置文件中冲突,合并一个并更新另一个,或者直接应用第二个并附上清晰的注释说明已被取代。
GitHub 远程加固
应用仓库/账户支持的内容,然后使用 gh 或 API 读取来验证实际设置。
有用的设置:
gh repo edit OWNER/REPO \
--delete-branch-on-merge \
--enable-squash-merge \
--enable-merge-commit=false \
--enable-rebase-merge=false \
--allow-update-branch
gh api --method PUT repos/OWNER/REPO/vulnerability-alerts --silent
gh api --method PUT repos/OWNER/REPO/automated-security-fixes --silent
gh api --method PUT repos/OWNER/REPO/actions/permissions/workflow \
-f default_workflow_permissions=read \
-F can_approve_pull_request_reviews=false --silent
对于 Actions 白名单,当工作流仅使用 GitHub 拥有的操作时,优先仅允许这些操作。如果需要第三方操作,则明确允许最小的集合。
公共仓库通常会解锁更多安全控制。免费计划上的私有仓库可能会阻止:
- 分支保护或规则集
- 代码扫描
- 密钥扫描
- 推送保护
请明确说明被阻止的内容。不要报告未启用的控制项为已启用。
分支保护
如果可用,请使用以下方式保护默认分支:
- 合并前要求拉取请求
- 要求 CI 状态检查通过
- 如果适合项目,要求分支保持最新
- 要求讨论已解决
- 阻止强制推送
- 阻止删除
- 如果使用仅压缩合并,要求线性历史
启用保护后,除非用户明确要求并理解绕过风险,否则停止直接推送到 main。
门控 PR 工具
如果使用自动化审查/门控工具,请确保远程仓库已有默认/基础分支,然后再要求门控创建 PR。空的远程仓库通常需要先进行一次初始的 main 推送;否则,门控可以验证并推送功能分支,但由于没有基础引用,PR 创建会失败。
发布清单
在 npm publish 之前:
npm whoami显示预期账户npm view PACKAGE_NAME name version确认名称状态- 待发布提交的 CI 为绿色
npm pack --dry-run已审查- README 中的安装/使用示例与实际行为一致
repository、homepage和bugs指向真实的公共仓库(如果 URL 未最终确定,则明确标记为 TODO)- 没有阻碍发布的 Dependabot/安全告警残留
- 版本号有意提升、已在 git 中打标签(
git tag vX.Y.Z && git push --tags)并在 CHANGELOG 或 GitHub Release 中反映 - 作用域包(
@scope/name)使用--access public发布(作用域包默认为受限/私有)
对于使用 Web/通行密钥/双因素认证的账户:
npm publish --auth-type=web
优先从 CI 使用来源证明发布,以获得可验证的供应链链接。来源证明需要公共包、受支持的 CI(如 GitHub Actions)以及作业上的 id-token: write 权限:
npm publish --provenance --access public
发布后:
npm view PACKAGE_NAME version
npm view PACKAGE_NAME bin files engines repository --json
npx -y PACKAGE_NAME --help
如果使用来源证明发布,请确认注册表在包页面上显示了来源证明 /“Built and signed” 认证信息。
最终响应
仅报告实际验证的内容:
- 已运行的包检查
- Tarball 结果
- 依赖 PR 决策
- 已创建或合并的提交/PR
- 已确认的 GitHub 设置
- 被计划/可见性阻止的控制项
- 已发布的版本标签和来源证明状态(如果已发布)
- 如果仍由用户操作,则发布命令或发布后验证