deploy.py
Rama main · réplica de lectura del repositorio original.
# -*- coding: utf-8 -*-
"""
Jambo API — Production Deployment Runner
Executes remote zero-downtime deployment protocol via SSH.
Supports native OpenSSH client or Paramiko fallback.
Usage: python3 deploy.py [vX.Y.Z]
"""
import os
import sys
import io
import subprocess
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
try:
from paramiko import SSHClient, AutoAddPolicy
HAS_PARAMIKO = True
except ImportError:
HAS_PARAMIKO = False
HOST = os.getenv('JAMBO_HOST', 'folina.o2switch.net')
USER = os.getenv('JAMBO_USER', 'gupi7723')
PASS = os.getenv('JAMBO_PASS')
KEY_FILE = os.getenv('JAMBO_SSH_KEY', '/root/.ssh/id_ed25519' if os.path.exists('/root/.ssh/id_ed25519') else None)
REMOTE = os.getenv('JAMBO_REMOTE_DIR', '/home/gupi7723/api.jambostack.site')
def run_command_subprocess(cmd):
print(f"\n>> Executing: {cmd}")
if PASS and not (KEY_FILE and os.path.exists(KEY_FILE or '')):
ssh_cmd = [
"sshpass", "-p", PASS,
"ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=15",
]
else:
ssh_cmd = [
"ssh",
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=15",
]
if KEY_FILE and os.path.exists(KEY_FILE):
ssh_cmd.extend(["-i", KEY_FILE])
ssh_cmd.append(f"{USER}@{HOST}")
ssh_cmd.append(f"cd {REMOTE} && {cmd}")
proc = subprocess.Popen(ssh_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace')
for line in proc.stdout:
print(f" {line.rstrip()}")
proc.wait()
if proc.returncode != 0:
print(f" FAILED with exit code {proc.returncode}")
return proc.returncode
def run_command_paramiko(ssh, cmd, timeout=300):
print(f"\n>> Executing: {cmd}")
_, stdout, stderr = ssh.exec_command(f"cd {REMOTE} && {cmd}", timeout=timeout)
for line in iter(stdout.readline, ""):
print(f" {line.rstrip()}")
exit_code = stdout.channel.recv_exit_status()
if exit_code != 0:
err = stderr.read().decode('utf-8', errors='replace')
print(f" FAILED with exit code {exit_code}")
if err.strip():
print(f" STDERR: {err.strip()}")
return exit_code
return 0
def main():
target_tag = sys.argv[1] if len(sys.argv) > 1 else None
print(f"> Deploying to {USER}@{HOST}:{REMOTE}...")
ssh = None
if HAS_PARAMIKO and PASS:
try:
ssh = SSHClient()
ssh.set_missing_host_key_policy(AutoAddPolicy())
connect_kwargs = {
'hostname': HOST,
'username': USER,
'timeout': 15,
}
if KEY_FILE and os.path.exists(KEY_FILE):
connect_kwargs['key_filename'] = KEY_FILE
elif PASS:
connect_kwargs['password'] = PASS
else:
connect_kwargs['allow_agent'] = True
connect_kwargs['look_for_keys'] = True
ssh.connect(**connect_kwargs)
except Exception as e:
print(f"Paramiko connection failed ({e}), falling back to OpenSSH subprocess...")
ssh = None
def execute_step(cmd):
if ssh is not None:
return run_command_paramiko(ssh, cmd)
return run_command_subprocess(cmd)
steps = [
("Git sync", f"git fetch --all --tags --prune --force && git checkout {target_tag}" if target_tag else "git pull origin main"),
("Composer dependencies", "php -d memory_limit=1G $(command -v composer || echo /usr/local/bin/composer) install --no-dev --prefer-dist --optimize-autoloader --no-interaction --no-scripts"),
("Frontend build", "if command -v npm >/dev/null 2>&1; then npm ci --prefer-offline && npm run build; else echo 'Notice: npm not found in server environment, skipping frontend build.'; fi"),
("Doctrine migrations", "php -d memory_limit=1G bin/console doctrine:migrations:migrate --no-interaction --all-or-nothing"),
("Cache warmup", "php -d memory_limit=1G bin/console cache:clear --no-warmup --env=prod && sleep 2 && php -d memory_limit=1G bin/console cache:warmup --env=prod"),
("File permissions hardening", "chmod 600 .env .env.local .env.backup_* 2>/dev/null || true; chmod 700 var var/cache var/log 2>/dev/null || true; chmod 644 public/uploads/.htaccess 2>/dev/null || true"),
]
def sync_frontend_assets():
local_build = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'public', 'build')
if not os.path.isdir(local_build):
return 0
print("\n--- [Step: Frontend asset synchronization (rsync)] ---")
ssh_opt = "ssh -o BatchMode=yes -o ConnectTimeout=15"
if KEY_FILE and os.path.exists(KEY_FILE):
ssh_opt += f" -i {KEY_FILE}"
rsync_cmd = [
"rsync", "-avz", "--delete",
"-e", ssh_opt,
f"{local_build}/",
f"{USER}@{HOST}:{REMOTE}/public/build/",
]
proc = subprocess.run(rsync_cmd, capture_output=True, text=True)
if proc.returncode == 0:
print(" Frontend assets synchronized successfully.")
else:
print(f" Notice during asset rsync: {proc.stderr.strip() or proc.stdout.strip()}")
return 0
for label, cmd in steps:
print(f"\n--- [Step: {label}] ---")
rc = execute_step(cmd)
if rc != 0:
print(f"\nABORTING: Step '{label}' failed.")
if ssh:
ssh.close()
sys.exit(1)
if label == "Frontend build":
sync_frontend_assets()
print("\n========================================================")
print(" DEPLOYMENT SUCCEEDED!")
print(" Live URL: https://api.jambostack.site")
print("========================================================")
if ssh:
ssh.close()
if __name__ == '__main__':
main()