diff --git a/client/electron/scripts/build-info.mjs b/client/electron/scripts/build-info.mjs index d73ff3f..46dc626 100644 --- a/client/electron/scripts/build-info.mjs +++ b/client/electron/scripts/build-info.mjs @@ -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; diff --git a/client/electron/test/criteria-hygiene.test.mjs b/client/electron/test/criteria-hygiene.test.mjs index 33fdc8b..03fd1ef 100644 --- a/client/electron/test/criteria-hygiene.test.mjs +++ b/client/electron/test/criteria-hygiene.test.mjs @@ -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 ` 必须先有对象。\n' + ' ★ 修法(**只 `git fetch` 不够** —— 它默认只抓 `refs/heads/*`,\n' diff --git a/deploy/install.sh b/deploy/install.sh index f3fe7cc..e833e87 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -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"