feat: 部署流程分阶段显示 + 排除shared符号链接 + 服务器重建symlink

This commit is contained in:
aiyimickey 2026-07-13 17:47:38 +08:00
parent c1a5174fd1
commit 3a4600ce5b
1 changed files with 116 additions and 32 deletions

View File

@ -24,13 +24,38 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ============================================================ # ============================================================
# 颜色和日志 # 颜色和日志
# ============================================================ # ============================================================
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m' GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; MAGENTA='\033[0;35m'; BOLD='\033[1m'; NC='\033[0m'
log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; } log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; }
warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; } warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
info() { printf "${CYAN}[i]${NC} %s\n" "$1"; } info() { printf "${CYAN}[i]${NC} %s\n" "$1"; }
error() { printf "${RED}[✗]${NC} %s\n" "$1"; } error() { printf "${RED}[✗]${NC} %s\n" "$1"; }
timing(){ printf " ${CYAN}${NC} %s\n" "$1"; } timing(){ printf " ${CYAN}${NC} %s\n" "$1"; }
# 阶段显示函数
phase_num=0
phase_start_time=0
phase() {
local phase_name="$1"
phase_num=$((phase_num + 1))
phase_start_time=$(date +%s)
echo ""
printf "${MAGENTA}${BOLD}════════════════════════════════════════════════════════════${NC}\n"
printf "${MAGENTA}${BOLD} [阶段 ${phase_num}] ${phase_name}${NC}\n"
printf "${MAGENTA}${BOLD}════════════════════════════════════════════════════════════${NC}\n"
}
phase_done() {
local end_time=$(date +%s)
local duration=$((end_time - phase_start_time))
printf "${GREEN}${BOLD} ✓ 阶段 ${phase_num} 完成 (${duration}s)${NC}\n"
}
phase_fail() {
local end_time=$(date +%s)
local duration=$((end_time - phase_start_time))
printf "${RED}${BOLD} ✗ 阶段 ${phase_num} 失败 (${duration}s)${NC}\n"
}
# ============================================================ # ============================================================
# 检测 macOS 和 Linux sed 差异 # 检测 macOS 和 Linux sed 差异
# ============================================================ # ============================================================
@ -418,7 +443,7 @@ build_on_server() {
--exclude='node_modules' --exclude='.next' --exclude='out' \ --exclude='node_modules' --exclude='.next' --exclude='out' \
--exclude='data' --exclude='./reports/*.docx' --exclude='docs' \ --exclude='data' --exclude='./reports/*.docx' --exclude='docs' \
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \ --exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
--exclude='.git' --exclude='._*' . || { --exclude='.git' --exclude='._*' --exclude='shared' . || {
error "打包失败" error "打包失败"
return 1 return 1
} }
@ -443,10 +468,17 @@ build_on_server() {
rsync -a --delete \ rsync -a --delete \
--exclude='node_modules' --exclude='.next' --exclude='data' --exclude='/reports/*.docx' --exclude='docs' \ --exclude='node_modules' --exclude='.next' --exclude='data' --exclude='/reports/*.docx' --exclude='docs' \
--exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \ --exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \
--exclude='._*' \ --exclude='._*' --exclude='shared' \
/tmp/deploy_${SITE}/ ${REMOTE_DIR}/ && \ /tmp/deploy_${SITE}/ ${REMOTE_DIR}/ && \
rm -rf /tmp/deploy_${SITE}" rm -rf /tmp/deploy_${SITE}"
# 确保 shared 符号链接正确(打包时已排除,需要重新创建)
ssh txjp "cd $REMOTE_DIR && if [ ! -L shared ]; then
rm -rf shared 2>/dev/null || true
ln -s ../shared shared
echo 'shared 符号链接已创建'
fi"
# 首次:上传 docker-compose.yml新增 .next 卷挂载),重启容器应用 # 首次:上传 docker-compose.yml新增 .next 卷挂载),重启容器应用
ssh txjp "cd $REMOTE_DIR && docker compose up -d" ssh txjp "cd $REMOTE_DIR && docker compose up -d"
@ -715,30 +747,54 @@ EOF
# 执行部署 # 执行部署
# ============================================================ # ============================================================
echo "" echo ""
printf "${BOLD}${CYAN}=========================================${NC}\n"
printf "${BOLD}${CYAN} 开始部署: ${SITE}.tlyq.ai${NC}\n"
printf "${BOLD}${CYAN}=========================================${NC}\n"
DEPLOY_START=$(date +%s)
case "$SITE" in case "$SITE" in
www) www)
phase "打包配置文件"
scp /Users/niuniu/programs/docker/www-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml scp /Users/niuniu/programs/docker/www-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml
scp /Users/niuniu/programs/docker/www-ai/nginx.conf txjp:$REMOTE_DIR/nginx.conf scp /Users/niuniu/programs/docker/www-ai/nginx.conf txjp:$REMOTE_DIR/nginx.conf
build_on_server || exit 1 ;; phase_done
phase "构建与部署"
build_on_server || { phase_fail; exit 1; }
phase_done ;;
cloud) cloud)
info "上传静态文件..." phase "上传静态文件"
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "cloud index.html 上传失败"; exit 1; } info "上传 index.html..."
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "cloud index.html 上传失败"; phase_fail; exit 1; }
scp "$LOCAL_DIR"/nginx.conf txjp:$REMOTE_DIR/nginx.conf 2>/dev/null || true scp "$LOCAL_DIR"/nginx.conf txjp:$REMOTE_DIR/nginx.conf 2>/dev/null || true
scp /Users/niuniu/programs/docker/cloud-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml 2>/dev/null || true scp /Users/niuniu/programs/docker/cloud-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml 2>/dev/null || true
ssh txjp "docker restart ${SITE}-ai" ;; phase_done
phase "重启容器"
ssh txjp "docker restart ${SITE}-ai"
phase_done ;;
token) token)
info "上传静态文件..." phase "上传静态文件"
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "token index.html 上传失败"; exit 1; } info "上传 index.html..."
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "token index.html 上传失败"; phase_fail; exit 1; }
scp "$LOCAL_DIR"/nginx.conf txjp:$REMOTE_DIR/nginx.conf 2>/dev/null || true scp "$LOCAL_DIR"/nginx.conf txjp:$REMOTE_DIR/nginx.conf 2>/dev/null || true
scp /Users/niuniu/programs/docker/token-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml 2>/dev/null || true scp /Users/niuniu/programs/docker/token-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml 2>/dev/null || true
ssh txjp "docker restart ${SITE}-ai" ;; phase_done
phase "重启容器"
ssh txjp "docker restart ${SITE}-ai"
phase_done ;;
issue) issue)
build_on_server "http://assets-ai:3000/api" || exit 1 phase "构建与部署"
info "部署 nginx 路由..." build_on_server "http://assets-ai:3000/api" || { phase_fail; exit 1; }
phase_done
phase "部署 nginx 路由"
ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/issue-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/issue-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true
docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload"
phase_done
# 验证 issue→assets API 连通性(带重试,应对同时部署时目标容器暂不可达) # 验证 issue→assets API 连通性(带重试,应对同时部署时目标容器暂不可达)
phase "检查 API 连通性"
info "检查 issue→assets API 连通性..." info "检查 issue→assets API 连通性..."
ssh txjp 'cat > /tmp/check-conn.js' << 'CHECKEOF' ssh txjp 'cat > /tmp/check-conn.js' << 'CHECKEOF'
const http = require("http"); const http = require("http");
@ -766,14 +822,20 @@ CHECKEOF
echo " $CONN_CHECK" echo " $CONN_CHECK"
else else
log "issue→assets API 连通性正常" log "issue→assets API 连通性正常"
fi ;; fi
phase_done ;;
assets) assets)
build_on_server || exit 1 phase "构建与部署"
info "部署 nginx 路由..." build_on_server || { phase_fail; exit 1; }
phase_done
phase "部署 nginx 路由"
ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/assets-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/assets-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true
docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload"
phase_done
# 验证 assets→issue API 连通性(带重试,应对同时部署时目标容器暂不可达) # 验证 assets→issue API 连通性(带重试,应对同时部署时目标容器暂不可达)
phase "检查 API 连通性"
info "检查 assets→issue API 连通性..." info "检查 assets→issue API 连通性..."
ssh txjp 'cat > /tmp/check-conn.js' << 'CHECKEOF' ssh txjp 'cat > /tmp/check-conn.js' << 'CHECKEOF'
const http = require("http"); const http = require("http");
@ -801,9 +863,14 @@ CHECKEOF
echo " $CONN_CHECK" echo " $CONN_CHECK"
else else
log "assets→issue API 连通性正常" log "assets→issue API 连通性正常"
fi ;; fi
phase_done ;;
oa) oa)
build_on_server || exit 1 phase "构建与部署"
build_on_server || { phase_fail; exit 1; }
phase_done
phase "配置环境变量"
# 注入 LLDAP admin 密码OA OIDC callback 需要验证 LLDAP 用户存在性) # 注入 LLDAP admin 密码OA OIDC callback 需要验证 LLDAP 用户存在性)
info "注入 LLDAP admin 密码到 OA .env..." info "注入 LLDAP admin 密码到 OA .env..."
ssh txjp " ssh txjp "
@ -814,15 +881,20 @@ CHECKEOF
|| echo \"LLDAP_ADMIN_PASSWORD=\$LLDAP_ADMIN_PASS\" >> $REMOTE_DIR/.env || echo \"LLDAP_ADMIN_PASSWORD=\$LLDAP_ADMIN_PASS\" >> $REMOTE_DIR/.env
fi fi
" "
info "部署 nginx 配置..." phase_done
phase "部署 nginx 配置"
ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/oa-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/oa-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true
docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload"
phase_done
phase "同步数据"
# 批量同步 LLDAP email 至各站点(新服务器首次部署填充历史数据) # 批量同步 LLDAP email 至各站点(新服务器首次部署填充历史数据)
info "同步 LLDAP 邮箱至各站点..." info "同步 LLDAP 邮箱至各站点..."
ssh txjp "node /root/docker/oa-ai/scripts/sync-emails-to-sites.js 2>/dev/null" || warn "邮箱同步失败(不影响 OA 主流程)" ssh txjp "node /root/docker/oa-ai/scripts/sync-emails-to-sites.js 2>/dev/null" || warn "邮箱同步失败(不影响 OA 主流程)"
;; phase_done ;;
ldap) ldap)
info "部署 LLDAP..." phase "部署 LLDAP"
scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml
scp "$LOCAL_DIR/Dockerfile" txjp:$REMOTE_DIR/Dockerfile scp "$LOCAL_DIR/Dockerfile" txjp:$REMOTE_DIR/Dockerfile
ssh txjp "cd $REMOTE_DIR && \ ssh txjp "cd $REMOTE_DIR && \
@ -850,28 +922,35 @@ CHECKEOF
chmod 600 .env && \ chmod 600 .env && \
sed -i '/3890:3890/d' docker-compose.yml 2>/dev/null || true && \ sed -i '/3890:3890/d' docker-compose.yml 2>/dev/null || true && \
docker compose up -d --build && docker compose restart" docker compose up -d --build && docker compose restart"
log "LLDAP 已部署" ;; log "LLDAP 已部署"
phase_done ;;
gitea) gitea)
info "部署 Gitea..." phase "部署 Gitea"
scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml
# 同步证书目录(若本地存在) # 同步证书目录(若本地存在)
[ -d "$LOCAL_DIR/cert" ] && scp -r "$LOCAL_DIR"/cert/* txjp:$REMOTE_DIR/cert/ 2>/dev/null || true [ -d "$LOCAL_DIR/cert" ] && scp -r "$LOCAL_DIR"/cert/* txjp:$REMOTE_DIR/cert/ 2>/dev/null || true
ssh txjp "cd $REMOTE_DIR && docker compose up -d && docker restart nginx-ai" ssh txjp "cd $REMOTE_DIR && docker compose up -d && docker restart nginx-ai"
log "Gitea 已部署" ;; log "Gitea 已部署"
phase_done ;;
sso) sso)
deploy_sso ;; phase "部署 Authelia SSO"
deploy_sso
phase_done ;;
monitor) monitor)
build_on_server || exit 1 phase "构建与部署"
info "部署 nginx 配置..." build_on_server || { phase_fail; exit 1; }
phase_done
phase "部署 nginx 配置"
ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/monitor-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/monitor-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true
docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" ;; docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload"
phase_done ;;
esac esac
# ============================================================ # ============================================================
# 运行时合规检查LESSONS-LEARNED # 运行时合规检查LESSONS-LEARNED
# ============================================================ # ============================================================
echo "" phase "运行时合规检查"
info "运行时合规检查..."
# LL #7: SSL 证书覆盖目标域名(仅 tlyq.ai 站点) # LL #7: SSL 证书覆盖目标域名(仅 tlyq.ai 站点)
if [[ "$SITE" != "ldap" ]]; then if [[ "$SITE" != "ldap" ]]; then
@ -925,8 +1004,7 @@ fi
# ============================================================ # ============================================================
# 验证 # 验证
# ============================================================ # ============================================================
echo "" phase "验证部署"
log "验证部署..."
BUILD_END=$(date +%s) BUILD_END=$(date +%s)
TOTAL_DUR=$((BUILD_END - BUILD_START)) TOTAL_DUR=$((BUILD_END - BUILD_START))
@ -950,9 +1028,15 @@ else
STATUS=$(ssh txjp "curl -s -o /dev/null -w '%{http_code}' '$URL' 2>/dev/null" 2>/dev/null || echo "???") STATUS=$(ssh txjp "curl -s -o /dev/null -w '%{http_code}' '$URL' 2>/dev/null" 2>/dev/null || echo "???")
fi fi
if [[ "$STATUS" == "200" || "$STATUS" == "307" || "$STATUS" == "301" ]]; then if [[ "$STATUS" == "200" || "$STATUS" == "307" || "$STATUS" == "301" ]]; then
log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 $URL" phase_done
echo ""
printf "${GREEN}${BOLD}════════════════════════════════════════════════════════════${NC}\n"
printf "${GREEN}${BOLD} 🎉 部署成功!${NC}\n"
printf "${GREEN}${BOLD} 总耗时: ${TOTAL_DUR}s | 访问: $URL${NC}\n"
printf "${GREEN}${BOLD}════════════════════════════════════════════════════════════${NC}\n"
exit 0 exit 0
else else
phase_done
warn "返回状态码: $STATUS,请检查" warn "返回状态码: $STATUS,请检查"
exit 1 exit 1
fi fi