From 3c8f18d4de12e62e08e0a643afbdea8eb248f756 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 14:12:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E9=A2=84=E6=A3=80=E6=A0=87=E7=9B=B8=E4=BD=8D=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=B9=B2=E8=B7=91=E9=87=8C=E5=8F=AA=20gate=20=E5=BE=97?= =?UTF-8?q?=E4=BD=8F=E6=9F=90=E4=B8=80=E6=AD=A5=E7=9A=84=E9=A2=84=E6=A3=80?= =?UTF-8?q?=E6=94=B9=E4=B8=BA"=E8=AE=B0=E8=B4=A6=E5=B9=B6=E7=BB=A7?= =?UTF-8?q?=E7=BB=AD=E3=80=81=E6=9C=AB=E5=B0=BE=E6=8C=89=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=20exit=202"=EF=BC=8C=E4=B8=8D=E5=86=8D=E8=AE=A9=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E9=81=93=E7=8E=AF=E5=A2=83=E7=BA=A2=E5=90=83=E6=8E=89?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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。 --- deploy/install.sh | 89 ++++++++++++++++++++++---------- deploy/lib/env-defaults.sh | 101 ++++++++++++++++++++++++++++++++----- 2 files changed, 151 insertions(+), 39 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index 149757e..acc7605 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -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 /…-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 diff --git a/deploy/lib/env-defaults.sh b/deploy/lib/env-defaults.sh index 848ad4a..ecf5b69 100644 --- a/deploy/lib/env-defaults.sh +++ b/deploy/lib/env-defaults.sh @@ -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 } + +