132 lines
6.6 KiB
Bash
132 lines
6.6 KiB
Bash
#!/usr/bin/env bash
|
||||
|
|
# ═══════════════════════════════════════════════════════════════════
|
|||
|
|
# rollback.sh — 回滚到上一个成功部署版本(backend+frontend 双 tag 原子切换)
|
|||
|
|
#
|
|||
|
|
# 用法:
|
|||
|
|
# bash deploy/rollback.sh # 手动回滚:versions.log 最近一次成功部署
|
|||
|
|
# bash deploy/rollback.sh <backend_sha> [<frontend_sha>] # 回滚到指定版本
|
|||
|
|
# bash deploy/rollback.sh --auto # 部署失败自动回滚(读本次 .pending-deploy)
|
|||
|
|
#
|
|||
|
|
# 双入口读不同来源(N1 核心洞:失败自动回滚绝不读 versions.log 判 destructive):
|
|||
|
|
# · 手动回滚: 旧 sha + destructive 从 versions.log 读(跳过 ROLLBACK 行,E5)
|
|||
|
|
# · --auto: 读本次 .pending-deploy(<sha> <sha> <destructive> <快照路径>)
|
|||
|
|
#
|
|||
|
|
# 数据回退主路径 = pg_restore predeploy 快照(数据 + schema 一起回,先清后恢复);
|
|||
|
|
# alembic downgrade 只反向 schema 不恢复数据,仅用于可逆非破坏性调整。
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||
|
|
REPO_ROOT="$(cd "$DEPLOY_DIR/.." && pwd)"
|
|||
|
|
cd "$REPO_ROOT"
|
|||
|
|
|
|||
|
|
COMPOSE_FILE="docker-compose.prod.yml"
|
|||
|
|
COMPOSE=(docker compose -f "$COMPOSE_FILE")
|
|||
|
|
|
|||
|
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
|||
|
|
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
|||
|
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
|||
|
|
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
|||
|
|
|
|||
|
|
# ── 参数 ──
|
|||
|
|
MODE="manual"
|
|||
|
|
BACKEND_SHA_ARG=""; FRONTEND_SHA_ARG=""
|
|||
|
|
case "${1:-}" in
|
|||
|
|
--auto) MODE="auto" ;;
|
|||
|
|
"") ;;
|
|||
|
|
-*) error "未知参数: $1(用法: rollback.sh [--auto | <backend_sha> [<frontend_sha>]])" ;;
|
|||
|
|
*) BACKEND_SHA_ARG="$1"; FRONTEND_SHA_ARG="${2:-$1}" ;;
|
|||
|
|
esac
|
|||
|
|
|
|||
|
|
# ── 前置:source .env + flock ──
|
|||
|
|
if [ ! -f "$REPO_ROOT/.env" ]; then
|
|||
|
|
error "未找到 ${REPO_ROOT}/.env。"
|
|||
|
|
fi
|
|||
|
|
set -a; source "$REPO_ROOT/.env"; set +a
|
|||
|
|
|
|||
|
|
exec 9>/tmp/scilit-deploy.lock
|
|||
|
|
flock -n 9 || error "已有部署/回滚在进行中(/tmp/scilit-deploy.lock 被锁)。"
|
|||
|
|
|
|||
|
|
# ── 解析回滚目标 ──
|
|||
|
|
# 从 versions.log 取最近一条非 ROLLBACK 行(E5:跳过回滚事件,防回滚循环)
|
|||
|
|
last_deploy_line() {
|
|||
|
|
awk '/^DEPLOY /{line=$0} END{print line}' "$DEPLOY_DIR/versions.log" 2>/dev/null || true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
OLD_BACKEND_SHA=""; OLD_FRONTEND_SHA=""; DESTRUCTIVE="否"; SNAPSHOT=""
|
|||
|
|
|
|||
|
|
if [ "$MODE" = "auto" ]; then
|
|||
|
|
[ -f "$DEPLOY_DIR/.pending-deploy" ] || error "--auto 需存在 .pending-deploy(本次部署失败残留),未找到。"
|
|||
|
|
read -r NEW_BACKEND_SHA NEW_FRONTEND_SHA DESTRUCTIVE SNAPSHOT < "$DEPLOY_DIR/.pending-deploy"
|
|||
|
|
# 上一个成功版本 = versions.log 最近一条 DEPLOY 行
|
|||
|
|
LINE="$(last_deploy_line)"
|
|||
|
|
[ -n "$LINE" ] || error "versions.log 无历史成功部署记录,无法确定回滚目标。"
|
|||
|
|
OLD_BACKEND_SHA="$(echo "$LINE" | awk '{print $2}')"
|
|||
|
|
OLD_FRONTEND_SHA="$(echo "$LINE" | awk '{print $3}')"
|
|||
|
|
info "自动回滚: 本次($NEW_BACKEND_SHA, destructive=$DESTRUCTIVE) → 上一版($OLD_BACKEND_SHA)"
|
|||
|
|
else
|
|||
|
|
if [ -n "$BACKEND_SHA_ARG" ]; then
|
|||
|
|
OLD_BACKEND_SHA="$BACKEND_SHA_ARG"; OLD_FRONTEND_SHA="$FRONTEND_SHA_ARG"
|
|||
|
|
# 指定版本:destructive 从 versions.log 匹配该 sha 的最近一行;缺省按否并提示
|
|||
|
|
LINE="$(awk -v s="$OLD_BACKEND_SHA" '$2==s || $3==s{line=$0} END{print line}' "$DEPLOY_DIR/versions.log" 2>/dev/null || true)"
|
|||
|
|
if [ -n "$LINE" ]; then
|
|||
|
|
DESTRUCTIVE="$(echo "$LINE" | awk '{print $4}')"
|
|||
|
|
else
|
|||
|
|
warn "versions.log 无 $OLD_BACKEND_SHA 记录,destructive 按「否」处理(数据不回退)。如该版本含破坏性迁移,请人工确认。"
|
|||
|
|
fi
|
|||
|
|
else
|
|||
|
|
LINE="$(last_deploy_line)"
|
|||
|
|
[ -n "$LINE" ] || error "versions.log 无历史成功部署记录。"
|
|||
|
|
OLD_BACKEND_SHA="$(echo "$LINE" | awk '{print $2}')"
|
|||
|
|
OLD_FRONTEND_SHA="$(echo "$LINE" | awk '{print $3}')"
|
|||
|
|
DESTRUCTIVE="$(echo "$LINE" | awk '{print $4}')"
|
|||
|
|
info "手动回滚: 最近成功部署 $OLD_BACKEND_SHA(destructive=$DESTRUCTIVE)"
|
|||
|
|
fi
|
|||
|
|
# 手动 destructive 数据回退:优先本次 predeploy 快照(无则最近一份,尽力而为)
|
|||
|
|
if [ "$DESTRUCTIVE" = "是" ]; then
|
|||
|
|
SNAPSHOT="$(ls -t /data/backups/predeploy_*.dump 2>/dev/null | head -1 || true)"
|
|||
|
|
[ -n "$SNAPSHOT" ] || error "destructive=是 但找不到 predeploy 快照(/data/backups/predeploy_*.dump),数据无法回退。"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 校验旧 tag 镜像存在 ──
|
|||
|
|
image_exists() { docker image inspect "$1" >/dev/null 2>&1; }
|
|||
|
|
if ! image_exists "scilit/backend:$OLD_BACKEND_SHA"; then
|
|||
|
|
error "镜像 scilit/backend:$OLD_BACKEND_SHA 不存在(可能已被镜像治理清理)。请用 docker tag 恢复或选择其他版本。"
|
|||
|
|
fi
|
|||
|
|
if ! image_exists "scilit/frontend:$OLD_FRONTEND_SHA"; then
|
|||
|
|
error "镜像 scilit/frontend:$OLD_FRONTEND_SHA 不存在。请用 docker tag 恢复或选择其他版本。"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 数据回退(destructive=是):pg_restore 快照,先清后恢复(L)──
|
|||
|
|
if [ "$DESTRUCTIVE" = "是" ]; then
|
|||
|
|
if [ "$MODE" = "manual" ]; then
|
|||
|
|
echo -e "${YELLOW}⚠️ 该版本含破坏性迁移,将用快照 ${SNAPSHOT} 覆盖当前数据库(pg_restore --clean)。
|
|||
|
|
当前库中的所有数据将回退到快照时间点。确认继续?(y/N)${NC}"
|
|||
|
|
read -r yn
|
|||
|
|
[ "$yn" = "y" ] || [ "$yn" = "Y" ] || error "已取消。"
|
|||
|
|
fi
|
|||
|
|
info "停止 backend/worker(避免恢复期间写入冲突)"
|
|||
|
|
"${COMPOSE[@]}" stop backend worker
|
|||
|
|
info "数据回退: pg_restore --clean --if-exists --no-owner ← $SNAPSHOT"
|
|||
|
|
PGPASSWORD="${PG_PASSWORD}" "${COMPOSE[@]}" exec -T postgres \
|
|||
|
|
pg_restore --clean --if-exists --no-owner -U scilit -d scilit < "$SNAPSHOT"
|
|||
|
|
info "数据回退完成"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# ── 双 tag 原子切换(E2:一次 up,绝不分两次)──
|
|||
|
|
info "切换: backend+frontend → $OLD_BACKEND_SHA / $OLD_FRONTEND_SHA"
|
|||
|
|
BACKEND_TAG="$OLD_BACKEND_SHA" FRONTEND_TAG="$OLD_FRONTEND_SHA" \
|
|||
|
|
"${COMPOSE[@]}" up -d --no-deps backend worker frontend
|
|||
|
|
|
|||
|
|
# ── 记录回滚事件 + 清理标记 ──
|
|||
|
|
echo "ROLLBACK $OLD_BACKEND_SHA $OLD_FRONTEND_SHA $DESTRUCTIVE $(date '+%Y-%m-%d %H:%M:%S %z')" \
|
|||
|
|
>> "$DEPLOY_DIR/versions.log"
|
|||
|
|
tail -n 200 "$DEPLOY_DIR/versions.log" > "$DEPLOY_DIR/versions.log.tmp"
|
|||
|
|
mv "$DEPLOY_DIR/versions.log.tmp" "$DEPLOY_DIR/versions.log"
|
|||
|
|
if [ "$MODE" = "auto" ]; then
|
|||
|
|
rm -f "$DEPLOY_DIR/.pending-deploy"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
info "回滚完成 ✅ 当前版本: $OLD_BACKEND_SHA"
|