""" Native (non-Docker) deploy for nomadro. Preferred path: git pull → npm/pip build → systemctl restart Keeps Caddy → 127.0.0.1:3055 / 8055 unchanged. PocketBase is expected as a host process on :8090. """ from __future__ import annotations import os import sys import time import paramiko sys.stdout.reconfigure(encoding="utf-8", errors="replace") HOST = "107.173.30.245" USER = "root" PASS = "Xiao4669805" GIT = "https://eric:Xiao4669805@gitea.dsx2020.com/eric/nomadweb.git" DOMAIN = "nomadweb.nomadro.com" REMOTE = "/opt/nomadweb" MARKER = f"{REMOTE}/.last_deploy_sha" BRANCH = os.environ.get("DEPLOY_BRANCH", "dev2") def run(client: paramiko.SSHClient, cmd: str, timeout: int = 900) -> tuple[int, str, str]: print("\n>>", cmd[:220].replace("\n", " ")) _, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode("utf-8", errors="replace") err = stderr.read().decode("utf-8", errors="replace") code = stdout.channel.recv_exit_status() if out.strip(): print(out[-6000:]) if err.strip(): print("LOG:", err[-2500:]) print("exit:", code) return code, out, err REMOTE_SCRIPT = r""" set -euo pipefail exec 9>/var/lock/nomadro-deploy.lock if ! flock -n 9; then echo "LOCK_BUSY" exit 75 fi DOMAIN="__DOMAIN__" GIT="__GIT__" REMOTE="__REMOTE__" MARKER="__MARKER__" cd "$REMOTE" git remote set-url origin "$GIT" git fetch --depth=80 origin __BRANCH__ # Force-align to origin/__BRANCH__ even if prior SFTP hotfixes left dirty trees git checkout -f -B __BRANCH__ origin/__BRANCH__ git reset --hard origin/__BRANCH__ git clean -fd \ -e deploy/nomadro-api.env \ -e backend/app/data/user_store.json \ -e backend/app/data/social_store.json \ -e backend/app/data/community_store.json \ -e backend/app/data/platform_store.json \ -e .last_deploy_sha # Install / refresh systemd units install -m 644 "$REMOTE/deploy/systemd/nomadro-web.service" /etc/systemd/system/nomadro-web.service install -m 644 "$REMOTE/deploy/systemd/nomadro-api.service" /etc/systemd/system/nomadro-api.service systemctl daemon-reload systemctl enable nomadro-web nomadro-api >/dev/null 2>&1 || true # Stop Docker app containers if they still hold ports (keep other stacks) if command -v docker >/dev/null 2>&1; then docker stop nomadflow-web nomadflow-api 2>/dev/null || true docker rm nomadflow-web nomadflow-api 2>/dev/null || true # optional: stop unused compose pb if host pb already serves :8090 docker stop nomadflow-pb 2>/dev/null || true fi NEW=$(git rev-parse HEAD) OLD=$(cat "$MARKER" 2>/dev/null || true) MODE=full if [ -n "$OLD" ] && git cat-file -e "${OLD}^{commit}" 2>/dev/null; then CHANGED=$(git diff --name-only "$OLD" "$NEW" || true) FE=0; BE=0 echo "$CHANGED" | grep -qE '^frontend/' && FE=1 || true echo "$CHANGED" | grep -qE '^backend/' && BE=1 || true echo "$CHANGED" | grep -qE '^deploy/systemd/' && FE=1 && BE=1 || true if [ -z "$CHANGED" ]; then MODE=noop elif [ "$FE" = 1 ] && [ "$BE" = 0 ]; then MODE=frontend elif [ "$BE" = 1 ] && [ "$FE" = 0 ]; then MODE=backend elif [ "$FE" = 1 ] || [ "$BE" = 1 ]; then MODE=full else MODE=noop fi fi # First native boot or missing venv/node_modules → full [ -d "$REMOTE/backend/.venv" ] || MODE=full [ -d "$REMOTE/frontend/node_modules" ] || MODE=full [ -d "$REMOTE/frontend/.next" ] || MODE=full echo "MODE=$MODE" echo "NEW=$NEW" echo "OLD=${OLD:-none}" 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 build_frontend() { cd "$REMOTE/frontend" if [ ! -d node_modules ] || echo "${CHANGED:-}" | grep -qE '^frontend/package(-lock)?\.json$'; then npm ci --legacy-peer-deps --include=dev --prefer-offline fi npm run build # Prepare standalone runtime layout for systemd 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 systemctl restart nomadro-web } build_backend() { cd "$REMOTE/backend" if [ ! -d .venv ]; then python3 -m venv .venv fi if [ ! -f .venv/.reqsha ] || ! cmp -s requirements.txt .venv/.reqsha 2>/dev/null; then .venv/bin/pip install -q -r requirements.txt cp requirements.txt .venv/.reqsha fi systemctl restart nomadro-api # Ensure PocketBase collections exist (idempotent) .venv/bin/python scripts/init_pb_collections.py || echo "PB init skipped" } CHANGED="${CHANGED:-}" case "$MODE" in noop) echo "No app changes — ensure services up" systemctl start nomadro-api nomadro-web ;; frontend) build_frontend systemctl start nomadro-api ;; backend) build_backend systemctl start nomadro-web ;; full) build_backend build_frontend ;; esac ok=0 for i in $(seq 1 30); do web=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3055/ || true) api=$(curl -s http://127.0.0.1:8055/api/v1/health || true) echo "try=$i web=$web api=$api" if echo "$api" | grep -q '"status":"ok"' && [ "$web" = "200" ]; then ok=1 break fi sleep 1 done systemctl --no-pager --full status nomadro-web nomadro-api | sed -n '1,40p' || true ss -lntp | grep -E '3055|8055|8090' || true curl -skI "https://${DOMAIN}" | head -6 curl -sk "https://${DOMAIN}/api/v1/health" echo "$NEW" > "$MARKER" echo "DEPLOY_ENGINE=native" [ "$ok" = 1 ] """ def main() -> int: t0 = time.time() client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(HOST, username=USER, password=PASS, timeout=30) script = ( REMOTE_SCRIPT .replace("__DOMAIN__", DOMAIN) .replace("__GIT__", GIT) .replace("__REMOTE__", REMOTE) .replace("__MARKER__", MARKER) .replace("__BRANCH__", BRANCH) ) code, out, _ = run( client, f"cat > /tmp/nomadro-deploy.sh <<'EOF'\n{script}\nEOF\nbash /tmp/nomadro-deploy.sh", timeout=900, ) mode = "unknown" for line in out.splitlines(): if line.startswith("MODE="): mode = line.split("=", 1)[1].strip() client.close() elapsed = int(time.time() - t0) if code == 75: print("Deploy aborted: another deploy is in progress") return 75 if code != 0: print(f"\nDeploy FAILED in {elapsed}s (mode={mode})") return code print(f"\nDeploy OK in {elapsed}s (mode={mode}, engine=native)") return 0 if __name__ == "__main__": raise SystemExit(main())