私有仓库搭建与管理的实战经验分享

UX-钰岩 工具 阅读 2,578
赞 15 收藏
二维码
手机扫码查看
反馈

又双叒叕翻车了,npm私有包发布失败

昨晚十一点半,我正准备收工,顺手给一个内部工具库打个新版本发到私有仓库,结果 npm publish 一敲,直接报错:

私有仓库搭建与管理的实战经验分享

404 Not Found - PUT https://registry.npmjs.org/@myorg/utils - Not found

我愣了一下,心想这不对啊,昨天还能发的。第一反应是账号登错了,赶紧 npm whoami 一看,账号是对的。然后怀疑作用域(scope)没配,查了下 .npmrc,也写了 @myorg:registry=https://registry.npmjs.org/,看起来没问题。

折腾了一个小时,试了删 node_modules、清缓存、重新登录、换网络、甚至重启电脑……最后发现,根本不是配置的问题 —— 是我本地的 npm registry 被悄悄覆盖了。

这里我踩了个大坑:.npmrc 的优先级太绕了

我一直以为项目根目录下的 .npmrc 是最高优先级,但其实 npm 的配置加载顺序是这样的(从低到高):

  • 默认配置(npm 内置)
  • npm 执行时传入的参数
  • 用户主目录下的 ~/.npmrc
  • 项目根目录下的 .npmrc
  • 命令行参数(比如 --registry=xxx

问题就出在我之前为了测试某个公开包,临时在 ~/.npmrc 里加了一行:

registry=https://registry.npmjs.org/

这一行会全局覆盖所有请求,包括私有作用域的包。虽然我在项目里写了:

@myorg:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=xxxxxx

但因为全局的 registry 配置存在,npm 根本不会去走作用域匹配逻辑。换句话说,只要全局 registry 存在,作用域规则可能就不会生效 —— 这个细节官方文档写得极其隐晦,我也是翻 issue 看到有人提才意识到。

后来我用 npm config list 查了一遍当前生效的配置,才发现:

; "user" config from /Users/me/.npmrc

registry = "https://registry.npmjs.org/"

; "project" config from /Users/me/project/.npmrc

@myorg:registry = "https://registry.npmjs.org/"
//registry.npmjs.org/:_authToken = "xxxxxx"

; node bin location = /usr/local/bin/node

看到没?registry 居然来自 user 配置!这就意味着整个项目的私有源设置都被顶掉了。

最终解决方案:强制清除全局 registry

解决方法倒是很简单,但我走了不少弯路。一开始我想着“那我就在项目里显式写一遍 registry 吧”,于是改成:

registry=https://registry.npmjs.org/
@myorg:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=xxxxxx

这样确实能发,但问题是——它会把所有依赖都指向公共源,万一公司内部还有其他私有依赖,就可能拉不下来。这不是长久之计。

后来我干脆把 ~/.npmrc 里的 registry 删了,只保留:

@myorg:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=xxxxxx

然后在项目里也保持一致。再运行 npm config list,终于看到:

; "project" config from /Users/me/project/.npmrc

@myorg:registry = "https://registry.npmjs.org/"
//registry.npmjs.org/:_authToken = "xxxxxx"
registry = "https://registry.npmjs.org/" ; via project config in .npmrc

这时候 registry 是由项目配置触发的,而不是全局污染的,心里踏实多了。

最后执行 npm publish,成功上传。松了口气。

核心配置就这么几行,但坑太多

现在我的标准私有包配置长这样:

@myorg:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=your-real-token-here

注意几个点:

  • 必须用 @scope:registry=xxx 明确指定作用域对应的源
  • 认证 token 要写成 //xxx/:_authToken 格式,不能写 _authauth
  • 不要在任何地方随意设置 registry=xxx,除非你清楚它会影响所有请求

如果你的团队用的是自建私有仓库(比如 Verdaccio),那就把地址换成自己的:

@myorg:registry=http://verdaccio.internal:4873/
//verdaccio.internal:4873/:_authToken=your-token

或者你也可以在命令行临时指定:

npm publish --registry http://verdaccio.internal:4873/

不过这种方式不适合 CI/CD,还是建议统一走 .npmrc 管理。

CI/CD 里还得小心环境变量泄露

顺带提一嘴,在 GitHub Actions 里部署私有包时,我原本是这么写的:

- name: Publish
  run: npm publish
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

你以为这样就行?错。默认情况下,npm 不会自动读取 NODE_AUTH_TOKEN 来填充 _authToken,除非你在 .npmrc 里声明:

//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}

或者用脚本动态生成:

// ci-setup.js
const fs = require('fs');
const token = process.env.NPM_TOKEN;
fs.writeFileSync('.npmrc', //registry.npmjs.org/:_authToken=${token}n);

我当时就是忘了这一步,导致 CI 总是 401,查了半天还以为是 token 权限问题。实际上只是没正确注入。

还有一种方案:npm login

其实还可以在 CI 里先执行 npm login,但这个命令是交互式的,需要处理 stdin。可以用下面这种方式:

echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc

这比 npm login 更轻量,也更适合自动化场景。我现在的 CI 脚本基本都用这种写法。

说点题外话:为什么不用 yarn?

我们项目之前用 yarn,但 yarn 对私有仓库的支持更迷。尤其是 yarn publish 时,它的认证机制和 npm 不完全兼容,有时候 token 会莫名其妙失效。后来我们统一切回 npm,反而省心很多。

当然如果你非要用 yarn,记得检查 .yarnrc 是否有类似配置:

"@myorg:registry" "https://registry.npmjs.org/"
//registry.npmjs.org/:_authToken "xxxxxx"

而且 yarn 会读 ~/.yarnrc.yarnrc,优先级也和 npm 不一样,更容易出问题。亲测有效:别折腾,用 npm 就完事了。

改完后还有个小问题,但不影响使用

现在每次执行 npm install,即使只装公开包,也会提示:

npm WARN Invalid validation result: {"valid":false,"location":"//registry.npmjs.org/"}

查了下是 npm 最近的一个 bug,和 token 校验有关,不影响实际功能。暂时无解,只能忍着。反正能跑就行,前端开发嘛,多少都有点将就。

以上是我踩坑后的总结

私有仓库看着简单,真搞起来一堆细节。最怕的就是那种“以前好好的,突然不行了”的问题,根本不知道是哪一层配置变了。

建议团队统一规范:.npmrc 必须提交到仓库,禁止手动修改 ~/.npmrc,CI/CD 使用环境变量注入 token。这样至少能保证一致性。

另外提醒一句:别信网上那些“一键配置”的 shell 脚本,很多都会偷偷改全局 registry,埋雷。

以上是我个人对私有包发布的踩坑记录,有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论