#!/usr/bin/env bash # ════════════════════════════════════════════════════════════════ # 国内域名机 · 真正的版本回滚 · rollback.sh # Sovereign: TCS-0002∞ · ICE-GL∞ · 国作登字-2026-A-00037559 # 守护: 铸渊 · ICE-GL-ZY001 # ════════════════════════════════════════════════════════════════ # # 与 bootstrap.sh 配套. bootstrap 在每次 update 之前会把当前 # nginx 配置 + .env.tune + portal systemd unit 打包成快照存进 # $DATA_ROOT/snapshots/-pre-/. 这里负责把它恢复回去. # # 用法: # bash rollback.sh # 回到上一个快照 (默认) # bash rollback.sh --list # 列出所有可用快照 # bash rollback.sh --to # 回到指定快照 # # 设计理念 (cc-004 强制自主): # - 数据 (portal 代码 / forgejo repo) **不动**, 只回滚配置层 # (nginx sites-available / .env.tune / systemd unit) # - 自动 reload nginx + 健康检查 # - 写出回执到 _logs/rollback-*.json + 追加 deploy-report.md # ════════════════════════════════════════════════════════════════ set -euo pipefail DATA_ROOT="${DATA_ROOT:-/data/guanghulab}" DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/guanghulab}" DOMAIN="${DOMAIN:-guanghulab.com}" SNAP_ROOT="$DATA_ROOT/snapshots" mkdir -p "$SNAP_ROOT" "$DEPLOY_ROOT/_logs" ACTION="rollback" TARGET_TS="" while [ "$#" -gt 0 ]; do case "$1" in --list) ACTION="list"; shift ;; --to) TARGET_TS="$2"; shift 2 ;; -h|--help) sed -n '1,30p' "$0"; exit 0 ;; *) echo "未知参数: $1" >&2; exit 2 ;; esac done # ─── 列出快照 ───────────────────────────────────────────────── list_snapshots() { if [ ! -d "$SNAP_ROOT" ] || [ -z "$(ls -A "$SNAP_ROOT" 2>/dev/null)" ]; then echo "(还没有快照. bootstrap.sh 在每次跑之前会自动建.)" return fi printf '%-32s %-12s %s\n' "快照时间戳" "大小" "备注" printf '%-32s %-12s %s\n' "--------------------------------" "------------" "------------------" for d in $(ls -1tr "$SNAP_ROOT"); do SZ="$(du -sh "$SNAP_ROOT/$d" 2>/dev/null | awk '{print $1}')" NOTE="$(cat "$SNAP_ROOT/$d/NOTE" 2>/dev/null || echo '-')" printf '%-32s %-12s %s\n' "$d" "$SZ" "$NOTE" done } if [ "$ACTION" = "list" ]; then list_snapshots exit 0 fi # ─── 选目标快照 ─────────────────────────────────────────────── if [ -z "$TARGET_TS" ]; then TARGET_TS="$(ls -1 "$SNAP_ROOT" 2>/dev/null | sort | tail -1 || true)" fi if [ -z "$TARGET_TS" ] || [ ! -d "$SNAP_ROOT/$TARGET_TS" ]; then echo "❌ 没有可用快照. 请先用 bootstrap.sh 跑一次 (会自动建快照)" >&2 echo " 或: bash rollback.sh --list" >&2 exit 1 fi SNAP_DIR="$SNAP_ROOT/$TARGET_TS" echo "═══════════════════════════════════════════════════════════" echo " [rollback · domain-cn] 准备回滚" echo "═══════════════════════════════════════════════════════════" echo " 目标快照: $TARGET_TS" echo " 快照路径: $SNAP_DIR" echo " 备注: $(cat "$SNAP_DIR/NOTE" 2>/dev/null || echo '-')" echo "─────────────────────────────────────────────────────────" # ─── 1. 当前态再存一份 (回滚也要可逆) ───────────────────────── NOW_TS="$(date +%Y%m%d-%H%M%S)" PRE_ROLLBACK="$SNAP_ROOT/$NOW_TS-pre-rollback" mkdir -p "$PRE_ROLLBACK/nginx" "$PRE_ROLLBACK/systemd" [ -d /etc/nginx/sites-available ] && cp -a /etc/nginx/sites-available "$PRE_ROLLBACK/nginx/" || true [ -d /etc/nginx/sites-enabled ] && cp -a /etc/nginx/sites-enabled "$PRE_ROLLBACK/nginx/" || true [ -f "$DATA_ROOT/.env.tune" ] && cp -a "$DATA_ROOT/.env.tune" "$PRE_ROLLBACK/" || true [ -f /etc/systemd/system/guanghulab-portal.service ] && \ cp -a /etc/systemd/system/guanghulab-portal.service "$PRE_ROLLBACK/systemd/" || true echo "preserved before rollback to $TARGET_TS" > "$PRE_ROLLBACK/NOTE" echo " [1/4] 当前态已保存到 $PRE_ROLLBACK" # ─── 2. 恢复 nginx 配置 (有快照才动) ───────────────────────── if [ -d "$SNAP_DIR/nginx/sites-available" ] && [ -n "$(ls -A "$SNAP_DIR/nginx/sites-available" 2>/dev/null)" ]; then echo " [2/4] 恢复 nginx sites-available ..." # 先清掉新装的 (避免残留), 再 cp 回快照里的 if [ -d /etc/nginx/sites-available ]; then find /etc/nginx/sites-available -type f -delete fi cp -a "$SNAP_DIR/nginx/sites-available/." /etc/nginx/sites-available/ || true else echo " [2/4] 快照里没有 nginx/sites-available (或为空), 跳过 — 不做任何破坏" fi if [ -d "$SNAP_DIR/nginx/sites-enabled" ] && [ -n "$(ls -A "$SNAP_DIR/nginx/sites-enabled" 2>/dev/null)" ]; then # sites-enabled 通常是软链, 重建即可 if [ -d /etc/nginx/sites-enabled ]; then find /etc/nginx/sites-enabled -mindepth 1 -delete fi cp -a "$SNAP_DIR/nginx/sites-enabled/." /etc/nginx/sites-enabled/ || true fi # ─── 3. 恢复 .env.tune + systemd unit ───────────────────────── echo " [3/4] 恢复配置 ..." if [ -f "$SNAP_DIR/.env.tune" ]; then mkdir -p "$DATA_ROOT" cp -a "$SNAP_DIR/.env.tune" "$DATA_ROOT/.env.tune" echo " ↳ .env.tune" elif [ -f "$DATA_ROOT/.env.tune" ]; then # 快照里没有 .env.tune (旧机器无此文件), 删除当前的 (回到"无"状态) rm -f "$DATA_ROOT/.env.tune" fi if [ -f "$SNAP_DIR/systemd/guanghulab-portal.service" ]; then cp -a "$SNAP_DIR/systemd/guanghulab-portal.service" /etc/systemd/system/guanghulab-portal.service systemctl daemon-reload echo " ↳ guanghulab-portal.service" elif [ -f /etc/systemd/system/guanghulab-portal.service ]; then rm -f /etc/systemd/system/guanghulab-portal.service systemctl daemon-reload fi # ─── 4. reload nginx + 健康检查 ────────────────────────────── echo " [4/4] reload nginx + 健康检查 ..." HEALTH_OK="false" if nginx -t >/dev/null 2>&1; then systemctl reload nginx >/dev/null 2>&1 || systemctl restart nginx >/dev/null 2>&1 || true # 80 端口 ping 一下 (HTTPS 需证书可能不在, 用 HTTP) for i in $(seq 1 15); do if curl -fs --max-time 3 "http://127.0.0.1/" >/dev/null 2>&1 \ || curl -fs --max-time 3 -k "https://127.0.0.1/" >/dev/null 2>&1 ; then HEALTH_OK="true" break fi sleep 1 done # nginx 至少能响应 (即使是 default 404), 也算 OK if [ "$HEALTH_OK" = "false" ]; then if systemctl is-active nginx >/dev/null 2>&1; then HEALTH_OK="degraded" fi fi else echo " ⚠️ nginx 配置语法错, 不 reload (旧的还在跑)" fi # ─── 回执 ───────────────────────────────────────────────────── RECEIPT="$DEPLOY_ROOT/_logs/rollback-$NOW_TS.json" cat > "$RECEIPT" <> "$DEPLOY_REPORT" 2>/dev/null || true echo "═══════════════════════════════════════════════════════════" case "$HEALTH_OK" in true) echo " [OK] 回滚到 $TARGET_TS 完成 · nginx 健康响应" ;; degraded) echo " [OK-DEGRADED] 已回滚 · nginx 在跑但 / 路由没配 (正常: portal 占位还没起)" ;; *) echo " [WARN] 已恢复配置, 但 nginx 健康检查未通过, 查 journalctl -u nginx" ;; esac echo " 回执: $RECEIPT" echo "═══════════════════════════════════════════════════════════" # degraded 也算成功 (域名机一开始没 portal 业务, 是预期的) if [ "$HEALTH_OK" = "false" ]; then exit 1 fi exit 0