私有仓库搭建与管理的实战经验分享
又双叒叕翻车了,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格式,不能写_auth或auth - 不要在任何地方随意设置
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,埋雷。
以上是我个人对私有包发布的踩坑记录,有更优的实现方式欢迎评论区交流。

暂无评论