From 3c2b7d138564a20689f22db36c32cbceaae410f8 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 25 Sep 2026 06:22:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E8=BF=98=E6=B8=85=E6=AC=A0?= =?UTF-8?q?=E8=B4=A6=20`redeploy-script-unguarded-steps`=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E5=9B=9B=E7=B1=BB=E8=A3=B8=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?=E6=8E=A5=E6=94=B6=E9=80=80=E5=87=BA=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 出口是这条欠账**自己写的**到期条件("下一次改 `deploy/` 下任一脚本时"): 当天因修 `--dry-run` 落地写入而动了这个脚本,所以一并还。 ## ① 前端同步三步 `rm -rf assets` / `rm -f index.html` / `cp -r dist` 原先调用点不接退出码, 而 `run()` 内部是 `eval`(脚本只有 `set -uo pipefail`,无 `-e`)⇒ 失败既不中断也不上报。 `cp` 那条尤其要紧:前端产物没拷进去 ⇒ go:embed 把**旧界面**打进二进制,而所有单测仍绿 (2026-09-14 踩过,见脚本内那段注释)。三处都改成 `|| { bad …; exit 2; }`。 ## ② `systemctl stop`(本条欠账原文点名的"stop 失败而状态没人看") 加它的理由比原文**更强一层**:`systemctl start` 对**已在运行**的服务是 **no-op** ⇒ stop 没成功时后面那句 start 什么也不做,**旧进程继续跑旧代码**,而脚本一路走到 后置验证、报"部署成功"。也就是说,"部署脚本跑过了 ≠ 线上跑的是当前代码"这个第 2 类漂移 会被脚本**自己在内部**造出来 —— 与今天线上那件事(09-19 二进制跑了 6 天)同形,只是成因在脚本内。 ## ③④ DRY_RUN 分支 am-sandbox 的 `go build`/`install`、以及 `install -d $PREFIX/bin` 与通知脚本 `install` 一并挪进 DRY_RUN 分支(上一提交 7a65b27 只修了后者的"会落地",这里补齐构建/安装两步)。 ## 边界(如实记,避免读成"全脚本已无裸步骤") 只覆盖 `redeploy-gateway.sh`。`redeploy-plugin.sh` / `install.sh` 的同类步骤仍未加守卫 —— 那是另一条欠账 `deploy-interrupt-trap-other-scripts` 的范围。 ## 验证 `bash -n` 通过;`--dry-run` rc=0 且不落地(只留 0 字节锁); Go 侧 `TestDebtLedgerMatchesMeasurement` / `TestDebtSummaryReadsAuthoritativeLedger` 通过; `node test/debt-visibility.test.mjs` 1/1 通过。 --- deploy/redeploy-gateway.sh | 26 ++++++++++++++++++++++---- docs/DEBTS.json | 7 ++++--- 2 files changed, 26 insertions(+), 7 deletions(-) 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",