#!/bin/sh # 回滚:VM 侧运行,把主机恢复到最近一份代码备份包并重启验证。 # # 用法:deploy/rollback.sh [user] # host:主机 IP 或域名(必填);user:SSH 用户(默认 root)。 # # 密码:与 deploy.sh 同款规则——绝不接受密码参数、不读 env 文件、 # 不打印任何密钥;SSH 密码由交互提示或调用方通道提供;需要代理时 # 用环境变量 SSH_PROXY_CMD 传入 ProxyCommand,未设则直连。 # # 回滚边界:只回代码,不回数据库(理由见 deploy.sh 头注释与 # deploy/README.md:数据库迁移是只加列的幂等操作,老代码仍能读新列)。 # 备份包来自每次部署前在主机上打的 # /opt/codex-app-server-backups/codex-app-server-<时间戳>.tar.gz, # 本脚本恢复其中最新的一份;主机上没有备份包时直接报错退出,不编造。 # # 与 host_deploy.sh 共享的约定(P2-ops,两脚本改动时必须同步, # deploy/tests/test_p2_ops.py 会双向核对,见文件尾注释): # - server unit 清单:codex-cloud-server 必含,codex-cloud-server-2 # 以 systemctl cat 探测为准(存在才纳入停/起与健康检查); # - worker:autoscaler active 时停/起 autoscaler,否则 cloud-worker # (与 host_deploy.sh 的 worker_unit() 同款); # - 成功标准:清单里每个 server 端口的 health 都通过(8765 必查, # server-2 存在时 8766 也必查),只通 8765 不算回滚成功—— # 否则 LB 上游的 server-2 仍跑故障代码,回滚等于没回完。 # # 仿真覆盖(仅测试用,生产用默认值):CODEX_ROLLBACK_APP_DIR、 # CODEX_ROLLBACK_BACKUP_DIR、CODEX_ROLLBACK_HEALTH_ATTEMPTS、 # CODEX_ROLLBACK_HEALTH_INTERVAL。默认值与旧行为完全一致。 set -eu HOST=${1:-} USER_ARG=${2:-root} if [ -z "$HOST" ]; then echo "用法:deploy/rollback.sh [user]" >&2 exit 2 fi ssh_cmd() { if [ -n "${SSH_PROXY_CMD:-}" ]; then ssh -o "ProxyCommand $SSH_PROXY_CMD" -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null "$@" else ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$@" fi } ssh_cmd "$USER_ARG@$HOST" 'sh -s' <<'HOST_SCRIPT' set -eu APP_DIR=${CODEX_ROLLBACK_APP_DIR:-/opt/codex-app-server} BACKUP_DIR=${CODEX_ROLLBACK_BACKUP_DIR:-/opt/codex-app-server-backups} HEALTH_ATTEMPTS=${CODEX_ROLLBACK_HEALTH_ATTEMPTS:-12} HEALTH_INTERVAL=${CODEX_ROLLBACK_HEALTH_INTERVAL:-5} # 与 host_deploy.sh 共享的 unit 清单约定(见本文件头注释): # server-1 必含,server-2 以 unit 存在为准;改动清单时两脚本同步改。 SERVER_UNITS="codex-cloud-server" HEALTH_PORTS="8765" if systemctl cat codex-cloud-server-2 >/dev/null 2>&1; then SERVER_UNITS="$SERVER_UNITS codex-cloud-server-2" HEALTH_PORTS="$HEALTH_PORTS 8766" fi if systemctl is-active -q codex-autoscaler 2>/dev/null; then WORKER_UNIT=codex-autoscaler else WORKER_UNIT=codex-cloud-worker fi check_health() { for port in $HEALTH_PORTS; do OUT=$(curl -s --max-time 5 "http://127.0.0.1:$port/health" || true) [ -n "$OUT" ] || return 1 if ! printf '%s' "$OUT" | python3 -c ' import json, sys try: d = json.load(sys.stdin) except Exception: sys.exit(1) sys.exit(0 if (d.get("ok") is True and d.get("backend") == "postgres" and d.get("db_ok") is True) else 1) '; then return 1 fi done return 0 } wait_health() { i=0 while [ "$i" -lt "$HEALTH_ATTEMPTS" ]; do if check_health; then return 0; fi sleep "$HEALTH_INTERVAL" i=$((i + 1)) done return 1 } BACKUP_FILE=$(ls -t "$BACKUP_DIR"/codex-app-server-*.tar.gz 2>/dev/null \ | head -n 1 || true) if [ -z "$BACKUP_FILE" ]; then echo "主机上没有代码备份包,无法回滚:$BACKUP_DIR" >&2 exit 1 fi echo "将恢复备份包:$BACKUP_FILE" echo "停服务:worker_unit=$WORKER_UNIT server_units=$SERVER_UNITS" systemctl stop "$WORKER_UNIT" $SERVER_UNITS || true find "$APP_DIR" -mindepth 1 -maxdepth 1 ! -name work -exec rm -rf {} + tar -xzf "$BACKUP_FILE" -C "$(dirname "$APP_DIR")" if id codexsvc >/dev/null 2>&1; then chown -R codexsvc:codexsvc "$APP_DIR" fi systemctl start $SERVER_UNITS "$WORKER_UNIT" if wait_health; then echo "回滚成功,全部端口 health 通过($HEALTH_PORTS),当前代码为备份包:$BACKUP_FILE" exit 0 fi echo "回滚后 health 仍失败(需全部端口通过),需人工介入" >&2 exit 1 HOST_SCRIPT