修复: 环境预检标相位——干跑里只 gate 得住某一步的预检改为"记账并继续、末尾按环境 exit 2",不再让第一道环境红吃掉全部诊断

pi 2026-09-15 报的是**我上一轮修复的代价**,而它是我两轮前刚从 `CHECK_GATE_RC` 上修好的
**同一个病**、只换了个入口。

## 一、病:`--check` 一条诊断都不跑

```
$ bash deploy/install.sh --check   → EXIT=2,输出 7 行,`========== test/` 命中 0 次
```

`env-defaults.sh` 在**开头** source,③c 的 GOCACHE 预检在那里 `exit 2`
⇒ 整个干跑在第一步就结束。被吃掉的正好是与 GOCACHE **毫无关系**的前端门禁
(typecheck / 判据 / build)—— 而"别让第一道红吃掉后面的诊断"是我两轮前刚立的性质。

## 二、修法:逐条标相位(不是整体开关)

两条规则都对、撞在一起:**部署路径**要 fail-fast;**`--check`** 的产品**就是诊断本身**。
所以按"这条预检拦得住的是哪一步"**逐条标记**:

| 预检 | 拦得住什么 | 相位 |
|---|---|---|
| ③c `GOCACHE` | 只 gate 得了 **go 那一步** | **可推迟**(记账 → 继续 → 末尾 exit 2)|
| ②b `TMPDIR` 可写/空间 | 构建与测试都要写它 | 可推迟(未标,留给下一轮按同一形状处置)|
| ① `HOME`、② `TMPDIR` 存在、③b 外部命令 | **诊断本身** | **不标,仍立刻 exit 2** |

推迟后由 `agentmail_env_flush` 在**诊断跑完后**统一报出并给退出码 **2**,
报文里写明"这与**某道门红了**不是同一件事"。
**部署路径行为完全不变**(实测:同一环境下 6 行、exit 2、0 条诊断)。

## 三、四象限实测(同一台机,只改 `GOCACHE`)

| 路径 | 环境 | 退出码 | 诊断文件数 |
|---|---|---|---|
| `--check` | 坏 | **2** | **23**(原先 0)|
| `--check` | 好 | **1**(前端 3 真红) | 23 |
| 部署 | 坏 | **2** | 0(fail-fast,不变) |

`--check` 零副作用仍成立(`git status` + `dist` 全量 sha 指纹前后一致)。

## 四、★ 实现过程中又两次"判据在,但走不到"——都是我实测抓的

**① 相位标记设在了 `source` 之后。** 我第一版把 `AGENTMAIL_ENV_DEFER=1` 放在原来的
参数解析处(第 24 行附近),而 `CHECK_ONLY` 在那里**还没赋值** ⇒ 条件为假 ⇒ **标记根本没设上**;
更坏的是**输出看不出区别**(照样一条诊断都不跑)。真因:标记是给 **source 期间**跑的预检看的,
而 `env-defaults.sh` 在 source 时就跑完了预检 ⇒ **参数解析必须整段挪到 source 之前**。
⇒ 已把解析块上移到 `REPO=` 之后、`source` 之前。

**② 末尾的 flush 放在前端门禁的 early-return 之后。** 于是"环境不足 + 前端门禁红"时
它**永远不会执行**(实测:退出码 2、末尾环境报文一行都没有)。已移到 `exit "${CHECK_GATE_RC:-0}"` 之前。

**③ 又一例同族:分类代码自己 `rm` 了要 grep 的文件** —— 上一轮已修,这轮复述在注释里当参照。

## 五、去重(这条是被实测数字逼出来的)

第一次跑通后报文说"环境不足(**2 项**)",而实际只有**一个**根因:
GOCACHE 不可写 会响两次 —— ③c 的预检、以及 `go vet` 因它跑不起来(`install.sh` 的分类步)。
⇒ `agentmail_env_defer KEY MESSAGE` 改成**按 KEY 去重**,两处共用 KEY `gocache`。
理由写进代码:**"数出来的问题个数"也是一种读数,读数错了就是又造了一个假数字。**

## 六、状态

build `checks=401 pass=400 fail=1 red=4`;install `389/389 fail=0 red=3`;`criteria-hygiene` 6/6。
This commit is contained in:
2026-09-15 14:12:48 +08:00
parent 633150cf39
commit 3c8f18d4de
2 changed files with 151 additions and 39 deletions

View File

