- 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)
215 lines
7.6 KiB
Python
215 lines
7.6 KiB
Python
"""部署:从 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()
|