Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets. Co-authored-by: Cursor <cursoragent@cursor.com>
191 lines
6.0 KiB
Python
191 lines
6.0 KiB
Python
"""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",
|
|
]
|
|
|
|
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())
|