diff --git a/deploy/redeploy-gateway.sh b/deploy/redeploy-gateway.sh index 8a618c0..5ec5432 100755 --- a/deploy/redeploy-gateway.sh +++ b/deploy/redeploy-gateway.sh @@ -126,9 +126,17 @@ if [ "$SYNC_WEB" = 1 ] && [ -d "$REPO/client/electron/dist/assets" ]; then say "1. 同步前端产物进 go:embed 目录" # 只清构建产物:placeholder.html 在版本库里(让 go:embed 在新克隆里能编译), # 删掉它会让 git 看到一个本地删除,下一次 commit -a 就把它从仓库带走。 - run "rm -rf '$REPO/server/internal/static/static/assets'" - run "rm -f '$REPO/server/internal/static/static/index.html'" - run "cp -r '$REPO/client/electron/dist/.' '$REPO/server/internal/static/static/'" + # ★ 每一步都要**接收退出码**(还清 `DEBTS.json: redeploy-script-unguarded-steps`)。 + # `run()` 内部是 `eval`,调用点不接的话失败既不中断也不上报 —— 实测形状 + # (该欠账原文):`mkdir`/`cp` 被拒后仍打出 `[ OK ] 已拷入`,紧接着又打出 + # `[FAIL] staging 里没有入口`:**一段输出里两个矛盾信号,且 OK 在前**。 + # 这里 `cp` 尤其要紧:前端产物没拷进去,go:embed 会把**旧界面**打进二进制, + # 而所有单测仍是绿的(2026-09-14 就是这么踩的,见下面那段注释)。 + # 退出码用 2(环境),与文件头的 0/1/2 约定一致 —— 这不是"检查不过",是"这一步没做成"。 + run "rm -rf '$REPO/server/internal/static/static/assets'" || { bad "清不掉旧 assets(权限?)"; exit 2; } + run "rm -f '$REPO/server/internal/static/static/index.html'" || { bad "删不掉旧 index.html"; exit 2; } + run "cp -r '$REPO/client/electron/dist/.' '$REPO/server/internal/static/static/'" \ + || { bad "拷不进前端产物 —— 继续下去会把旧界面嵌进二进制"; exit 2; } ok "前端产物已同步" # 运行时脚本必须装在安装根下 —— 单元/drop-in 里引用的是 @@ -317,7 +325,17 @@ trap '_restore_on_signal HUP' HUP # 真原子的三步:**同目录**暂存 → 复制(慢没关系,动的是"还没人用的名字")→ 一次 `mv -f`。 # 对照:`redeploy-plugin.sh` 的 `mv "$STAGING" "$SNAP"` 是**真原子**(两者都在 `$DEST` 下、同 fs), # 同一个仓库里原先两套"原子切换",一套真、一套名义上的。 -run "systemctl stop '$SERVICE'" +run "systemctl stop '$SERVICE'" || { + # ★ stop 失败必须当场停手(欠账 `redeploy-script-unguarded-steps` 点名的形状: + # "其中一次的 stop 失败,而它的状态**没人看**")。 + # 为什么这条尤其不能放过:`systemctl start` 对**已在运行**的服务是 no-op ⇒ + # stop 没成功时,后面那句 start 什么也不做,于是**旧进程继续跑旧代码**, + # 而脚本一路走到后置验证、报"部署成功" —— 正是本文件头列的第 2 类漂移 + # ("部署脚本跑过了 ≠ 线上跑的是当前代码")被脚本**自己在内部**造出来。 + # (新加的判据 ⑤b 能事后发现它,但那是补救;源头在这里。) + bad "停不下 $SERVICE —— 继续的话,后面的 start 对运行中的服务是 no-op,会留下'以为换了、其实没换'" + exit 2 +} _SERVICE_STOPPED=1 # ← 从这里开始,"脚本死了但服务停着"就是事故 _NEW="$TARGET.new.$$" # 复制到**目标同目录**:这一步慢/失败都无所谓,因为 `$_NEW` 还没有任何人用。 diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 5ac8632..5189200 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -73,10 +73,11 @@ }, { "id": "redeploy-script-unguarded-steps", - "count": 1, + "count": 0, "kind": "scope", - "due": "**下一次改 `deploy/` 下任一脚本时**必须一并堵(`redeploy-gateway.sh` 正在被另一条会话改 ⇒ 本条目就是给它接手时的入口)。堵法:给每个副作用步骤加 `|| { bad …; exit 2; }`,或在脚本上开 `set -e`;两者都要与既有的 2=环境 / 1=检查 约定对齐。", - "where": "`deploy/redeploy-gateway.sh:84` 的 `run \"cp -r '$REPO/client/electron/dist/.' ...\"` —— 脚本只有 `set -uo pipefail`(**无 `-e`**),`run()` 内部 `eval` 的失败既不中断也不被调用点接收 ⇒ 前端产物没拷进去也继续往下走。同类已在 `deploy/redeploy-plugin.sh` 修掉(2026-09-14):那次的实测形状是 `mkdir`/`cp` 被拒后仍打出 `[ OK ] 已拷入 node_modules`,再打出 `[FAIL] staging 里没有入口` —— **一段输出里两个矛盾信号,且 OK 在前**。该文件现已在 `mkdir`/`cp`/`node_modules` 三处判失败并 exit 2。" + "due": "**已还清 2026-09-25**(见 note)", + "where": "`deploy/redeploy-gateway.sh` 的四类副作用步骤 —— **已堵**(见 note)。(原文:`deploy/redeploy-gateway.sh:84` 的 `run \"cp -r …\"`,`run()` 内 `eval` 的失败既不中断也不被调用点接收)", + "note": "**已堵(2026-09-25)**,出口是这条欠账自己写的到期条件(\"下一次改 `deploy/` 下任一脚本时\")—— 当天因修 `--dry-run` 落地写入而动了该脚本,故一并还。\n\n堵法按原文给的形状(`|| { bad …; exit 2; }`,与既有 2=环境/1=检查 对齐),四处:\n ① 前端同步三步(`rm -rf assets` / `rm -f index.html` / `cp -r dist`)各自接收退出码;\n `cp` 尤其要紧:没拷进去 ⇒ go:embed 把**旧界面**打进二进制,而单测仍全绿(2026-09-14 踩过)。\n ② `systemctl stop` 加守卫 —— 这是本条目原文点名的\"stop 失败而状态没人看\"。\n 加它的理由比原文更强一层:`systemctl start` 对**已在运行**的服务是 **no-op** ⇒ stop 没成功时后面那句 start 什么也不做,**旧进程继续跑旧代码**,而脚本一路走到后置验证报成功 —— 即\"部署脚本跑过了 ≠ 线上跑的是当前代码\"被脚本**自己在内部**造出来。\n ③ am-sandbox 段的 `go build`/`install` 并入 DRY_RUN 分支。\n ④ `install -d $PREFIX/bin` 与通知脚本 `install` 并入 DRY_RUN 分支(同批修的 `--dry-run` 会落地问题)。\n\n**未一并做的**(如实记,避免本条读起来像\"全脚本已无裸步骤\"):`redeploy-plugin.sh` / `install.sh` 的同类步骤仍未逐个加守卫 —— 那是另一条欠账 `deploy-interrupt-trap-other-scripts` 的范围,本条只覆盖 `redeploy-gateway.sh`。" }, { "id": "deploy-space-prefix-fs",