Files
MailUI4Agents/deploy/prune-deploy-artifacts.sh
JianFeeeee 42f01c7478 fix: 三处"判定对、但信号假"——豁免的自检覆盖、rm 失败仍报"已删除"、show_diff 截断证据
pi 通读后逐处指出,三条都实测复现:

**1. `jsonTestOnlyChange`/`testOnlyDrift`/`runtimeOnly` 没有任何自检碰过。**
他的质疑成立:我上一封说"配了六个样本",那六个样本是**开发时的内联脚本、没进文件**
(他读了全文,找不到——我核了,确实没有)。而这段逻辑是文件里**唯一一处"把红变成绿"
的代码**,也是唯一没有判据的代码,失效方向恰好是"比恒黄更坏"那个(假绿)。
已补 **4 条走真路径的样本**(真临时树 + `diffSummary(d,{repoDir,snapDir})`):
★只差 `scripts.test` ⇒ 不算运行时漂移且必须说出豁免了哪条键;
★`scripts.start` 变了 ⇒ 必须算运行时;★解析不了 ⇒ 不许豁免;★不传 ctx ⇒ 不豁免。

★ 写样本时被一对**同义不同数**的字段绊住:断言 `runtimeDrift === 0` 却得到 1 ——
`diffSummary` 报的是**原始**检测数,而 `checkHost` 自己又减了一遍豁免。
判据自己产出两个矛盾口径,与"注释里两组矛盾的写点计数"同族 ⇒ **统一**:
`runtimeDrift` = 减掉豁免后的结论,被豁免的只出现在 `testOnly` 里;
`checkHost` 删掉自算的 `runtimeOnly`。不传 ctx 时行为与旧版逐字相同。

**2. `prune-deploy-artifacts.sh` 的 `del()`:`rm` 的结果没人看。**
脚本是 `set -uo pipefail`(**无 -e**)⇒ `rm -rf` 失败(权限/只读挂载/immutable)后
照样打"删除 …(NMB)"、收尾汇总"**已删除 N 项,释放约 X MB**"
——**一次失败之后仍产出成功措辞**(与 `redeploy-plugin.sh` 里"被拒还打 [ OK ]"同族)。
已改:判失败即报错 `exit 2`;`removed`/`freed_kb` **只在成功后累加**;并加抽验
(`rm` 报成功但路径还在 ⇒ 报错),不信单一退出码。
⚠️ **这一条的失败路径我没能实测**:本机以 root 跑,权限拦不住 `rm`;
`unshare -r` 建只读挂载被拒(`/proc/self/uid_map: Permission denied`);
tmpfs 无 `chattr +i`。要覆盖得在带 CAP_LINUX_IMMUTABLE 的环境用 immutable 文件,
或给 `del()` 加 `RM` 注入点。**我没有把"改过"说成"验过"。**

**3. `check-shared-libs.sh` 的 `show_diff` 被 `set -e` 当场中止。**
`cmp` 有差异返回 1 ⇒ `pipefail` 让函数返回 1 ⇒ 独立调用触发 `set -e` ⇒ **脚本当场死**。
实测(脚本级,fixture 树里造两处分叉:pi 与 zcode):
    旧版:报告 1 处分叉、收尾汇总 0 次
    新版:报告 2 处分叉、收尾汇总 0 次(收尾那句本来只在成功时打,见下)
两者退出码都是 1(判定一直是对的)—— 所以**光量退出码看不见这个 bug**,
正是 pi 说的"判定对、证据被截断"。而且旧版连 `fail=1` 都执行不到:
退出码 1 是 `set -e` 给的。加 `|| true` 后遍历跑完。
2026-09-14 20:35:01 +08:00

