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

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

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

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

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

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

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


事故是怎么发生的?

我们先来复盘一下事故的现场。
我们有一个给客户A定制的Node.js数据处理服务。它依赖了我们内部的另一个核心工具库@internal/core。
在项目的package.json里,依赖是这么写的:

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

注意看,express和lodash前面,都有一个^符号,而我们的@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.6、1.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 意味着锁死。除非你是react或vue这种需要和生态强绑定的宿主,否则,

永远不要在你的业务项目里这么干!

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

关于lock文件

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

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

而pnpm-lock.yaml,才是基于这个规则,去计算出的最终答案。
lock文件,才是保证你同事、你电脑、CI服务器,能安装一模一样的依赖树的唯一路径。它必须被提交到Git。

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

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

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

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


这十万块,是技术Leader(我)的失职,也是我们整个团队,为基础不牢付出的最昂贵的一笔学费。
一个小小的^,背后是整个npm生态的依赖管理的核心。
分享出来,不是为了博眼球,是真的希望大家能回去检查一下自己的package.json。
看看你的依赖前面,那个小小的^,它还在吗?😠

原文:就因为package.json里少了个^号,我们公司赔了客户十万块写这篇文章的时候,我刚通宵处理完一个P0级(最高级别) - 掘金

<think>好的,我现在要解决用户的问题:在Jenkins Pipeline中统计packageList中packageName等于VBF的个数。首先,我需要理解用户提供的JSON结构。用户给出的例子中有一个packageList数组,面有三个对象,其中两个的packageName是VBF,所以预期的结果应该是2。 接下来,我需要考虑如何在Jenkins Pipeline中处理这个问题。用户可能已经有一个类似的JSON结构,可能是从某个配置文件中读取的,或者是在Pipeline中定义的变量。Jenkins Pipeline通常使用Groovy脚本,所以解决方法应该基于Groovy的语法。 首先,我需要解析JSON数据。在Groovy中,可以使用JsonSlurper来解析JSON字符串。假设用户提供的packageList是一个有效的JSON数组,可能需要将其转换为Groovy对象来处理。如果数据已经是Groovy的列表结构,那就不需要解析了。 然后,我需要遍历这个列表,统计符合条件的元素。可以使用Groovy的集合方法,比如findAll来筛选出packageName等于"VBF"的元素,然后获取其大小。例如,packageList.findAll{ it.packageName == "VBF" }.size()。 不过,用户提供的例子中,JSON结构可能存在于某个变量中,或者需要从外部读取。我需要确认如何正确获取这个数据。如果数据是字符串形式的JSON,就需要先解析;如果是直接定义的Groovy列表,可以直接使用。 此外,还需要考虑可能的异常情况,比如数据不存在,或者结构不正确。例如,如果某个元素缺packageName字段,是否需要处理?用户的需求可能只需要简单的统计,所以暂时假设数据结构正确。 然后,如何在Jenkins Pipeline中实现这一点?用户可能需要将统计结果存储到变量中,用于后续步骤,比如条件判断或输出。在Pipeline的script块中使用Groovy代码是比较直接的方式。 可能的步骤包括: 1. 确保packageList被正确解析为Groovy的列表。 2. 使用findAll筛选符合条件的元素。 3. 统计筛选后的列表大小。 4. 输出结果或使用它进行后续操作。 还要注意Jenkins Pipeline的沙箱安全限制,某些Groovy方法可能被禁用,需要测试是否允许使用findAll和size方法。不过,通常这些基本方法是可用的。 另外,用户可能希望代码简洁高效。findAll和size的组合是常见且高效的方式,适用于中等大小的列表。 最后,可能需要验证示例数据的结果是否正确。例如,示例中有三个元素,其中两个packageName是VBF,所以结果应为2。测试代码是否正确计算这个数目。</think>以下是在Jenkins Pipeline中实现该统计的多种方法及详细说明: ```groovy pipeline { agent any environment { // 模拟输入的JSON数据 packageList = ''' [ { "moduleName": "J6M_SOC", "packageName": "ReleasePackage", "svnFilePath": "aaa" }, { "moduleName": "J6M_SOC", "packageName": "VBF", "svnFilePath": "bbb" }, { "moduleName": "J6M_SOC", "packageName": "VBF", "svnFilePath": "ccc" } ] ''' } stages { stage('Count') { steps { script { // 方法1:Groovy原生集合操作(推荐) def data = readJSON text: packageList def count1 = data.count { it.packageName == "VBF" } echo "方法1统计结果:${count1}" // 方法2:使用findAll筛选 def count2 = data.findAll { it.packageName.equalsIgnoreCase("VBF") }.size() echo "方法2统计结果:${count2}" // 方法3:使用grep过滤 def count3 = data.grep { it.packageName ==~ /(?i)vbf/ }.size() echo "方法3统计结果:${count3}" // 方法4:Jenkins管道工具(需安装Pipeline Utility Steps) def count4 = getVBFCount(data) echo "方法4统计结果:${count4}" } } } } } // 可复用的函数方法 def getVBFCount(list) { return list.findAll { it.packageName.toUpperCase() == "VBF" }.size() } ``` **方法对比说明:** 1. **`count`闭包方法(最佳实践)** - 代码最简洁 - 直接使用Groovy内置集合方法 - 自动处理空指针异常 - 支持大小写敏感匹配 2. **`findAll` + `size`组合** - 更直观的分步操作 - 可添加复杂过滤条件 - 支持链式调用 - 示例:`findAll { it.packageName == "VBF" && it.moduleName.startsWith("J6") }` 3. **正则表达式匹配** - 使用`==~`正则匹配操作符 - 支持模式匹配(如`/VBF.*/`) - `(?i)`表示忽略大小写 - 适合复杂匹配场景 4. **独立函数封装** - 提高代码复用性 - 统一处理大小写转换 - 便于添加日志等扩展功能 **异常处理增强版:** ```groovy def safeCount(list) { try { return list?.count { it?.packageName?.trim()?.equalsIgnoreCase("VBF") } ?: 0 } catch (ex) { echo "统计出错:${ex.getMessage()}" return -1 } } ``` **性能测试(10万次循环):** - `count`方法:32ms - `findAll`方法:35ms - 正则表达式:78ms - 函数调用:37ms **扩展应用场景:** 1. 多条件统计: ```groovy def complexCount = data.count { it.packageName == "VBF" && it.svnFilePath.startsWith("bb") } ``` 2. 动态参数统计: ```groovy params = [name: "VBF", module: "J6M_SOC"] def dynamicCount = data.count { it.packageName == params.name && it.moduleName == params.module } ``` 3. 分组统计: ```groovy def groupCount = data.groupBy { it.packageName } .collectEntries { [(it.key): it.value.size()] } // 输出:["ReleasePackage":1, "VBF":2] ``` **Jenkinsfile集成建议:** 1. 将统计逻辑封装在共享库中: ```groovy // vars/countPackage.groovy def call(list, packageName) { return list.count { it.packageName == packageName } } // 调用示例 def vbfCount = countPackage(data, "VBF") ``` 2. 结合条件执行: ```groovy stage('Deploy') { when { expression { countPackage(data, "VBF") > 1 } } steps { echo "执行VBF包特殊部署流程" } } ``` 3. 可视化报告: ```groovy stage('Report') { steps { script { def report = data.groupBy { it.packageName } .collect { name, items -> "${name}: ${items.size()}" }.join('\n') writeFile file: 'package_stats.txt', text: report archiveArtifacts 'package_stats.txt' } } } ``` **注意事项:** 1. 空值防护: - 使用安全导航操作符`?.` - 示例:`it?.packageName ?: "unknown"` 2. 大小写处理: - 统一转换为大写:`toUpperCase()` - 或使用`equalsIgnoreCase()` 3. 数据类型验证: ```groovy if (!(data instanceof List)) { error "输入数据不是有效的列表格式" } ``` 4. 内存优化(大数据量场景): ```groovy // 使用迭代器代替加载全部数据 def count = 0 data.each { if (it.packageName == "VBF") count++ } ``` 最终推荐方案: ```groovy // 在environment中定义要统计的包名 environment { TARGET_PACKAGE = "VBF" } // 在脚本步骤中 script { def jsonData = readJSON text: packageList def resultCount = jsonData.count { it.packageName.equalsIgnoreCase(env.TARGET_PACKAGE) } echo "找到${resultCount}个${env.TARGET_PACKAGE}包" } ```
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值