Jenkins 构建失败排查记录:mvn -U 把新版依赖被远程旧版覆盖
起因
某天我在 bgin 分支提了一个改动,触发 Jenkins 构建后发现失败,报错信息是:
[ERROR] /opt/jenkins/data/workspace/…/KaspaCoinbaseLedgerImportService.java:[353,53] cannot find symbol
[ERROR] symbol: variable SYSTEM_TYPE_KASPA_WALLET
[ERROR] location: class com.intax.core.constants.KaspaImportServiceConstants
奇怪的是:
- 这个常量明明已经在 intax-core 的 bgin 分支上定义了
- 本地编译完全没问题
- 其他环境(metaalpha 等)构建正常,只有 bgin 失败
- 而且失败发生在 intax-data 模块,跟我这次改的 intax-web-tob 一点关系都没有
排查了一通,挖出了一个挺反直觉的 bug,记录下来。
项目背景
这是个多仓的 Maven 工程:
- intax-platform(主项目,一个 Maven 父 pom)
- intax-core(基础工具库,被其他项目依赖)
- intax-data(数据模块,依赖 intax-core)
- intax-web-tob(Web 模块,依赖 intax-core)
四个仓独立 git 管理,各自维护 bgin、metaalpha 等多条客户分支。Jenkins 通过参数化构建,根据 BRANCH 和 BUILDENVIRONMENT 参数拉对应分支、用对应 maven profile 打包。
最近有人在 bgin 分支上做了 kaspa-wallet 改造:
- intax-core 加了个常量 SYSTEM_TYPE_KASPA_WALLET = “Kaspa Wallet”
- intax-data 引用了这个常量
代码本身没毛病,问题出在 Jenkins 编译流程上。
Pipeline 流程梳理
Step 1 — Checkout Parent Project 拉 intax-platform 主仓代码
Step 2 — Checkout Submodules 拉 intax-core / intax-data / intax-web-tob 三个子仓代码
Step 3 — Build Parent Project 在主仓根目录跑 mvn clean install,reactor 模式一次性编译所有模块,jar 装进本地 ~/.m2
Step 4 — Build Data Module (并行) cp -r 复制源码到 -bgin 后缀目录,单独再编 intax-data 和 intax-web-tob,产出最终 jar
Step 5 — Archive JARs
Step 6 — Deploy
Step 7 — Cleanup
前两步只是 git clone / git pull / git reset --hard,纯拉代码不编译。
第三步才是第一次编译。这一步在主项目根目录跑 mvn clean install,Maven reactor 模式把所有模块(包括 intax-core / intax-data / intax-web-tob)一次性全编了,jar 装进 Jenkins 本地的 ~/.m2/repository/com/ruoyi/ 缓存目录。这一步是成功的,新版 intax-core(带 SYSTEM_TYPE_KASPA_WALLET)进了本地缓存。
第四步单独再编 intax-data 和 intax-web-tob,就在这里出问题。
根因:一个反直觉的矛盾
第四步的 mvn 命令里带了 -U 参数:
mvn clean package -U -P${BUILDENVIRONMENT} -DskipTests
-U 的语义是"强制去远程仓库刷新 SNAPSHOT 依赖"。
听起来很合理 —— “我要拿最新的”。但在这套 Pipeline 里,这个参数变成了灾难,原因是一个非常隐蔽的矛盾:
┌─────────────────────────────────┬─────────────────────────────────────────────────────┬───────────────────────────────────────────────────┐
│ 哪里的 intax-core │ 当前状态 │ 谁写进去的 │
├─────────────────────────────────┼─────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ Jenkins 服务器本地 ~/.m2 │ 第三步刚装进来的新版(带 SYSTEM_TYPE_KASPA_WALLET) │ 这次 Pipeline 自己 │
├─────────────────────────────────┼─────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ 远程仓库(内网 Nexus / 公共镜像) │ 早就 deploy 上去的旧版(无 SYSTEM_TYPE_KASPA_WALLET) │ 历史上某次构建,可能几周前从别的分支 deploy 上去的 │
└─────────────────────────────────┴─────────────────────────────────────────────────────┴───────────────────────────────────────────────────┘
注意:mvn install 只装本地,不会往远程推。要往远程推得用 mvn deploy,这个 Pipeline 里没用。
所以每次构建,实际发生的事是这样的:
第三步:在本地默默装一份新版 intax-core
↓
第四步:启动 mvn -U
↓
“我去远程问问有没有更新的?”
↓
远程递回那本祖传旧版 intax-core(还是几周前的)
↓
maven 把远程旧版下载下来,覆盖了本地刚装的新版
↓
intax-data 编译时取依赖 → 从本地缓存拿到那个被覆盖的旧版
↓
找不到 SYSTEM_TYPE_KASPA_WALLET → cannot find symbol → 失败
生产者(Step 3)只写本地,消费者(Step 4)又跑去读远程,两边目标完全不一致,导致新版每次都被旧版覆盖。
为什么别的环境没事,只有 bgin 出问题
- metaalpha 等其他分支:这些分支根本没有 kaspa-wallet 的改造,intax-core 不带这个常量,intax-data 也不引用。所以即使本地新版被远程"等价的旧版"覆盖,实际内容一样,不影响编译。
- bgin 分支:只有 bgin 新增了这个常量、新增了对它的使用。intax-data 必须拿到 bgin 版的 intax-core 才能编过,但 maven 反复给它一份旧的 → 永远失败。
也就是说,这个 bug 是潜伏很久了的,bgin 一旦引入"远程没有的新依赖",就立刻暴露。
为什么我的 commit 跟这事一点关系没有
我这次改的是 intax-web-tob 的报表代码,完全没碰 kaspa 相关。Jenkins 日志里 intax-web-tob 那个 stage 是 BUILD SUCCESS,只有 intax-data 那个 stage 报 cannot find symbol。
也就是说,就算我把我的 commit 完全 revert 掉,Jenkins 还是会因为这个 bug 失败。这是 kaspa-wallet 那批 commit + Jenkins Pipeline -U 参数的组合 bug,跟我的提交无关。
修复
两步:
- 清掉本地缓存里被污染的旧 jar(已被覆盖的现状要清掉,否则下次还是用这个旧的):
rm -rf /root/.m2/repository/com/ruoyi/intax-core
rm -rf /root/.m2/repository/com/ruoyi/intax-data
rm -rf /root/.m2/repository/com/ruoyi/intax-web-tob
- 改 Jenkinsfile,去掉 3 处 -U(根治,防止下次再污染):
stage(‘Build Parent Project’) {
dir(env.PARENT_DIR) {
sh 'mvn clean install -U -DskipTests'
}sh 'mvn clean install -DskipTests'
}
stage(‘Build Data’) {
dir(buildDir) {
sh "mvn clean package -U -P${params.BUILDENVIRONMENT} -DskipTests"
}sh "mvn clean package -P${params.BUILDENVIRONMENT} -DskipTests"
}
stage(‘Build Web-TOB’) {
dir(buildDir) {
sh "mvn clean package -U -P${params.BUILDENVIRONMENT} -DskipTests"
}sh "mvn clean package -P${params.BUILDENVIRONMENT} -DskipTests"
}
去掉 -U 之后,maven 只用本地缓存里 Step 3 刚装的新版 jar,不会再去远程问。
复盘:几个反直觉的点
- mvn install 的命名容易误导
英文 install 听起来像"装到正式位置去",实际上只是把 jar 拷贝到本地 ~/.m2/ 缓存。要往远程仓库推得用 mvn deploy,完全不同的命令。
- mvn -U 不一定让你拿到"更新"的版本
-U 的逻辑是"去远程检查是否有更新的 SNAPSHOT"。它根据 maven-metadata.xml 的 lastUpdated 时间戳决定要不要下载。如果远程那份 jar 是几周前 deploy 的,本地是几分钟前 install 的,理论上本地更新,但 Maven 的实际行为受 timestamp 解析逻辑和 SNAPSHOT 版本号(5.2.0-20251112.143022-7 这种)影响,边缘情况下会出现下载远程旧版覆盖本地新版。
- 这种 bug 表现得像"代码出问题"
报错 cannot find symbol 长得像源码缺失。但源码是好的(git reset --hard origin/bgin 之后工作区文件验证过)。问题在 Maven 依赖解析层,源码层面看不出来。要定位必须看 Jenkins 日志里 Downloading from … 这些行,才能发现远程在搞鬼。
- 多客户分支 + 共享 Maven coords 的天坑
com.ruoyi:intax-core:5.2.0-SNAPSHOT 这个 coords 所有分支共用。bgin 装一份、metaalpha 装一份、main 装一份,远程仓库里永远只有最后 deploy 的那一份,谁的版本谁就赢。如果远程被 main 分支占了,bgin 永远拿不到自己的新代码。
要根治这个,要么:
- 不同分支用不同的版本号(比如 5.2.0-bgin-SNAPSHOT)
- 或者不要在 CI 流程里依赖远程 SNAPSHOT,Reactor 内部全编(也就是把 Step 4 去掉,Step 3 reactor 一次编完直接产 jar)
- 或者像本次的修复一样,只用本地缓存,关掉 -U
总结
┌──────────┬────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 维度 │ 内容 │
├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 表象 │ bgin 分支 Jenkins 构建失败 cannot find symbol SYSTEM_TYPE_KASPA_WALLET │
├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 真因 │ Jenkinsfile 第 4 步 mvn -U 主动去远程仓库刷新 intax-core,远程的旧版覆盖了第 3 步本地刚装的新版 │
├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 本质矛盾 │ Step 3 只写本地缓存(install),Step 4 又去读远程(-U),生产者和消费者目标不一致 │
├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 修复 │ 1) 清 ~/.m2 里被污染的旧 jar;2) Jenkinsfile 去掉 -U(3 处) │
├──────────┼────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 教训 │ mvn install 只装本地 ≠ 推远程;-U 不一定让你拿到真正最新的;多分支共用 Maven coords 是个长期隐患 │
└──────────┴────────────────────────────────────────────────────────────────────────────────────────────────┘
整件事下来最深的感悟是:CI 排错时,代码层面看不出问题不代表代码有问题,有时候 Maven / Pipeline 这层的"自作聪明"才是真凶。
