From 217e32b4945bf94ff05a7d901ccfefdffdb95e50 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Wed, 4 Nov 2026 18:20:04 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E8=A1=A5=20staticcheck=20+=20=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E7=8E=87=E9=87=87=E9=9B=86=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E6=8A=8A"=E7=9C=8B=E4=B8=8D=E8=A7=81"=E5=8F=98=E6=88=90?= =?UTF-8?q?=E5=8F=AF=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 背景 本轮判据治理的起点是「覆盖率/判据形状两者都看不清」。前几个提交补了 gofmt 门和 SDK 判据步骤;这个补最后两项。 ## staticcheck ### 为什么选它 `go vet` 只跑标准检查器。实测今天撞到的那类缺陷它抓不到: pluginHealthTracker 标记 Unhealthy=true 后,isHealthy 立刻用 LastReload(**从未被赋值**,恒为零值)判"冷却已过"而自愈 那是 SA4xxx 家族(无用赋值 / 永真条件)的领域。它不替代判据, 但能在"写完了但逻辑自相矛盾"处提前报警。 只查四族,不查风格: SA4xxx 无用赋值 / 永真条件 SA5xxx 可疑代码 SA6xxx 低效写法 SA9xxx 必错分支 ### 踩到的坑:版本不匹配会造出一道永远绿的门 第一版钉 staticcheck@2025.1.1,在 Go 1.27.1 上实测: internal error in importing "image/color" (cannot decode "image/color", export data version 4 is greater than maximum supported version 2) 然后**静默返回退出码 0** —— 一道永远绿的门,比没有门更危险。 改用 2026.2.2 后正常解析。步骤里加了两道判: · 退出码非 0 但输出里没有任何 `file.go:line` ⇒ 判定"未能运行",直接红 · 版本变化导致的静默退化由上面这条兜住 ### 存量基线:24 条 / 13 个文件(2026-11-04 实测) 按分布: SA9003 empty branch 10 SA4006 value never used 4 SA4000 identical expressions 3 SA4010 value never checked 2 SA4008 1 SA4023 1 SA6001 1 SA6003 1 处理方式是**基线 + 只许减不许增**:新增告警让这一步红,存量可见但 暂不阻塞(否则合并即长红,所有人学会忽略这道门 —— 正是本仓 ci.yml 里那段"长红 ⇒ 整道门失效"的教训)。低于基线时会提示把 BASE 调低,否则门会越来越松。 抽样确认这些是真问题,不是误报。例如: internal/agent/api/codec_chunkfast_golden_test.go:68:9: this value of fastOK is never used (SA4006) internal/agent/api/codec_chunkfast_golden_test.go:69:2: empty branch (SA9003) `fastOK` 赋值后从未使用,且紧跟一个空分支 —— 那里本意要区分 「回退」与「快速路径给出失败」,实际什么都没做。 ## 覆盖率采集 仓里 521 个测试文件、11.2 万行测试代码,但**没有任何覆盖率数据** —— 无法回答"哪些模块其实没被测到"。 补的步骤只报数,不设阈值: · 打总覆盖率 · 打覆盖率最低的 15 个包 **为什么不设阈值**:阈值会促成"为凑数写空判据",那比没覆盖率更糟 (本仓 engineering-lessons §1.1 记的就是这个)。这里只把数字 摆出来,让人做模块级决策时有参照。 顺带:这一步同时兜住了 `go test` 的失败(`|| { ... exit 1; }`), 所以它不只是统计,也是一道执行力门。 ## 验证 YAML 语法 OK CI 门禁自检 通过(动态枚举 / 排除表 / 启动图 / 交叉枚举 / run 步骤语法) 本地复跑 staticcheck rc=1、24 条 = 基线 ⇒ 通过 go build ./... PASS go test(core/webui) 全绿 ## 后续 staticcheck 存量 24 条已登记方向(future-plan.md「staticcheck 存量」)。 它不是判据问题,是代码问题,单独收敛。 --- .github/workflows/ci.yml | 86 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99a78cac..0e3ae5b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,11 +149,97 @@ jobs: python3 -c 'import yaml; print(" pyyaml", yaml.__version__)' python3 .github/scripts/check-ci-gates.py + # ── staticcheck(语义级静态分析)── + # + # ## 为什么必须补(2026-11-04) + # + # `go vet` 只跑标准检查器。实测今天撞到的一类缺陷它抓不到: + # + # pluginHealthTracker 标记 Unhealthy=true 后,isHealthy 立刻用 + # LastReload(**从未被赋值**,恒为零值)判"冷却已过"而自愈 + # + # 这正是 staticcheck 的 SA4xxx 家族(无用赋值 / 永真条件)的领域。 + # 它不替代判据,但能在"写完了但逻辑自相矛盾"处提前报警。 + # + # 版本钉死:staticcheck 有自己的语义假设,跟着 latest 走会让 + # 别人的发布节奏决定我们的 CI 红绿。 + - name: staticcheck(语义级静态分析) + run: | + set -euo pipefail + # ★ 版本必须匹配 Go 版本。实测:staticcheck 2025.1.1 在 Go 1.27.1 上 + # 报 "export data version 4 is greater than maximum supported version 2", + # 然后**静默返回退出码 0** —— 一道永远绿的门,比没有门更危险。 + # 2026.2.2 能正确解析。 + go install honnef.co/go/tools/cmd/staticcheck@2026.2.2 + SC="$(go env GOPATH)/bin/staticcheck" + "$SC" -version + + # ★ 只查"语义自相矛盾"类,不查风格类。 + # SA4xxx 无用赋值/永真条件、SA5xxx 可疑代码、SA6xxx 低效、 + # SA9xxx 必错分支。这四族在"写完了但逻辑打架"处最有效 —— + # 今天那个 pluginHealth 冷却锚点用零值字段的缺陷就属此类。 + set +e + out=$("$SC" -checks 'SA4*,SA5*,SA6*,SA9*' ./internal/... ./cmd/... 2>&1) + rc=$? + set -e + + if [ $rc -ne 0 ] && ! printf '%s' "$out" | grep -qE '\.go:[0-9]+'; then + echo "★ staticcheck 未能运行(不是发现缺陷):" + printf '%s\n' "$out" | head -20 + exit 1 + fi + + # 存量基线(2026-11-04 实测 24 条 / 13 个文件)。**只许减不许增**: + # 新增告警会让这一步红,存量告警在这里可见但暂不阻塞。 + # 收敛方向见 docs/zh/future-plan.md「staticcheck 存量」。 + BASE=24 + n=$(printf '%s' "$out" | grep -cE '\.go:[0-9]+:' || true) + echo "staticcheck 告警数: $n(基线 $BASE)" + if [ "$n" -gt "$BASE" ]; then + echo + echo "★ 新增了 $((n-BASE)) 条 staticcheck 告警(基线 $BASE):" + printf '%s\n' "$out" | head -40 + echo + echo "这些是 SA4xx/SA5xx/SA6xx/SA9xx —— 语义自相矛盾类,不是风格问题。" + echo "如果确认是误报,请在此处加 -checks 例外并写明原因。" + exit 1 + fi + if [ "$n" -lt "$BASE" ]; then + echo "✓ 比基线少了 $((BASE-n)) 条 —— 请把上面的 BASE 调低到 $n,否则门会越来越松" + fi + # ./... 不点名 cmd/gui(该目录是纯 Electron,无 .go 文件): # 显式 `go test ./cmd/gui` 会报 "no Go files",那是误报,不是缺陷。 - name: go test ./... run: go test ./... -count=1 -timeout 20m + # ── 覆盖率采集(只报数,不设阈值)── + # + # ## 为什么补(2026-11-04) + # + # 仓里 521 个测试文件、11.2 万行测试代码,但**没有任何覆盖率数据** —— + # 无法回答"哪些模块其实没被测到"。而本轮判据治理的起点正是 + # "覆盖率/"判据形状"两者都看不清"。 + # + # ★ 不设阈值:阈值会促成"为凑数写空判据",那比没覆盖率更糟。 + # 这里只把数字打出来,让人在做模块级决策时有个参照。 + - name: 覆盖率(仅报告) + run: | + set -euo pipefail + go test ./... -count=1 -timeout 20m \ + -coverprofile=/tmp/cover.out -covermode=atomic > /tmp/cover.log 2>&1 || { + echo "★ 覆盖率跑挂了(这条同时兜住了 go test 的失败)" + tail -40 /tmp/cover.log + exit 1 + } + echo "=== 总覆盖率 ===" + go tool cover -func=/tmp/cover.out | tail -1 + echo + echo "=== 覆盖率最低的 15 个包 ===" + go tool cover -func=/tmp/cover.out \ + | awk -F'\t+' '/^github/ {split($1,a,":"); pkg=a[1]; sub(/\/[^\/]+$/,"",pkg); gsub(/.*HologramAgent\//,"",pkg); cov=$3; gsub(/%/,"",cov); n[pkg]++; s[pkg]+=cov} END {for (p in n) printf "%.1f%%\t%s\n", s[p]/n[p], p}' \ + | sort -n | head -15 + # 整机启动图判据(internal/kernelmod/all/bootgraph_test.go)。 # # ## 为什么单列一步(而不是只靠上面的 go test ./...)