type
Post
status
Published
date
Apr 12, 2026
slug
CISummarize
summary
tags
技术探索
category
icon
password
这篇文章总结改Bug的经验:
报错分析,复现问题,定位、分析代码,定点修复,多层验证修复
我的两个案例:
dubbo-go-hessian2PR #389:workflow、测试命令、JDK 兼容和日志噪音混在一起。
llm-access-gatewayStage 7:main workflow 连续失败,最后定位到是 Go 流式 fallback 测试里的并发同步出了问题。
阶段 | 关键问题 | 典型动作 |
报错分析 | 报错到底来自哪一层 | 拆 workflow step、拆测试职责、区分 warning 和 failure |
复现问题 | 能不能不依赖完整 CI 重现核心问题 | 本地命令、Linux 容器、逐包收缩 |
定位代码 | 哪段代码真正解释了现象 | workflow、runtime helper、router test |
最小修复 | 哪个改动刚好修掉根因 | 改触发语义、拆命令、改同步方式 |
多层验证 | 修复是否只是偶然通过 | actionlint、vet、test、race、stage7 contract、workflow green |
Case 1:Dubbo Hessian2 PR #389
1. 报错信息
它们来自不同层:
- workflow 触发语义
- action runtime 版本
- Go test 输出噪音
- JDK 运行时差异
- benchmark 和 correctness test 的职责混跑
2. 报错分析
我先把报错信息拆开来,由报错信息分析出了这三个原因导致CI报错
第一层是 workflow 触发语义。旧 action warning 是
pull_request_target ,会使用目标分支上的 workflow,所以 PR 分支里的 workflow 更新并不会直接体现在这条检查里。第二层是日志噪音。
Matcher failed 是 go test -v 输出过大后,GitHub Actions issue matcher 正则匹配超时。这是 CI 可观测性问题,不是 Go 语义错误。第三层是测试职责。benchmark、coverage、race test 原来容易混在一起,导致一个报错里夹了多个含义。
3. 复现问题
actionlint:验证 workflow 语义,而不是靠 GitHub 页面猜
go test ./... -race:验证包级回归
go test . -race -coverprofile=coverage.txt:单独验证 coverage 产物
go test . -run '^$' -bench . -race:让 benchmark 独立运行,不再隐式重复整套单测
我把问题拆成四个复现,分别验证是否是这些地方出了问题
4. 定位、分析代码
我根据报错原因定位到三个位置:
位置 | 干什么的? |
.github/workflows/github-actions.yml | 解释旧 action warning 为什么能在 PR 上持续出现,说明问题来自 workflow 触发语义而不是业务代码 |
java_runtime_test.go | 解释同一套测试为什么会在不同 JDK 下表现不同,说明问题来自 runtime 差异而不是 Hessian 语义本身 |
decode_benchmark_test.go | 解释 benchmark 校验为什么会被 map 顺序污染,说明问题来自测试而不是解码逻辑的问题 |
5. 定点修复
我用多个最小修复分别闭环各自的信号来源:
- 移除
pull_request_target对 PR workflow 观察的干扰
- 升级旧 action 版本
- 去掉
go test -v带来的 matcher timeout 噪音
- 把 benchmark、coverage、race test 拆成不同命令职责
- 把 JDK 差异收敛进 runtime helper
多次小修复后,CI顺利通过,之前试过一次大补丁,反而修完这个点,另一个点又出问题,一直改,返工改麻了:

6. 多层验证
最终验证链路是:
最终提交链是逐层分析细化出来的:
最终 PR 状态是
2 / 2 passed。这个结果证明 workflow、runtime、test command 和 benchmark 报错都被拆开并各自闭环了。
Case 2:LLM Gateway Stage 7

1. 报错信息
这个报错看不出问题在哪,因为
Stage 7 static contract 同时覆盖:- Go tests
- Go vet
- deployment 资产
- Grafana dashboard JSON
- Stage 7 required assets
所以这里最容易犯的错,就是把
exit code 1 当成一个单点问题。2. 报错分析
第一步是把一个大 step 拆成多个可定位 step:
拆完之后,问题从“Stage 7 不过”细化成了“Stage 7 Go tests 不过”。
这一步把模糊失败第一次变成了可解释失败。
3. 复现问题
这次有了经验,很快就复现到了问题所在
先把 Go tests 改成逐包串行并打印 package:
逐包收缩后的原始输出先把失败压到 package 和单个测试:
这条收缩链很关键:
我把失败从主 workflow 收缩成了最小验证操作。
4. 定位、分析代码
这个出问题的测试验证的是 streaming fallback 最敏感的一段语义:
旧测试的问题在于,它把“状态可见”误当成了“事件已发生”。
旧写法是:
atomic.Bool 只保证读写安全,不保证写它的 goroutine 已经被调度。测试读到 secondary chunk,只能说明 fallback 已经发生,不能说明 primary goroutine 已经观察到 ctx.Done()。得出是并发测试语义出了问题
5. 定点修复
修复把断言对齐真实事件,用 channel 表达同步:
- secondary chunk 证明 fallback 已经发生
primaryCanceled证明 primary attempt 的 cancel 事件已经被 goroutine 观察到
也就是说,断言从“读到一个状态”变成了“等到一个事件”。
6. 多层验证
验证链路分三层:
最后 main workflow 恢复通过,
runtime-ci 的关键 job 重新变绿。我先把失败缩成一个最小复现入口,再把它压到并发语义边界,最后用更符合事件模型的断言收口。
两个案例共同点
阶段 | Dubbo Hessian2 PR #389 | LLM Gateway Stage 7 |
报错信息 | workflow warning、Go test、matcher timeout 混在一起 | Stage 7 static contract 只给粗失败 |
报错分析 | 区分 workflow、日志噪音、JDK、benchmark/test 职责 | 拆 Stage 7 contract 为多个可定位 step |
复现问题 | 用 actionlint、go test、benchmark 建立独立入口 | 从 workflow 缩到 package,再缩到单个测试 |
代码分析 | workflow、JDK helper、benchmark test | provider router stream fallback test |
最小修复 | 分层修补 workflow、runtime、test command | 把状态断言改成事件同步 |
多层验证 | actionlint、vet、test、race、benchmark、CI | package 压测、Stage 7 static、main workflow |
结论
以后改Bug都要这样spec:报错分析,复现问题,定位、分析代码,定点修复,多层验证修复。
题外话

无论再小的问题,都要认真细致完成,别把问题想太简单了,狮子搏兔亦用全力!
分享
