#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" HOST="${STUDIO_HOST:-58.38.22.103}" REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}" APP_DIR="${REMOTE_ROOT}/Memind" BACKUP_DIR="${REMOTE_ROOT}/backups/memind" ARCHIVE_DIR="${REMOTE_ROOT}/archives" HEALTH_URL="${STUDIO_HEALTH_URL:-http://127.0.0.1:8081/api/status}" PORTAL_LABEL="cn.tkmind.memind-portal" LAUNCHD_GUI="gui/$(id -u)" AUTO_YES=0 RELEASE_ID="" usage() { cat <<'EOF' 用法: bash scripts/rollback-portal-runtime-prod.sh [--yes] bash scripts/rollback-portal-runtime-prod.sh --list [--limit N] 说明: 将 103 Portal 回退到指定 release 发布前的 live 目录。 RELEASE_ID 形如 20260701-075736-16828a5(与 release-portal-runtime-prod.sh 输出一致)。 回退优先级: 1. /Users/john/Project/archives/Memind-source-before- 2. /Users/john/Project/backups/memind/memind-full--before.tar.gz 当前失败的 live 目录会移入 archive,便于事后排查。 示例: bash scripts/rollback-portal-runtime-prod.sh --list bash scripts/rollback-portal-runtime-prod.sh 20260701-075736-16828a5 --yes EOF } while [[ $# -gt 0 ]]; do case "$1" in --yes|-y) AUTO_YES=1 ;; --list) ssh -o BatchMode=yes "${HOST}" "bash -s" <<'REMOTE' set -euo pipefail echo "=== 当前 live manifest ===" if [[ -f /Users/john/Project/Memind/.release-manifest.txt ]]; then cat /Users/john/Project/Memind/.release-manifest.txt else echo "(无 manifest)" fi echo echo "=== 可回退 archive(最近 10 条)===" ls -1dt /Users/john/Project/archives/Memind-source-before-* 2>/dev/null | head -10 || true echo echo "=== 可回退 full backup(最近 10 条)===" ls -1dt /Users/john/Project/backups/memind/memind-full-*-before.tar.gz 2>/dev/null | head -10 || true REMOTE exit 0 ;; -h|--help) usage exit 0 ;; *) if [[ -z "${RELEASE_ID}" ]]; then RELEASE_ID="$1" else echo "未知参数: $1" >&2 usage >&2 exit 1 fi ;; esac shift done if [[ -z "${RELEASE_ID}" ]]; then usage >&2 exit 1 fi say() { printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*" } say "103 回退预检查" ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo rollback-ssh-ok" >/dev/null ARCHIVED_SOURCE="${ARCHIVE_DIR}/Memind-source-before-${RELEASE_ID}" FULL_BACKUP="${BACKUP_DIR}/memind-full-${RELEASE_ID}-before.tar.gz" ssh -o BatchMode=yes "${HOST}" "test -d '${ARCHIVED_SOURCE}' || test -f '${FULL_BACKUP}'" || { echo "找不到 release ${RELEASE_ID} 的回退源:" >&2 echo " ${ARCHIVED_SOURCE}" >&2 echo " ${FULL_BACKUP}" >&2 echo "可先运行: bash scripts/rollback-portal-runtime-prod.sh --list" >&2 exit 1 } if [[ "${AUTO_YES}" -ne 1 ]]; then say "回退确认" echo "目标主机: ${HOST}" echo "回退 release: ${RELEASE_ID}" echo "当前 live: ${APP_DIR}" ssh -o BatchMode=yes "${HOST}" " if [[ -d '${ARCHIVED_SOURCE}' ]]; then echo 'source=archived_source'; fi if [[ -f '${FULL_BACKUP}' ]]; then echo 'source=full_backup'; fi if [[ -f '${APP_DIR}/.release-manifest.txt' ]]; then echo '--- current manifest ---'; cat '${APP_DIR}/.release-manifest.txt'; fi " read -r -p "确认回退到 ${RELEASE_ID} 发布前版本? [y/N] " confirm /dev/null 2>&1 || true lsof -tiTCP:8081 -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true sleep 2 lsof -tiTCP:8081 -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true say "准备回退目录" rm -rf "${RESTORE_DIR}" mkdir -p "${RESTORE_DIR}" if [[ -d "${ARCHIVED_SOURCE}" ]]; then say "从 archived_source 恢复: ${ARCHIVED_SOURCE}" cp -a "${ARCHIVED_SOURCE}/." "${RESTORE_DIR}/" elif [[ -f "${FULL_BACKUP}" ]]; then say "从 full backup 恢复: ${FULL_BACKUP}" tar -xzf "${FULL_BACKUP}" -C "${RESTORE_DIR}" else echo "rollback source missing" >&2 exit 1 fi say "归档当前失败 live 目录" if [[ -d "${APP_DIR}" ]]; then rm -rf "${FAILED_LIVE}" mv "${APP_DIR}" "${FAILED_LIVE}" fi mv "${RESTORE_DIR}" "${APP_DIR}" cat > "${APP_DIR}/.rollback-manifest.txt" < "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" < Label ${PORTAL_LABEL} ProgramArguments ${APP_DIR}/scripts/run-memind-portal-prod.sh WorkingDirectory ${APP_DIR} EnvironmentVariables PATH /opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin RunAtLoad KeepAlive ThrottleInterval 10 StandardOutPath ${HOME}/Library/Logs/memind-portal.log StandardErrorPath ${HOME}/Library/Logs/memind-portal.log EOF launchctl bootstrap "${LAUNCHD_GUI}" "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" >/dev/null 2>&1 || true launchctl enable "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || true launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}" >/dev/null 2>&1 || \ nohup "${APP_DIR}/scripts/run-memind-portal-prod.sh" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 & else nohup /opt/homebrew/opt/node@24/bin/node "${APP_DIR}/server.mjs" >> "${HOME}/Library/Logs/memind-portal.log" 2>&1 & fi say "健康检查" for _ in $(seq 1 60); do portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)" if [[ "${portal_code}" == "200" ]]; then break fi sleep 2 done portal_code="$(curl -s -o /dev/null -w '%{http_code}' "${HEALTH_URL}" || true)" if [[ "${portal_code}" != "200" ]]; then echo "rollback health check failed: portal=${portal_code}" >&2 exit 1 fi say "回退完成" printf 'live_dir=%s\n' "${APP_DIR}" printf 'failed_live=%s\n' "${FAILED_LIVE}" printf 'rolled_back_release=%s\n' "${RELEASE_ID}" REMOTE_SCRIPT say "103 回退后状态" ssh -o BatchMode=yes "${HOST}" "curl -s '${HEALTH_URL}' && echo && cat '${APP_DIR}/.rollback-manifest.txt' 2>/dev/null || true"