Files
easyai/update.sh
T
wangbo 195e2e5a98
Test deployment public URL (Linux) / test-linux (push) Waiting to run
Test start.ps1 (Windows) / test-windows (push) Waiting to run
feat(deploy): 增加受控更新与环境配置迁移
2026-09-27 11:12:55 +08:00

283 lines
10 KiB
Bash
Executable File
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.
#!/bin/bash
set -e # 发生错误时终止脚本执行
# 仅支持 -h/--help 快速查看
if [[ "${1:-}" =~ ^(-h|--help)$ ]]; then
echo "用法: $0"
echo ""
echo "默认拉取仓库并更新镜像;也可通过环境变量跳过交互选择。"
echo ""
echo "环境变量:"
echo " UPDATE_MODE=full|image"
echo " full 更新并拉取仓库(git pull)+ 更新镜像并重启"
echo " image 仅更新镜像并重启(跳过 git pull)"
echo " UPDATE_SKIP_REPO_UPDATE=true|false"
echo " true 等同 UPDATE_MODE=image"
echo " false 等同 UPDATE_MODE=full"
echo " UPDATE_HEALTH_TIMEOUT_SECONDS=秒数"
echo " 等待核心服务恢复可用的最长时间,默认 300 秒"
echo ""
echo "仓库更新后会自动执行 scripts/update-env.d 中的幂等环境配置迁移。"
exit 0
fi
# 进入脚本所在目录(项目根目录)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$SCRIPT_DIR"
# 命令行内选择更新方式(默认:更新并拉取仓库)
SKIP_REPO_UPDATE=false
normalize_bool() {
local value
value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
case "$value" in
1|true|yes|y|on) echo "true" ;;
0|false|no|n|off|"") echo "false" ;;
*)
echo "❌ 布尔变量值不合法: $1(支持 true/false/1/0/yes/no/y/n/on/off)" >&2
return 1
;;
esac
}
resolve_update_mode() {
local mode
if [ -n "${UPDATE_SKIP_REPO_UPDATE:-}" ]; then
mode="$(normalize_bool "$UPDATE_SKIP_REPO_UPDATE")" || exit 1
SKIP_REPO_UPDATE="$mode"
echo "使用环境变量: UPDATE_SKIP_REPO_UPDATE=$SKIP_REPO_UPDATE"
return
fi
if [ -n "${UPDATE_MODE:-}" ]; then
mode="$(printf '%s' "$UPDATE_MODE" | tr '[:upper:]' '[:lower:]')"
case "$mode" in
full|repo|pull|git|1)
SKIP_REPO_UPDATE=false
;;
image|images|docker|skip-git|skip_repo|skip-repo|2)
SKIP_REPO_UPDATE=true
;;
*)
echo "❌ UPDATE_MODE 只能是 full 或 image,当前为: $UPDATE_MODE"
exit 1
;;
esac
echo "使用环境变量: UPDATE_MODE=$UPDATE_MODE"
return
fi
if [ "${UPDATE_NON_INTERACTIVE:-0}" = "1" ] || [ "${CI:-}" = "true" ] || [ ! -t 0 ]; then
echo "使用默认更新方式: 更新并拉取仓库(git pull)+ 更新镜像并重启"
SKIP_REPO_UPDATE=false
return
fi
echo ""
echo "请选择更新方式:"
echo " [1] 更新并拉取仓库(git pull)+ 更新镜像并重启(默认)"
echo " [2] 仅更新镜像并重启(跳过 git pull)"
read -r -p "请选择 [1/2,回车默认 1]: " choice
choice="${choice:-1}"
case "$choice" in
2) SKIP_REPO_UPDATE=true ;;
1) ;;
*) echo "❌ 无效选择,将使用默认:更新并拉取仓库"; SKIP_REPO_UPDATE=false ;;
esac
}
resolve_update_mode
# 拉取整个仓库更新
if [ "$SKIP_REPO_UPDATE" = false ]; then
echo ""
echo "==========================="
echo "📥 拉取仓库最新代码"
echo "==========================="
if [ ! -d .git ]; then
echo "❌ 当前目录不是 Git 仓库,无法执行 git pull"
echo " 请使用 git clone 克隆项目后即可使用 update.sh 更新"
echo " 克隆命令: git clone https://git.51easyai.com/wangbo/easyai && cd easyai"
exit 1
fi
before_revision="$(git rev-parse HEAD)"
echo "📥 正在执行 git pull --ff-only..."
if git pull --ff-only; then
echo "✅ 仓库已更新到最新版本"
else
echo "❌ git pull 失败,请检查网络或远程仓库配置"
exit 1
fi
# 确保环境配置文件存在(从 .sample 生成,不覆盖已有文件)
echo ""
echo "📝 检查环境配置文件..."
[ ! -f .env ] && [ -f .env.sample ] && cp .env.sample .env && echo " ✓ .env"
[ ! -f .env.tools ] && [ -f .env.tools.sample ] && cp .env.tools.sample .env.tools && echo " ✓ .env.tools"
[ ! -f .env.ASG ] && [ -f .env.ASG.sample ] && cp .env.ASG.sample .env.ASG && echo " ✓ .env.ASG"
[ ! -f .env.AMS ] && [ -f .env.AMS.sample ] && cp .env.AMS.sample .env.AMS && echo " ✓ .env.AMS"
after_revision="$(git rev-parse HEAD)"
if [ "$before_revision" != "$after_revision" ] && [ "${EASYAI_UPDATE_REEXECUTED:-0}" != "1" ]; then
echo "🔄 检测到部署仓库已更新,切换到新版更新脚本继续执行..."
exec env EASYAI_UPDATE_REEXECUTED=1 UPDATE_SKIP_REPO_UPDATE=true "$SCRIPT_DIR/update.sh"
fi
echo ""
else
echo "⏭️ 跳过仓库更新,仅更新镜像并重启"
echo ""
fi
# 升级已有部署时,将空值、历史默认 JWT 密钥和缺失的 WS ticket 密钥
# 一次性替换为持久化随机值,并确保网关声明 ws_ticket;已有强密钥保持不变。
if [ ! -f .env ]; then
echo "❌ 未找到 .env,请先执行 start.sh 完成初始化"
exit 1
fi
# 每次更新都执行版本化、幂等的环境配置迁移。迁移只补缺失/空值或必要的列表项,
# 不覆盖用户已经配置的非空值。后续新增必需配置时只需增加编号迁移文件。
bash ./scripts/apply-update-env-migrations.sh .env "$SCRIPT_DIR"
# 老部署缺少绑定配置时,根据现有公开 API 地址迁移:域名/本机保持回环,IP 直连保持可用。
# shellcheck source=scripts/init-server-http-bind-ip.sh
. ./scripts/init-server-http-bind-ip.sh
init_server_http_bind_ip .env
# shellcheck source=scripts/init-security-env.sh
. ./scripts/init-security-env.sh
init_security_env .env upgrade
# 在更新镜像和重建服务前迁移并校验后端公开 API 地址。
# shellcheck source=scripts/init-public-api-base-url.sh
. ./scripts/init-public-api-base-url.sh
init_public_api_base_url .env upgrade "${DEPLOY_PUBLIC_API_BASE_URL:-}"
# 旧部署可能缺少浏览器 Origin;在重建主服务前从已校验的公开 API 地址补齐。
# shellcheck source=scripts/init-security-origin.sh
. ./scripts/init-security-origin.sh
init_security_origin .env
echo "==========================="
echo "🚀 开始自动安装 Docker 和 Docker Compose"
echo "==========================="
# 检查 Docker 是否已安装
if command -v docker &> /dev/null; then
echo "✅ Docker 已安装,跳过安装步骤"
else
# 原有的 Docker 安装步骤
echo "📦 安装依赖..."
sudo apt update -y
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common gnupg lsb-release
echo "🔑 添加 Docker GPG 密钥..."
curl -fsSL https://mirrors.nwafu.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
echo "🌍 添加 Docker 源..."
sudo add-apt-repository -y "deb [arch=amd64] https://mirrors.nwafu.edu.cn生成注释ker-ce/linux/ubuntu/ $(lsb_release -cs) stable"
sudo apt-get update -y
echo "🐳 安装 Docker..."
sudo apt-get install -y docker.io
echo "✅ 启动并设置 Docker 开机自启..."
sudo systemctl enable docker
sudo systemctl start docker
fi
# 检测并使用正确的 Docker Compose 命令
# 优先使用新版本的 docker compose(作为 docker 的子命令)
if docker compose version &> /dev/null; then
DOCKER_COMPOSE_CMD="docker compose"
echo "✅ 检测到 Docker Compose (新版本: docker compose)"
elif command -v docker-compose &> /dev/null; then
DOCKER_COMPOSE_CMD="docker-compose"
echo "✅ 检测到 Docker Compose (旧版本: docker-compose)"
else
echo "⚙️ 安装 Docker Compose..."
COMPOSE_BIN_URL="https://static.51easyai.com/docker-compose-linux-x86_64"
TMP_COMPOSE_BIN="/tmp/docker-compose-linux-x86_64"
curl -fL "$COMPOSE_BIN_URL" -o "$TMP_COMPOSE_BIN"
sudo install -m 0755 "$TMP_COMPOSE_BIN" /usr/bin/docker-compose
rm -f "$TMP_COMPOSE_BIN"
DOCKER_COMPOSE_CMD="docker-compose"
echo "✅ Docker Compose 安装完成"
fi
echo "📌 Docker 运行状态:"
sudo systemctl status docker --no-pager || true
echo "📌 Docker Compose 版本:"
$DOCKER_COMPOSE_CMD version
echo "🎉 Docker 和 Docker Compose 已就绪!"
wait_for_core_services() {
local timeout_seconds="${UPDATE_HEALTH_TIMEOUT_SECONDS:-300}"
local deadline=$((SECONDS + timeout_seconds))
local services=(easyai-server ws-gateway easyai-web)
local service container_id state running health all_ready
if ! [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]]; then
echo "❌ UPDATE_HEALTH_TIMEOUT_SECONDS 必须是正整数,当前为: $timeout_seconds"
return 1
fi
echo "⏳ 正在等待核心服务恢复可用(最长 ${timeout_seconds} 秒)..."
while (( SECONDS < deadline )); do
all_ready=true
for service in "${services[@]}"; do
container_id="$(sudo $DOCKER_COMPOSE_CMD ps -q "$service" 2>/dev/null || true)"
if [ -z "$container_id" ]; then
all_ready=false
continue
fi
state="$(sudo docker inspect --format '{{.State.Running}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id" 2>/dev/null || true)"
running="${state%%|*}"
health="${state#*|}"
if [ "$running" != "true" ] || { [ "$health" != "none" ] && [ "$health" != "healthy" ]; }; then
all_ready=false
fi
done
if [ "$all_ready" = true ]; then
echo "✅ 核心服务已恢复可用"
return 0
fi
sleep 5
done
echo "❌ 更新后核心服务未在 ${timeout_seconds} 秒内恢复可用"
echo " 当前状态:"
sudo $DOCKER_COMPOSE_CMD ps easyai-server ws-gateway easyai-web || true
echo " 请运行 '$DOCKER_COMPOSE_CMD logs --tail=100 easyai-server ws-gateway easyai-web' 查看原因"
return 1
}
# 旧 Redis 若仍把 AOF/RDB 留在容器写层,直接 up 会丢失数据。
sudo ./scripts/check-redis-persistence.sh
echo "🚀 重新启动EasyAI"
echo "📦 当前磁盘空间:"
df -h "$SCRIPT_DIR" | tail -n 1
if ! sudo $DOCKER_COMPOSE_CMD pull; then
echo "❌ 镜像拉取失败,EasyAI 未被标记为更新成功"
echo " 请检查网络、镜像仓库权限和服务器磁盘空间后重试"
exit 1
fi
if ! sudo $DOCKER_COMPOSE_CMD up -d; then
echo "❌ 容器启动失败,EasyAI 未被标记为更新成功"
exit 1
fi
if ! wait_for_core_services; then
exit 1
fi
echo "🎉 EasyAI 应用更新成功"