Files
backend/deploy/rollback.sh
T
34047007@qq.com c55d096769
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run
feat: deploy/deploy.sh 自动化部署体系 — 双tag镜像/快照/迁移门控/回滚,退役根deploy.sh
- compose 四服务补 image: ${BACKEND_TAG}/${FRONTEND_TAG} 插值(换 tag 回滚前提)
- deploy/deploy.sh:工作区校验→PREV_SHA→pull→predeploy 快照(-Fc)→destructive 判定→
  build+双tag→.pending-deploy→迁移退出码门控→up -d --no-deps→健康轮询→versions.log→
  镜像治理;支持 --dry-run / flock / 基础设施健康门 / 多head预检
- deploy/rollback.sh:双入口(手动读 versions.log 跳 ROLLBACK 行 / --auto 读
  .pending-deploy);destructive 走 pg_restore --clean;双 tag 原子切换
- deploy/hotfix.sh:紧急单文件 cp + 强制收口提醒
- .gitignore 补 .pending-deploy/versions.log(运行时状态)
- 退役根 deploy.sh + deploy.env.example(SSH 旧模型,被 deploy/deploy.sh 取代)
2026-08-10 16:30:43 +08:00

132 lines
6.6 KiB
Bash
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
# ═══════════════════════════════════════════════════════════════════
# 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_SHAdestructive=$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"