Files
diary-news/scripts/deploy_pull.py
Mavis 8d73f4fb28 fix(llm+worker+deploy): 兼容老 prompt 模板 + 消除 startup_run 日志噪音
- enrichment: 新增 _safe_format (基于 _SafeDict),缺失占位符保留原样不抛 KeyError。
  _enrich_format / _enrich_classify / _enrich_image / _enrich_commentary
  全部走 _safe_format,数据库里老 prompt(不支持 {body})不再让整条 article 卡住。
  复现: 388183 classify 一直 KeyError,enrichment_loop 反复重试它,316 篇全卡在 n/a。
- workers.__main__: startup_run 从 IntervalTrigger(minutes=0) 改成 DateTrigger
  (只跑一次),消除 'maximum number of running instances reached' 刷屏 WARNING。
- deploy_pull: 改 _connect 自动识别 RSA / Ed25519 / ECDSA key(原硬编码 Ed25519Key)
2026-06-08 21:20:43 +08:00

215 lines
7.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""部署:从 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 <sha> # 手动回退
环境变量(覆盖默认值):
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:
# 依次尝试 RSA / Ed25519 / ECDSA(paramiko 5 没有统一入口)
pkey: Any = None
last_err: Exception | None = None
for loader in (paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey):
try:
pkey = loader.from_private_key_file(ssh_key)
break
except Exception as e:
last_err = e
if pkey is None:
raise RuntimeError(f"无法解析 SSH key {ssh_key}: {last_err}")
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()