Keep FastAPI as the only backend; add media upload, ntfy hook, and PB collection bootstrap for production. Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
6.2 KiB
Python
228 lines
6.2 KiB
Python
"""
|
|
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 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"
|
|
|
|
|
|
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 dev1
|
|
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 .last_deploy_sha
|
|
git checkout -B dev1 origin/dev1
|
|
git reset --hard origin/dev1
|
|
|
|
# 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)
|
|
)
|
|
|
|
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())
|