346 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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` 永远保留)、同一时间戳的数据库/附件备份算**一个备份集**留最新 N 集。
# 发布纪律要求"有回滚目标",所以不是全清。
# ② ★ **绝不删正在使用的快照**:任何进程的 cmdline 里出现过的快照路径一律跳过。
# 删掉它 = 进程一重启就找不到自己的代码(而这类事故此前真发生过)。
# ③ ★★ **在线数据库永不入列**:`$ROOT/data/agentmail.db`(含 `-wal`/`-shm`)一旦出现在
# 删除清单里,脚本立刻**报错退出**,不做任何删除。备份删错了还能重建,
# 在线库删错了就是数据没了 —— 这条闸门必须比"我检查过了"硬。
# 判据 `--self-check` 两侧都验:干净样本删完在线库还在;把超窗口的备份换成
# 指向在线库的符号链接,脚本必须**拒跑**(不是"没删")。
#
# 用法:
# bash deploy/prune-deploy-artifacts.sh # 干跑,只报告
# bash deploy/prune-deploy-artifacts.sh --apply # 真删
# bash deploy/prune-deploy-artifacts.sh --self-check # 自检:坏样本必须红、干净样本必须绿
# KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 KEEP_BUILD_STAGES=0 bash ... --apply # 调保留窗口
set -uo pipefail
ROOT="${AGENTMAIL_ROOT:-/opt/agentmail}"
KEEP_GATEWAY="${KEEP_GATEWAY:-3}"
KEEP_SNAPSHOTS="${KEEP_SNAPSHOTS:-3}"
KEEP_DB_BACKUPS="${KEEP_DB_BACKUPS:-1}"
KEEP_TMP_DB="${KEEP_TMP_DB:-2}"
KEEP_BUILD_STAGES="${KEEP_BUILD_STAGES:-1}"
TMPD="${PRUNE_TMP_DIR:-/tmp}"
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1
freed_kb=0
removed=0
skipped=0
report() { printf ' %s %s\n' "$1" "$2"; }
# 在线库:唯一的"碰了就回不来"的东西。
LIVE_DB_REAL="$(readlink -f "$ROOT/data/agentmail.db" 2>/dev/null || echo "$ROOT/data/agentmail.db")"
# 收集所有"正在被进程使用"的路径(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" real
real="$(readlink -f "$path" 2>/dev/null || echo "$path")"
case "$real" in
"$LIVE_DB_REAL"|"$LIVE_DB_REAL-wal"|"$LIVE_DB_REAL-shm")
printf '\n [FATAL] 删除清单里出现在线数据库:%s(真实路径 %s)\n' "$path" "$real" >&2
printf ' 脚本的判定有 bug —— 拒绝执行,删一个备份可以重建,删在线库回不来。\n' >&2
exit 1;;
esac
local kb; kb=$(du -sk "$path" 2>/dev/null | cut -f1); kb=${kb:-0}
# ★ `rm` 的结果必须看:本脚本是 `set -uo pipefail`(**没有 -e**),
# 于是 `rm -rf` 失败(权限、只读挂载、immutable)之后脚本照样往下走,
# 先打"删除 …(NMB)"、收尾再汇总"**已删除 N 项,释放约 X MB**" ——
# **一次失败之后仍然产出成功措辞**(pi 评审 2026-09-14 抓到,实测确认)。
# 这与 `redeploy-plugin.sh` 里"被拒之后照样打 `[ OK ]`"是同一族,也是本仓库
# 反复记录的那个病:从失败里产出一份看着正常的报告。
# 所以:判失败即报错并 `exit 2`(环境/权限问题),且 `removed`/`freed_kb`
# **只在成功后才累加**,收尾汇总不再虚报。
if [ "$APPLY" = "1" ]; then
if ! rm -rf "$path"; then
printf '
[FAIL] 删不掉:%s(权限/只读挂载/immutable?)\n' "$path" >&2
printf ' 已停在中途 —— 后面的判断没跑,收尾汇总也不会报"已删除"。\n' >&2
exit 2
fi
# 删完还要**抽验**:`rm -rf` 对某些情况会"成功"却留东西(例如非空目录里
# 有不可删项时,某些实现返回 0)。存在即报错,别信退出码单一信号。
if [ -e "$path" ]; then
printf '
[FAIL] `rm -rf` 报了成功但 %s 还在\n' "$path" >&2
exit 2
fi
fi
freed_kb=$((freed_kb + kb)); removed=$((removed + 1))
report " $([ "$APPLY" = 1 ] && echo 删除 || echo 待删)" "$path($((kb/1024))MB)"
}
# ---------------------------------------------------------------- 自检
# 判据本身也要有判据:干净样本必须绿(该删的删、该留的留、在线库不动),
# 坏样本必须红(把超窗口的备份换成指向在线库的符号链接 → 必须拒跑)。
# 自检跑在临时假根上,`$TMPD` 也指到假根里 —— 绝不碰真机上的东西。
if [ "${1:-}" = "--self-check" ]; then
fails=0; total=0
ck() { total=$((total+1)); if [ "$2" = "1" ]; then echo " 通过 $1"; else echo " 失败 $1"; fails=$((fails+1)); fi; }
mktree() {
local t; t="$(mktemp -d "$TMPD/am-prune-selftest-XXXXXX")"
mkdir -p "$t/data" "$t/backups" "$t/tmp" \
"$t/plugins/demo/20260101-000000" "$t/plugins/demo/20260102-000000" \
"$t/plugins/demo/20260103-000000" "$t/plugins/demo/20260104-000000"
ln -s 20260104-000000 "$t/plugins/demo/current"
: > "$t/agentmail-gateway"
# mtime 必须各不同:`ls -1t` 对同一秒的文件靠名字兜底排序,
# 早期版本的自检就是因此把"保留哪一份"判成反的(脚本没错,鱼缸错了)。
for i in 1 2 3 4; do
echo x > "$t/agentmail-gateway.bak-2026010$i-000000"
touch -d "2026-01-0$i 00:00:00" "$t/agentmail-gateway.bak-2026010$i-000000"
touch -d "2026-01-0$i 00:00:00" "$t/plugins/demo/2026010$i-000000"
done
: > "$t/data/agentmail.db" # ← 在线库(假)
: > "$t/data/agentmail.db-wal"
: > "$t/data/agentmail.db.bak-20260105-000000" # 最新备份集 → 保留
: > "$t/backups/agentmail-20251201-000000.db" # 老备份集 → 整集删(连同附件包与 -shm/-wal)
echo yy > "$t/backups/agentmail-20251201-000000.db-shm"
: > "$t/backups/agentmail-20251201-000000.db-wal"
: > "$t/backups/attachments-20251201-000000.tar.gz"
for i in 1 2 3; do
: > "$t/tmp/agentmail-pre-deploy-2026010$i-000000.db"
touch -d "2026-01-0$i 00:00:00" "$t/tmp/agentmail-pre-deploy-2026010$i-000000.db"
: > "$t/tmp/agentmail-gateway-build-2026010$i-000000"
touch -d "2026-01-0$i 00:00:00" "$t/tmp/agentmail-gateway-build-2026010$i-000000"
done
echo "$t"
}
run() {
local t="$1" mode="${2:-}"
# ★ 自检只允许在**自己刚建的临时假根**上跑。这条闸门是事后补的,因为它真出过事:
# 早先 `run()` 传的是 `ROOT=…`,而脚本读的是 `AGENTMAIL_ROOT` —— 前缀赋值看起来
# "传进去了"(`$TMPD` 那一路用的是 PRUNE_TMP_DIR,确实进去了),于是自检的
# `--apply` 打在了**生产根**上:删掉一批回滚备份。教训不是"下次小心",
# 而是自检必须**结构上**碰不到生产:传对变量名 + 一条根目录必须在临时区的硬闸门。
case "$t" in
"$TMPD"/am-prune-selftest-*) ;;
*) echo " [FATAL] 自检根目录不在临时区:$t —— 拒跑" >&2; exit 1;;
esac
AGENTMAIL_ROOT="$t" PRUNE_TMP_DIR="$t/tmp" KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 KEEP_DB_BACKUPS=1 KEEP_TMP_DB=1 KEEP_BUILD_STAGES=1 \
bash "$0" $mode
}
echo
echo "判据自检(先证明清理器能分辨该删/该留,再证明它拦得住误删):"
# 自检前后对比生产根的清单指纹 —— "自检不会碰生产"要有判据,不靠自觉。
_tree_sig() {
# 在线库自身的大小一直在变(WAL 在写),所以把 live 三件套排除掉再看指纹,
# 否则这条判据会因为别人的正常写入而无关红。
find "$1" -maxdepth 3 \
\( -path "$1/data/attachments" -o -name 'agentmail.db' -o -name 'agentmail.db-wal' -o -name 'agentmail.db-shm' \) -prune -o \
-printf '%p %s\n' 2>/dev/null | sort
}
SIG_BEFORE="$(_tree_sig "$ROOT")"
T="$(mktree)"
out="$(run "$T")"; rc=$? # 干跑:看报告
ck "干净样本:干跑成功(退出码 $rc)" "$([ "$rc" = 0 ] && echo 1 || echo 0)"
ck "干净样本:窗口内的网关二进制保留" "$(printf '%s' "$out" | grep -qF "保留 $T/agentmail-gateway.bak-20260104-000000" && echo 1 || echo 0)"
ck "干净样本:窗口外的网关二进制待删" "$(printf '%s' "$out" | grep -qF "待删 $T/agentmail-gateway.bak-20260101-000000" && echo 1 || echo 0)"
ck "干净样本:窗口外的插件快照待删" "$(printf '%s' "$out" | grep -qF "待删 $T/plugins/demo/20260101-000000" && echo 1 || echo 0)"
ck "干净样本:老备份集整集待删(含附件包)" \
"$(printf '%s' "$out" | grep -qF "待删 $T/backups/attachments-20251201-000000.tar.gz" && echo 1 || echo 0)"
ck "干净样本:最新备份集保留" "$(printf '%s' "$out" | grep -qF "保留 $T/data/agentmail.db.bak-20260105-000000" && echo 1 || echo 0)"
ck "干净样本:在线库不在清单里" "$(printf '%s' "$out" | grep -qE '/data/agentmail\.db(' && echo 0 || echo 1)"
run "$T" --apply >/dev/null; rc=$? # 真删:看结果
ck "干净样本:真删成功(退出码 $rc)" "$([ "$rc" = 0 ] && echo 1 || echo 0)"
ck "干净样本:在线库与它的 -wal 仍在" "$([ -f "$T/data/agentmail.db" ] && [ -f "$T/data/agentmail.db-wal" ] && echo 1 || echo 0)"
ck "干净样本:窗口外没了、窗口内还在" \
"$([ ! -e "$T/agentmail-gateway.bak-20260101-000000" ] && [ -e "$T/agentmail-gateway.bak-20260104-000000" ] && echo 1 || echo 0)"
ck "干净样本:老备份集整集删除、最新备份集保留" \
"$([ ! -e "$T/backups/agentmail-20251201-000000.db" ] && [ ! -e "$T/backups/attachments-20251201-000000.tar.gz" ] && [ -e "$T/data/agentmail.db.bak-20260105-000000" ] && echo 1 || echo 0)"
ck "干净样本:/tmp 备份只留窗口内那 1 份" \
"$([ -e "$T/tmp/agentmail-pre-deploy-20260103-000000.db" ] && [ ! -e "$T/tmp/agentmail-pre-deploy-20260101-000000.db" ] && echo 1 || echo 0)"
ck "干净样本:/tmp 构建暂存只留窗口内那 1 份" \
"$([ -e "$T/tmp/agentmail-gateway-build-20260103-000000" ] && [ ! -e "$T/tmp/agentmail-gateway-build-20260101-000000" ] && echo 1 || echo 0)"
# 坏样本:超窗口的那个备份是指向在线库的符号链接 —— 必须**拒跑**,而不是"删了才发现"
B="$(mktree)"; rm -f "$B/backups/agentmail-20251201-000000.db"
ln -s "$B/data/agentmail.db" "$B/backups/agentmail-20251201-000000.db"
bout="$(run "$B" --apply 2>&1)"; brc=$?
ck "坏样本(备份是指向在线库的链接):必须拒跑(实际退出码 $brc)" "$([ "$brc" != 0 ] && echo 1 || echo 0)"
ck "坏样本:拒绝理由要点名在线数据库" "$(printf '%s' "$bout" | grep -q "在线数据库" && echo 1 || echo 0)"
ck "坏样本:在线库仍在" "$([ -f "$B/data/agentmail.db" ] && echo 1 || echo 0)"
# 自检最要紧的一条:生产根在这两轮自检里**一个文件都没动**。
ck "自检没有碰生产根($ROOT 清单不变)" "$([ "$(_tree_sig "$ROOT")" = "$SIG_BEFORE" ] && echo 1 || echo 0)"
rm -rf "$T" "$B"
echo
if [ "$fails" != 0 ]; then
echo " 判据自检失败 $fails/$total 项 —— 清理器本身不可信,不要用它删东西" >&2
exit 1
fi
echo " 判据自检全绿($total 项)。"
exit 0
fi
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_SNAPSHOTS 份,current 永远保留)"
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 " 保留" "$s(current 指向)"; 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
# 回滚窗口的含义:每个插件除了 `current` 之外还应该留着至少一份"上一版",
# 否则回滚没有目标(发布纪律第四章)。这里只报告,**不造快照** —— 造不出来的东西
# 不该假装有。
tsnaps=0
for s in "${snaps[@]}"; do [ "$(basename "$s")" = "current" ] || tsnaps=$((tsnaps+1)); done
if [ "$tsnaps" -le 1 ]; then report " ⚠ 无回滚目标(只有 current 那一份)" "$name"; fi
done
# 数据库/附件备份。两个来源各写各的时间戳,但都是"某一刻的库快照":
# · `$ROOT/backups/agentmail-<ts>.db` + `attachments-<ts>.tar.gz` —— deploy/reset-demo.sh
# 重置演示数据前留的档;
# · `$ROOT/data/agentmail.db.bak-<ts>` —— 早期手工留档。
# 同一个 `<ts>` 的文件算**一个备份集**(库和它的附件包必须同生共死,拆开留没有意义),
# 一起进出窗口。顺序按**文件名里的时间戳**排,不按 mtime —— mtime 会被"顺手打开看一眼"
# 改掉(实测:09-02 的两个备份被 09-08 的一次访问改了 mtime)。
# 没有时间戳的文件一律不碰:判定不了就不删(宁可漏,不可误删)。
echo "==> 旧数据库/附件备份(保留最新 $KEEP_DB_BACKUPS 个备份集)"
declare -A SET_MEMBERS=()
for f in "$ROOT"/backups/* "$ROOT"/data/*.bak-*; do
[ -e "$f" ] || continue
ts="$(printf '%s' "$(basename "$f")" | grep -oE '[0-9]{8}-[0-9]{6}' | head -1 || true)"
if [ -z "$ts" ]; then
report " 跳过(文件名无时间戳,判定不了)" "$f"; skipped=$((skipped+1)); continue
fi
SET_MEMBERS[$ts]="${SET_MEMBERS[$ts]:-} $f"
done
if [ "${#SET_MEMBERS[@]}" -eq 0 ]; then
report " (没有备份集)" ""
else
mapfile -t sets < <(printf '%s\n' "${!SET_MEMBERS[@]}" | sort -r)
for i in "${!sets[@]}"; do
ts="${sets[$i]}"
# ★ **整集一起判"在用"**,不能只跳那一个文件(pi 评审 2026-09-14 指出,实测确认)。
# 原先 `in_use "$f"` 命中就 `continue` —— 只跳过**那一个**,同一 `<ts>` 的其余成员
# 照删。备份集被拆开,与这段自己的注释("拆开留没有意义")相反;
# 在**删除**那一支(`i >= KEEP_DB_BACKUPS`)后果更难看:正在被使用的那个留了下来,
# 而同一时间戳的 `-wal`/`-shm` 被删掉 —— **"留"的那份配上被删的兄弟才是完整一套**,
# 结果留下的是残的。所以先算一次整集的在用状态,再决定整集动不动。
set_in_use=0
for f in ${SET_MEMBERS[$ts]}; do
if in_use "$f"; then set_in_use=1; break; fi
done
if [ "$set_in_use" -eq 1 ]; then
report " ★跳过整集(有成员正在被使用)" "$ts"
for f in ${SET_MEMBERS[$ts]}; do skipped=$((skipped+1)); done
continue
fi
for f in ${SET_MEMBERS[$ts]}; do
if [ "$i" -lt "$KEEP_DB_BACKUPS" ]; then
case "$f" in
# 保留集里的 `-shm`/`-wal` 是"打开看过一眼"的残渣:`-shm` 是内存索引,
# sqlite 自己会重建;`-wal` 只在**非空**时才可能含未合并写入,非空就不动它。
*-shm) del "$f";;
*-wal) if [ -s "$f" ]; then report " *跳过(wal 非空,可能含未合并写入)" "$f"; skipped=$((skipped+1)); else del "$f"; fi;;
*) report " 保留" "$f";;
esac
else
del "$f"
fi
done
done
fi
# 构建暂存:`redeploy-gateway.sh` 先把网关构建到 `$TMPD` 再 `install` 过去(为了原子
# 替换),**用完没有任何人删过它** —— 每次部署留下一个 24MB。2026-09-14 实测 7 份
# / 162MB。这不是"几百 MB 而已":本机 `$TMPD` 是 9.8G 的 tmpfs,当时已被占满
# (可用 0 字节),连 `go build` 都进不去 —— 而部署的第一步就是构建。
#
# 窗口留 1 份而不是 0:正常路径下部署脚本自己会收(见 redeploy-gateway.sh 末尾),
# 留下的只可能是**失败**的那次;失败时那一份正好用来事后查,删了就没现场了。
echo "==> $TMPD 的构建暂存(保留最新 $KEEP_BUILD_STAGES 份)"
mapfile -t stages < <(ls -1t "$TMPD"/agentmail-gateway-build-* 2>/dev/null)
for i in "${!stages[@]}"; do
[ "$i" -lt "$KEEP_BUILD_STAGES" ] && { report " 保留" "${stages[$i]}"; continue; }
# 二进制可能正被某个部署进程 install 到一半 —— 与快照同理,用过的路径一律先问一句。
if in_use "${stages[$i]}"; then report " ★跳过(正在被使用)" "${stages[$i]}"; skipped=$((skipped+1)); continue; fi
del "${stages[$i]}"
done
echo "==> $TMPD 的部署前数据库备份(保留最新 $KEEP_TMP_DB 份)"
mapfile -t dbs < <(ls -1t "$TMPD"/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
# 会话清理(prune-test-sessions.sh)前的备份是**那次删除**唯一的回滚点,
# 所以另算一条窗口、且窗口到了也要说清楚它是什么再删。
echo "==> $TMPD 的会话清理前备份(保留最新 $KEEP_TMP_DB 份)"
mapfile -t prunedbs < <(ls -1t "$TMPD"/agentmail-pre-prune-*.db 2>/dev/null)
for i in "${!prunedbs[@]}"; do
[ "$i" -lt "$KEEP_TMP_DB" ] && { report " 保留(会话归档的回滚点)" "${prunedbs[$i]}"; continue; }
del "${prunedbs[$i]}"
done
echo
if [ "$APPLY" = "1" ]; then
echo " 已删除 $removed 项,释放约 $((freed_kb/1024))MB;跳过(在用/不能判)$skipped 项"
echo " 已确认:删除清单不含在线数据库 $(basename "$LIVE_DB_REAL")。"
else
echo " 干跑:将删除 $removed 项,可释放约 $((freed_kb/1024))MB;跳过(在用/不能判)$skipped 项"
echo " 已确认:删除清单不含在线数据库 $(basename "$LIVE_DB_REAL")。"
echo " 加 --apply 执行。"
fi