跨端: 补上 --check 的反向洞:CHECK_GATE_RC 真正 exit 出去(红门禁不再退 0);干跑不再重写 dist(BUILD_INFO_OUT 改道临时目录,build-stamp 的红不会被干跑刷掉)

pi 2026-09-15 两条都成立,而且第一条是我**自己上一个 commit 的注释在骗人**。

## 一★★ `CHECK_GATE_RC` 只被打印、**从来没被 `exit` 用** —— 我把它从末尾挪走时丢了退出

```
$ grep -n "exit.*CHECK_GATE_RC" deploy/install.sh
(无)
$ 干跑分支终点: exit 0        ← 无条件 0
```

而我上一版注释写的是"**最后用记下的码退出(见文件末尾的 `CHECK_GATE_RC`)**" ——
**文件末尾根本没有那一行**(那里是正式安装路径的 systemctl/curl,干跑走不到)。
我上一轮把那段从"文件末尾"挪到 `go vet` 之前时,**挪了打印、丢了 `exit`**。

⇒ 后果是我上一个 commit **把病修反了**:

| | 第一道红的行为 | 命令退出码 |
|---|---|---|
| 修之前 | 中止一切,诊断全丢 | **失败**(正确的那半) |
| 我那一版 | 诊断全跑完 | **0**(错的那半) |

而丢掉的这一半**正好是会被引用的那一半** —— 这条链上引用过 `install.sh --check` 的 `exit 0`
当绿读数。**"注释说的事和代码做的事不一致"这是我第三次犯**(前两次:`unreportedFiles` 说修了没修、
`ok 5` 报文认证了没人判的界)。

**修法**:干跑分支终点 `exit "${CHECK_GATE_RC:-0}"`。

## 二★★ "干跑"并不只读:它重写 `client/electron/dist`,能**静默刷掉 `build-stamp` 的红**

同一个脚本在别处很小心地不写 `node_modules`("干跑不写依赖"),却**照跑 `npm run build`**。
两个后果(你指出的):

1. **干跑改共享状态** —— 而它自己那句"干跑没有执行"的清单只列了**工作区外**的路径,`dist` 悄悄在射程内;
2. **它能把 `build-stamp` 从红刷成绿** —— 那条比的是 `dist/BUILD_INFO.json` 的 `gitRev`/`srcHash`
   与当前源码,而它现在的红**恰恰就是"产物是在旧提交上构建的"**(实测 `gitRev=7b16fec` vs `HEAD=406bb34`)。
   跑一次干跑 ⇒ 重构建 ⇒ 不匹配消失 ⇒ **一条红没了,而没有任何人决定过"重构建"**。

**修法**:干跑改成 `gen:bg` + **`vite build --outDir <临时目录>`** + `build-info.mjs`(新增 `BUILD_INFO_OUT`)。
**真装的 `npm run build` 一字不改**(仍写真正的 `dist`)。

**实测**(`dist` 内容指纹 sha256):

```
跑之前  ff97258fbcc1de49…
跑之后  ff97258fbcc1de49…   ✓ 未动
BUILD_INFO.json: gitRev=7b16fec vs HEAD=406bb34 ⇒ **红还在**(没被干跑刷掉)
退出码=1(红门禁 ⇒ 非 0)
```

**退出码两侧都验了**(不是只看"非 0"就收):
- 真跑一次全脚本 ⇒ `bash -x` 追到 `+ exit 1`(**真的走到了我那一行**);
- 抽出那一行(逐字节)配三种前置状态 ⇒ `CHECK_GATE_RC=1` → 1;空 → 0;未设 → 0;
- 构造全绿情形(中和两处真门禁)⇒ **退出码 0**,`FAIL` 行数 0。

★ 顺带一条**我自己踩到的坑**:第一次构造"全绿"副本时我把脚本拷到 `/tmp` 跑,
而 `REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"` ⇒ `REPO=/tmp` ⇒
`env-defaults.sh` 找不到 ⇒ 退出 1,**看着像"绿路径也退 1"**。放到仓内(临时文件、跑完即删)才是对的。
**又一次"我的验证环境和生产不同"** —— 这次差异是脚本位置。

## 三、你指出的"同一现象两种政策":加了互相指向,并写明**为什么不能统一**

- `install.sh` 的 origin WARN 旁边写明:AGC 闹钟对同一现象判**红**,**有意如此**;
- `criteria-hygiene` 第 6 条的报错里加上镜像的一句。

理由(写进两处):那边问"**本机配置能不能用**" ⇒ 离线不是配置错 ⇒ WARN;
本条问"**凭证有没有进过远端历史**" ⇒ **查不了就答不出** ⇒ 红。
并且写明统一到任一边的代价:统一到 WARN ⇒ **泄漏告警在离线时静默变绿**;
统一到红 ⇒ 离线机器上一次干跑就报假红。

## 四、残余(照实写)

