chore(deploy): 清理部署残留(1.0GB)+ 把清理写成可复跑脚本

用户说「清理一下」。实测 /opt/agentmail 累计 1.3GB:
- 网关旧二进制 33 份 × 24MB ≈ 790MB(每次 redeploy 留一份,从 09-05 起没清过)
- 插件快照:opencode 6 份 339MB、dsh 7 份 195MB、zcode 16 份、pi 9 份
- /tmp 里的部署前数据库备份 10 份(tmpfs,占内存)

新增 `deploy/prune-deploy-artifacts.sh`(可复跑,不手敲 rm),两条硬规矩:
① **保留回滚窗口**:网关留最新 3 份、每个插件留最新 3 份快照,`current` 永远保留
(发布纪律要求有回滚目标,所以不是全清);
② ★ **绝不删正在使用的快照**:扫 /proc 的 cmdline 与 cwd,命中就跳过。

执行结果:**/opt/agentmail 1305MB → 302MB(释放 1003MB)**,7 个服务全部 active,
`current` 指向未变,`check-deploy-drift` 仍报「四个宿主都在跑当前代码」。

## 过程中的一处自纠

"在用"闸门第一版有**自我匹配**缺陷:我的测试命令把路径写在命令行里,于是扫 /proc 时
扫到了自己 ⇒ 对不存在的路径也判"在用"(与 `pkill -f` 杀掉自己那条命令同一类)。
已改为排除本进程及其祖先链,并**换正确方式重测**(路径从文件读、不进 cmdline):
在用快照判"在用" ✓、不存在的路径不误判 ✓ —— 判据两侧都验过才敢执行删除。

文档:docs/DEV-TOOLING.md 记了用法与那两条规矩。
This commit is contained in:
2026-09-14 08:43:54 +08:00
parent 51789ee72e
commit 69b1887eae
2 changed files with 123 additions and 0 deletions

