#!/usr/bin/env python3 """Deploy frontend (+ accounting) to production; preserve .env; install npm deps.""" from __future__ import annotations import base64 import os import sys import tarfile from pathlib import Path import paramiko ROOT = Path(__file__).resolve().parents[1] APP_HOST, APP_USER, APP_PASS = "192.168.10.162", "morteza", "Moli@5404" REMOTE = "/home/morteza/torbatyar" EXCLUDE_DIRS = { ".git", "node_modules", ".next", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".idea", ".vscode", } EXCLUDE_FILES = {".DS_Store", "Thumbs.db", ".env"} def connect(): c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) c.connect(APP_HOST, username=APP_USER, password=APP_PASS, timeout=30, allow_agent=False, look_for_keys=False) return c def run(c, cmd, *, sudo=False, timeout=900): cmd = cmd.replace("\r\n", "\n").replace("\r", "\n").strip() + "\n" b64 = base64.b64encode(cmd.encode()).decode() if sudo: full = f"echo {APP_PASS!r} | sudo -S -p '' bash -c 'echo {b64} | base64 -d | bash'" else: full = f"bash -lc 'echo {b64} | base64 -d | bash'" print("\n>>>", cmd[:220].replace("\n", " | ")) _, out, err = c.exec_command(full, timeout=timeout, get_pty=True) text = (out.read() + err.read()).decode("utf-8", "replace") code = out.channel.recv_exit_status() print(text[-5000:].encode("ascii", "replace").decode("ascii")) return code, text def make_tarball() -> Path: out = ROOT / "scripts" / "_deploy_frontend_bundle.tar.gz" print(f"Creating {out} ...") with tarfile.open(out, "w:gz") as tar: for dirpath, dirnames, filenames in os.walk(ROOT): p = Path(dirpath) dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS and not d.startswith(".cursor")] for name in filenames: fp = p / name rel = fp.relative_to(ROOT) if any(part in EXCLUDE_DIRS for part in rel.parts): continue if rel.name in EXCLUDE_FILES or rel.suffix in {".pyc", ".log"}: continue if rel.as_posix().startswith("scripts/_deploy"): continue tar.add(fp, arcname=str(rel).replace("\\", "/")) print(f"Size: {out.stat().st_size / 1024 / 1024:.1f} MB") return out def main() -> int: tarball = make_tarball() c = connect() try: sftp = c.open_sftp() print("Uploading...") sftp.put(str(tarball), "/tmp/torbatyar_fe_bundle.tar.gz") sftp.close() code, _ = run( c, f""" set -e cd {REMOTE} cp -a .env /tmp/torbatyar.env.bak tar -xzf /tmp/torbatyar_fe_bundle.tar.gz -C {REMOTE} cp /tmp/torbatyar.env.bak .env rm -f /tmp/torbatyar_fe_bundle.tar.gz # keep public HTTPS urls sed -i 's|^NEXT_PUBLIC_IDENTITY_API_URL=.*|NEXT_PUBLIC_IDENTITY_API_URL=https://identity.torbatyar.ir|' .env sed -i 's|^NEXT_PUBLIC_BACKEND_URL=.*|NEXT_PUBLIC_BACKEND_URL=https://api.torbatyar.ir|' .env sed -i 's|^NEXT_PUBLIC_API_BASE_URL=.*|NEXT_PUBLIC_API_BASE_URL=https://api.torbatyar.ir|' .env sed -i 's|^NEXT_PUBLIC_KEYCLOAK_URL=.*|NEXT_PUBLIC_KEYCLOAK_URL=https://auth.torbatyar.ir|' .env sed -i 's|^NEXT_PUBLIC_ACCOUNTING_API_URL=.*|NEXT_PUBLIC_ACCOUNTING_API_URL=https://accounting.torbatyar.ir|' .env grep -E '^NEXT_PUBLIC_' .env """, sudo=True, timeout=180, ) if code != 0: raise RuntimeError("extract failed") code, _ = run( c, f""" set -e cd {REMOTE} docker compose up -d --build accounting-service frontend # install any new npm deps into named volume docker compose exec -T frontend npm install docker compose restart frontend sleep 12 docker compose ps frontend accounting-service docker compose exec -T frontend printenv NEXT_PUBLIC_ACCOUNTING_API_URL curl -sf http://127.0.0.1:8002/health echo curl -s -o /dev/null -w 'accounting=%{{http_code}}\\n' http://127.0.0.1:3000/accounting curl -s -o /dev/null -w 'fiscal=%{{http_code}}\\n' http://127.0.0.1:3000/accounting/fiscal """, sudo=True, timeout=1800, ) if code != 0: raise RuntimeError("compose/npm failed") finally: c.close() print("\n=== DEPLOY DONE ===") print("https://torbatyar.ir/accounting") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except Exception as exc: print(f"FATAL: {exc}", file=sys.stderr) raise