@ -10,6 +10,39 @@ set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# ══════════════════════════════════════════════════════════════════════════════
# ./deploy/install.sh --check # 干跑:只跑所有门禁与前置检查,**不写系统目录**
#
# 为什么要有 --check(pi 2026-09-14):发布路径是**最少被跑的门** ——
# `check-shared-libs.sh` 一直红着没人发现,正是因为它只挂在 install.sh 里。
# 而"最后一个能跑这一步的人"(有 root 的那个人)每次都在**改完一堆东西之后**才第一次跑它,
# 于是第一次失败就发生在最贵的时候。干跑把"门自己跑得动吗"提前暴露出来。
#
# ⚠️ ★★ **参数解析必须在 `source env-defaults.sh` 之前**(pi 2026-09-15 的相位标记要它):
# 那条相位标记(`AGENTMAIL_ENV_DEFER`)是给**source 期间**跑的预检看的,
# 而 `env-defaults.sh` 在 source 时就把预检跑完了 ⇒ 标记必须在 source **之前**就位。
# 我第一版把解析留在原处、只在 source 之后设标记 ⇒ **标记永远没生效**,
# 而输出看不出区别(`--check` 照样一条诊断都不跑)—— 靠实测才抓到。
# ══════════════════════════════════════════════════════════════════════════════
CHECK_ONLY=0
ALLOW_DIRTY=0
ALLOW_STALE=0
GIT_HOOKS=0
for a in "$@"; do
case "$a" in
--check) CHECK_ONLY=1 ;;
--allow-dirty) ALLOW_DIRTY=1 ;;
--allow-stale) ALLOW_STALE=1 ;;
--git-hooks) GIT_HOOKS=1 ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) echo "未知参数:$a(支持 --check / --allow-dirty / --allow-stale / --git-hooks)" >&2; exit 2 ;;
esac
done
if [[ $CHECK_ONLY -eq 1 ]]; then
AGENTMAIL_ENV_DEFER=1
export AGENTMAIL_ENV_DEFER
fi
# 环境自足:一处给全(HOME/TMPDIR/GOMODCACHE/PATH),不在每条命令旁各写预检。
# 理由与四次历史见该文件的头注释 —— pi 的原话是"否则第五次一定会来"。
# shellcheck source=./lib/env-defaults.sh
@ -27,27 +60,6 @@ PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}"
ETC=/etc/agentmail
#
# ./deploy/install.sh --check # 干跑:只跑所有门禁与前置检查,**不写系统目录**
#
# 为什么要有 --check(pi 2026-09-14):发布路径是**最少被跑的门** ——
# `check-shared-libs.sh` 一直红着没人发现,正是因为它只挂在 install.sh 里。
# 而"最后一个能跑这一步的人"(有 root 的那个人)每次都在**改完一堆东西之后**才第一次跑它,
# 于是第一次失败就发生在最贵的时候。干跑把"门自己跑得动吗"提前暴露出来。
CHECK_ONLY=0
ALLOW_DIRTY=0
ALLOW_STALE=0
GIT_HOOKS=0
for a in "$@"; do
case "$a" in
--check) CHECK_ONLY=1 ;;
--allow-dirty) ALLOW_DIRTY=1 ;;
--allow-stale) ALLOW_STALE=1 ;;
--git-hooks) GIT_HOOKS=1 ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) echo "未知参数:$a(支持 --check / --allow-dirty / --allow-stale / --git-hooks)" >&2; exit 2 ;;
esac
done
# ── 版本库钩子:把 `core.hooksPath` 指向仓库里的 `.githooks/` ──
#
@ -447,12 +459,25 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
echo " 上面报错里出现了缓存/权限/空间类的字样 —— 它常常**指着源码文件**报错," >&2
echo " 但一行 .go 都没问题(实测形状:open <GOCACHE>/…-d: permission denied)。" >&2
echo " 药方:GOCACHE=<可写目录> 重跑。按仓库约定这算**环境**(退出码 2)。" >&2
exit 2
# ★★ 干跑里**不在这里退**(pi 2026-09-15 的相位标记):这一步只 gate 得了 go 那一段,
# 而干跑的产品是诊断 ⇒ 记账、继续,让后面与它无关的门(干的产物自证等)照跑,
# 最后由末尾的 `agentmail_env_flush` 统一按环境 exit 2。
# ⚠️ 我第一版**没改这里**,于是末尾那个 flush **永远走不到**
# (实测:退出码 2,末尾环境报文一行都没有)—— 又一个"判据在,但走不到"。
if [ "${AGENTMAIL_ENV_DEFER:-0}" = "1" ]; then
# 用**同一个 KEY** `gocache`:这条与 ③c 的预检是**同一个根因**
#(缓存不可写 ⇒ go 跑不起来),不该被数成两项。
agentmail_env_defer "gocache" "go vet / go test 跑不起来:缓存/权限/空间问题(见上面原文)。
药方:GOCACHE=<可写目录> 重跑。"
else
exit 2
fi
else
rm -f "$GOOUT"
echo " [FAIL] go vet / go test 不过 —— 先修好再安装" >&2
echo " (本次输出里**没有**缓存/权限/空间类字样 ⇒ 按**代码缺陷**处理。)" >&2
exit 1
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 通过"
@ -475,6 +500,18 @@ EOF
# ★ **无条件 `exit 0` 是上一个版本的洞**(pi 2026-09-15):干跑在门禁红时也报成功,
# 而这条链上引用过 `--check` 的 `exit 0` 当绿读数 ⇒ 那个读数不可信了。
# 现在只要前面记下了非零码就把那个码传出去。**"诊断跑完了" ≠ "门禁都过了"。**
#
# ★★ 推迟的环境问题在这里统一报(pi 2026-09-15):它们**不是"某道门红了"**,
# 而是"**有一步跑不起来**"。所以优先级最高、退出码 **2(环境)**,
# 而诊断**已经全部跑完**(这正是推迟到末尾的理由:环境预检在开头 exit 2
# 会把与它毫无关系的门一并吃掉 —— 那是本仓修过一次的老毛病,入口换成了环境预检而已)。
# ⚠️ 位置也得对:我第一版把这段放在**前端门禁的 early-return 之后**,
# 于是"环境不足 + 前端门禁红"时它**永远不会执行** —— 又一次"判据在,但走不到"。
# 实测抓到(`--check` 仍退 1、环境报文一行都没有)。
if ! agentmail_env_flush; then
echo ' ⇒ 退出码 2(环境)。**注意这与「某道门红了(1)」是两件事**:环境不足时,"门绿"也装不上。' >&2
exit 2
fi
exit "${CHECK_GATE_RC:-0}"
fi

