"""Fast deploy: api-only (~1 min) or web rebuild without npm install (~8 min).""" from __future__ import annotations import argparse import sys import time from pathlib import Path import paramiko sys.stdout.reconfigure(encoding="utf-8", errors="replace") HOST = "107.173.30.245" USER = "root" PASS = "Xiao4669805" REMOTE = "/opt/nomadweb" DOMAIN = "nomadweb.nomadro.com" API_FILES = [ "backend/app/main.py", "backend/app/schemas.py", "backend/app/routers/api.py", "backend/app/routers/social.py", "backend/app/routers/community.py", "backend/app/routers/payment.py", "backend/app/routers/auth.py", "backend/app/services/auth.py", "backend/app/services/social_store.py", "backend/app/services/community_store.py", "backend/app/services/payment_providers.py", "backend/app/services/payment_service.py", "backend/app/services/meetup_live.py", "backend/app/data/community_data.py", "backend/app/data/social_profiles.py", "backend/app/data/mock_data.py", "backend/app/data/platform_data.py", "backend/app/data/digital_content.py", "backend/app/data/city_details_data.py", ] WEB_FILES = [ "frontend/src/app/globals.css", "frontend/src/app/sitemap.ts", "frontend/src/components/Navbar.tsx", "frontend/src/components/Footer.tsx", "frontend/src/components/GlobalSearch.tsx", "frontend/src/components/SiteShell.tsx", "frontend/src/components/DatingClient.tsx", "frontend/src/components/DatingLikesClient.tsx", "frontend/src/components/ChatClient.tsx", "frontend/src/components/MeetupsClient.tsx", "frontend/src/components/MeetupsHostClient.tsx", "frontend/src/components/CommunityHubClient.tsx", "frontend/src/components/CommunityNewClient.tsx", "frontend/src/components/DiscussionDetailClient.tsx", "frontend/src/components/GigsClient.tsx", "frontend/src/components/PricingClient.tsx", "frontend/src/components/NotificationsClient.tsx", "frontend/src/components/MemberProfileClient.tsx", "frontend/src/lib/api.ts", "frontend/src/lib/types.ts", "frontend/src/lib/i18n/dictionaries.ts", "frontend/src/app/dating/likes/page.tsx", "frontend/src/app/gigs/page.tsx", "frontend/src/app/pricing/page.tsx", "frontend/src/app/notifications/page.tsx", "frontend/src/app/members/[id]/page.tsx", "frontend/src/app/community/new/page.tsx", "frontend/src/app/meetups/host/page.tsx", ] RESTART_API = """ set -euo pipefail systemctl restart nomadro-api for i in $(seq 1 20); do api=$(curl -s http://127.0.0.1:8055/api/v1/health || true) echo "try=$i $api" echo "$api" | grep -q '"status":"ok"' && exit 0 sleep 1 done exit 1 """ REBUILD_WEB = f""" set -euo pipefail exec 9>/var/lock/nomadro-deploy.lock if ! flock -n 9; then echo LOCK_BUSY; exit 75; fi export NEXT_TELEMETRY_DISABLED=1 export NEXT_PUBLIC_API_URL=https://{DOMAIN}/api/v1 export NEXT_PUBLIC_SITE_URL=https://{DOMAIN} export NODE_ENV=production cd {REMOTE}/frontend echo BUILD_START npm run build mkdir -p .next/standalone/.next rm -rf .next/standalone/public .next/standalone/.next/static cp -a public .next/standalone/public cp -a .next/static .next/standalone/.next/static rm -rf .next/standalone/content mkdir -p .next/standalone/content cp -a content .next/standalone/content systemctl restart nomadro-web for i in $(seq 1 30); do web=$(curl -s -o /dev/null -w '%{{http_code}}' http://127.0.0.1:3055/ || true) echo "try=$i web=$web" [ "$web" = "200" ] && exit 0 sleep 1 done exit 1 """ def ensure_dir(sftp: paramiko.SFTPClient, path: str) -> None: parts = path.strip("/").split("/") cur = "" for p in parts: cur += "/" + p try: sftp.stat(cur) except FileNotFoundError: try: sftp.mkdir(cur) except OSError: pass def upload(files: list[str], root: Path) -> paramiko.SSHClient: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) print("Connecting...", flush=True) client.connect(HOST, username=USER, password=PASS, timeout=30) sftp = client.open_sftp() n = len(files) for i, rel in enumerate(files, 1): local = root / rel if not local.exists(): print(f"[{i}/{n}] SKIP missing {rel}", flush=True) continue remote = f"{REMOTE}/{rel}" print(f"[{i}/{n}] UPLOAD {rel}", flush=True) ensure_dir(sftp, str(Path(remote).parent).replace("\\", "/")) sftp.put(str(local), remote) sftp.close() return client def run_remote(client: paramiko.SSHClient, script: str, timeout: int = 900) -> int: _, stdout, stderr = client.exec_command(script, timeout=timeout) while True: line = stdout.readline() if not line: break print(line.rstrip(), flush=True) err = stderr.read().decode("utf-8", errors="replace") if err.strip(): print("LOG:", err[-2000:], flush=True) return stdout.channel.recv_exit_status() def main() -> int: parser = argparse.ArgumentParser(description="Fast nomadro deploy") parser.add_argument("mode", choices=["api", "web", "all"], help="api=~1min, web=~8min, all=both") args = parser.parse_args() root = Path(__file__).resolve().parents[1] t0 = time.time() if args.mode in ("api", "all"): print("=== API deploy ===", flush=True) client = upload(API_FILES, root) code = run_remote(client, RESTART_API, timeout=60) client.close() if code != 0: print(f"API deploy failed in {int(time.time() - t0)}s") return code print(f"API OK in {int(time.time() - t0)}s", flush=True) if args.mode in ("web", "all"): print("=== WEB deploy (no npm install) ===", flush=True) client = upload(WEB_FILES, root) code = run_remote(client, REBUILD_WEB, timeout=900) client.close() if code == 75: print("Another deploy is running — retry in a minute") return 75 if code != 0: print(f"WEB deploy failed in {int(time.time() - t0)}s") return code print(f"Done in {int(time.time() - t0)}s (mode={args.mode})") return 0 if __name__ == "__main__": raise SystemExit(main())