feat: 构建新鲜度自动检查(deploy-ai.sh + deploy-monitor.sh)
This commit is contained in:
parent
3991ce5134
commit
3dd394d943
470
deploy-ai.sh
470
deploy-ai.sh
|
|
@ -34,9 +34,14 @@ is_mac() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# macOS 的 sed -i 需要空后缀 '',Linux 不需要
|
# macOS 的 sed -i 需要空后缀 '',Linux 不需要
|
||||||
SED_I_BACKUP='-i'
|
# sed 原地编辑(处理 macOS BSD sed 与 Linux GNU sed 差异)
|
||||||
|
# sed_i: 函数形式,用于直接调用。_sed_i: 变量形式,用于 find -exec(find 只能调用二进制)
|
||||||
if is_mac; then
|
if is_mac; then
|
||||||
SED_I_BACKUP='-i ""'
|
sed_i() { sed -i '' "$@"; }
|
||||||
|
_sed_i='sed -i ""'
|
||||||
|
else
|
||||||
|
sed_i() { sed -i "$@"; }
|
||||||
|
_sed_i='sed -i'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -52,6 +57,15 @@ while [[ $# -gt 0 ]]; do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 部署前合规检查(CLAUDE.md + LESSONS-LEARNED.md)
|
||||||
|
# ============================================================
|
||||||
|
echo ""
|
||||||
|
bash "$(dirname "$0")/check-compliance.sh" || {
|
||||||
|
error "合规检查未通过,部署已中止"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 选择站点
|
# 选择站点
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -72,11 +86,11 @@ echo " 9) sso.tlyq.ai (Authelia SSO — 统一认证中心)"
|
||||||
echo " 10) monitor.tlyq.ai (告警监控中心)"
|
echo " 10) monitor.tlyq.ai (告警监控中心)"
|
||||||
echo ""
|
echo ""
|
||||||
printf "请输入编号 (1/2/3/4/5/6/7/8/9/10): "
|
printf "请输入编号 (1/2/3/4/5/6/7/8/9/10): "
|
||||||
read choice
|
read -t 30 choice || { echo "超时未输入,退出"; exit 1; }
|
||||||
|
|
||||||
SITE=""; LOCAL_DIR=""; REMOTE_DIR=""; CONTAINER=""
|
SITE=""; LOCAL_DIR=""; REMOTE_DIR=""; CONTAINER=""
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1) SITE="www"; LOCAL_DIR="/Users/niuniu/programs/docker/www-ai/src"; REMOTE_DIR="/tmp/project-extract-2/turing-engine" ;;
|
1) SITE="www"; LOCAL_DIR="/Users/niuniu/programs/docker/www-ai/src"; REMOTE_DIR="/root/docker/www-ai"; CONTAINER="www-ai" ;;
|
||||||
2) SITE="cloud"; LOCAL_DIR="/Users/niuniu/programs/docker/cloud-ai/html"; REMOTE_DIR="/root/docker/cloud-ai/html" ;;
|
2) SITE="cloud"; LOCAL_DIR="/Users/niuniu/programs/docker/cloud-ai/html"; REMOTE_DIR="/root/docker/cloud-ai/html" ;;
|
||||||
3) SITE="token"; LOCAL_DIR="/Users/niuniu/programs/docker/token-ai/html"; REMOTE_DIR="/root/docker/token-ai/html" ;;
|
3) SITE="token"; LOCAL_DIR="/Users/niuniu/programs/docker/token-ai/html"; REMOTE_DIR="/root/docker/token-ai/html" ;;
|
||||||
4) SITE="issue"; LOCAL_DIR="/Users/niuniu/programs/docker/issue-ai"; REMOTE_DIR="/root/docker/issue-ai"; CONTAINER="issue-ai" ;;
|
4) SITE="issue"; LOCAL_DIR="/Users/niuniu/programs/docker/issue-ai"; REMOTE_DIR="/root/docker/issue-ai"; CONTAINER="issue-ai" ;;
|
||||||
|
|
@ -98,27 +112,26 @@ echo ""
|
||||||
# ============================================================
|
# ============================================================
|
||||||
replace_url() {
|
replace_url() {
|
||||||
local mode="$1"
|
local mode="$1"
|
||||||
local from to
|
|
||||||
if [[ "$mode" == "prod" ]]; then
|
|
||||||
from="localhost"; to="https://www.tlyq.ai"
|
|
||||||
else
|
|
||||||
from="https://www.tlyq.ai"; to="localhost"
|
|
||||||
fi
|
|
||||||
case "$SITE" in
|
case "$SITE" in
|
||||||
www)
|
www)
|
||||||
cd "$LOCAL_DIR"
|
cd "$LOCAL_DIR"
|
||||||
find src -type f -name '*.tsx' -exec sed $SED_I_BACKUP \
|
if [[ "$mode" == "prod" ]]; then
|
||||||
-e "s|${from}:6174|https://cloud.tlyq.ai|g" \
|
find src -type f -name '*.tsx' -exec $_sed_i \
|
||||||
-e "s|${from}:6175|https://token.tlyq.ai|g" \
|
-e 's|http://localhost:6174|https://cloud.tlyq.ai|g' \
|
||||||
-e "s|https://cloud.tlyq.ai|${from}:6174|g" \
|
-e 's|http://localhost:6175|https://token.tlyq.ai|g' \
|
||||||
-e "s|https://token.tlyq.ai|${from}:6175|g" \
|
{} + 2>/dev/null || true
|
||||||
{} + 2>/dev/null || true ;;
|
else
|
||||||
cloud)
|
find src -type f -name '*.tsx' -exec $_sed_i \
|
||||||
sed $SED_I_BACKUP "s|${from}:6173|https://www.tlyq.ai|g" "$LOCAL_DIR/index.html" 2>/dev/null || true
|
-e 's|https://cloud.tlyq.ai|http://localhost:6174|g' \
|
||||||
sed $SED_I_BACKUP "s|https://www.tlyq.ai|${from}:6173|g" "$LOCAL_DIR/index.html" 2>/dev/null || true ;;
|
-e 's|https://token.tlyq.ai|http://localhost:6175|g' \
|
||||||
token)
|
{} + 2>/dev/null || true
|
||||||
sed $SED_I_BACKUP "s|${from}:6173|https://www.tlyq.ai|g" "$LOCAL_DIR/index.html" 2>/dev/null || true
|
fi ;;
|
||||||
sed $SED_I_BACKUP "s|https://www.tlyq.ai|${from}:6173|g" "$LOCAL_DIR/index.html" 2>/dev/null || true ;;
|
cloud|token)
|
||||||
|
if [[ "$mode" == "prod" ]]; then
|
||||||
|
sed_i 's|http://localhost:6173|https://www.tlyq.ai|g' "$LOCAL_DIR/index.html" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
sed_i 's|https://www.tlyq.ai|http://localhost:6173|g' "$LOCAL_DIR/index.html" 2>/dev/null || true
|
||||||
|
fi ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +141,7 @@ replace_oa_urls() {
|
||||||
cd "$LOCAL_DIR"
|
cd "$LOCAL_DIR"
|
||||||
if [[ "$mode" == "prod" ]]; then
|
if [[ "$mode" == "prod" ]]; then
|
||||||
# A. 前端导航卡片 → 生产域名
|
# A. 前端导航卡片 → 生产域名
|
||||||
sed $SED_I_BACKUP \
|
sed_i \
|
||||||
-e 's|http://localhost:6177|https://assets.tlyq.ai|g' \
|
-e 's|http://localhost:6177|https://assets.tlyq.ai|g' \
|
||||||
-e 's|http://localhost:6176|https://issue.tlyq.ai|g' \
|
-e 's|http://localhost:6176|https://issue.tlyq.ai|g' \
|
||||||
-e 's|http://localhost:6173|https://www.tlyq.ai|g' \
|
-e 's|http://localhost:6173|https://www.tlyq.ai|g' \
|
||||||
|
|
@ -136,36 +149,37 @@ replace_oa_urls() {
|
||||||
-e 's|http://localhost:6175|https://token.tlyq.ai|g' \
|
-e 's|http://localhost:6175|https://token.tlyq.ai|g' \
|
||||||
src/app/page.tsx
|
src/app/page.tsx
|
||||||
# B. 服务端内部 API 调用 → Docker 容器名(assets-ai:3000 / issue-ai:3000)
|
# B. 服务端内部 API 调用 → Docker 容器名(assets-ai:3000 / issue-ai:3000)
|
||||||
find src/app/api -name "*.ts" -exec sed $SED_I_BACKUP \
|
find src/app/api -name "*.ts" -exec $_sed_i \
|
||||||
-e "s|'http://localhost:6177'|'http://assets-ai:3000'|g" \
|
-e "s|'http://localhost:6177'|'http://assets-ai:3000'|g" \
|
||||||
-e "s|'http://localhost:6176'|'http://issue-ai:3000'|g" \
|
-e "s|'http://localhost:6176'|'http://issue-ai:3000'|g" \
|
||||||
-e "s|\`http://localhost:\${site === 'assets' ? 6177 : 6176}\`|\`http://\${site}-ai:3000\`|g" \
|
-e "s|\`http://localhost:\${site === 'assets' ? 6177 : 6176}\`|\`http://\${site}-ai:3000\`|g" \
|
||||||
{} +
|
{} +
|
||||||
# C. 退出登录重定向
|
# C. 退出登录重定向
|
||||||
sed $SED_I_BACKUP "s|'http://localhost:6179'|'https://oa.tlyq.ai'|g" src/app/api/auth/logout/route.ts
|
sed_i "s|'http://localhost:6179'|'https://oa.tlyq.ai'|g" src/app/api/auth/logout/route.ts
|
||||||
else
|
else
|
||||||
# 恢复本地 URL
|
# 恢复本地 URL
|
||||||
sed $SED_I_BACKUP \
|
sed_i \
|
||||||
-e 's|https://assets.tlyq.ai|http://localhost:6177|g' \
|
-e 's|https://assets.tlyq.ai|http://localhost:6177|g' \
|
||||||
-e 's|https://issue.tlyq.ai|http://localhost:6176|g' \
|
-e 's|https://issue.tlyq.ai|http://localhost:6176|g' \
|
||||||
-e 's|https://www.tlyq.ai|http://localhost:6173|g' \
|
-e 's|https://www.tlyq.ai|http://localhost:6173|g' \
|
||||||
-e 's|https://cloud.tlyq.ai|http://localhost:6174|g' \
|
-e 's|https://cloud.tlyq.ai|http://localhost:6174|g' \
|
||||||
-e 's|https://token.tlyq.ai|http://localhost:6175|g' \
|
-e 's|https://token.tlyq.ai|http://localhost:6175|g' \
|
||||||
src/app/page.tsx
|
src/app/page.tsx
|
||||||
find src/app/api -name "*.ts" -exec sed $SED_I_BACKUP \
|
find src/app/api -name "*.ts" -exec $_sed_i \
|
||||||
-e "s|'http://assets-ai:3000'|'http://localhost:6177'|g" \
|
-e "s|'http://assets-ai:3000'|'http://localhost:6177'|g" \
|
||||||
-e "s|'http://issue-ai:3000'|'http://localhost:6176'|g" \
|
-e "s|'http://issue-ai:3000'|'http://localhost:6176'|g" \
|
||||||
-e "s|\`http://\${site}-ai:3000\`|\`http://localhost:\${site === 'assets' ? 6177 : 6176}\`|g" \
|
-e "s|\`http://\${site}-ai:3000\`|\`http://localhost:\${site === 'assets' ? 6177 : 6176}\`|g" \
|
||||||
{} +
|
{} +
|
||||||
sed $SED_I_BACKUP "s|'https://oa.tlyq.ai'|'http://localhost:6179'|g" src/app/api/auth/logout/route.ts
|
sed_i "s|'https://oa.tlyq.ai'|'http://localhost:6179'|g" src/app/api/auth/logout/route.ts
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
to_prod() { [[ "$SITE" == "www" || "$SITE" == "cloud" || "$SITE" == "token" ]] && replace_url prod; }
|
to_prod() { [[ "$SITE" == "www" || "$SITE" == "cloud" || "$SITE" == "token" ]] && replace_url prod; }
|
||||||
to_local() { [[ "$SITE" == "www" || "$SITE" == "cloud" || "$SITE" == "token" ]] && replace_url local; }
|
to_local() { [[ "$SITE" == "www" || "$SITE" == "cloud" || "$SITE" == "token" ]] && replace_url local; }
|
||||||
trap 'to_local; [[ "$SITE" == "oa" ]] && replace_oa_urls local' EXIT
|
# 注意: SIGKILL (kill -9) 无法被 trap 捕获,暴力终止后本地文件可能残留生产 URL
|
||||||
|
trap 'to_local; [[ "$SITE" == "oa" ]] && replace_oa_urls local; rm -f /tmp/${SITE}.tar.gz /tmp/shared-deploy.tar.gz' EXIT SIGINT SIGTERM
|
||||||
[[ "$SITE" == "oa" ]] && replace_oa_urls prod
|
[[ "$SITE" == "oa" ]] && replace_oa_urls prod
|
||||||
[[ "$SITE" != "issue" && "$SITE" != "assets" && "$SITE" != "oa" ]] && to_prod
|
to_prod # to_prod() 内部已做 SITE 判断,无需外层条件
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# sso-ai 部署(Docker 容器,无 npm 构建)
|
# sso-ai 部署(Docker 容器,无 npm 构建)
|
||||||
|
|
@ -176,10 +190,11 @@ deploy_sso() {
|
||||||
# 1. 上传 docker-compose.yml 和 config/
|
# 1. 上传 docker-compose.yml 和 config/
|
||||||
log "上传配置文件..."
|
log "上传配置文件..."
|
||||||
scp /Users/niuniu/programs/docker/sso-ai/docker-compose.yml txjp:/root/docker/sso-ai/docker-compose.yml
|
scp /Users/niuniu/programs/docker/sso-ai/docker-compose.yml txjp:/root/docker/sso-ai/docker-compose.yml
|
||||||
# 备份现有配置(用于恢复 JWKS 密钥等动态生成的值)
|
# 使用生产配置(configuration.prod.yml → configuration.yml),覆盖前备份
|
||||||
ssh txjp "cp /root/docker/sso-ai/config/authelia/configuration.yml /root/docker/sso-ai/config/authelia/configuration.yml.bak 2>/dev/null || true"
|
ssh txjp "cp /root/docker/sso-ai/config/authelia/configuration.yml /root/docker/sso-ai/config/authelia/configuration.yml.\$(date +%Y%m%d_%H%M%S).bak 2>/dev/null || true"
|
||||||
# 使用生产配置(configuration.prod.yml → configuration.yml)
|
|
||||||
scp /Users/niuniu/programs/docker/sso-ai/config/authelia/configuration.prod.yml txjp:/root/docker/sso-ai/config/authelia/configuration.yml
|
scp /Users/niuniu/programs/docker/sso-ai/config/authelia/configuration.prod.yml txjp:/root/docker/sso-ai/config/authelia/configuration.yml
|
||||||
|
# 确保 TLS 证书存在(首次部署或证书丢失时自动生成)
|
||||||
|
ssh txjp "ls /root/docker/sso-ai/config/authelia/certs/cert.pem 2>/dev/null || (mkdir -p /root/docker/sso-ai/config/authelia/certs && openssl req -x509 -newkey rsa:4096 -keyout /root/docker/sso-ai/config/authelia/certs/key.pem -out /root/docker/sso-ai/config/authelia/certs/cert.pem -days 3650 -nodes -subj '/CN=sso.tlyq.ai' 2>/dev/null && echo '已生成自签名证书')"
|
||||||
# 上传其他 config 文件(如 notifications.yml)
|
# 上传其他 config 文件(如 notifications.yml)
|
||||||
scp /Users/niuniu/programs/docker/sso-ai/config/authelia/notifications.yml txjp:/root/docker/sso-ai/config/authelia/notifications.yml 2>/dev/null || true
|
scp /Users/niuniu/programs/docker/sso-ai/config/authelia/notifications.yml txjp:/root/docker/sso-ai/config/authelia/notifications.yml 2>/dev/null || true
|
||||||
|
|
||||||
|
|
@ -209,7 +224,7 @@ ENVEOF
|
||||||
log "注入 LDAP 密码和 JWKS 密钥..."
|
log "注入 LDAP 密码和 JWKS 密钥..."
|
||||||
ssh txjp "cd /root/docker/sso-ai && \
|
ssh txjp "cd /root/docker/sso-ai && \
|
||||||
LLDAP_PASSWORD=\$(docker exec lldap printenv LLDAP_ADMIN_PASSWORD) && \
|
LLDAP_PASSWORD=\$(docker exec lldap printenv LLDAP_ADMIN_PASSWORD) && \
|
||||||
sed -i \"s|PRODUCTION_LDAP_PASSWORD|\$LLDAP_PASSWORD|\" config/authelia/configuration.yml 2>/dev/null || true && \
|
python3 -c \"import sys; content=open('config/authelia/configuration.yml').read(); content=content.replace('PRODUCTION_LDAP_PASSWORD', sys.argv[1]); open('config/authelia/configuration.yml','w').write(content)\" \"\$LLDAP_PASSWORD\" && \
|
||||||
# 如果 JWKS key 仍是占位符,从现有配置或生成新密钥
|
# 如果 JWKS key 仍是占位符,从现有配置或生成新密钥
|
||||||
if grep -q 'OIDC_JWKS_KEY_PLACEHOLDER' config/authelia/configuration.yml; then
|
if grep -q 'OIDC_JWKS_KEY_PLACEHOLDER' config/authelia/configuration.yml; then
|
||||||
EXISTING_KEY=\$(cat /root/docker/sso-ai/config/authelia/configuration.yml.bak 2>/dev/null | sed -n '/key: |/,/-----END/{p}' || true)
|
EXISTING_KEY=\$(cat /root/docker/sso-ai/config/authelia/configuration.yml.bak 2>/dev/null | sed -n '/key: |/,/-----END/{p}' || true)
|
||||||
|
|
@ -237,14 +252,23 @@ open('config/authelia/configuration.yml', 'w').write(content)
|
||||||
log "部署 nginx 配置..."
|
log "部署 nginx 配置..."
|
||||||
scp /Users/niuniu/programs/docker/nginx-proxy-ai/conf.d/sso-ai.conf txjp:/root/docker/nginx-proxy-ai/conf.d/sso-ai.conf
|
scp /Users/niuniu/programs/docker/nginx-proxy-ai/conf.d/sso-ai.conf txjp:/root/docker/nginx-proxy-ai/conf.d/sso-ai.conf
|
||||||
|
|
||||||
# 5. 启动容器
|
# 5. 仅启动 authelia(redis 和 lldap 为独立容器,不通过本 compose 管理)
|
||||||
log "启动 Authelia..."
|
log "启动 Authelia..."
|
||||||
ssh txjp "cd /root/docker/sso-ai && docker compose up -d"
|
ssh txjp "cd /root/docker/sso-ai && docker compose up -d --no-deps authelia"
|
||||||
|
|
||||||
# 6. 验证
|
# 6. 验证
|
||||||
sleep 5
|
sleep 5
|
||||||
ssh txjp "curl -sk https://127.0.0.1:9091/api/health" && log "Authelia 健康检查通过" || warn "Authelia 可能未就绪"
|
ssh txjp "curl -sk https://127.0.0.1:9091/api/health" && log "Authelia 健康检查通过" || warn "Authelia 可能未就绪"
|
||||||
|
|
||||||
|
# 6.1 检查 OIDC token_endpoint_auth_method 是否匹配(LESSONS-LEARNED #48)
|
||||||
|
sleep 3
|
||||||
|
if ssh txjp "docker logs authelia --since 30s 2>&1 | grep -q 'Client authentication failed.*token_endpoint_auth_method'"; then
|
||||||
|
warn "检测到 OIDC token_endpoint_auth_method 不匹配!检查 Authelia configuration.yml 中所有 client 是否设置了 token_endpoint_auth_method: 'client_secret_post'"
|
||||||
|
ssh txjp "docker logs authelia --since 30s 2>&1 | grep 'Client authentication failed' | tail -3"
|
||||||
|
else
|
||||||
|
log "OIDC token_endpoint_auth_method 一致性检查通过"
|
||||||
|
fi
|
||||||
|
|
||||||
# 7. 重载 nginx
|
# 7. 重载 nginx
|
||||||
ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" && log "nginx 已重载" || warn "nginx 重载失败"
|
ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" && log "nginx 已重载" || warn "nginx 重载失败"
|
||||||
|
|
||||||
|
|
@ -254,13 +278,14 @@ open('config/authelia/configuration.yml', 'w').write(content)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 核心构建函数(服务器上直接 npm run build)
|
# 核心构建函数(服务器上直接 npm run build)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
SNAPSHOT_FILE="/tmp/.snapshot.${SITE}.md5"
|
SNAPSHOT_DIR="/root/docker/.snapshots"
|
||||||
|
SNAPSHOT_FILE="${SNAPSHOT_DIR}/.snapshot.${SITE}.md5"
|
||||||
BUILD_START=$(date +%s)
|
BUILD_START=$(date +%s)
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 同步 shared 共享库(快照比对,无变化跳过)
|
# 同步 shared 共享库(快照比对,无变化跳过)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
SHARED_SNAPSHOT="/tmp/.snapshot.shared.md5"
|
SHARED_SNAPSHOT="${SNAPSHOT_DIR}/.snapshot.shared.md5"
|
||||||
SHARED_LOCAL="/Users/niuniu/programs/docker/shared"
|
SHARED_LOCAL="/Users/niuniu/programs/docker/shared"
|
||||||
SHARED_REMOTE="/root/docker/shared"
|
SHARED_REMOTE="/root/docker/shared"
|
||||||
|
|
||||||
|
|
@ -269,14 +294,26 @@ sync_shared() {
|
||||||
|
|
||||||
info "检查 shared 共享库..."
|
info "检查 shared 共享库..."
|
||||||
local local_md5
|
local local_md5
|
||||||
local_md5=$(find "$SHARED_LOCAL" \
|
if is_mac; then
|
||||||
-not -path '*/node_modules/*' \
|
local_md5=$(find "$SHARED_LOCAL" \
|
||||||
-not -path '*/.git/*' \
|
-not -path '*/node_modules/*' \
|
||||||
-not -name '._*' \
|
-not -path '*/.git/*' \
|
||||||
-type f \
|
-not -name '._*' -not -name '.DS_Store' \
|
||||||
-exec md5 -q {} \; 2>/dev/null \
|
-type f \
|
||||||
| sort \
|
-exec md5 -q {} \; 2>/dev/null \
|
||||||
| md5 -q)
|
| sort \
|
||||||
|
| md5 -q)
|
||||||
|
else
|
||||||
|
local_md5=$(find "$SHARED_LOCAL" \
|
||||||
|
-not -path '*/node_modules/*' \
|
||||||
|
-not -path '*/.git/*' \
|
||||||
|
-not -name '._*' -not -name '.DS_Store' \
|
||||||
|
-type f \
|
||||||
|
-exec md5sum {} \; 2>/dev/null \
|
||||||
|
| sed 's/ .*//' \
|
||||||
|
| sort \
|
||||||
|
| md5sum | cut -d' ' -f1)
|
||||||
|
fi
|
||||||
|
|
||||||
local prev_md5
|
local prev_md5
|
||||||
prev_md5=$(ssh txjp "cat ${SHARED_SNAPSHOT} 2>/dev/null" 2>/dev/null || echo "")
|
prev_md5=$(ssh txjp "cat ${SHARED_SNAPSHOT} 2>/dev/null" 2>/dev/null || echo "")
|
||||||
|
|
@ -294,12 +331,27 @@ sync_shared() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
COPYFILE_DISABLE=1 tar czf /tmp/shared-deploy.tar.gz -C /Users/niuniu/programs/docker \
|
COPYFILE_DISABLE=1 tar czf /tmp/shared-deploy.tar.gz -C /Users/niuniu/programs/docker \
|
||||||
--exclude='node_modules' --exclude='.git' --exclude='._*' shared/ 2>/dev/null
|
--exclude='node_modules' --exclude='.git' --exclude='._*' shared/ 2>/dev/null || {
|
||||||
scp /tmp/shared-deploy.tar.gz txjp:/tmp/shared-deploy.tar.gz
|
error "shared 打包失败"
|
||||||
ssh txjp "rm -rf ${SHARED_REMOTE} && mkdir -p ${SHARED_REMOTE} && \
|
return 1
|
||||||
tar xzf /tmp/shared-deploy.tar.gz -C /root/docker/ && \
|
}
|
||||||
echo '${local_md5}' > ${SHARED_SNAPSHOT}"
|
scp /tmp/shared-deploy.tar.gz txjp:/tmp/shared-deploy.tar.gz || {
|
||||||
|
error "shared 上传失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# 先解压到临时目录,成功后再替换(避免先删后建时解压失败导致 shared 丢失)
|
||||||
|
ssh txjp "rm -rf /tmp/shared_tmp && mkdir -p /tmp/shared_tmp && \
|
||||||
|
tar xzf /tmp/shared-deploy.tar.gz -C /tmp/shared_tmp && \
|
||||||
|
rm -rf ${SHARED_REMOTE} && mkdir -p ${SHARED_REMOTE} && \
|
||||||
|
mv /tmp/shared_tmp/shared/* ${SHARED_REMOTE}/ && \
|
||||||
|
rm -rf /tmp/shared_tmp && \
|
||||||
|
echo '${local_md5}' > ${SHARED_SNAPSHOT}" || {
|
||||||
|
error "shared 远端解压或替换失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
log "shared 已同步到 ${SHARED_REMOTE}"
|
log "shared 已同步到 ${SHARED_REMOTE}"
|
||||||
|
# 安装 shared 自身依赖(如 ldapts),确保各站点构建时能正确解析
|
||||||
|
ssh txjp "cd ${SHARED_REMOTE} && [ -f package.json ] && npm install --prefer-offline 2>&1 | tail -3" || warn "shared 依赖安装失败,部分站点可能构建异常"
|
||||||
}
|
}
|
||||||
|
|
||||||
build_on_server() {
|
build_on_server() {
|
||||||
|
|
@ -307,29 +359,22 @@ build_on_server() {
|
||||||
|
|
||||||
# 0. 同步 shared 共享库(所有使用 @shared 的站点自动受益)
|
# 0. 同步 shared 共享库(所有使用 @shared 的站点自动受益)
|
||||||
sync_shared
|
sync_shared
|
||||||
|
# 确保 shared 自身依赖已安装(如 ldapts),否则站点构建时模块解析失败
|
||||||
|
ssh txjp "cd ${SHARED_REMOTE} && [ -f package.json ] && [ ! -d node_modules ] && npm install --prefer-offline 2>&1 | tail -3 || true" 2>/dev/null
|
||||||
|
|
||||||
# 1. 计算源码快照(含 shared,确保 shared 变化也触发重构建)
|
# 1. 计算源码快照(含 shared,确保 shared 变化也触发重构建)
|
||||||
log "检查源码是否有变化..."
|
log "检查源码是否有变化..."
|
||||||
local_md5=$( (find "$LOCAL_DIR" \
|
# 平台兼容:macOS md5 -q / Linux md5sum
|
||||||
-not -path '*/node_modules/*' \
|
FIND_EXCLUDE="-not -path '*/node_modules/*' -not -path '*/.next/*' -not -path '*/out/*' -not -path '*/data/*' -not -path '*/reports/*' -not -path '*/docs/*' -not -path '*/.env.local' -not -path '*/.env.development' -not -path '*/.env.production' -not -path '*/.git/*' -not -name '._*' -not -name '.DS_Store'"
|
||||||
-not -path '*/.next/*' \
|
if is_mac; then
|
||||||
-not -path '*/out/*' \
|
local_md5=$( (find "$LOCAL_DIR" $FIND_EXCLUDE -type f -exec md5 -q {} \; ; \
|
||||||
-not -path '*/data/*' \
|
find "$SHARED_LOCAL" -not -path '*/node_modules/*' -not -path '*/.git/*' -not -name '._*' -not -name '.DS_Store' -type f -exec md5 -q {} \; 2>/dev/null) \
|
||||||
-not -path '*/reports/*' \
|
| sort | md5 -q)
|
||||||
-not -path '*/docs/*' \
|
else
|
||||||
-not -path '*/.env*' \
|
local_md5=$( (find "$LOCAL_DIR" $FIND_EXCLUDE -type f -exec md5sum {} \; ; \
|
||||||
-not -path '*/.git/*' \
|
find "$SHARED_LOCAL" -not -path '*/node_modules/*' -not -path '*/.git/*' -not -name '._*' -not -name '.DS_Store' -type f -exec md5sum {} \; 2>/dev/null) \
|
||||||
-not -name '._*' \
|
| sed 's/ .*//' | sort | md5sum | cut -d' ' -f1)
|
||||||
-type f \
|
fi
|
||||||
-exec md5 -q {} \; ; \
|
|
||||||
find "$SHARED_LOCAL" \
|
|
||||||
-not -path '*/node_modules/*' \
|
|
||||||
-not -path '*/.git/*' \
|
|
||||||
-not -name '._*' \
|
|
||||||
-type f \
|
|
||||||
-exec md5 -q {} \; 2>/dev/null) \
|
|
||||||
| sort \
|
|
||||||
| md5 -q)
|
|
||||||
echo " 源码快照: ${local_md5}"
|
echo " 源码快照: ${local_md5}"
|
||||||
|
|
||||||
prev_md5=$(ssh txjp "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "")
|
prev_md5=$(ssh txjp "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "")
|
||||||
|
|
@ -357,7 +402,7 @@ build_on_server() {
|
||||||
|
|
||||||
if [[ "$MODE" == "skip" || "$MODE" == "restart" ]]; then
|
if [[ "$MODE" == "skip" || "$MODE" == "restart" ]]; then
|
||||||
ssh txjp "cd $REMOTE_DIR && docker compose down && docker compose up -d"
|
ssh txjp "cd $REMOTE_DIR && docker compose down && docker compose up -d"
|
||||||
log "容器已重建"
|
log "容器已重启"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -409,7 +454,10 @@ build_on_server() {
|
||||||
|
|
||||||
# 安装可能新增的依赖(已有依赖走缓存,很快)
|
# 安装可能新增的依赖(已有依赖走缓存,很快)
|
||||||
log "安装新增依赖..."
|
log "安装新增依赖..."
|
||||||
ssh txjp "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5 || true"
|
ssh txjp "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5" || {
|
||||||
|
error "npm install 失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -n "$extra_env" ]]; then
|
if [[ -n "$extra_env" ]]; then
|
||||||
ssh txjp "sed -i 's|ASSETS_API_URL=.*|ASSETS_API_URL=${extra_env}|' $REMOTE_DIR/.env 2>/dev/null \
|
ssh txjp "sed -i 's|ASSETS_API_URL=.*|ASSETS_API_URL=${extra_env}|' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
|
@ -490,31 +538,69 @@ build_on_server() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 自动生成缺失的密钥(首次部署场景)
|
# 自动生成缺失的密钥(首次部署场景)
|
||||||
# JWT_SECRET:所有站点都需要
|
# 先确保 .env 文件存在且权限为 600(防止后续写入期间权限暴露)
|
||||||
if ! ssh txjp "grep -q '^JWT_SECRET=' $REMOTE_DIR/.env 2>/dev/null"; then
|
ssh txjp "touch $REMOTE_DIR/.env && chmod 600 $REMOTE_DIR/.env" 2>/dev/null
|
||||||
JWT_GEN=$(openssl rand -hex 32)
|
|
||||||
ssh txjp "echo 'JWT_SECRET=${JWT_GEN}' >> $REMOTE_DIR/.env"
|
# JWT_SECRET:所有 .tlyq.ai 子站点必须共用同一个密钥(方案 B:校验+按需修复)
|
||||||
info "已自动生成 JWT_SECRET"
|
# 从服务器共享位置读取权威值(首次自动生成并持久化)
|
||||||
|
JWT_SECRET=$(ssh txjp "cat /root/docker/.shared_jwt_secret 2>/dev/null" 2>/dev/null || echo "")
|
||||||
|
if [[ -z "$JWT_SECRET" ]]; then
|
||||||
|
JWT_SECRET=$(openssl rand -hex 32)
|
||||||
|
# 部署前备份旧共享文件(如存在)
|
||||||
|
ssh txjp "cp /root/docker/.shared_jwt_secret /root/docker/.shared_jwt_secret.bak.\$(date +%Y%m%d) 2>/dev/null || true"
|
||||||
|
ssh txjp "cat > /root/docker/.shared_jwt_secret && chmod 600 /root/docker/.shared_jwt_secret" << EOF
|
||||||
|
${JWT_SECRET}
|
||||||
|
EOF
|
||||||
|
info "已生成共享 JWT_SECRET(所有站点共用)"
|
||||||
fi
|
fi
|
||||||
# OIDC_CLIENT_SECRET:从 Authelia 配置读取或警告
|
|
||||||
|
# 校验站点 .env 中的 JWT_SECRET,不一致时自动修正(防御手动修改)
|
||||||
|
local current_jwt
|
||||||
|
current_jwt=$(ssh txjp "grep '^JWT_SECRET=' $REMOTE_DIR/.env 2>/dev/null | cut -d= -f2" 2>/dev/null || echo "")
|
||||||
|
if [[ -z "$current_jwt" ]]; then
|
||||||
|
ssh txjp "cat >> $REMOTE_DIR/.env" << EOF
|
||||||
|
JWT_SECRET=${JWT_SECRET}
|
||||||
|
EOF
|
||||||
|
elif [[ "$current_jwt" != "$JWT_SECRET" ]]; then
|
||||||
|
warn "检测到 JWT_SECRET 与共享密钥不一致,已自动修正"
|
||||||
|
ssh txjp "sed -i '/^JWT_SECRET=/d' $REMOTE_DIR/.env" # 删旧行(命令不含密钥)
|
||||||
|
ssh txjp "cat >> $REMOTE_DIR/.env" << EOF # heredoc 写新行
|
||||||
|
JWT_SECRET=${JWT_SECRET}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
# OIDC_CLIENT_SECRET:从服务器现有 .env 读取(Authelia 配置存储的是 bcrypt 哈希,不可逆用)
|
||||||
|
# 全程在服务器侧执行,避免 secret 跨 SSH 边界传输
|
||||||
if [[ "$SITE" == "oa" || "$SITE" == "assets" || "$SITE" == "issue" || "$SITE" == "monitor" ]]; then
|
if [[ "$SITE" == "oa" || "$SITE" == "assets" || "$SITE" == "issue" || "$SITE" == "monitor" ]]; then
|
||||||
if ! ssh txjp "grep -q '^OIDC_CLIENT_SECRET=' $REMOTE_DIR/.env 2>/dev/null"; then
|
if ! ssh txjp "grep -q '^OIDC_CLIENT_SECRET=' $REMOTE_DIR/.env 2>/dev/null"; then
|
||||||
# 尝试从 Authelia 配置读取对应的 client_secret 哈希
|
ssh txjp "
|
||||||
OIDC_SECRET_KEY="${SITE}_OIDC_SECRET"
|
found=0
|
||||||
OIDC_SECRET_VAL=$(ssh txjp "grep -A1 'client_id:.*${OIDC_ID}' /root/docker/sso-ai/config/authelia/configuration.yml 2>/dev/null | grep client_secret | head -1 | sed \"s/.*client_secret: *'\\(.*\\)'/\\1/\" || true" 2>/dev/null)
|
for d in oa-ai assets-ai issue-ai monitor-ai; do
|
||||||
if [[ -n "$OIDC_SECRET_VAL" && "$OIDC_SECRET_VAL" != *"PLACEHOLDER"* ]]; then
|
if [ \"\$d\" != \"${SITE}-ai\" ] && [ -f /root/docker/\$d/.env ]; then
|
||||||
ssh txjp "echo 'OIDC_CLIENT_SECRET=${OIDC_SECRET_VAL}' >> $REMOTE_DIR/.env"
|
val=\$(grep '^OIDC_CLIENT_SECRET=' /root/docker/\$d/.env 2>/dev/null | head -1 | cut -d= -f2)
|
||||||
info "已从 Authelia 配置注入 OIDC_CLIENT_SECRET"
|
if [ -n \"\$val\" ] && [ \"\$val\" != *SECRET_HASH* ] && [ \"\$val\" != *PLACEHOLDER* ] && ! echo \"\$val\" | grep -q '^__.*__\$'; then
|
||||||
else
|
echo \"OIDC_CLIENT_SECRET=\$val\" >> $REMOTE_DIR/.env
|
||||||
warn "OIDC_CLIENT_SECRET 未设置,请从 Authelia 配置获取并手动配置"
|
found=1
|
||||||
fi
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ \$found -eq 0 ]; then
|
||||||
|
echo 'ERROR: OIDC_CLIENT_SECRET 未设置且无法从其他站点获取,请手动配置后重新部署' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
" && info "已从其他站点复用 OIDC_CLIENT_SECRET" || {
|
||||||
|
error "OIDC_CLIENT_SECRET 未设置且无法从其他站点获取,请手动配置后重新部署"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# LOCALADMIN_PASSWORD:monitor 专属
|
# LOCALADMIN_PASSWORD:monitor 专属
|
||||||
if [[ "$SITE" == "monitor" ]]; then
|
if [[ "$SITE" == "monitor" ]]; then
|
||||||
if ! ssh txjp "grep -q '^LOCALADMIN_PASSWORD=' $REMOTE_DIR/.env 2>/dev/null"; then
|
if ! ssh txjp "grep -q '^LOCALADMIN_PASSWORD=' $REMOTE_DIR/.env 2>/dev/null"; then
|
||||||
LOCALADMIN_GEN=$(openssl rand -hex 16)
|
LOCALADMIN_GEN=$(openssl rand -hex 16)
|
||||||
ssh txjp "echo 'LOCALADMIN_PASSWORD=${LOCALADMIN_GEN}' >> $REMOTE_DIR/.env"
|
ssh txjp "cat >> $REMOTE_DIR/.env" << EOF
|
||||||
|
LOCALADMIN_PASSWORD=${LOCALADMIN_GEN}
|
||||||
|
EOF
|
||||||
info "已自动生成 LOCALADMIN_PASSWORD"
|
info "已自动生成 LOCALADMIN_PASSWORD"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -525,14 +611,20 @@ build_on_server() {
|
||||||
# 设置 .env 文件权限为 600(仅 root 可读写,防止密码泄露)
|
# 设置 .env 文件权限为 600(仅 root 可读写,防止密码泄露)
|
||||||
ssh txjp "chmod 600 $REMOTE_DIR/.env 2>/dev/null || true"
|
ssh txjp "chmod 600 $REMOTE_DIR/.env 2>/dev/null || true"
|
||||||
|
|
||||||
# 5. 服务器上直接 npm run build(不重建 Docker)
|
# 5. 检查宿主机与容器 node 版本一致性(防止 native 模块 ABI 不兼容)
|
||||||
|
HOST_NODE_VER=$(ssh txjp "node -v 2>/dev/null | cut -d. -f1 | tr -d 'v'" 2>/dev/null || echo "0")
|
||||||
|
if [[ "$HOST_NODE_VER" != "" && "$HOST_NODE_VER" != "20" ]]; then
|
||||||
|
warn "宿主机 node 版本 (v${HOST_NODE_VER}) 与容器内 node:20-alpine 不一致,native 模块(如 bcrypt)可能出现 ABI 不兼容"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. 服务器上直接 npm run build(不重建 Docker)
|
||||||
log "服务器上执行 npm run build..."
|
log "服务器上执行 npm run build..."
|
||||||
build_start=$(date +%s)
|
build_start=$(date +%s)
|
||||||
# 使用 tee 避免管道导致退出码丢失,并过滤无用输出
|
# 使用 tee 避免管道导致退出码丢失,并过滤无用输出
|
||||||
ssh txjp "cd ${REMOTE_DIR} && npm run build 2>&1" | \
|
ssh txjp "cd ${REMOTE_DIR} && npm run build 2>&1" | \
|
||||||
grep -vE "^(info|warn|npm warn|audited|packages|funding|vulnerability|npm notice|New major)" | \
|
grep -vE "^(info|warn|npm warn|audited|packages|funding|vulnerability|npm notice|New major)" | \
|
||||||
tail -15
|
tail -50
|
||||||
build_exit_code=${PIPESTATUS[0]}
|
build_exit_code=${PIPESTATUS[0]} # PIPESTATUS 为 bash 内建变量,脚本已声明 #!/usr/bin/env bash
|
||||||
build_end=$(date +%s)
|
build_end=$(date +%s)
|
||||||
build_dur=$((build_end - build_start))
|
build_dur=$((build_end - build_start))
|
||||||
timing "构建耗时: ${build_dur}s"
|
timing "构建耗时: ${build_dur}s"
|
||||||
|
|
@ -542,26 +634,46 @@ build_on_server() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 静态导出站点(www):next build 产出 out/,移至 html/ 供 nginx 托管
|
||||||
|
if [[ "$SITE" == "www" ]]; then
|
||||||
|
ssh txjp "cd ${REMOTE_DIR} && rm -rf html && mv out html 2>/dev/null" || {
|
||||||
|
error "静态导出目录迁移失败(out/ → html/)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
info "静态导出已迁移至 html/"
|
||||||
|
fi
|
||||||
|
|
||||||
# 清理 standalone 中的 musl 原生模块(Alpine builder 残留,Debian runner 不兼容)
|
# 清理 standalone 中的 musl 原生模块(Alpine builder 残留,Debian runner 不兼容)
|
||||||
if [[ "$SITE" == "issue" || "$SITE" == "assets" || "$SITE" == "oa" || "$SITE" == "monitor" ]]; then
|
if [[ "$SITE" == "www" || "$SITE" == "issue" || "$SITE" == "assets" || "$SITE" == "oa" || "$SITE" == "monitor" ]]; then
|
||||||
ssh txjp "rm -rf $REMOTE_DIR/.next/standalone/node_modules/@img/sharp-linuxmusl-x64 \
|
ssh txjp "rm -rf $REMOTE_DIR/.next/standalone/node_modules/@img/sharp-linuxmusl-x64 \
|
||||||
$REMOTE_DIR/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64 \
|
$REMOTE_DIR/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64 \
|
||||||
$REMOTE_DIR/.next/standalone/node_modules/@next/swc-linux-x64-musl 2>/dev/null; echo '已清理 musl 残留'"
|
$REMOTE_DIR/.next/standalone/node_modules/@next/swc-linux-x64-musl 2>/dev/null; echo '已清理 musl 残留'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 6. 重建容器(应用 .next 卷挂载;若存在 Dockerfile 则 --build 确保原生模块兼容;
|
# 6. 同步 docker-compose.yml(确保服务器 compose 与本地一致)
|
||||||
|
local compose_src="$(echo "$LOCAL_DIR" | sed 's|/src$||')"
|
||||||
|
scp "$compose_src/docker-compose.yml" "txjp:$REMOTE_DIR/docker-compose.yml" 2>/dev/null || warn "docker-compose.yml 上传失败"
|
||||||
|
|
||||||
|
# 7. 重建容器(应用 .next 卷挂载;若存在 Dockerfile 则 --build 确保原生模块兼容;
|
||||||
# 无论有无 Dockerfile,restart 确保 Next.js 进程重新加载 .next)
|
# 无论有无 Dockerfile,restart 确保 Next.js 进程重新加载 .next)
|
||||||
log "重建容器..."
|
log "重建容器..."
|
||||||
local build_flag=""
|
local build_flag=""
|
||||||
ssh txjp "test -f $REMOTE_DIR/Dockerfile" 2>/dev/null && build_flag="--build"
|
if ssh txjp "test -f $REMOTE_DIR/Dockerfile" 2>/dev/null; then
|
||||||
|
build_flag="--build"
|
||||||
|
else
|
||||||
|
info "无 Dockerfile,跳过镜像重建"
|
||||||
|
fi
|
||||||
|
|
||||||
# Docker 无法跟随指向构建上下文外的符号链接,临时将 shared 符号链接替换为实际目录
|
# Docker 无法跟随指向构建上下文外的符号链接,临时将 shared 符号链接替换为实际目录
|
||||||
ssh txjp "cd $REMOTE_DIR && if [ -L shared ]; then
|
ssh txjp "cd $REMOTE_DIR && if [ -L shared ]; then
|
||||||
target=\$(readlink shared)
|
target=\$(readlink shared)
|
||||||
rm shared
|
cp -r \$target shared_tmp && rm shared && mv shared_tmp shared && \
|
||||||
cp -r \$target shared
|
echo 'shared 符号链接已临时替换为实际目录' || \
|
||||||
echo 'shared 符号链接已临时替换为实际目录'
|
{ echo '错误: shared 符号链接替换失败'; exit 1; }
|
||||||
fi"
|
fi" || {
|
||||||
|
error "shared 符号链接替换失败,中止部署"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
ssh txjp "cd $REMOTE_DIR && docker compose up -d $build_flag && docker compose restart"
|
ssh txjp "cd $REMOTE_DIR && docker compose up -d $build_flag && docker compose restart"
|
||||||
|
|
||||||
|
|
@ -575,7 +687,7 @@ build_on_server() {
|
||||||
# 7. 清理构建产生的悬空镜像和缓存(根据磁盘使用率动态调整)
|
# 7. 清理构建产生的悬空镜像和缓存(根据磁盘使用率动态调整)
|
||||||
log "清理构建缓存..."
|
log "清理构建缓存..."
|
||||||
# 清理悬空镜像(无标签或未被引用的中间层)
|
# 清理悬空镜像(无标签或未被引用的中间层)
|
||||||
ssh txjp "docker image prune -f 2>/dev/null || true"
|
ssh txjp "docker image prune -f 2>/dev/null" || warn "docker image prune 失败"
|
||||||
|
|
||||||
# 根据磁盘使用率动态调整构建缓存清理策略
|
# 根据磁盘使用率动态调整构建缓存清理策略
|
||||||
DISK_USAGE=$(ssh txjp "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50")
|
DISK_USAGE=$(ssh txjp "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50")
|
||||||
|
|
@ -583,7 +695,7 @@ build_on_server() {
|
||||||
warn "磁盘使用率 ${DISK_USAGE}%,执行紧急清理"
|
warn "磁盘使用率 ${DISK_USAGE}%,执行紧急清理"
|
||||||
ssh txjp "docker builder prune -f 2>/dev/null || true"
|
ssh txjp "docker builder prune -f 2>/dev/null || true"
|
||||||
# 截断容器日志(占磁盘大户)
|
# 截断容器日志(占磁盘大户)
|
||||||
ssh txjp "truncate -s 0 /var/lib/docker/containers/*/\*-json.log 2>/dev/null || true"
|
ssh txjp "truncate -s 0 /var/lib/docker/containers/\$(docker inspect --format '{{.Id}}' ${CONTAINER} 2>/dev/null)/\*-json.log 2>/dev/null || true"
|
||||||
elif [ "$DISK_USAGE" -gt 85 ]; then
|
elif [ "$DISK_USAGE" -gt 85 ]; then
|
||||||
info "磁盘使用率 ${DISK_USAGE}%,执行激进清理(6小时)"
|
info "磁盘使用率 ${DISK_USAGE}%,执行激进清理(6小时)"
|
||||||
ssh txjp "docker builder prune -f --filter 'until=6h' 2>/dev/null || true"
|
ssh txjp "docker builder prune -f --filter 'until=6h' 2>/dev/null || true"
|
||||||
|
|
@ -596,7 +708,7 @@ build_on_server() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 8. 保存快照
|
# 8. 保存快照
|
||||||
ssh txjp "echo '$local_md5' > ${SNAPSHOT_FILE}"
|
ssh txjp "mkdir -p ${SNAPSHOT_DIR} && echo '$local_md5' > ${SNAPSHOT_FILE}"
|
||||||
|
|
||||||
# 9. 清理远程临时文件(源码包、连通性检查脚本)
|
# 9. 清理远程临时文件(源码包、连通性检查脚本)
|
||||||
ssh txjp "rm -f /tmp/${SITE}.tar.gz /tmp/shared-deploy.tar.gz /tmp/check-conn.js /tmp/.env.prod.template 2>/dev/null || true"
|
ssh txjp "rm -f /tmp/${SITE}.tar.gz /tmp/shared-deploy.tar.gz /tmp/check-conn.js /tmp/.env.prod.template 2>/dev/null || true"
|
||||||
|
|
@ -608,20 +720,26 @@ build_on_server() {
|
||||||
echo ""
|
echo ""
|
||||||
case "$SITE" in
|
case "$SITE" in
|
||||||
www)
|
www)
|
||||||
build_on_server ;;
|
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
|
||||||
|
build_on_server || exit 1 ;;
|
||||||
cloud)
|
cloud)
|
||||||
info "上传静态文件..."
|
info "上传静态文件..."
|
||||||
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html 2>/dev/null || true
|
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "cloud index.html 上传失败"; exit 1; }
|
||||||
|
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
|
||||||
ssh txjp "docker restart ${SITE}-ai" ;;
|
ssh txjp "docker restart ${SITE}-ai" ;;
|
||||||
token)
|
token)
|
||||||
info "上传静态文件..."
|
info "上传静态文件..."
|
||||||
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html 2>/dev/null || true
|
scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "token index.html 上传失败"; exit 1; }
|
||||||
|
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
|
||||||
ssh txjp "docker restart ${SITE}-ai" ;;
|
ssh txjp "docker restart ${SITE}-ai" ;;
|
||||||
issue)
|
issue)
|
||||||
build_on_server "http://assets-ai:3000/api" || exit 1
|
build_on_server "http://assets-ai:3000/api" || exit 1
|
||||||
info "部署 nginx 路由..."
|
info "部署 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 -s reload 2>/dev/null || true"
|
docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload"
|
||||||
|
|
||||||
# 验证 issue→assets API 连通性(带重试,应对同时部署时目标容器暂不可达)
|
# 验证 issue→assets API 连通性(带重试,应对同时部署时目标容器暂不可达)
|
||||||
info "检查 issue→assets API 连通性..."
|
info "检查 issue→assets API 连通性..."
|
||||||
|
|
@ -647,16 +765,16 @@ tryConnect();
|
||||||
CHECKEOF
|
CHECKEOF
|
||||||
CONN_CHECK=$(ssh txjp "docker cp /tmp/check-conn.js issue-ai:/tmp/check-conn.js && docker exec issue-ai sh -c 'cd /app && NODE_PATH=/app/node_modules node /tmp/check-conn.js'" 2>&1)
|
CONN_CHECK=$(ssh txjp "docker cp /tmp/check-conn.js issue-ai:/tmp/check-conn.js && docker exec issue-ai sh -c 'cd /app && NODE_PATH=/app/node_modules node /tmp/check-conn.js'" 2>&1)
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "issue→assets API 连通性检查失败!请检查 ASSETS_API_KEY 是否在 assets-ai 中注册"
|
warn "issue→assets API 连通性检查失败!站点已正常部署,请检查 ASSETS_API_KEY 是否在 assets-ai 中注册"
|
||||||
echo " $CONN_CHECK"
|
echo " $CONN_CHECK"
|
||||||
exit 1
|
else
|
||||||
fi
|
log "issue→assets API 连通性正常"
|
||||||
log "issue→assets API 连通性正常" ;;
|
fi ;;
|
||||||
assets)
|
assets)
|
||||||
build_on_server || exit 1
|
build_on_server || exit 1
|
||||||
info "部署 nginx 路由..."
|
info "部署 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 -s reload 2>/dev/null || true"
|
docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload"
|
||||||
|
|
||||||
# 验证 assets→issue API 连通性(带重试,应对同时部署时目标容器暂不可达)
|
# 验证 assets→issue API 连通性(带重试,应对同时部署时目标容器暂不可达)
|
||||||
info "检查 assets→issue API 连通性..."
|
info "检查 assets→issue API 连通性..."
|
||||||
|
|
@ -682,13 +800,23 @@ tryConnect();
|
||||||
CHECKEOF
|
CHECKEOF
|
||||||
CONN_CHECK=$(ssh txjp "docker cp /tmp/check-conn.js assets-ai:/tmp/check-conn.js && docker exec assets-ai sh -c 'cd /app && NODE_PATH=/app/node_modules node /tmp/check-conn.js'" 2>&1)
|
CONN_CHECK=$(ssh txjp "docker cp /tmp/check-conn.js assets-ai:/tmp/check-conn.js && docker exec assets-ai sh -c 'cd /app && NODE_PATH=/app/node_modules node /tmp/check-conn.js'" 2>&1)
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
error "assets→issue API 连通性检查失败!请检查 ISSUE_API_KEY 是否在 issue-ai 中注册"
|
warn "assets→issue API 连通性检查失败!站点已正常部署,请检查 ISSUE_API_KEY 是否在 issue-ai 中注册"
|
||||||
echo " $CONN_CHECK"
|
echo " $CONN_CHECK"
|
||||||
exit 1
|
else
|
||||||
fi
|
log "assets→issue API 连通性正常"
|
||||||
log "assets→issue API 连通性正常" ;;
|
fi ;;
|
||||||
oa)
|
oa)
|
||||||
build_on_server || exit 1
|
build_on_server || exit 1
|
||||||
|
# 注入 LLDAP admin 密码(OA OIDC callback 需要验证 LLDAP 用户存在性)
|
||||||
|
info "注入 LLDAP admin 密码到 OA .env..."
|
||||||
|
ssh txjp "
|
||||||
|
LLDAP_ADMIN_PASS=\$(docker exec lldap printenv LLDAP_ADMIN_PASSWORD 2>/dev/null || echo '')
|
||||||
|
if [ -n \"\$LLDAP_ADMIN_PASS\" ]; then
|
||||||
|
grep -q '^LLDAP_ADMIN_PASSWORD=' $REMOTE_DIR/.env 2>/dev/null \
|
||||||
|
&& sed -i \"s|^LLDAP_ADMIN_PASSWORD=.*|LLDAP_ADMIN_PASSWORD=\$LLDAP_ADMIN_PASS|\" $REMOTE_DIR/.env \
|
||||||
|
|| echo \"LLDAP_ADMIN_PASSWORD=\$LLDAP_ADMIN_PASS\" >> $REMOTE_DIR/.env
|
||||||
|
fi
|
||||||
|
"
|
||||||
info "部署 nginx 配置..."
|
info "部署 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"
|
||||||
|
|
@ -701,28 +829,38 @@ CHECKEOF
|
||||||
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 && \
|
||||||
if [ ! -f .env ]; then \
|
if [ ! -f .env ]; then \
|
||||||
LLDAP_JWT=\$(docker exec lldap printenv LLDAP_JWT_SECRET 2>/dev/null) && \
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^lldap\$'; then \
|
||||||
LLDAP_PASS=\$(docker exec lldap printenv LLDAP_LDAP_USER_PASS 2>/dev/null) && \
|
LLDAP_JWT=\$(docker exec lldap printenv LLDAP_JWT_SECRET 2>/dev/null || echo '') && \
|
||||||
LLDAP_ADMIN=\$(docker exec lldap printenv LLDAP_ADMIN_PASSWORD 2>/dev/null) && \
|
LLDAP_PASS=\$(docker exec lldap printenv LLDAP_LDAP_USER_PASS 2>/dev/null || echo '') && \
|
||||||
printf '%s\n' \
|
LLDAP_ADMIN=\$(docker exec lldap printenv LLDAP_ADMIN_PASSWORD 2>/dev/null || echo '') ; \
|
||||||
\"LLDAP_JWT_SECRET=\$LLDAP_JWT\" \
|
else \
|
||||||
\"LLDAP_LDAP_USER_PASS=\$LLDAP_PASS\" \
|
LLDAP_JWT=\$(openssl rand -hex 32) ; \
|
||||||
\"LLDAP_LDAP_BASE_DN=dc=tlyq,dc=ai\" \
|
LLDAP_PASS=\$(openssl rand -hex 16) ; \
|
||||||
\"LLDAP_HTTP_PORT=17170\" \
|
LLDAP_ADMIN=\$(openssl rand -hex 16) ; \
|
||||||
\"LLDAP_LDAP_PORT=3890\" \
|
echo '首次部署,已生成随机密码' ; \
|
||||||
\"LLDAP_DATABASE_PATH=/data/users.db\" \
|
fi && \
|
||||||
\"LLDAP_ADMIN_PASSWORD=\$LLDAP_ADMIN\" \
|
printf '%s\n' \
|
||||||
> .env && \
|
\"LLDAP_JWT_SECRET=\$LLDAP_JWT\" \
|
||||||
fi && \
|
\"LLDAP_LDAP_USER_PASS=\$LLDAP_PASS\" \
|
||||||
sed -i '/3890:3890/d' docker-compose.yml 2>/dev/null || true && \
|
\"LLDAP_LDAP_BASE_DN=dc=tlyq,dc=ai\" \
|
||||||
docker compose up -d --build && docker compose restart"
|
\"LLDAP_HTTP_PORT=17170\" \
|
||||||
log "LLDAP 已部署" ;;
|
\"LLDAP_LDAP_PORT=3890\" \
|
||||||
gitea)
|
\"LLDAP_DATABASE_PATH=/data/users.db\" \
|
||||||
info "部署 Gitea..."
|
\"LLDAP_ADMIN_PASSWORD=\$LLDAP_ADMIN\" \
|
||||||
scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml
|
> .env && \
|
||||||
ssh txjp "cd $REMOTE_DIR && docker compose up -d && docker restart nginx-ai"
|
fi && \
|
||||||
log "Gitea 已部署" ;;
|
chmod 600 .env && \
|
||||||
|
sed -i '/3890:3890/d' docker-compose.yml 2>/dev/null || true && \
|
||||||
|
docker compose up -d --build && docker compose restart"
|
||||||
|
log "LLDAP 已部署" ;;
|
||||||
|
gitea)
|
||||||
|
info "部署 Gitea..."
|
||||||
|
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
|
||||||
|
ssh txjp "cd $REMOTE_DIR && docker compose up -d && docker restart nginx-ai"
|
||||||
|
log "Gitea 已部署" ;;
|
||||||
sso)
|
sso)
|
||||||
deploy_sso ;;
|
deploy_sso ;;
|
||||||
monitor)
|
monitor)
|
||||||
|
|
@ -732,6 +870,61 @@ CHECKEOF
|
||||||
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" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 运行时合规检查(LESSONS-LEARNED)
|
||||||
|
# ============================================================
|
||||||
|
echo ""
|
||||||
|
info "运行时合规检查..."
|
||||||
|
|
||||||
|
# LL #7: SSL 证书覆盖目标域名(仅 tlyq.ai 站点)
|
||||||
|
if [[ "$SITE" != "ldap" ]]; then
|
||||||
|
CERT_DOMAIN=$(ssh txjp "openssl x509 -in /etc/letsencrypt/live/www.tlyq.ai/fullchain.pem -noout -text 2>/dev/null | grep 'DNS:'" 2>/dev/null || echo "")
|
||||||
|
if echo "$CERT_DOMAIN" | grep -q "${SITE}.tlyq.ai"; then
|
||||||
|
log "SSL 证书覆盖 ${SITE}.tlyq.ai"
|
||||||
|
else
|
||||||
|
warn "SSL 证书可能未覆盖 ${SITE}.tlyq.ai(LESSONS-LEARNED #7)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# LL #20: nginx 重载后确认可用
|
||||||
|
if ssh txjp "docker exec nginx-ai nginx -t 2>&1" 2>/dev/null | grep -q "successful"; then
|
||||||
|
log "nginx 配置检查通过"
|
||||||
|
else
|
||||||
|
warn "nginx 配置检查未通过,请检查 upstream 容器状态(LESSONS-LEARNED #20/#21)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# LL #22/#23: 数据库文件可访问(仅含 DB 站点)
|
||||||
|
case "$SITE" in
|
||||||
|
issue|assets|oa|monitor|gitea)
|
||||||
|
DB_PATH=""
|
||||||
|
case "$SITE" in
|
||||||
|
issue) DB_PATH="/app/data/issue.db" ;;
|
||||||
|
assets) DB_PATH="/app/data/assets.db" ;;
|
||||||
|
oa) DB_PATH="/data/other-sites/assets/assets.db /data/other-sites/issue/issue.db" ;;
|
||||||
|
monitor) DB_PATH="/app/data/monitor.db" ;;
|
||||||
|
gitea) DB_PATH="/data/gitea.db" ;;
|
||||||
|
esac
|
||||||
|
for db in $DB_PATH; do
|
||||||
|
if ssh txjp "docker exec ${CONTAINER} test -f $db" 2>/dev/null; then
|
||||||
|
log "数据库文件 $db 可访问"
|
||||||
|
else
|
||||||
|
warn "数据库文件 $db 不可访问(LESSONS-LEARNED #22/#23)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 构建新鲜度检查:编译产物必须新于源码(自动适配所有 Next.js 站点)
|
||||||
|
if ssh txjp "test -f /root/docker/${REMOTE_DIR}/.next/standalone/server.js" 2>/dev/null; then
|
||||||
|
BUILD_MTIME=$(ssh txjp "stat -c %Y /root/docker/${REMOTE_DIR}/.next/standalone/server.js" 2>/dev/null || echo 0)
|
||||||
|
SRC_MTIME=$(ssh txjp "find /root/docker/${REMOTE_DIR}/src -type f \( -name '*.ts' -o -name '*.tsx' \) -exec stat -c %Y {} + 2>/dev/null | sort -rn | head -1" 2>/dev/null || echo 0)
|
||||||
|
if [ "$BUILD_MTIME" -lt "$SRC_MTIME" ] 2>/dev/null; then
|
||||||
|
warn "构建产物旧于源码!请确保 npm run build 已重新执行"
|
||||||
|
else
|
||||||
|
log "构建新鲜度检查通过"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 验证
|
# 验证
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -757,8 +950,7 @@ esac
|
||||||
if [[ "$SITE" == "ldap" ]]; then
|
if [[ "$SITE" == "ldap" ]]; then
|
||||||
STATUS=$(ssh txjp "docker exec lldap wget -q -O - http://127.0.0.1:17170 > /dev/null 2>&1 && echo 200 || echo 000" 2>/dev/null || echo "???")
|
STATUS=$(ssh txjp "docker exec lldap wget -q -O - http://127.0.0.1:17170 > /dev/null 2>&1 && echo 200 || echo 000" 2>/dev/null || echo "???")
|
||||||
else
|
else
|
||||||
# -k: 服务器本地使用自签名证书,必须跳过 TLS 验证
|
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}' -k '$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"
|
log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 $URL"
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,15 @@ else
|
||||||
sudo npm run build
|
sudo npm run build
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 构建新鲜度检查
|
||||||
|
BUILD_MTIME=$(ssh "$REMOTE_HOST" "stat -c %Y /root/docker/monitor-ai/.next/standalone/server.js" 2>/dev/null || echo 0)
|
||||||
|
SRC_MTIME=$(ssh "$REMOTE_HOST" "find /root/docker/monitor-ai/src -type f \( -name '*.ts' -o -name '*.tsx' \) -exec stat -c %Y {} + 2>/dev/null | sort -rn | head -1" 2>/dev/null || echo 0)
|
||||||
|
if [ "$BUILD_MTIME" -lt "$SRC_MTIME" ] 2>/dev/null; then
|
||||||
|
echo "⚠️ 构建产物旧于源码!请确保 npm run build 已重新执行"
|
||||||
|
else
|
||||||
|
echo "✅ 构建新鲜度检查通过"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[4/5] 配置首次部署..."
|
echo "[4/5] 配置首次部署..."
|
||||||
if [ "$MODE" = "remote" ]; then
|
if [ "$MODE" = "remote" ]; then
|
||||||
# 生成密钥
|
# 生成密钥
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue