From 31a7b08f03f8fa01bf16efa45bdcce8b9bf1635c Mon Sep 17 00:00:00 2001 From: wenfp Date: Mon, 27 Jul 2026 12:53:13 +0800 Subject: [PATCH] update --- .env.example | 1 + AGENTS.md | 3 +- app/AGENTS.md | 5 +- app/config.py | 6 +++ app/routers/deploy.py | 103 +++++++++++++++++++++++++++--------------- 5 files changed, 79 insertions(+), 39 deletions(-) diff --git a/.env.example b/.env.example index fa00686..aeffee2 100644 --- a/.env.example +++ b/.env.example @@ -7,5 +7,6 @@ DEPLOY_BRANCH=main DEPLOY_SCRIPT_PATH=/www/wwwroot/next.zizaifutian.com/admin/scripts/deploy_front.sh DEPLOY_COMMAND_TIMEOUT=600 DEPLOY_LOCK_PATH=/tmp/py-web-deploy.lock +DEPLOY_LOG_PATH=/tmp/py-web-deploy.log # 可选:填服务器 `dirname $(which node)` 的结果,解决 webhook 进程拿到旧 Node 的问题。 DEPLOY_NODE_BIN_DIR=/usr/bin diff --git a/AGENTS.md b/AGENTS.md index 4ef1ded..fc02023 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,7 +92,7 @@ pnpm dev - 新增后端业务模块时,优先新增 `app/routers/.py` 和对应 `app/schemas.py` 模型。 - 新增前端业务模块时,优先新增 `front/src/views//`、`front/src/api/.ts`,并在 `front/src/router/index.ts` 注册路由。 - 登录认证已接入,后台路由由前端守卫检查登录状态;认证令牌由后端 HttpOnly Cookie 管理,前端不保存 token。 -- 自动部署接口为 `/deploy/webhook`,生产环境经 Nginx 的 `/api/deploy/webhook` 暴露给 Gitea;部署密钥、项目目录、前端目录、部署分支和脚本路径通过 `.env` 或环境变量配置。 +- 自动部署接口为 `/deploy/webhook`,生产环境经 Nginx 的 `/api/deploy/webhook` 暴露给 Gitea;部署密钥、项目目录、前端目录、部署分支、脚本路径和日志路径通过 `.env` 或环境变量配置。 ## 修改记录约定 @@ -116,3 +116,4 @@ pnpm dev - 2026-07-23:商品模型新增商品描述字段,后端补充 `products.description` 迁移、Pydantic 模型和 CRUD 读写,前端商品 API 类型与商品编辑基础信息表单同步。验证:后端编译通过,前端 `pnpm typecheck` 和 `pnpm build` 通过,运行态接口测试待安装 `bcrypt` 后执行。 - 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,后端增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位,前端订单管理页增加微信预下单入口;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,前端 `pnpm typecheck` 和 `pnpm build` 通过,真实预下单和支付通知待填写微信商户配置后联调。 - 2026-07-27:新增 Gitea 自动部署 webhook,后端 `/deploy/webhook` 校验 Gitea HMAC 签名或部署 token 后执行 `scripts/deploy_front.sh`,脚本负责拉取代码、安装前端依赖并执行 `pnpm build`;新增 `.env.example` 记录部署环境变量。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。 +- 2026-07-27:自动部署 webhook 从同步构建调整为后台构建,接口校验通过后立即返回 `202 accepted`,后台执行结果写入 `DEPLOY_LOG_PATH`,避免 Gitea Delivery 等待前端构建超时。验证:后端编译通过,webhook 后台任务触发测试通过。 diff --git a/app/AGENTS.md b/app/AGENTS.md index 8a69266..694dfab 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -88,8 +88,8 @@ OSS_ENDPOINT、OSS_BUCKET_NAME、OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS - 自动部署接口为 `/deploy/webhook`,生产环境通常通过 Nginx 映射为 `/api/deploy/webhook` 给 Gitea 调用。 - Gitea Webhook 应配置 `Secret`,后端会按原始请求体校验 `X-Gitea-Signature` 的 HMAC-SHA256 签名;也兼容 `X-Hub-Signature-256`。 -- 部署配置通过 `.env` 或环境变量提供:`DEPLOY_WEBHOOK_SECRET`、`DEPLOY_PROJECT_DIR`、`DEPLOY_FRONT_DIR`、`DEPLOY_BRANCH`、`DEPLOY_SCRIPT_PATH`、`DEPLOY_COMMAND_TIMEOUT`、`DEPLOY_LOCK_PATH`;如 webhook 进程拿到旧 Node,可设置 `DEPLOY_NODE_BIN_DIR` 指向新 Node 所在目录。 -- 部署脚本为 `scripts/deploy_front.sh`,负责在服务器项目目录执行 `git pull`、进入前端目录安装依赖并执行 `pnpm build`。 +- 部署配置通过 `.env` 或环境变量提供:`DEPLOY_WEBHOOK_SECRET`、`DEPLOY_PROJECT_DIR`、`DEPLOY_FRONT_DIR`、`DEPLOY_BRANCH`、`DEPLOY_SCRIPT_PATH`、`DEPLOY_COMMAND_TIMEOUT`、`DEPLOY_LOCK_PATH`、`DEPLOY_LOG_PATH`;如 webhook 进程拿到旧 Node,可设置 `DEPLOY_NODE_BIN_DIR` 指向新 Node 所在目录。 +- 部署脚本为 `scripts/deploy_front.sh`,负责在服务器项目目录执行 `git pull`、进入前端目录安装依赖并执行 `pnpm build`;webhook 接口只投递后台任务并立即返回 `202 accepted`,构建结果查看 `DEPLOY_LOG_PATH`。 - webhook 密钥不得写入源码或文档;`.env.example` 只能保留占位值。 ## 验证 @@ -113,3 +113,4 @@ OSS_ENDPOINT、OSS_BUCKET_NAME、OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS - 2026-07-23:商品模型新增 `description` 商品描述字段,后端补充 `products.description` 轻量迁移、Pydantic 模型和 `/products` CRUD 读写。验证:后端编译通过,运行态接口测试待安装 `bcrypt` 后执行。 - 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,真实预下单和通知验签待填写微信商户配置后联调。 - 2026-07-27:新增自动部署 webhook 模块,`/deploy/webhook` 支持 Gitea HMAC 签名校验、部署分支过滤、并发锁和构建脚本超时控制,`scripts/deploy_front.sh` 负责拉取代码并构建前端。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。 +- 2026-07-27:自动部署 webhook 改为后台执行构建,接口在签名校验、分支检查和加锁后立即返回 `202 accepted`,后台任务将构建结果写入部署日志,避免 Gitea Delivery 因等待响应超时。验证:后端编译通过,webhook 后台任务触发测试通过。 diff --git a/app/config.py b/app/config.py index e4f804f..bcc89f8 100644 --- a/app/config.py +++ b/app/config.py @@ -55,6 +55,8 @@ DEPLOY_BRANCH = os.getenv("DEPLOY_BRANCH", "") DEPLOY_SCRIPT_PATH = os.getenv("DEPLOY_SCRIPT_PATH", str(PROJECT_ROOT / "scripts" / "deploy_front.sh")) DEPLOY_COMMAND_TIMEOUT = int(os.getenv("DEPLOY_COMMAND_TIMEOUT", "600")) DEPLOY_LOCK_PATH = os.getenv("DEPLOY_LOCK_PATH", "/tmp/py-web-deploy.lock") +DEPLOY_LOG_PATH = os.getenv("DEPLOY_LOG_PATH", "/tmp/py-web-deploy.log") +DEPLOY_NODE_BIN_DIR = os.getenv("DEPLOY_NODE_BIN_DIR", "") @dataclass(frozen=True) @@ -160,6 +162,8 @@ class DeploySettings: script_path: str command_timeout: int lock_path: str + log_path: str + node_bin_dir: str @property def is_configured(self) -> bool: @@ -174,4 +178,6 @@ deploy_settings = DeploySettings( script_path=DEPLOY_SCRIPT_PATH, command_timeout=DEPLOY_COMMAND_TIMEOUT, lock_path=DEPLOY_LOCK_PATH, + log_path=DEPLOY_LOG_PATH, + node_bin_dir=DEPLOY_NODE_BIN_DIR, ) diff --git a/app/routers/deploy.py b/app/routers/deploy.py index d1b54b4..5a6b402 100644 --- a/app/routers/deploy.py +++ b/app/routers/deploy.py @@ -2,11 +2,12 @@ import hashlib import hmac import os import subprocess +from datetime import datetime from pathlib import Path from typing import Any from urllib.parse import parse_qs -from fastapi import APIRouter, HTTPException, Request, status +from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status from app.config import deploy_settings @@ -113,8 +114,66 @@ def release_lock(fd: int) -> None: pass -@router.post("/webhook") -async def deploy_webhook(request: Request) -> dict[str, Any]: +def append_deploy_log(content: str) -> None: + log_path = Path(deploy_settings.log_path) + log_path.parent.mkdir(parents=True, exist_ok=True) + with log_path.open("a", encoding="utf-8") as file: + file.write(content) + if not content.endswith("\n"): + file.write("\n") + + +def build_deploy_env() -> dict[str, str]: + env = os.environ.copy() + env.update( + { + "DEPLOY_PROJECT_DIR": deploy_settings.project_dir, + "DEPLOY_FRONT_DIR": deploy_settings.front_dir, + "DEPLOY_BRANCH": deploy_settings.branch, + "DEPLOY_NODE_BIN_DIR": deploy_settings.node_bin_dir, + } + ) + return env + + +def run_deploy_task(lock_fd: int) -> None: + started_at = datetime.utcnow().replace(microsecond=0).isoformat() + append_deploy_log(f"\n[{started_at}] deploy started") + try: + result = subprocess.run( + [deploy_settings.script_path], + cwd=deploy_settings.project_dir, + env=build_deploy_env(), + capture_output=True, + text=True, + timeout=deploy_settings.command_timeout, + check=False, + ) + finished_at = datetime.utcnow().replace(microsecond=0).isoformat() + append_deploy_log( + "\n".join( + [ + f"[{finished_at}] deploy finished returncode={result.returncode}", + "--- stdout ---", + output_tail(result.stdout, 20000), + "--- stderr ---", + output_tail(result.stderr, 20000), + "--- end ---", + ] + ) + ) + except subprocess.TimeoutExpired: + finished_at = datetime.utcnow().replace(microsecond=0).isoformat() + append_deploy_log(f"[{finished_at}] deploy timeout after {deploy_settings.command_timeout}s") + except Exception as exc: + finished_at = datetime.utcnow().replace(microsecond=0).isoformat() + append_deploy_log(f"[{finished_at}] deploy error: {exc}") + finally: + release_lock(lock_fd) + + +@router.post("/webhook", status_code=status.HTTP_202_ACCEPTED) +async def deploy_webhook(request: Request, background_tasks: BackgroundTasks) -> dict[str, Any]: body = await request.body() verify_webhook_secret(request, body) @@ -134,38 +193,10 @@ async def deploy_webhook(request: Request) -> dict[str, Any]: ) lock_fd = acquire_lock() - try: - env = os.environ.copy() - env.update( - { - "DEPLOY_PROJECT_DIR": deploy_settings.project_dir, - "DEPLOY_FRONT_DIR": deploy_settings.front_dir, - "DEPLOY_BRANCH": deploy_settings.branch, - } - ) - result = subprocess.run( - [str(script_path)], - cwd=deploy_settings.project_dir, - env=env, - capture_output=True, - text=True, - timeout=deploy_settings.command_timeout, - check=False, - ) - except subprocess.TimeoutExpired as exc: - raise HTTPException( - status_code=status.HTTP_504_GATEWAY_TIMEOUT, - detail=f"部署命令超过 {deploy_settings.command_timeout} 秒未完成", - ) from exc - finally: - release_lock(lock_fd) + background_tasks.add_task(run_deploy_task, lock_fd) - response = { - "status": "success" if result.returncode == 0 else "failed", - "returncode": result.returncode, - "stdout": output_tail(result.stdout), - "stderr": output_tail(result.stderr), + return { + "status": "accepted", + "message": "已接收推送事件,前端构建已在后台开始", + "log_path": deploy_settings.log_path, } - if result.returncode != 0: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=response) - return response