fix(deploy): 还清欠账 redeploy-script-unguarded-steps —— 四类裸步骤接收退出码

出口是这条欠账**自己写的**到期条件("下一次改 `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 通过。
This commit is contained in:
2026-09-25 06:22:09 +08:00
parent c6a497f4ed
commit 3c2b7d1385
2 changed files with 26 additions and 7 deletions

View File

@ -126,9 +126,17 @@ if [ "$SYNC_WEB" = 1 ] && [ -d "$REPO/client/electron/dist/assets" ]; then
say "1. 同步前端产物进 go:embed 目录" say "1. 同步前端产物进 go:embed 目录"
# 只清构建产物:placeholder.html 在版本库里(让 go:embed 在新克隆里能编译), # 只清构建产物:placeholder.html 在版本库里(让 go:embed 在新克隆里能编译),
# 删掉它会让 git 看到一个本地删除,下一次 commit -a 就把它从仓库带走。 # 删掉它会让 git 看到一个本地删除,下一次 commit -a 就把它从仓库带走。
run "rm -rf '$REPO/server/internal/static/static/assets'" # ★ 每一步都要**接收退出码**(还清 `DEBTS.json: redeploy-script-unguarded-steps`)。
run "rm -f '$REPO/server/internal/static/static/index.html'" # `run()` 内部是 `eval`,调用点不接的话失败既不中断也不上报 —— 实测形状
run "cp -r '$REPO/client/electron/dist/.' '$REPO/server/internal/static/static/'" # (该欠账原文):`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 "前端产物已同步" ok "前端产物已同步"
# 运行时脚本必须装在安装根下 —— 单元/drop-in 里引用的是 # 运行时脚本必须装在安装根下 —— 单元/drop-in 里引用的是
@ -317,7 +325,17 @@ trap '_restore_on_signal HUP' HUP
# 真原子的三步:**同目录**暂存 → 复制(慢没关系,动的是"还没人用的名字")→ 一次 `mv -f`。 # 真原子的三步:**同目录**暂存 → 复制(慢没关系,动的是"还没人用的名字")→ 一次 `mv -f`。
# 对照:`redeploy-plugin.sh` 的 `mv "$STAGING" "$SNAP"` 是**真原子**(两者都在 `$DEST` 下、同 fs), # 对照:`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 # ← 从这里开始,"脚本死了但服务停着"就是事故 _SERVICE_STOPPED=1 # ← 从这里开始,"脚本死了但服务停着"就是事故
_NEW="$TARGET.new.$$" _NEW="$TARGET.new.$$"
# 复制到**目标同目录**:这一步慢/失败都无所谓,因为 `$_NEW` 还没有任何人用。 # 复制到**目标同目录**:这一步慢/失败都无所谓,因为 `$_NEW` 还没有任何人用。

View File

@ -73,10 +73,11 @@
}, },
{ {
"id": "redeploy-script-unguarded-steps", "id": "redeploy-script-unguarded-steps",
"count": 1, "count": 0,
"kind": "scope", "kind": "scope",
"due": "**下一次改 `deploy/` 下任一脚本时**必须一并堵(`redeploy-gateway.sh` 正在被另一条会话改 ⇒ 本条目就是给它接手时的入口)。堵法:给每个副作用步骤加 `|| { bad …; exit 2; }`,或在脚本上开 `set -e`;两者都要与既有的 2=环境 / 1=检查 约定对齐。", "due": "**已还清 2026-09-25**(见 note)",
"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。" "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", "id": "deploy-space-prefix-fs",