# GHCS v0.2 Step2 · deploy_router.py · 一键部署代码

## 文件1 · `backend/deploy_router.py`（新建）

```python
"""
GHCS v0.2 - deploy router
一键部署：git pull → install deps → restart service
"""

import os
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from auth import verify_token

logger = logging.getLogger("ghcs.deploy")
router = APIRouter(prefix="/api/deploy", tags=["deploy"])

SSH_KEY_PATH = os.getenv("GHCS_SSH_KEY", os.path.expanduser("~/.ssh/id_rsa"))

GHCS_SERVER = {
    "host": "43.139.207.172",
    "port": 22,
    "username": "root",
    "key_filename": SSH_KEY_PATH,
}

# 可部署的服务白名单
DEPLOY_WHITELIST = {
    "console-api": {
        "name": "GHCS console-api",
        "server": GHCS_SERVER,
        "repo_dir": "/root/guanghu-dev/ghcs-mono",
        "steps": [
            "cd /root/guanghu-dev/ghcs-mono && git pull origin main",
            "cd /root/guanghu-dev/ghcs-mono/backend && source venv/bin/activate && pip install -r requirements.txt -q",
            "systemctl restart console-api",
        ],
        "verify_cmd": "systemctl is-active console-api",
    },
}

def _ssh_exec(server_config, command, timeout=60):
    """SSH远程执行命令"""
    import paramiko
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        client.connect(
            hostname=server_config["host"],
            port=server_config["port"],
            username=server_config["username"],
            key_filename=server_config["key_filename"],
            timeout=10,
        )
        stdin, stdout, stderr = client.exec_command(command, timeout=timeout)
        exit_code = stdout.channel.recv_exit_status()
        return {
            "stdout": stdout.read().decode("utf-8").strip(),
            "stderr": stderr.read().decode("utf-8").strip(),
            "exit_code": exit_code,
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"SSH error: {str(e)}")
    finally:
        client.close()

@router.post("/{service_key}")
async def deploy_service(service_key: str, username: str = Depends(verify_token)):
    """一键部署：git pull → install → restart"""
    if service_key not in DEPLOY_WHITELIST:
        raise HTTPException(status_code=404, detail=f"unknown service: {service_key}")

    svc = DEPLOY_WHITELIST[service_key]
    logger.info(f"[DEPLOY] user={username} service={service_key} time={datetime.now().isoformat()}")

    results = []
    for i, step in enumerate(svc["steps"]):
        logger.info(f"[DEPLOY STEP {i+1}] {step[:80]}")
        result = _ssh_exec(svc["server"], step, timeout=120)
        results.append({
            "step": i + 1,
            "command": step[:80],
            "stdout": result["stdout"][-500:] if result["stdout"] else "",
            "stderr": result["stderr"][-500:] if result["stderr"] else "",
            "exit_code": result["exit_code"],
        })
        if result["exit_code"] != 0:
            logger.error(f"[DEPLOY FAILED] step={i+1} stderr={result['stderr']}")
            return {
                "service": service_key,
                "success": False,
                "failed_step": i + 1,
                "steps": results,
                "timestamp": datetime.now().isoformat(),
            }

    # 验证服务状态
    verify = _ssh_exec(svc["server"], svc["verify_cmd"])
    status = verify["stdout"]

    logger.info(f"[DEPLOY OK] service={service_key} status={status}")
    return {
        "service": service_key,
        "success": True,
        "status_after": status,
        "steps": results,
        "timestamp": datetime.now().isoformat(),
    }

@router.get("/{service_key}/check")
async def check_deploy_status(service_key: str, username: str = Depends(verify_token)):
    """检查服务器代码是否落后于 Gitea"""
    if service_key not in DEPLOY_WHITELIST:
        raise HTTPException(status_code=404, detail=f"unknown service: {service_key}")

    svc = DEPLOY_WHITELIST[service_key]
    repo_dir = svc["repo_dir"]

    result = _ssh_exec(
        svc["server"],
        f"cd {repo_dir} && git fetch origin main -q 2>&1 && echo 'LOCAL:' && git log --oneline -1 HEAD && echo 'REMOTE:' && git log --oneline -1 origin/main",
    )
    lines = result["stdout"].split("\n")
    local_commit = ""
    remote_commit = ""
    for i, line in enumerate(lines):
        if line.startswith("LOCAL:") and i + 1 < len(lines):
            local_commit = lines[i + 1]
        if line.startswith("REMOTE:") and i + 1 < len(lines):
            remote_commit = lines[i + 1]

    return {
        "service": service_key,
        "server_commit": local_commit,
        "gitea_commit": remote_commit,
        "needs_deploy": local_commit != remote_commit,
    }
```

---

## 文件2 · `backend/main.py`（改动两处）

### 改动 A · 顶部加一行 import

在 `from servers_router import router as servers_router` 后面加一行：

```python
from deploy_router import router as deploy_router
```

### 改动 B · 挂载 router

在 `app.include_router(servers_router)` 后面加一行：

```python
app.include_router(deploy_router)
```

---

## 接口说明

| 接口 | 方法 | 作用 |
| --- | --- | --- |
| `POST /api/deploy/console-api` | POST | 一键部署 GHCS · git pull → pip install → restart |
| `GET /api/deploy/console-api/check` | GET | 检查服务器代码是否落后于 Gitea |

---

## 部署流程

1. Mac 本地改代码 → `git push origin main`
2. 调 `POST /api/deploy/console-api`
3. 服务器自动执行：`git pull` → `pip install` → `systemctl restart`
4. 返回每一步的执行结果 + 最终服务状态

---

## ⚠️ 注意

- venv 路径假设在 `/root/guanghu-dev/ghcs-mono/backend/venv/`，如果实际路径不同需要改 steps 里的 source 路径
- 当前只支持部署 console-api（GHCS 自身），后续可以扩展 DEPLOY_WHITELIST 加入其他服务