""" Fast incremental deploy for nomadro. - Pulls latest origin/dev1 - Rebuilds only services touched since last deploy - Uses BuildKit cache mounts (npm/pip) - Single remote critical section under flock (no concurrent collide) - 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[:200].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[-5000:]) if err.strip(): print("LOG:", err[-2000:]) 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__" COMPOSE="__COMPOSE__" export DOMAIN \ NEXT_PUBLIC_API_URL="https://${DOMAIN}/api/v1" \ NEXT_PUBLIC_SITE_URL="https://${DOMAIN}" \ CORS_ORIGINS="https://${DOMAIN},http://${DOMAIN}" \ DOCKER_BUILDKIT=1 \ COMPOSE_DOCKER_CLI_BUILD=1 \ BUILDKIT_PROGRESS=plain cd "$REMOTE" git remote set-url origin "$GIT" git fetch --depth=80 origin git checkout -B dev1 origin/dev1 git reset --hard origin/dev1 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 '^docker-compose\.prod\.yml$' && 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 echo "MODE=$MODE" echo "NEW=$NEW" echo "OLD=${OLD:-none}" case "$MODE" in noop) echo "No app changes — skip build" ;; frontend) docker compose -f "$COMPOSE" build frontend docker compose -f "$COMPOSE" up -d --no-deps frontend ;; backend) docker compose -f "$COMPOSE" build backend docker compose -f "$COMPOSE" up -d --no-deps backend ;; full) docker compose -f "$COMPOSE" build docker compose -f "$COMPOSE" up -d ;; esac ok=0 for i in $(seq 1 25); 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 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" echo "$NEW" > "$MARKER" [ "$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("__COMPOSE__", COMPOSE) ) # Upload and run as one locked remote job 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})") return 0 if __name__ == "__main__": raise SystemExit(main())