就因为package.json里少了个^号,我们公司赔了客户十万块

写这篇文章的时候,我刚通宵处理完一个P0级(最高级别)的线上事故,天刚亮,烟灰缸是满的🚬。

事故的原因,说出来你可能不信,不是什么服务器宕机,也不是什么黑客攻击,就因为我们package.json里的一个依赖项,少写了一个小小的^(脱字符号)

这个小小的失误,导致我们给客户A的数据计算模块,在一次平平无奇的依赖更新后,全线崩溃。而我们,直到客户的业务方打电话来投诉,才发现问题。

等我们回滚、修复、安抚客户,已经是7个小时后。按照合同的SLA(服务等级协议),我们公司需要为这次长时间的服务中断,赔付客户十万块

老板在事故复盘会上,倒没说什么重话,只是默默地把合同复印件放在了桌上。

今天,我不想抱怨什么,只想把这个价值 十万块 的教训,原原本本地分享出来,希望能给所有前端、乃至所有工程师,敲响一个警钟。


事故是怎么发生的?

我们先来复盘一下事故的现场。

我们有一个给客户A定制的Node.js数据处理服务。它依赖了我们内部的另一个核心工具库@internal/core

在项目的package.json里,依赖是这么写的:

JSON 复制代码
{
  "name": "customer-a-service",
  "dependencies": {
    "@internal/core": "1.3.5",
    "express": "^4.18.2",
    "lodash": "^4.17.21"
    // ...
  }
}

注意看,expresslodash前面,都有一个^符号,而我们的@internal/core没有

这个^代表什么?它告诉npm/pnpm/yarn:"我希望安装1.x.x版本里,大于等于 1.3.5最新版本。"

而没有^,代表什么?它代表:我 安装1.3.5这一个版本,锁死它,不许变。

问题就出在这里。

上周,core库的同事,修复了一个严重的性能Bug,发布了1.3.6版本,并且在公司群里通知了所有人。

我们组里负责这个项目的同学,看到了通知,也很负责任。他想:core库升级了,我也得跟着升。

于是,他看了看package.json,发现项目里用的是1.3.5。他以为,只要他去core库的仓库,把1.3.5这个tag删掉,然后把1.3.6的tag打上去,CI/CD在下次部署时,重新pnpm install,就会自动拉取到最新的代码。

他错了!


最致命的锁死版本

因为我们的依赖写的是"1.3.5",而不是"^1.3.5",所以我们的pnpm-lock.yaml文件里,把这个依赖的解析规则,彻底锁死在了1.3.5

无论core库的同事怎么发布1.3.61.3.7,甚至2.0.0...

只要我们不去手动 修改package.json,我们的CI/CD流水线,在执行pnpm install时,永远、永远,都只会去寻找那个被写死的1.3.5版本。

然后,灾难发生了。

core库的同事,在发布1.3.6后,为了保持仓库整洁,就把1.3.5那个旧的git tag删掉了

然后,客户A的项目,某天下午需要做一个常规的文案更新,触发了部署流水线。

流水线执行到pnpm install时,pnpm拿着lock文件,忠实地去找@internal/core@1.3.5这个包...

"Error: Package '1.3.5' not found."

流水线崩溃了。一个本该5分钟完成的文案更新,导致了整个服务7个小时的宕机😖


十万块换来的血泪教训

事故复盘会上,我们所有人都沉默了。我们复盘的,不是谁的锅,而是我们对依赖管理这个最基础的认知,出了多大的偏差。

^ (Caret) 和 ~ (Tilde) 不是选填,而是必填

  • ^ (脱字符)^1.3.5 意味着 1.x.x (x >= 5)。这是最推荐 的写法。它允许我们自动享受到所有 非破坏性 的小版本和补丁更新(比如1.3.6, 1.4.0),这也是npm install默认的行为。
  • ~ (波浪号)~1.3.5 意味着 1.3.x (x >= 5)。它只允许补丁更新,不允许小版本更新。太保守了,一般不推荐。
  • (啥也不写)1.3.5 意味着锁死 。除非你是reactvue这种需要和生态强绑定的宿主,否则,永远不要在你的业务项目里这么干!

我们团队现在强制规定,所有package.json里的依赖,必须、必须、必须 使用^

关于lock文件

我们以前对lock文件(pnpm-lock.yaml, package-lock.json)的理解太浅了,以为它只是个缓存。

现在我才明白,package.json里的^1.3.5,只是在定义一个规则。

而pnpm-lock.yaml,才是基于这个规则,去计算出的最终答案。

lock文件,才是保证你同事、你电脑、CI服务器,能安装一模一样 的依赖树的唯一路径。它必须被提交到Git

依赖更新,是一个主动的行为,不是被动的

我们以前太天真了,以为只要依赖发了新版,我们就该自动用上。

这次事故,让我们明白:依赖更新,是一个严肃的、需要主动管理和测试的行为。

我们现在的流程是:

  1. 使用pnpm update --interactivepnpm会列出所有可以安全更新的包(基于^规则)。
  2. 本地测试:在本地跑一遍完整的测试用例,确保没问题。
  3. 提交PR :把更新后的pnpm-lock.yaml文件,作为一个单独的PR提交,并写清楚更新了哪些核心依赖。
  4. CI/CD验证 :让CI/CD在staging环境,用这个新的lock文件,跑一遍完整的E2E(端到端)测试。

这十万块,是技术Leader(我)的失职,也是我们整个团队,为基础不牢付出的最昂贵的一笔学费。

一个小小的^,背后是整个npm生态的依赖管理的核心。

分享出来,不是为了博眼球,是真的希望大家能回去检查一下自己的package.json

看看你的依赖前面,那个小小的^,它还在吗?😠

相关推荐
晴殇i6 小时前
尤雨溪创立的 VoidZero 完成 1250 万美元 A 轮融资,加速整合前端工具链生态
前端·vue.js
一大树6 小时前
MutationObserver 完整用法指南
前端
一晌小贪欢6 小时前
【Html模板】赛博朋克风格数据分析大屏(已上线-可预览)
前端·数据分析·html·数据看板·看板·电商大屏·大屏看板
墨寒博客栈6 小时前
Linux基础常用命令
java·linux·运维·服务器·前端
野生龟7 小时前
designable和formily实现简单的低代码平台学习
前端
路多辛7 小时前
为什么我要做一个开发者工具箱?聊聊 Kairoa 的诞生
前端·后端
jerryinwuhan7 小时前
理论及算法_时间抽取论文
前端·算法·easyui
秋子aria7 小时前
模块的原理及使用
前端·javascript
菜市口的跳脚长颌7 小时前
一个 Vite 打包配置,引发的问题—— global: 'globalThis'
前端·vue.js·vite