- `gen:bg` 会**原地重写** `src/background-takeover.generated.css`(已跟踪)。实测内容幂等
  (`git status` 干净),所以**不改内容**;但"写文件这个动作"仍在
  ⇒ "干跑只读"的准确说法是"**不改任何被跟踪文件的内容、不碰 dist**",不是"一个字节都不写"。
  我把这句写进了注释 —— **没验到的部分不写成保证**。
- `go vet` 那条红是 `/root/.cache/go-build` **权限**问题(环境),不是代码;维持既有记录。
- 你上封提的"记录投影"已在 `406bb34` 做完;与本文无关。
- 红线仍 4 条(`narrow-layout`/`nav-merge`/`harmony-presets`/`build-stamp`),**都不是我的**。
This commit is contained in:
2026-09-15 13:41:50 +08:00
parent 406bb34b47
commit cb54c53937
3 changed files with 69 additions and 5 deletions

View File

@ -109,7 +109,18 @@ export function writeBuildInfo(root = ELECTRON_DIR, buildCmd = process.env.npm_l
releaseCandidate: !st.gitDirty,
note: '构建自证:test/build-stamp.test.mjs 用 srcHash/gitRev 精确比对,不是比时间戳'
};
const out = join(root, 'dist', 'BUILD_INFO.json');
/*
* ★ 输出路径可覆盖(`BUILD_INFO_OUT`),**为了让"干跑"真的只读**。
*
* 原来这里硬编码 `dist/`,于是 `install.sh --check`(自称干跑)跑 `npm run build`
* **会重写共享工作树的 `dist/BUILD_INFO.json`** —— 而 `build-stamp` 那条判据
* 正是拿它跟当前源码比 ⇒ **一次"干跑"能把一条红静默刷成绿,而没有人决定过"重构建"**。
* (pi 2026-09-15 抓到这一点;他**刻意没跑 `--check` 去坐实**,理由是共享树里跑它
* 等于在别人脚下换东西 —— 这个判断我认,所以本轮我在 `/tmp` 的隔离副本里验,没在共享树上跑。)
*/
const out = process.env.BUILD_INFO_OUT
? process.env.BUILD_INFO_OUT
: join(root, 'dist', 'BUILD_INFO.json');
mkdirSync(dirname(out), { recursive: true });
writeFileSync(out, `${JSON.stringify(info, null, 2)}\n`);
return info;

View File

