From 633150cf39b35df2ddd8da15374e2dd9b927243d Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 14:03:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20go=20vet=20=E7=9A=84?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=A4=B1=E8=B4=A5=E4=B8=8D=E8=AE=B8=E5=86=92?= =?UTF-8?q?=E5=85=85=E4=BB=A3=E7=A0=81=E7=BC=BA=E9=99=B7=E2=80=94=E2=80=94?= =?UTF-8?q?env-defaults=20=E8=A1=A5=20GOCACHE=20=E9=A2=84=E6=A3=80?= =?UTF-8?q?=EF=BC=88=E7=BC=BA=E9=A1=B9=EF=BC=89+=20=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E6=8C=89=E8=BE=93=E5=87=BA=E5=88=86=E7=B1=BB?= =?UTF-8?q?=EF=BC=8C=E5=91=BD=E4=B8=AD=E7=BC=93=E5=AD=98/=E6=9D=83?= =?UTF-8?q?=E9=99=90/=E7=A9=BA=E9=97=B4=E8=B5=B0=E9=80=80=E5=87=BA?= =?UTF-8?q?=E7=A0=81=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 2026-09-15 指出的是**报文**的问题,而规矩本来就在 `lib/env-defaults.sh:25`: "失败要**说清是环境问题**并按仓库约定用退出码 2(环境),**不要让它冒充代码缺陷**"。 ## 一、缺项:③ 预检了**模块**缓存,没预检 **build** 缓存 ``` $ go vet ./... ⇒ internal/middleware/auth.go:9:2: open /root/.cache/go-build/55/…-d: permission denied [FAIL] go vet / go test 不过 —— **先修好再安装** ``` 报错**指着源码文件**说 permission denied,跟着那句话把人送去改代码 —— 而一行 `.go` 都没问题。 **处置(两道)**: 1. `env-defaults.sh` 新增 ③c:**GOCACHE 实测可写**(写一个探针文件,不只看权限位 —— root 无视权限位,`-w` 会骗人),不可用 ⇒ 人话 + **exit 2**; 2. `install.sh` 那一步按**输出分类**:命中 `permission denied` / `module cache not found` / `no space left` / `GOCACHE` ⇒ **退出码 2(环境)**、并且**不再说"先修好"**; 没命中 ⇒ 仍按代码缺陷(退出码 1),报文里**明说**是哪种。 **实测(分类逻辑逐字节抽出、只替换"怎么产生输出")**: | 喂进的输出 | 退出码 | |---|---| | `… open /root/.cache/go-build/55/x-d: permission denied` | **2** | | `go: module cache not found: neither GOMODCACHE nor GOPATH is set` | **2** | | `./internal/repo/x.go:12:2: undefined: Foo` | **1** | | `--- FAIL: TestBar` | **1** | ## 二、★ 实现过程中两次把自己的判据写成"永远不会响",都是**实测**抓到的 这两条我记下来,因为它们是同一个形状、而且**都在我手上当天发生**: **① 先 `rm` 再 `grep`** —— 第一版我写成 ``` cat "$GOOUT" >&2 rm -f "$GOOUT" # ← 先删 if grep -qE 'permission denied|…' "$GOOUT"; then # ← 再读,永远读不到 ``` ⇒ 分类**永远不触发**,所有失败都会被报成代码缺陷(**恰好是 pi 报的那个问题原样保留**)。 改成"先分类、再删"。 **② `set -e` 在 `if ! ( … )` 里被**抑制** —— 探针**恒绿**: ``` if ! ( set -e; : > "$GOCACHE/.agentmail-probe"; rm -f … ); then … ``` 实测 `GOCACHE=/proc/self/nonexistent-gc` 时 `mkdir` 与重定向**都报 No such file or directory**, 而探针 **rc=0** ⇒ 判据说"能写"。真因:bash 在**条件上下文**里抑制 `errexit`, 于是子壳不中止、最后一条命令(`rm -f`)返回 0。 ⇒ 改成**显式** `|| exit 1`,不依赖 shell 选项在特定上下文里的语义。 **③ 附带:我的预检自己造了一个目录** —— 第一版直接 `mkdir -p "$_am_gocache"`, 而 `go env GOCACHE` 在本沙箱里返回字面量 `off` ⇒ **在当前目录里创建了 `./off`**, 探针写相对路径必然"成功" ⇒ **假绿 + 副作用**("预检"改了工作树,正是刚从 `dist`/`gen:bg` 上 修掉的那一类)。已清掉(`rmdir`),并加了"**必须是绝对路径**,否则 exit 2"。 口径也改成**不依赖 `go env GOCACHE`**:`${GOCACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/go-build}` (与本文件"只设未设的"一致),`go env` 只作为旁证。 ## 三、实测:本机这个缺项是**真的在响** ``` GOCACHE=(未设) ⇒ rc=2 GOCACHE=off ⇒ rc=2 GOCACHE=./rel ⇒ rc=2 GOCACHE=/proc/self/nope ⇒ rc=2 GOCACHE=/tmp/gc-ok ⇒ rc=0 ``` 而且它**不是**误报 —— 直接问 go 是最权威的: ``` $ GOCACHE=/root/.cache/go-build go build ./internal/repo/ open /root/.cache/go-build/a4/…-d: permission denied ``` ⇒ 本机 go 确实写不了 build 缓存 ⇒ precheck 判环境、**exit 2** 是对的。 (`HOME` 未设时 env-defaults 会兜成 `/root`,所以推导出的正是 go 自己会用的那个路径。) ## 四、端到端(`--check` + 可写 GOCACHE) ``` $ GOCACHE=/tmp/gc-ok bash deploy/install.sh --check → 退出码 1 [FAIL] 前端门禁没过(退出码 1)—— 上面标了 [FAIL] 的那一处就是 [ OK ] go vet + go test 通过 + exit 1 ← bash -x 追到终点那一行 ``` - `go vet` 变绿(缓存可写)⇒ 说明 ③c **不是**无条件拦人,只在真不可用时拦; - 退出码 1 来自**前端门禁那 3 条真红**,不是环境; - 干跑仍然零副作用:`git status` + `dist` 全量 sha 的指纹**前后一致** ✓。 ★ 照实写的取舍:`--check` 现在在 GOCACHE 不可用时会**提前 exit 2**, 于是"诊断全部跑完"那段不会执行。这是**有意的** —— 按仓库约定环境问题用 2 且要早说, 而"跑不完的诊断"本身是环境问题的症状。不是"第一道红吃掉诊断"那个老毛病(那条已修)。 --- deploy/install.sh | 29 +++++++++++++++- deploy/lib/env-defaults.sh | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/deploy/install.sh b/deploy/install.sh index 008fc51..149757e 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -424,10 +424,37 @@ if [[ $CHECK_ONLY -eq 1 ]]; then fi # 环境不再在这里逐个预检 —— 开头 source 的 env-defaults.sh 已经保证 # HOME/TMPDIR/GOMODCACHE/PATH 齐备(那道预检是"第四次"的补丁,已被一处取代)。 - if ! ( cd "$REPO/server" && go vet ./... && go test ./... ); then + # ★★ 环境失败**不许冒充代码缺陷**(pi 2026-09-15;规矩本来就在 `lib/env-defaults.sh:25`: + # "失败要说清是环境问题…不要让它冒充代码缺陷")。 + # + # 实测过的形状:`go vet ./...` 报 + # internal/middleware/auth.go:9:2: open /root/.cache/go-build/55/…-d: permission denied + # —— 它**指着源码文件**说 permission denied,而一行 .go 都没问题;跟着本步原来那句 + # "不过 —— 先修好再安装" 就把人送去改代码。pi 用 `go vet ./internal/repo/`(通过) + # vs `go vet ./...`(死在缓存条目)两条命令就分开了 ⇒ 真因是 build 缓存不可用。 + # + # 现在**两道**:① `env-defaults.sh` 的 ③c 预检 GOCACHE(可写、探针实测); + # ② 这里按**输出分类**兜住剩下的(预检过不了的在 ① 就 exit 2 了,这里是二手保险)。 + # 分类命中 ⇒ 退出码 **2(环境)**,并且**不再说"先修好"**。 + GOOUT="$(mktemp -t agentmail-govet-XXXXXX)" + if ! ( cd "$REPO/server" && go vet ./... && go test ./... ) >"$GOOUT" 2>&1; then + cat "$GOOUT" >&2 + # ⚠️ 分类必须在 `rm` **之前**读那个文件 —— 先删再 grep 会让这条分类**永远不触发** + # (那正是本仓反复出现的形状:判据存在但永远不会响)。 + if grep -qE 'permission denied|cannot find (package|module)|no space left on device|module cache not found|build cache is required|GOCACHE' "$GOOUT"; then + rm -f "$GOOUT" + echo " [FAIL] go vet / go test **跑不起来**(环境问题,不是代码缺陷)" >&2 + echo " 上面报错里出现了缓存/权限/空间类的字样 —— 它常常**指着源码文件**报错," >&2 + echo " 但一行 .go 都没问题(实测形状:open /…-d: permission denied)。" >&2 + echo " 药方:GOCACHE=<可写目录> 重跑。按仓库约定这算**环境**(退出码 2)。" >&2 + exit 2 + fi + rm -f "$GOOUT" echo " [FAIL] go vet / go test 不过 —— 先修好再安装" >&2 + echo " (本次输出里**没有**缓存/权限/空间类字样 ⇒ 按**代码缺陷**处理。)" >&2 exit 1 fi + rm -f "$GOOUT" echo " [ OK ] go vet + go test 通过" cat <<'EOF' diff --git a/deploy/lib/env-defaults.sh b/deploy/lib/env-defaults.sh index ac619db..848ad4a 100644 --- a/deploy/lib/env-defaults.sh +++ b/deploy/lib/env-defaults.sh @@ -168,6 +168,75 @@ fi export AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS# }" +# ③c `GOCACHE`(go 的 **build** 缓存)—— pi 评审 2026-09-15 指出的**缺项**。 +# +# ★ 上面 ③ 预检的是 `GOMODCACHE`/`GOPATH`(**模块**缓存),而 `go vet`/`go test`/`go build` +# 还要用 **build** 缓存。缺了/不可用时的报错长这样: +# +# internal/middleware/auth.go:9:2: open /root/.cache/go-build/55/559b2f74…-d: permission denied +# +# —— 它**指着源码文件**说 permission denied,看起来完全像代码缺陷;而 `install.sh` 那一步 +# 接着打 `[FAIL] go vet / go test 不过 —— **先修好再安装**` ⇒ 把人送去改代码。 +# pi 一条命令就把它分开了:`go vet ./internal/repo/` 通过、`go vet ./...` 死在缓存条目上 +# ⇒ 真因是 build 缓存不可用,**不是任何 .go 文件有问题**。 +# **这正是本文件上面第 25 行那条纪律要拦的东西**: +# "失败要**说清是环境问题**…**不要让它冒充代码缺陷**"。 +# +# ★ 为什么必须**实测可写**而不是只判 `-d`:build cache 是**要写**的(未命中就写条目)。 +# 只判存在 ⇒ 目录在就放行,go 一写就 EACCES —— 本仓那条老毛病 +# (见 ① HOME 的注释:"对已存在的不可写目录,`mkdir -p` 会返回成功 ⇒ 必须单独判 `-w`")。 +# +# ⚠️ `go env GOCACHE` 可能返回字面量 `off`(实测:本机沙箱下就是 `off`)—— +# 那**不是目录**,按"拿不到可用的 build 缓存"处理,**不要**把 `off` 读成"用户特意关掉了、 +# 所以放行":此时 go 确实用不了缓存,报错照样会冒充代码缺陷。 +if command -v go >/dev/null 2>&1; then + # 口径与本文件一致(**只设未设的**):GOCACHE 已给就用它,没给才推导。 + _am_gocache="${GOCACHE:-}" + if [ -z "$_am_gocache" ]; then + # 与 go 自己的默认同式:`$XDG_CACHE_HOME` 优先,否则 `$HOME/.cache`。 + # ⚠️ **不能只靠 `go env GOCACHE`**:本机实测它在沙箱里返回字面量 `off` + # (HOME 对 go 不可用时的表现),而 `HOME`/`XDG_CACHE_HOME` 都可用时它返回正常路径。 + _am_gocache="${XDG_CACHE_HOME:-$HOME/.cache}/go-build" + GOCACHE="$_am_gocache" + export GOCACHE + AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS GOCACHE=$GOCACHE" + fi + # ⚠️⚠️ **必须判"是绝对路径"** —— 这是我实测踩到的:`go env GOCACHE` 在本沙箱里返回 + # 字面量 `off`,而我第一版直接 `mkdir -p "$_am_gocache"` ⇒ **在当前目录里造出一个 ./off**! + # 探针去写一个相对路径必然"成功" ⇒ 判据**假绿**,而且**留下副作用** + # ("预检"自己改了工作树 —— 正是本仓刚从 `dist`/`gen:bg` 上修掉的那一类)。 + # ⇒ 非绝对路径**不可能是** go 真正会用的缓存目录 ⇒ 按"拿不到可用缓存"处理。 + case "$_am_gocache" in + /*) : ;; + *) + printf '\n [FAIL] 环境不足:GOCACHE 不是绝对路径(实得「%s」)\n' "$_am_gocache" >&2 + printf ' 这是**环境问题**,不是代码问题。\n' >&2 + printf ' 本机实测:沙箱里 `go env GOCACHE` 会给出字面量 `off` —— 那表示**它用不了 build 缓存**。\n' >&2 + printf ' 药方:GOCACHE=<可写目录的绝对路径> 重跑。\n' >&2 + exit 2 + ;; + esac + mkdir -p "$_am_gocache" 2>/dev/null || true + # 真的写一个探针文件 —— 与 go 自己要做的事**同形**(不只看权限位;root 无视权限位,`-w` 骗人)。 + # + # ⚠️⚠️ **不要用 `set -e` 做这件事**(我第一版就是这么写的,**探针恒绿**): + # `if ! ( set -e; … )` 里那个子壳是**条件的一部分**,而 bash 在条件上下文里 + # **抑制 errexit** ⇒ 写失败也不中止 ⇒ 子壳最后一条命令(`rm -f`)返回 0 ⇒ + # 判据永远说"能写"。实测:`GOCACHE=/proc/self/nonexistent-gc` 时 + # `mkdir` 与重定向**都报 No such file or directory**,而探针 rc=0。 + # ⇒ 用**显式**的 `|| exit 1`,把失败直接变成退出码,不依赖 shell 选项的上下文语义。 + if ! ( { : > "$_am_gocache/.agentmail-probe"; } 2>/dev/null || exit 1; rm -f "$_am_gocache/.agentmail-probe" ); then + printf '\n [FAIL] 环境不足:GOCACHE=%s 不存在或不可写\n' "$_am_gocache" >&2 + printf ' 这是**环境问题**,不是代码问题。\n' >&2 + printf ' 症状(会冒充代码缺陷):`go vet ./...` 报\n' >&2 + printf ' internal/…: open %s/…-d: permission denied\n' "$_am_gocache" >&2 + printf ' —— 它指着源码文件说 permission denied,而实际一行 .go 都没问题。\n' >&2 + printf ' 药方:GOCACHE=<可写目录> 重跑(或修 %s 的权限)。\n' "$_am_gocache" >&2 + exit 2 + fi + unset _am_gocache +fi + # ②b TMPDIR **可写 + 有空间** —— 与 ① HOME 同一条规则,必须一起落。 # # ★ pi 评审 2026-09-14:① 那边我写了"`mkdir -p` 对已存在的不可写目录会返回成功 ⇒ 必须单独判 `-w`",