""" Fast incremental deploy for nomadro. - Pulls latest origin/dev1 - Rebuilds only services touched since last deploy - Uses BuildKit cache mounts (npm/pip) - Polls health instead of fixed long sleep """ 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" COMPOSE = "docker-compose.prod.yml" def run(client: paramiko.SSHClient, cmd: str, timeout: int = 900) -> tuple[int, str, str]: print("\n>>", cmd[:180]) _, 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[-5000:]) if err.strip(): # build logs often go to stderr print("LOG:", err[-2500:]) print("exit:", code) return code, out, err 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) env = ( f"DOMAIN={DOMAIN} " f"NEXT_PUBLIC_API_URL=https://{DOMAIN}/api/v1 " f"NEXT_PUBLIC_SITE_URL=https://{DOMAIN} " f"CORS_ORIGINS=https://{DOMAIN},http://{DOMAIN} " "DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 " "BUILDKIT_PROGRESS=plain" ) code, _, _ = run( client, f"cd {REMOTE} && git remote set-url origin {GIT} && " f"git fetch --depth=50 origin && " f"git checkout -B dev1 origin/dev1 && " f"git reset --hard origin/dev1", ) if code != 0: client.close() return code # Decide what changed vs last successful deploy detect = f""" set -e cd {REMOTE} NEW=$(git rev-parse HEAD) OLD=$(cat {MARKER} 2>/dev/null || true) if [ -z "$OLD" ] || ! git cat-file -e "$OLD^{{commit}}" 2>/dev/null; then echo "MODE=full" echo "NEW=$NEW" exit 0 fi CHANGED=$(git diff --name-only "$OLD" "$NEW" || true) echo "OLD=$OLD" echo "NEW=$NEW" echo "CHANGED_BEGIN" echo "$CHANGED" echo "CHANGED_END" FE=0; BE=0 echo "$CHANGED" | grep -qE '^frontend/' && FE=1 || true echo "$CHANGED" | grep -qE '^backend/' && BE=1 || true echo "$CHANGED" | grep -qE '^docker-compose\\.prod\\.yml$' && FE=1 && BE=1 || true if [ -z "$CHANGED" ]; then echo "MODE=noop" elif [ "$FE" = 1 ] && [ "$BE" = 0 ]; then echo "MODE=frontend" elif [ "$BE" = 1 ] && [ "$FE" = 0 ]; then echo "MODE=backend" elif [ "$FE" = 1 ] || [ "$BE" = 1 ]; then echo "MODE=full" else echo "MODE=noop" fi """ code, out, _ = run(client, detect, timeout=60) if code != 0: client.close() return code mode = "full" new_sha = "" for line in out.splitlines(): if line.startswith("MODE="): mode = line.split("=", 1)[1].strip() if line.startswith("NEW="): new_sha = line.split("=", 1)[1].strip() print(f"\nDeploy mode: {mode}") if mode == "noop": print("No changes since last deploy — skip build") elif mode == "frontend": code, _, _ = run( client, f"cd {REMOTE} && export {env} && " f"docker compose -f {COMPOSE} build frontend && " f"docker compose -f {COMPOSE} up -d --no-deps frontend", timeout=900, ) elif mode == "backend": code, _, _ = run( client, f"cd {REMOTE} && export {env} && " f"docker compose -f {COMPOSE} build backend && " f"docker compose -f {COMPOSE} up -d --no-deps backend", timeout=600, ) else: code, _, _ = run( client, f"cd {REMOTE} && export {env} && " f"docker compose -f {COMPOSE} build && " f"docker compose -f {COMPOSE} up -d", timeout=900, ) if code != 0: client.close() return code # Fast health poll (max ~20s) instead of fixed sleep 12 health = f""" ok=0 for i in $(seq 1 20); 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" echo "$api" | grep -q '"status":"ok"' && [ "$web" = "200" ] && ok=1 && break sleep 1 done docker ps --format 'table {{{{.Names}}}}\\t{{{{.Status}}}}\\t{{{{.Ports}}}}' | grep nomadflow || true curl -skI https://{DOMAIN} | head -6 curl -sk https://{DOMAIN}/api/v1/health [ "$ok" = 1 ] """ code, _, _ = run(client, health, timeout=60) if code != 0: client.close() return code if new_sha: run(client, f"echo {new_sha} > {MARKER}", timeout=20) client.close() elapsed = int(time.time() - t0) print(f"\nDeploy OK in {elapsed}s (mode={mode})") return 0 if __name__ == "__main__": raise SystemExit(main())