107
deploy/prune-deploy-artifacts.sh Executable file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env bash
# 清理部署残留:旧网关二进制、旧插件快照、/tmp 里的部署前数据库备份。
#
# # 为什么要脚本而不是手敲 rm
#
# 这些东西**天天在长**:每次 redeploy 留一个 24MB 的网关二进制、每个插件留一份快照
# opencode/dsh 的快照含 node_modules一份几十 MB。2026-09-14 实测:
# /opt/agentmail 1.3G,其中旧二进制 ~790MB、插件快照 ~540MB。
# 手敲的清理不可复现、下次还得再想一遍,而且很容易漏掉"这一份还在被用"的判断。
#
# # 两条硬规矩
#
# ① **保留回滚窗口**:网关二进制留最新 N 份、每个插件留最新 N 份快照
# `current` 永远保留)。发布纪律要求"有回滚目标",所以不是全清。
# ② ★ **绝不删正在使用的快照**:任何进程的 cmdline 里出现过的快照路径一律跳过。
# 删掉它 = 进程一重启就找不到自己的代码(而这类事故此前真发生过)。
#
# 用法:
# bash deploy/prune-deploy-artifacts.sh # 干跑,只报告
# bash deploy/prune-deploy-artifacts.sh --apply # 真删
# KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 bash ... --apply # 调保留窗口
set -uo pipefail
ROOT="${AGENTMAIL_ROOT:-/opt/agentmail}"
KEEP_GATEWAY="${KEEP_GATEWAY:-3}"
KEEP_SNAPSHOTS="${KEEP_SNAPSHOTS:-3}"
KEEP_TMP_DB="${KEEP_TMP_DB:-2}"
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1
freed_kb=0
removed=0
skipped=0
report() { printf ' %s %s\n' "$1" "$2"; }
# 收集所有"正在被进程使用"的路径cmdline + cwd用来做安全闸。
# 自己这条命令的 cmdline 里就可能含这个路径(例如调用方把路径写在命令行里),
# 于是"检查在用"会永远为真 —— 与 `pkill -f` 杀掉自己那条命令是同一个坑。
# 所以先把本进程及其祖先链排除掉。
_SELF_CHAIN=" "
_p=$$
while [ -n "$_p" ] && [ "$_p" != "1" ] && [ "$_p" != "0" ]; do
_SELF_CHAIN="$_SELF_CHAIN$_p "
_p=$(awk '{print $4}' "/proc/$_p/stat" 2>/dev/null || echo "")
done
in_use() {
local needle="$1" p pid
for p in /proc/[0-9]*; do
pid=${p#/proc/}
[ -r "$p/cmdline" ] || continue
case "$_SELF_CHAIN" in *" $pid "*) continue;; esac
if tr '\0' ' ' <"$p/cmdline" 2>/dev/null | grep -qF "$needle"; then return 0; fi
local cwd; cwd=$(readlink "$p/cwd" 2>/dev/null || true)
case "$cwd" in *"$needle"*) return 0;; esac
done
return 1
}
del() {
local path="$1"
local kb; kb=$(du -sk "$path" 2>/dev/null | cut -f1); kb=${kb:-0}
if [ "$APPLY" = "1" ]; then rm -rf "$path"; fi
freed_kb=$((freed_kb + kb)); removed=$((removed + 1))
report " $([ "$APPLY" = 1 ] && echo 删除 || echo 待删)" "$path$((kb/1024))MB"
}
echo "==> 网关旧二进制(保留最新 $KEEP_GATEWAY 份)"
mapfile -t bins < <(ls -1t "$ROOT"/agentmail-gateway.bak-* 2>/dev/null)
for i in "${!bins[@]}"; do
[ "$i" -lt "$KEEP_GATEWAY" ] && { report " 保留" "${bins[$i]}"; continue; }
if in_use "${bins[$i]}"; then report " ★跳过(正在被使用)" "${bins[$i]}"; skipped=$((skipped+1)); continue; fi
del "${bins[$i]}"
done
echo "==> 插件快照(每个插件保留最新 $KEEP_SNAPSHOTScurrent 永远保留)"
for plug in "$ROOT"/plugins/*/; do
[ -d "$plug" ] || continue
name=$(basename "$plug")
mapfile -t snaps < <(ls -1dt "$plug"*/ 2>/dev/null | sed 's:/$::')
for i in "${!snaps[@]}"; do
s="${snaps[$i]}"
[ "$(basename "$s")" = "current" ] && continue
# current 指向的那一份也必须保留
cur=$(readlink -f "$plug/current" 2>/dev/null || true)
[ "$cur" = "$(readlink -f "$s")" ] && { report " 保留" "$scurrent 指向)"; continue; }
if [ "$i" -lt "$KEEP_SNAPSHOTS" ]; then report " 保留" "$s"; continue; fi
if in_use "$s"; then report " ★跳过(正在被使用)" "$s"; skipped=$((skipped+1)); continue; fi
del "$s"
done
done
echo "==> /tmp 的部署前数据库备份(保留最新 $KEEP_TMP_DB 份)"
mapfile -t dbs < <(ls -1t /tmp/agentmail-pre-deploy-*.db 2>/dev/null)
for i in "${!dbs[@]}"; do
[ "$i" -lt "$KEEP_TMP_DB" ] && { report " 保留" "${dbs[$i]}"; continue; }
del "${dbs[$i]}"
done
echo
if [ "$APPLY" = "1" ]; then
echo " 已删除 $removed 项,释放约 $((freed_kb/1024))MB跳过在用$skipped"
else
echo " 干跑:将删除 $removed 项,可释放约 $((freed_kb/1024))MB跳过在用$skipped"
echo " 加 --apply 执行。"
fi

View File

@ -67,3 +67,19 @@ pi-lens 在编辑文件后会走 smart-default 回退选择格式化器(见它
- `biome.jsonc`:只挡得住 biome**挡不住 prettier** —— 两者是并列的候选格式化器,
各有各的配置。所以真正的开关是 `.pi-lens.json`,它两条改写路径一起关。
## 部署残留清理(`deploy/prune-deploy-artifacts.sh`
部署会留下两类持续增长的东西:网关旧二进制(每份 ~24MB与插件快照opencode/dsh 的
快照含 node_modules一份几十 MB。2026-09-14 实测 `/opt/agentmail` 累计到 **1.3GB**
其中旧二进制 ~790MB、快照 ~540MB。
```bash
bash deploy/prune-deploy-artifacts.sh # 干跑,只报告
bash deploy/prune-deploy-artifacts.sh --apply # 真删(默认:网关留 3 份、每插件留 3 份快照)
```
两条硬规矩:**保留回滚窗口**(发布纪律要求有回滚目标,所以不是全清);**绝不删正在使用的
快照**(扫 /proc 的 cmdline 与 cwd命中就跳过 —— 删掉它进程一重启就找不到自己的代码)。
注意那个"在用"判断必须排除**本进程及其祖先链**:调用方常把路径写在命令行里,
不排除就会出现"永远判为在用"(与 `pkill -f` 杀掉自己那条命令同一个坑,已写进脚本注释)。