修复: 环境预检标相位——干跑里只 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