背景:t_763e8701 交付了 bitemporal_e2e_test.go,但该测试默认 t.Skip (需 ZHIYI_BITEMPORAL_SOCK 指向运行中的 sidecar)。没人设环境变量 ⇒ 这道『机器闸门』永不开火 ⇒ 等于不存在(机制铁律:写完无人调用 = 零效力)。 改动:do_verify 增加 verify_e2e_bitemporal() —— 用本次已安装的 sidecar 开临时 socket + 临时 data-dir(不碰生产库), 跑 Go e2e(真实 LanceDB 写→读回),失败即 die。 起因事故:t_3ada332b 单测全 PASS 但生产 no-op —— 三列在 LanceDB 边界 被静默丢弃(Go payload 缺 3 键 + Rust schema 缺 3 列), validityExpired 永远拿到空串 ⇒ 降权永不触发。本闸门即该事故的守门人。 验证(双向): - 通过路径:deploy.sh --verify-only 全绿,含『✔ bi-temporal e2e 闸门通过』 - 失败路径:把测试指向不存在的 socket ⇒ go test 返回 FAIL(非零)⇒ 函数 rc≠0 即 die
157 lines
7.6 KiB
Bash
Executable File
157 lines
7.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 织忆 MemoryWeave 统一部署脚本 — build two + install two + restart two + verify
|
||
# 2026-10-08 · t_f57b8bbf · 根治「重启 zhiyid 不重启 sidecar → Rust 改部署后 sidecar 跑旧二进制静默故障」事故
|
||
#
|
||
# 用法:
|
||
# ./scripts/deploy.sh 完整部署: 构建两+装两+停两+启两+校验
|
||
# ./scripts/deploy.sh --verify-only 只跑校验(不构建/不安装/不重启)—— 反例测试/人工体检用
|
||
# make deploy = ./scripts/deploy.sh
|
||
set -euo pipefail
|
||
|
||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
BIN_DIR="${BIN_DIR:-/home/muc/bin}"
|
||
|
||
# --allow-dirty:显式承担「把在途改动编进生产二进制」的风险(默认拒绝)
|
||
ALLOW_DIRTY=0
|
||
for _a in "$@"; do [ "$_a" = "--allow-dirty" ] && ALLOW_DIRTY=1; done
|
||
|
||
# 构建产物(持久路径,均已 gitignore)
|
||
BUILD_GO="$REPO_DIR/go/cmd/zhiyid/zhiyid"
|
||
BUILD_RUST="$REPO_DIR/rust/target/release/zhiyi-consolidate"
|
||
# 安装落点(与 ~/.config/systemd/user/*.service 的 ExecStart 一致)
|
||
INSTALL_GO="$BIN_DIR/zhiyid-new"
|
||
INSTALL_RUST="$BIN_DIR/zhiyi-consolidate"
|
||
# 服务名(用户级 systemd)
|
||
SVC_GO="zhiyid.service"
|
||
SVC_RUST="zhiyi-consolidate.service"
|
||
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:7821/health}"
|
||
|
||
log() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*"; }
|
||
die() { printf '\033[1;31m[deploy:FAIL]\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
# 校验单个服务的运行态是否=本次构建的二进制
|
||
# 判据优先级(纪律): 副作用(PID/inode/校验和) > 返回值 > 自报
|
||
verify_service() {
|
||
local name="$1" build_path="$2" pid exe run_sha build_sha
|
||
# ① is-active
|
||
[ "$(systemctl --user is-active "$name")" = "active" ] || die "$name 非 active"
|
||
# ② 取 MainPID
|
||
pid="$(systemctl --user show -p MainPID --value "$name")"
|
||
[ -n "$pid" ] && [ "$pid" != "0" ] || die "$name: MainPID 为空/0"
|
||
# ③ 进程 exe 不得指向已删除 inode(旧二进制被 install 覆盖后、进程仍跑旧 inode → readlink 含 "(deleted)")
|
||
exe="$(readlink "/proc/$pid/exe" 2>/dev/null || true)"
|
||
case "$exe" in
|
||
"") die "$name: 无法 readlink /proc/$pid/exe" ;;
|
||
*" (deleted)"*) die "$name: 进程仍跑已删除 inode → 二进制被换过但进程没重启 ($exe)" ;;
|
||
esac
|
||
# ④ 运行中二进制校验和 == 本次构建产物(抓「磁盘装了新版但进程仍跑旧版」静默故障)
|
||
run_sha="$(sha256sum "/proc/$pid/exe" 2>/dev/null | awk '{print $1}')"
|
||
build_sha="$(sha256sum "$build_path" 2>/dev/null | awk '{print $1}')"
|
||
[ -n "$run_sha" ] || die "$name: 无法读取运行中二进制"
|
||
[ "$run_sha" = "$build_sha" ] || die "$name: 运行中二进制 ≠ 本次构建产物 (运行=$run_sha 构建=$build_sha) → sidecar 静默跑旧版"
|
||
log "✔ $name active · PID=$pid · exe=$exe · 校验和匹配本次构建"
|
||
}
|
||
|
||
# 深健康检查: /health 连 sidecar 可达性(P0 已做,故障返 503)→ 期望 200
|
||
verify_health() {
|
||
local code
|
||
code="$(curl -s -o /dev/null -w '%{http_code}' -m 5 "$HEALTH_URL")"
|
||
[ "$code" = "200" ] || die "/health 返回 $code (期望 200) → zhiyid 或 sidecar 故障"
|
||
log "✔ /health = $code"
|
||
}
|
||
|
||
# 🔴 构建前洁净检查(2026-10-14 加)。起因(当晚三次同族事故):
|
||
# ① c345ed5 把另一任务在途的 server.go 布线一起提交 ⇒ HEAD 直接编译不过
|
||
# ② 本脚本原先**直接从工作树构建** ⇒ 任一 worker 的在途改动会被顺手编进**生产二进制**,
|
||
# 而且部署后「运行二进制校验和匹配」会通过 —— 校验和只能证明「跑的是本次构建」,
|
||
# **不能证明「本次构建来自已提交的代码」**。这是验证链上的一个洞。
|
||
# 判据:`git status --porcelain`(含未跟踪,不含已忽略)必须为空。
|
||
# 未跟踪文件也要拦:`internal/peer` 那类事故正是「已提交代码引用了未跟踪文件」——
|
||
# 反之未跟踪的新源文件被编进去,二进制就与 commit 不一致。
|
||
preflight_clean_tree() {
|
||
if [ "$ALLOW_DIRTY" = "1" ]; then
|
||
log "⚠️ --allow-dirty:跳过洁净检查(本次构建含未提交改动,风险自负)"
|
||
return 0
|
||
fi
|
||
local dirty
|
||
dirty="$(git -C "$REPO_DIR" status --porcelain 2>/dev/null || true)"
|
||
if [ -n "$dirty" ]; then
|
||
printf '%s\n' "$dirty" | sed 's/^/ /' >&2
|
||
die "工作树不洁净 → 拒绝构建(会把在途改动编进生产二进制,部署后校验和仍会通过,无法事后察觉)。
|
||
处置:先提交/收工;确需带脏部署则显式加 --allow-dirty 并在卡上记录原因。"
|
||
fi
|
||
log "✔ 洁净检查通过(含未跟踪文件:无残留)"
|
||
}
|
||
|
||
do_build() {
|
||
preflight_clean_tree
|
||
log "构建 Go → $BUILD_GO"
|
||
( cd "$REPO_DIR/go" && CGO_ENABLED=1 go build -o cmd/zhiyid/zhiyid ./cmd/zhiyid )
|
||
log "构建 Rust → $BUILD_RUST"
|
||
( cd "$REPO_DIR/rust" && cargo build --release )
|
||
}
|
||
|
||
do_install() {
|
||
log "停止 $SVC_GO + $SVC_RUST"
|
||
systemctl --user stop "$SVC_GO" || true
|
||
systemctl --user stop "$SVC_RUST" || true
|
||
# 用 install 而非 cp:install 先写临时再 rename,避免 Text file busy(进程占着旧 inode 时 cp 会 EBUSY)
|
||
log "install 二进制(不用 cp,避免 Text file busy)"
|
||
install -m 0755 "$BUILD_GO" "$INSTALL_GO"
|
||
install -m 0755 "$BUILD_RUST" "$INSTALL_RUST"
|
||
log "daemon-reload"
|
||
systemctl --user daemon-reload
|
||
log "启动 $SVC_GO + $SVC_RUST"
|
||
systemctl --user start "$SVC_GO"
|
||
systemctl --user start "$SVC_RUST"
|
||
sleep 2
|
||
}
|
||
|
||
# bi-temporal e2e 闸门(2026-10-14 加):
|
||
# 为什么:`bitemporal_e2e_test.go` 默认 `t.Skip`(需 sidecar),
|
||
# 若无人设 ZHIYI_BITEMPORAL_SOCK ⇒ **闸门永不开火** ⇒ 等于不存在。
|
||
# 机制铁律「写完无人调用 = 零效力」⇒ 接进部署流程:每次部署后自动跑一遍。
|
||
# 跑法:用**本次已安装的** sidecar 开一个临时 socket + 临时 data-dir(不碰生产库),
|
||
# 跑 Go e2e(真实 LanceDB 写→读回)。失败即 die —— 这正是 2026-10-14
|
||
# 「只改 Go 不改 Rust ⇒ 三列在 LanceDB 边界静默丢 ⇒ 生产 no-op」的守门人。
|
||
verify_e2e_bitemporal() {
|
||
local sock=/tmp/mw-deploy-bit.sock data=/tmp/mw-deploy-bitdata log=/tmp/mw-deploy-bit.log spid rc
|
||
rm -rf "$data" "$sock"; mkdir -p "$data"
|
||
"$INSTALL_RUST" --mode socket --socket "$sock" --data-dir "$data" >"$log" 2>&1 &
|
||
spid=$!
|
||
for _ in $(seq 1 30); do [ -S "$sock" ] && break; sleep 0.5; done
|
||
if [ ! -S "$sock" ]; then
|
||
kill "$spid" 2>/dev/null
|
||
die "bi-temporal 闸门:临时 sidecar 未能监听 $sock(见 $log)"
|
||
fi
|
||
( cd "$REPO_DIR/go" && CGO_ENABLED=1 ZHIYI_BITEMPORAL_SOCK="$sock" \
|
||
go test -run TestBitemporalE2E ./internal/storage/ >/tmp/mw-deploy-bit-gotest.log 2>&1 )
|
||
rc=$?
|
||
kill "$spid" 2>/dev/null
|
||
for _ in $(seq 1 5); do kill -0 "$spid" 2>/dev/null || break; sleep 1; done
|
||
kill -0 "$spid" 2>/dev/null && kill -9 "$spid" 2>/dev/null
|
||
if [ "$rc" -ne 0 ]; then
|
||
sed 's/^/ /' /tmp/mw-deploy-bit-gotest.log | tail -15 >&2
|
||
die "bi-temporal 端到端闸门 FAIL(exit=$rc)—— 三列在 LanceDB 真实写→读回环断了。
|
||
典型根因:只改 Go payload 未改 Rust schema / 只改 Rust 未改 payload。"
|
||
fi
|
||
log "✔ bi-temporal e2e 闸门通过(三列经真实 LanceDB 写→读回 完整往返)"
|
||
}
|
||
|
||
do_verify() {
|
||
log "校验 ..."
|
||
verify_service "$SVC_GO" "$BUILD_GO"
|
||
verify_service "$SVC_RUST" "$BUILD_RUST"
|
||
verify_health
|
||
verify_e2e_bitemporal
|
||
log "✅ 部署成功: 两服务 active · 进程无 (deleted) · 运行二进制=本次构建 · /health=200 · e2e 闸门通过"
|
||
}
|
||
|
||
if [ "${1:-}" = "--verify-only" ]; then
|
||
do_verify
|
||
exit 0
|
||
fi
|
||
|
||
do_build
|
||
do_install
|
||
do_verify |