Keep ephemeral scripts/_*.py ignored while making one-shot production deploy reproducible from git. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""One-shot deploy to https://cyd.smallzhiyuns.com. See DEPLOY.md."""
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import os
|
|
import sys
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
|
|
# Defaults match DEPLOY.md (overridable via env).
|
|
HOST = os.environ.get("CYD_DEPLOY_HOST", "192.227.152.110")
|
|
USER = os.environ.get("CYD_DEPLOY_USER", "root")
|
|
PASSWORD = os.environ.get("CYD_DEPLOY_PASS", "Leaf448772566@")
|
|
REMOTE_DIR = "/home/dsx2016/docker/cydweb"
|
|
CADDY_DIR = "/home/dsx2016/docker/caddy"
|
|
DOMAIN = "cyd.smallzhiyuns.com"
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
EXCLUDE_DIRS = {"node_modules", ".next", ".git", ".cursor", "__pycache__"}
|
|
|
|
|
|
def should_skip(rel: str) -> bool:
|
|
parts = Path(rel).parts
|
|
if any(p in EXCLUDE_DIRS for p in parts):
|
|
return True
|
|
if rel.replace("\\", "/").startswith("scripts/_"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def make_tarball() -> bytes:
|
|
buf = io.BytesIO()
|
|
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
|
for path in ROOT.rglob("*"):
|
|
if not path.is_file():
|
|
continue
|
|
rel = path.relative_to(ROOT).as_posix()
|
|
if should_skip(rel):
|
|
continue
|
|
tar.add(path, arcname=rel)
|
|
return buf.getvalue()
|
|
|
|
|
|
def main() -> None:
|
|
if not PASSWORD:
|
|
raise SystemExit("Set CYD_DEPLOY_PASS (see DEPLOY.md)")
|
|
|
|
print(f"Deploy target: {USER}@{HOST} -> {REMOTE_DIR} ({DOMAIN})")
|
|
print("Creating archive…")
|
|
data = make_tarball()
|
|
print(f"Archive size: {len(data) / 1024 / 1024:.1f} MB")
|
|
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
client.connect(HOST, username=USER, password=PASSWORD, timeout=30)
|
|
|
|
def run(cmd: str, timeout: int = 600) -> str:
|
|
print(f"$ {cmd}")
|
|
stdin, 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()
|
|
text = (out + err).encode("ascii", errors="replace").decode("ascii")
|
|
if text.strip():
|
|
print(text)
|
|
if code != 0:
|
|
raise RuntimeError(f"cmd failed ({code}): {cmd}")
|
|
return out
|
|
|
|
run(f"mkdir -p {REMOTE_DIR}")
|
|
sftp = client.open_sftp()
|
|
remote_tar = "/tmp/cydweb-deploy.tar.gz"
|
|
print(f"Uploading to {remote_tar}…")
|
|
with sftp.file(remote_tar, "wb") as rf:
|
|
rf.write(data)
|
|
sftp.close()
|
|
|
|
run(f"find {REMOTE_DIR} -mindepth 1 -maxdepth 1 -exec rm -rf {{}} +")
|
|
run(f"tar -xzf {remote_tar} -C {REMOTE_DIR} && rm -f {remote_tar}")
|
|
run(f"find {REMOTE_DIR}/firmware -name firmware.bin | wc -l")
|
|
run(f"find {REMOTE_DIR}/firmware -name preview.html | wc -l")
|
|
|
|
run(f"cd {REMOTE_DIR} && docker compose build", timeout=1800)
|
|
run(f"cd {REMOTE_DIR} && docker compose up -d")
|
|
run(
|
|
f"cd {CADDY_DIR} && docker compose exec -T caddy "
|
|
f"caddy reload --config /etc/caddy/Caddyfile || true"
|
|
)
|
|
run("sleep 4; curl -sI http://127.0.0.1:3020/en | head -15")
|
|
run(f"curl -sI https://{DOMAIN}/en | head -20")
|
|
run(
|
|
f"curl -s https://{DOMAIN}/en | grep -o 'umami.dsx2020.com/script.js' | head -3 || true"
|
|
)
|
|
|
|
client.close()
|
|
print(f"DONE — https://{DOMAIN}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|