From 293939000d1c9f5407a8ee94039574cc0496fc32 Mon Sep 17 00:00:00 2001 From: chenpei Date: Thu, 30 Jul 2026 18:16:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=87=AA=E5=8A=A8=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E4=BB=A3=E7=90=86=E4=B8=8A=E6=B8=B8=E6=BC=82?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/git-incremental-deploy.ps1 | 49 ++++++++++++++- scripts/local-proxy-reconcile.sh | 95 ++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100755 scripts/local-proxy-reconcile.sh diff --git a/scripts/git-incremental-deploy.ps1 b/scripts/git-incremental-deploy.ps1 index 42d8d61..1f1f65a 100644 --- a/scripts/git-incremental-deploy.ps1 +++ b/scripts/git-incremental-deploy.ps1 @@ -446,6 +446,9 @@ test -f Dockerfile || fail "Dockerfile missing after Git sync" test -f package.json || fail "package.json missing after Git sync" test -f "app/api/uploads/[...path]/route.ts" || fail "app/api/uploads fallback route missing" test -f "app/uploads/[...path]/route.ts" || fail "app/uploads fallback route missing" +test -f "scripts/local-proxy-reconcile.sh" || fail "local proxy reconciler missing" + +PROXY_RECONCILE_SCRIPT_B64="$(base64 -w 0 scripts/local-proxy-reconcile.sh)" ok "Synced to commit $COMMIT_SHA" log "Image tag: $COMMIT_TAG" @@ -573,6 +576,9 @@ PROXY_CONF="${PROXY_ROOT}/nginx.conf" PROXY_PID="${PROXY_ROOT}/nginx.pid" PROXY_SERVICE="${CONTAINER_NAME}-local-proxy.service" CURRENT_FILE="${PROXY_ROOT}/current-container" +PROXY_RECONCILE_SCRIPT="${PROXY_ROOT}/reconcile-upstream.sh" +PROXY_RECONCILE_SERVICE="${CONTAINER_NAME}-local-proxy-reconcile.service" +PROXY_RECONCILE_TIMER="${CONTAINER_NAME}-local-proxy-reconcile.timer" BLUE_CONTAINER="${CONTAINER_NAME}-blue" GREEN_CONTAINER="${CONTAINER_NAME}-green" PUBLIC_TRAFFIC_SYNC_ROOT="/mnt/PoolOne/apps/youyidian-3d/public-traffic-sync" @@ -802,6 +808,46 @@ EOF systemctl enable "$PROXY_SERVICE" >/dev/null 2>&1 || true } +install_proxy_reconciler() { + mkdir -p "$PROXY_ROOT" + printf '%s' "$PROXY_RECONCILE_SCRIPT_B64" | base64 -d > "$PROXY_RECONCILE_SCRIPT" + chmod 700 "$PROXY_RECONCILE_SCRIPT" + + cat > "/etc/systemd/system/${PROXY_RECONCILE_SERVICE}" < "/etc/systemd/system/${PROXY_RECONCILE_TIMER}" </dev/null 2>&1 || true + systemctl restart "$PROXY_RECONCILE_TIMER" +} + switch_proxy() { ensure_proxy_service if is_proxy_running; then @@ -913,6 +959,7 @@ if ! curl -fsS --max-time 8 "http://127.0.0.1:${PORT}/api/health" >/tmp/git-depl fi printf '%s\n' "$NEXT_CONTAINER" > "$CURRENT_FILE" +install_proxy_reconciler ok "Stable local entry is healthy on port $PORT" docker logs --tail 20 "$NEXT_CONTAINER" 2>&1 || true @@ -950,7 +997,7 @@ PROD_SCRIPT export_script="$(printf '%s' "$PROD_PAYLOAD" | base64 -w 0)" ssh -o StrictHostKeyChecking=no "$PROD_SERVER_USER@$PROD_SERVER_IP" \ - "TARGET='$TARGET' CONTAINER_NAME='$CONTAINER_NAME' PORT='$PORT' REGISTRY_COMMIT='$REGISTRY_COMMIT' COMMIT_SHA='$COMMIT_SHA' ENV_FILE_PATH='/mnt/PoolOne/apps/youyidian-3d/$ENV_FILE' PUBLIC_APP_URL='$PUBLIC_APP_URL' CORS_ALLOWED_ORIGINS='$CORS_ALLOWED_ORIGINS' DINGTALK_CALLBACK_ORIGIN='$DINGTALK_CALLBACK_ORIGIN' DINGTALK_INTERNAL_RETURN_ORIGINS='$DINGTALK_INTERNAL_RETURN_ORIGINS' S3_PUBLIC_DOMAIN='$S3_PUBLIC_DOMAIN' DOWNLOAD_TOKEN_SECRET='$DOWNLOAD_TOKEN_SECRET' NETWORK_NAME='youyidian-3d_app-network' UPLOADS_MOUNT='/mnt/PoolOne/apps/youyidian-3d/uploads' ROLLBACK_ON_FAILURE='$ROLLBACK_ON_FAILURE' bash -lc \"printf '%s' '$export_script' | base64 -d | bash\"" + "TARGET='$TARGET' CONTAINER_NAME='$CONTAINER_NAME' PORT='$PORT' REGISTRY_COMMIT='$REGISTRY_COMMIT' COMMIT_SHA='$COMMIT_SHA' ENV_FILE_PATH='/mnt/PoolOne/apps/youyidian-3d/$ENV_FILE' PUBLIC_APP_URL='$PUBLIC_APP_URL' CORS_ALLOWED_ORIGINS='$CORS_ALLOWED_ORIGINS' DINGTALK_CALLBACK_ORIGIN='$DINGTALK_CALLBACK_ORIGIN' DINGTALK_INTERNAL_RETURN_ORIGINS='$DINGTALK_INTERNAL_RETURN_ORIGINS' S3_PUBLIC_DOMAIN='$S3_PUBLIC_DOMAIN' DOWNLOAD_TOKEN_SECRET='$DOWNLOAD_TOKEN_SECRET' PROXY_RECONCILE_SCRIPT_B64='$PROXY_RECONCILE_SCRIPT_B64' NETWORK_NAME='youyidian-3d_app-network' UPLOADS_MOUNT='/mnt/PoolOne/apps/youyidian-3d/uploads' ROLLBACK_ON_FAILURE='$ROLLBACK_ON_FAILURE' bash -lc \"printf '%s' '$export_script' | base64 -d | bash\"" ok "Production deployment command completed" if [ "$TARGET" = "production" ]; then diff --git a/scripts/local-proxy-reconcile.sh b/scripts/local-proxy-reconcile.sh new file mode 100755 index 0000000..9191a0f --- /dev/null +++ b/scripts/local-proxy-reconcile.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTAINER_NAME="${CONTAINER_NAME:?CONTAINER_NAME is required}" +PORT="${PORT:?PORT is required}" +PROXY_ROOT="${PROXY_ROOT:?PROXY_ROOT is required}" +PROXY_SERVICE="${PROXY_SERVICE:?PROXY_SERVICE is required}" + +CURRENT_FILE="${PROXY_ROOT}/current-container" +PROXY_CONF="${PROXY_ROOT}/nginx.conf" +PROXY_PID="${PROXY_ROOT}/nginx.pid" +LOCK_FILE="/run/${CONTAINER_NAME}-local-proxy-reconcile.lock" + +log() { + printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" +} + +exec 9>"$LOCK_FILE" +if command -v flock >/dev/null 2>&1; then + flock -n 9 || exit 0 +fi + +if [ ! -s "$CURRENT_FILE" ] || [ ! -s "$PROXY_CONF" ]; then + log "WARN" "proxy state files are missing" + exit 1 +fi + +current_container="$(tr -d '\r\n' < "$CURRENT_FILE")" +if [ -z "$current_container" ]; then + log "WARN" "current container is empty" + exit 1 +fi + +running="$(docker inspect -f '{{.State.Running}}' "$current_container" 2>/dev/null || true)" +current_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$current_container" 2>/dev/null || true)" +if [ "$running" != "true" ] || [ -z "$current_ip" ]; then + log "WARN" "current container is unavailable: ${current_container}" + exit 1 +fi + +if ! curl -fsS --max-time 8 "http://${current_ip}:3000/api/health" >/dev/null; then + log "WARN" "current container health check failed: ${current_container} ${current_ip}:3000" + exit 1 +fi + +configured_ip="$( + sed -nE 's/^[[:space:]]*server ([0-9.]+):3000 .*/\1/p' "$PROXY_CONF" | + head -n 1 +)" + +entry_ok=0 +if curl -fsS --max-time 8 "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then + entry_ok=1 +fi + +if [ "$configured_ip" = "$current_ip" ] && [ "$entry_ok" = "1" ]; then + exit 0 +fi + +backup="${PROXY_CONF}.bak.reconcile.$(date +%Y%m%d-%H%M%S)" +temp_conf="$(mktemp "${PROXY_CONF}.tmp.XXXXXX")" +trap 'rm -f "$temp_conf"' EXIT + +if [ "$configured_ip" != "$current_ip" ]; then + cp -a "$PROXY_CONF" "$backup" + sed -E \ + "0,/server [0-9.]+:3000 /s//server ${current_ip}:3000 /" \ + "$PROXY_CONF" > "$temp_conf" + + if ! grep -q "server ${current_ip}:3000 " "$temp_conf"; then + log "ERROR" "failed to update nginx upstream" + exit 1 + fi + + /usr/sbin/nginx -t -p "${PROXY_ROOT}/" -c "$temp_conf" + mv "$temp_conf" "$PROXY_CONF" +fi + +if ! systemctl reload "$PROXY_SERVICE"; then + if [ -f "$backup" ]; then + cp -a "$backup" "$PROXY_CONF" + /usr/sbin/nginx -t -p "${PROXY_ROOT}/" -c "$PROXY_CONF" >/dev/null + systemctl reload "$PROXY_SERVICE" || true + fi + log "ERROR" "failed to reload ${PROXY_SERVICE}" + exit 1 +fi + +sleep 1 +if ! curl -fsS --max-time 8 "http://127.0.0.1:${PORT}/api/health" >/dev/null; then + log "ERROR" "stable local entry is still unhealthy after reconciliation" + exit 1 +fi + +log "OK" "upstream reconciled: ${configured_ip:-missing} -> ${current_ip}"