update
This commit is contained in:
+3
-2
@@ -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 后台任务触发测试通过。
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+67
-36
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user