diff --git a/scripts/deploy_update.py b/scripts/deploy_update.py new file mode 100644 index 0000000..560134f --- /dev/null +++ b/scripts/deploy_update.py @@ -0,0 +1,179 @@ +""" +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())