View File

@ -42,6 +42,67 @@
# 否则"环境本来就齐"与"忘了 source"在输出上完全同形(又是"看起来在兜、其实没兜")。
AGENTMAIL_ENV_DEFAULTS=""
# ══════════════════════════════════════════════════════════════════════════════
# ★★ 环境预检的**相位标记**(pi 2026-09-15 提议;用的是本仓既有词汇)。
#
# 冲突原话(两条规则都对,撞在一起了):
# · **部署路径**:环境不足就该 fail-fast —— 这是本文件存在的理由;
# · **`install.sh --check`**:它的**产品就是诊断本身** ⇒ "只知道一件事"正是要避免的结果。
# 实测的坏结果(pi 报、我复现):③c 的 GOCACHE 预检在**开头** source 时 exit 2
# ⇒ `--check` **一条诊断都不跑**(输出 7 行、`========== test/` 命中 0 次)。
# 于是"别让第一道红吃掉后面的诊断"(我两轮前刚从 `CHECK_GATE_RC` 上修好的那条)
# **又以环境预检为入口重新成立了**,而被吃掉的恰是与 GOCACHE **毫无关系**的前端门禁。
#
# 口径(**逐条标记,不是整体开关**):每条预检自己声明"我拦得住的是哪一步"。
# · 只 gate 得了**某一步**(GOCACHE → go 那步)⇒ 标记为**可推迟**:记账、继续、
# 最后由调用者统一按**环境**退出(与 `CHECK_GATE_RC` 同形:先传出去、再跑完、最后按记录退出);
# · gate 得住**诊断本身**(HOME/TMPDIR 没了,"跑判据"这件事就不可信;外部命令缺了,套件根本起不来)
# ⇒ **不标记**,仍然立刻 exit 2。理由写在各自旁边。
#
# 这样"环境不足要说清"与"别让第一道红吃掉诊断"**各自都还在**。
# ⚠️ 变量必须在**任何预检之前**初始化为空 —— 否则第一条预检 append 到一个未定义变量上,
# 在 `set -u` 下会直接炸(而那会被读成"预检坏了",不是"环境不足")。
AGENTMAIL_ENV_DEFER_RECORD=""
AGENTMAIL_ENV_DEFERRED=0
AGENTMAIL_ENV_DEFER_KEYS=""
# 记一条"可推迟的环境问题"。
#
# ★ `agentmail_env_defer KEY MESSAGE`:**按 KEY 去重**(默认 KEY = 消息本身)。
# 为什么要去重:同一个**根因**会在两处各响一次 —— 例如 GOCACHE 不可写
# (③c 预检)与 `go vet` 因它跑不起来(`install.sh` 的分类步)。
# 不去重就会出现"环境不足(2 项)",而实际只有**一个**问题
# (我实测到了:报 2 项、两条说的是同一件事)—— 那会把一个根因读成两个,
# 而"数出来的问题个数"也是一种读数,读数错了就等于又造了一个假数字。
agentmail_env_defer() {
_am_key="${1:-}"
_am_msg="${2:-$1}"
case " $AGENTMAIL_ENV_DEFER_KEYS " in
*" $_am_key "*) return 0 ;; # 同一个根因已经记过 ⇒ 不重复计数
esac
AGENTMAIL_ENV_DEFERRED=$((AGENTMAIL_ENV_DEFERRED + 1))
AGENTMAIL_ENV_DEFER_KEYS="$AGENTMAIL_ENV_DEFER_KEYS $_am_key"
if [ -z "$AGENTMAIL_ENV_DEFER_RECORD" ]; then
AGENTMAIL_ENV_DEFER_RECORD="$_am_msg"
fi
return 0
}
# 调用者(干跑路径)在诊断跑完后调用:把推迟的环境问题**统一报出来**并给退出码 **2**。
# 与"第一道红"的区别写在输出里,免得下一个人把它读成同一种东西。
agentmail_env_flush() {
if [ "${AGENTMAIL_ENV_DEFERRED:-0}" -eq 0 ]; then
return 0
fi
printf '\n [FAIL] 环境不足(%d 项)—— **这是环境问题,不是代码缺陷**:\n' "$AGENTMAIL_ENV_DEFERRED" >&2
printf '%s\n' "$AGENTMAIL_ENV_DEFER_RECORD" >&2
printf ' ★ 与"某道门红了"**不是同一件事**:这里报的是"**有一步跑不起来**"。\n' >&2
printf ' ★ 上面的诊断**已经全部跑完**(推迟到末尾正是为了这个 —— 环境预检在开头 exit 2\n' >&2
printf ' 会把与它无关的门一并吃掉;那是本仓修过一次的老毛病,入口换成了环境预检而已)。\n' >&2
printf ' ⇒ 按仓库约定用**退出码 2(环境)**。\n' >&2
return 2
}
# ③b 本文件**自己**依赖的命令(pi 评审 2026-09-15)。
#
# ★ 要害:这张表原先只登记了**调用者**的命令(`AGENTMAIL_REQUIRE`),而本文件自己用了
@ -209,11 +270,16 @@ if command -v go >/dev/null 2>&1; then
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
_am_gc_msg="GOCACHE 不是绝对路径(实得「${_am_gocache}」)——
本机实测:沙箱里 \`go env GOCACHE\` 会给出字面量 \`off\`,那表示它用不了 build 缓存。
药方:GOCACHE=<可写目录的绝对路径> 重跑。"
if [ "${AGENTMAIL_ENV_DEFER:-0}" = "1" ]; then
agentmail_env_defer "gocache" "$_am_gc_msg"
else
printf '\n [FAIL] 环境不足:%s\n' "$_am_gc_msg" >&2
printf ' 这是**环境问题**,不是代码问题。\n' >&2
exit 2
fi
;;
esac
mkdir -p "$_am_gocache" 2>/dev/null || true
@ -225,16 +291,23 @@ if command -v go >/dev/null 2>&1; then
# 判据永远说"能写"。实测:`GOCACHE=/proc/self/nonexistent-gc` 时
# `mkdir` 与重定向**都报 No such file or directory**,而探针 rc=0。
# ⇒ 用**显式**的 `|| exit 1`,把失败直接变成退出码,不依赖 shell 选项的上下文语义。
#
# ★ 这里标**可推迟**(`AGENTMAIL_ENV_DEFER`):GOCACHE 只 gate 得了**go 那一步**,
# 前端门禁(typecheck / 判据 / build)与它**毫无关系** ⇒ 干跑里没必要因为它全停。
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
_am_gc_msg="GOCACHE=${_am_gocache} 不存在或不可写 —— 症状(会冒充代码缺陷):
\`go vet ./...\` 报 internal/…: open ${_am_gocache}/…-d: permission denied,
它指着源码文件说 permission denied,而实际一行 .go 都没问题。
药方:GOCACHE=<可写目录> 重跑(或修 ${_am_gocache} 的权限)。"
if [ "${AGENTMAIL_ENV_DEFER:-0}" = "1" ]; then
agentmail_env_defer "gocache" "$_am_gc_msg"
else
printf '\n [FAIL] 环境不足:%s\n' "$_am_gc_msg" >&2
printf ' 这是**环境问题**,不是代码问题。\n' >&2
exit 2
fi
fi
unset _am_gocache
unset _am_gocache _am_gc_msg
fi
# ②b TMPDIR **可写 + 有空间** —— 与 ① HOME 同一条规则,必须一起落。
@ -322,3 +395,5 @@ agentmail_env_report() {
printf ' 环境兜底:(无 —— 调用者已提供全部)\n'
fi
}