From 8d2c0855acb9214271e98bb304ab1a3a52d5777e Mon Sep 17 00:00:00 2001 From: Mavis Date: Mon, 8 Jun 2026 14:44:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20=E6=96=B0=E5=A2=9E=20deploy=5F?= =?UTF-8?q?pull.py=20=E8=BF=9C=E7=A8=8B=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E6=8B=89=E5=8F=96/=E5=9B=9E=E6=BB=9A=E5=B7=A5=E5=85=B7(?= =?UTF-8?q?=E5=85=8D=E5=AF=86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy_pull.py | 204 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 scripts/deploy_pull.py diff --git a/scripts/deploy_pull.py b/scripts/deploy_pull.py new file mode 100644 index 0000000..d017e19 --- /dev/null +++ b/scripts/deploy_pull.py @@ -0,0 +1,204 @@ +"""部署:从 Gitea 拉取最新代码到远程服务器(免密)。 + +行为: +- 服务器项目目录不存在?git clone : git fetch + reset --hard +- 拉前记录当前 HEAD(用于失败回滚) +- 失败回滚:git reset --hard <之前 sha> +- 输出 commit hash / log / working tree 状态 / 跟本地 main 对比 + +用法: + python scripts/deploy_pull.py + python scripts/deploy_pull.py --dry-run + python scripts/deploy_pull.py --repo-dir /opt/diary-news + python scripts/deploy_pull.py --rollback # 手动回退 + +环境变量(覆盖默认值): + DEPLOY_HOST, DEPLOY_PORT, DEPLOY_USER, + DEPLOY_REPO_DIR, DEPLOY_REPO_URL, DEPLOY_SSH_KEY +""" +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path + +import paramiko + +# ===== 默认配置(适配当前项目)===== +DEFAULT_HOST = "207.57.129.228" +DEFAULT_PORT = 19717 +DEFAULT_USER = "root" +DEFAULT_REPO_DIR = "/root/diary-news" +DEFAULT_REPO_URL = "http://124.223.26.33:3000/xiaji/diary-news.git" +DEFAULT_SSH_KEY = os.path.expanduser("~/.ssh/id_ed25519") + + +def _run(c: paramiko.SSHClient, cmd: str, timeout: int = 60) -> tuple[int, str, str]: + si, so, se = c.exec_command(cmd, timeout=timeout) + out = so.read().decode(errors="replace") + err = se.read().decode(errors="replace") + rc = so.channel.recv_exit_status() + return rc, out, err + + +def _connect(host: str, port: int, user: str, ssh_key: str) -> paramiko.SSHClient: + pkey = paramiko.Ed25519Key.from_private_key_file(ssh_key) + c = paramiko.SSHClient() + c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + c.connect(host, port=port, username=user, pkey=pkey, timeout=30, + allow_agent=False, look_for_keys=False) + return c + + +def _local_head() -> str: + """本地 HEAD sha(用作同步参考)。""" + try: + r = subprocess.run( + ["git", "rev-parse", "HEAD"], + capture_output=True, text=True, cwd=str(Path(__file__).resolve().parents[1]), + check=True, + ) + return r.stdout.strip() + except Exception as e: + return f"unknown:{e}" + + +def deploy( + host: str = DEFAULT_HOST, + port: int = DEFAULT_PORT, + user: str = DEFAULT_USER, + repo_dir: str = DEFAULT_REPO_DIR, + repo_url: str = DEFAULT_REPO_URL, + ssh_key: str = DEFAULT_SSH_KEY, + branch: str = "main", + dry_run: bool = False, + rollback: str | None = None, +) -> int: + if not Path(ssh_key).exists(): + print(f"ERROR: SSH key 不存在: {ssh_key}", file=sys.stderr) + return 2 + + print(f"=== 连接 {user}@{host}:{port} ===") + c = _connect(host, port, user, ssh_key) + print(f"✓ 已免密登录") + + try: + # 1) 手动回退 + if rollback: + print(f"\n=== 手动回退到 {rollback[:12]} ===") + rc, out, err = _run(c, f"cd {repo_dir} && git reset --hard {rollback}", timeout=30) + if rc != 0: + print(f"✗ 回退失败: {err}") + return 1 + print("✓ 已回退") + rc, head, _ = _run(c, f"cd {repo_dir} && git rev-parse HEAD", timeout=10) + print(f"现在 HEAD: {head.strip()[:12]}") + return 0 + + # 2) 探查:目录在不在? + rc, exists, _ = _run(c, f"test -d {repo_dir}/.git && echo EXISTS || echo MISSING", timeout=10) + action = "pull" if "EXISTS" in exists else "clone" + + # 3) 拉前 HEAD(用于回滚) + before_sha: str | None = None + if action == "pull": + rc, h, _ = _run(c, f"cd {repo_dir} && git rev-parse HEAD", timeout=10) + before_sha = h.strip() + print(f"拉前 HEAD: {before_sha[:12]}") + + if dry_run: + print(f"DRY-RUN: 将要 {action} from {repo_url} → {repo_dir}") + return 0 + + # 4) 拉 + print(f"\n=== {action.upper()} ===") + if action == "clone": + cmd = f"git clone --depth 50 {repo_url} {repo_dir}" + else: + cmd = ( + f"cd {repo_dir} && " + f"git fetch origin {branch} && " + f"git reset --hard origin/{branch}" + ) + rc, out, err = _run(c, cmd, timeout=180) + + if rc != 0: + print(f"✗ {action} 失败 (rc={rc})") + if out.strip(): print(f"stdout:\n{out}") + if err.strip(): print(f"stderr:\n{err}") + # 回滚(只在 pull 时) + if before_sha and action == "pull": + print(f"\n=== 回退到 {before_sha[:12]} ===") + rrc, _, rerr = _run(c, f"cd {repo_dir} && git reset --hard {before_sha}", timeout=30) + if rrc == 0: + print(f"✓ 已回退到 {before_sha[:12]}") + else: + print(f"✗ 回退失败: {rerr}") + return 1 + + print(f"✓ {action} 成功") + if out.strip(): print(out) + + # 5) 拉后状态 + rc, head_sha, _ = _run(c, f"cd {repo_dir} && git rev-parse HEAD", timeout=10) + head_sha = head_sha.strip() + print(f"\n拉后 HEAD: {head_sha[:12]}") + + rc, log, _ = _run(c, f"cd {repo_dir} && git log --oneline -5", timeout=10) + print("--- 最近的 commit ---") + print(log) + + rc, status, _ = _run(c, f"cd {repo_dir} && git status --porcelain", timeout=10) + if status.strip(): + print(f"⚠️ working tree 不干净:\n{status}") + else: + print("✓ working tree 干净") + + # 6) 跟本地 main 对比 + local = _local_head() + print(f"\n=== 同步对比 ===") + print(f"本地 HEAD: {local[:12]}") + print(f"服务器 HEAD: {head_sha[:12]}") + if local.startswith(head_sha): + print("✓ 服务器 == 本地") + else: + print("ℹ️ 服务器跟本地不完全一致(可能本地未推送,或服务器在另一条 commit 上)") + + # 7) 总结 + print(f"\n=== 部署报告 ===") + print(f"服务器: {user}@{host}:{port}") + print(f"项目目录: {repo_dir}") + print(f"动作: {action}") + print(f"最终 HEAD: {head_sha[:12]}") + return 0 + finally: + c.close() + + +def main() -> None: + p = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + p.add_argument("--host", default=os.environ.get("DEPLOY_HOST", DEFAULT_HOST)) + p.add_argument("--port", type=int, default=int(os.environ.get("DEPLOY_PORT", str(DEFAULT_PORT)))) + p.add_argument("--user", default=os.environ.get("DEPLOY_USER", DEFAULT_USER)) + p.add_argument("--repo-dir", default=os.environ.get("DEPLOY_REPO_DIR", DEFAULT_REPO_DIR)) + p.add_argument("--repo-url", default=os.environ.get("DEPLOY_REPO_URL", DEFAULT_REPO_URL)) + p.add_argument("--ssh-key", default=os.environ.get("DEPLOY_SSH_KEY", DEFAULT_SSH_KEY)) + p.add_argument("--branch", default="main") + p.add_argument("--dry-run", action="store_true") + p.add_argument("--rollback", metavar="SHA", help="手动回退到指定 commit") + args = p.parse_args() + sys.exit(deploy( + host=args.host, port=args.port, user=args.user, + repo_dir=args.repo_dir, repo_url=args.repo_url, + ssh_key=args.ssh_key, branch=args.branch, + dry_run=args.dry_run, rollback=args.rollback, + )) + + +if __name__ == "__main__": + main()