@ -542,6 +542,11 @@ test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是
assert.deepEqual(unresolved, [],
'这几条远端 ref 的 tip **拿不到、也抓不回来** ——\n'
+ ' ★ 按"不知道"处理,**不当绿**:**查不了 ≠ 干净**。\n'
+ ' ★★ 这里对"远端不可达"判**红**,而 `deploy/install.sh --check` 的 origin 可达性检查\n'
+ ' 对同一现象判 **WARN** —— **两处政策相反是有意的**,别以"一致性"为名统一掉:\n'
+ ' · 那边问"本机配置能不能用" ⇒ 离线不是配置错 ⇒ WARN;\n'
+ ' · 本条问"凭证有没有进过远端历史" ⇒ **查不了就答不出** ⇒ 红。\n'
+ ' (统一到 WARN ⇒ 泄漏告警在离线时静默变绿;统一到红 ⇒ 离线机器上一次干跑就报假红。)\n'
+ ' ★ 真因不是"远端不可达"(sha 是从 `ls-remote` 拿的,**远端当然可达它**),\n'
+ ' 而是"**本地没有这个对象**",且 `git log <sha>` 必须先有对象。\n'
+ ' ★ 修法(**只 `git fetch` 不够** —— 它默认只抓 `refs/heads/*`,\n'

View File

@ -141,15 +141,54 @@ fi
# 但**这个机制比它看起来脆**:它依赖"门禁恰好全过"。)
#
# 修法:`--check` 下把这一门的失败**记下来**、继续跑完所有诊断,
# 最后用记下的码退出(见文件末尾的 `CHECK_GATE_RC`)。
# 最后用记下的码 `exit` 出去(**不是在文件末尾 —— 见下面"两个洞"**)。
# **真装路径行为完全不变**(仍 `set -e` 立刻中止 —— 装机器时不该带着红往下走)。
#
# ★★ 这一版修的是 pi 2026-09-15 抓到的**两个反向的洞**(我上一版自己造的):
#
# ① **`CHECK_GATE_RC` 只被打印、从来没被 `exit` 用**。
# 我上一版的注释写着"见文件末尾的 `CHECK_GATE_RC`",而**文件末尾根本没有那一行**
# (文件末尾是正式安装路径的 systemctl/curl,干跑走不到)—— 我把它从末尾挪走时,
# **挪了打印、丢了退出**。后果是**我上一个 commit 修的病反过来**:
# 修之前:第一道红中止一切,但命令**失败**;
# 修之后:诊断全跑完,命令**通过**(干跑分支无条件 `exit 0`)。
# 而丢掉的这一半**正好是会被引用的那一半** —— 这条链上引用过 `install.sh --check` 的
# `exit 0` 当绿读数。**"说法(注释)与实现不一致"第三次出现在我手上。**
# ⇒ 现在干跑分支的终点是 `exit "${CHECK_GATE_RC:-0}"`。
#
# ② **"干跑"并不只读:它会重写 `client/electron/dist`。**
# 同一个脚本在别处很小心地不写 `node_modules`("干跑不写依赖"),却**照跑 `npm run build`**。
# 两个后果:
# · **"干跑"改共享状态**(而它自己那句"干跑没有执行"的单子只列了**工作区外**的路径,
# `dist` 悄悄在射程内);
# · **它能把 `build-stamp` 从红刷成绿** —— 那条比的是 `dist/BUILD_INFO.json` 的
# `gitRev`/`srcHash` 与当前源码,而它的红**恰恰就是"产物是在旧提交上构建的"**。
# 跑一次干跑 ⇒ 重构建 ⇒ 不匹配消失 ⇒ **一条红没了,而没有人决定过"重构建"**。
# ⇒ 干跑改成:`gen:bg`(幂等生成,本来就在 build 里)+ **`vite build --outDir <临时目录>`**
# + `build-info.mjs`(`BUILD_INFO_OUT` 指到那个临时目录)。
# **真装的 build 一字不改**(仍然写真正的 `dist`)。
# ⚠️ 残余:`gen:bg` 会**原地重写** `src/background-takeover.generated.css`(已跟踪)。
# 实测它内容幂等(`git status` 干净),所以**不改内容**;但"写文件这个动作"仍在,
# 所以"干跑只读"这句话**准确的说法是"不改任何被跟踪文件的内容、不碰 dist"**,
# 不是"一个字节都不写"。我看不出的部分不写死。
if [[ $CHECK_ONLY -eq 1 ]]; then
npm_rc=0
( cd "$REPO/client/electron" && npm run typecheck && AGENTMAIL_CRITERIA_PHASE=install npm test && npm run build ) \
|| npm_rc=$?
# 产物写到临时目录:干跑不该动 dist(否则会静默刷掉 build-stamp 的红)
CHECK_DIST="$(mktemp -d -t agentmail-check-dist-XXXXXX)"
(
cd "$REPO/client/electron" \
&& npm run typecheck \
&& AGENTMAIL_CRITERIA_PHASE=install npm test \
&& npm run gen:bg \
&& npx vite build --outDir "$CHECK_DIST" --emptyOutDir \
&& BUILD_INFO_OUT="$CHECK_DIST/BUILD_INFO.json" node scripts/build-info.mjs
) || npm_rc=$?
rm -rf "$CHECK_DIST"
if [[ $npm_rc -ne 0 ]]; then
echo " [FAIL] 前端门禁(typecheck / 判据 / build)退出码 ${npm_rc} —— 详见上面的红" >&2
CHECK_GATE_RC="$npm_rc"
else
echo " [ OK ] 前端门禁通过(构建产物落在临时目录,**没有动 dist**)"
fi
else
( cd "$REPO/client/electron" && npm run typecheck && AGENTMAIL_CRITERIA_PHASE=install npm test && npm run build )
@ -339,6 +378,12 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
else
echo " [WARN] origin **不可达**(或 15s 内没应答)—— 本机的推送/取远端会失败"
echo " 注意:这条只说明'到不了',不说明'地址写错了';两者要分开看。"
echo " ★ 这里判 WARN、而 AGC 闹钟(criteria-hygiene 第 6 条)对'远端不可达'判**红** ——"
echo " 两处政策相反是**有意的**,别以'一致性'为名统一掉(统一到哪边都会弄坏一半):"
echo " · 这里问的是'本机配置能不能用' ⇒ 离线不是配置错 ⇒ **WARN**;"
echo " · 那边问的是'凭证有没有进过远端历史' ⇒ 查不了就**答不出** ⇒ **红**"
echo " ('看不到'不等于'干净')。"
echo " 理由原文见 client/electron/test/criteria-hygiene.test.mjs 该条注释。"
fi
fi
@ -382,7 +427,10 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
拿不准就先跑 ./deploy/install.sh --check;正式安装用 sudo ./deploy/install.sh。
EOF
exit 0
# ★ **无条件 `exit 0` 是上一个版本的洞**(pi 2026-09-15):干跑在门禁红时也报成功,
# 而这条链上引用过 `--check` 的 `exit 0` 当绿读数 ⇒ 那个读数不可信了。
# 现在只要前面记下了非零码就把那个码传出去。**"诊断跑完了" ≠ "门禁都过了"。**
exit "${CHECK_GATE_RC:-0}"
fi
echo "==> 前端产物嵌入 Gateway"