feat(ci): P2债务清理 + Production部署门禁(PyJWT升级/死代码清理/生产健康检查) #324
@@ -1740,6 +1740,50 @@ jobs:
|
||||
needs: [build-production-api, build-production-worker, build-production-web]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
shell: sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
python3 - <<'INNERPY'
|
||||
import io, os, tarfile, time, urllib.request, urllib.error
|
||||
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
||||
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
||||
last_err = None
|
||||
for attempt in range(5):
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
archive = response.read()
|
||||
break
|
||||
except urllib.error.HTTPError as e:
|
||||
last_err = e
|
||||
if e.code >= 500 and attempt < 4:
|
||||
wait = 2 ** attempt
|
||||
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
raise
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
if attempt < 4:
|
||||
wait = 2 ** attempt
|
||||
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
else:
|
||||
raise last_err
|
||||
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
||||
for member in tar.getmembers():
|
||||
name = member.name
|
||||
if name == root_prefix[:-1]:
|
||||
continue
|
||||
if name.startswith(root_prefix):
|
||||
member.name = name[len(root_prefix):]
|
||||
if member.name:
|
||||
tar.extract(member, '.')
|
||||
INNERPY
|
||||
- name: Install SSH client
|
||||
shell: sh
|
||||
run: |
|
||||
@@ -1797,6 +1841,52 @@ jobs:
|
||||
|
||||
echo "$DEPLOY_B64" | base64 -d | ssh -p 22222 -i "$key_path" "$production_user@$production_host" "IMAGE_TAG='${GITHUB_REF_NAME}' REGISTRY_TOKEN='${REGISTRY_TOKEN}' sh"
|
||||
|
||||
- name: Production smoke test (健康检查门禁)
|
||||
if: success()
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
API_BASE="https://api.xiaoxiajianji.com"
|
||||
WEB_BASE="https://saas.xiaoxiajianji.com"
|
||||
|
||||
echo "=== 生产部署门禁:外部健康检查 ==="
|
||||
echo "等待服务启动稳定(30s)..."
|
||||
sleep 30
|
||||
|
||||
echo "--- Check 1: API health endpoint ---"
|
||||
for i in $(seq 1 20); do
|
||||
HEALTH=$(curl -sf --max-time 10 "${API_BASE}/health") && break
|
||||
echo " Attempt $i/20: not ready yet, waiting 5s..."
|
||||
sleep 5
|
||||
done
|
||||
if [ -z "$HEALTH" ]; then
|
||||
echo "FAIL: API /health unreachable after 100s"
|
||||
echo "生产环境健康检查未通过,部署失败!"
|
||||
echo "(回滚机制待接入,当前需手动回滚)"
|
||||
exit 1
|
||||
fi
|
||||
echo "API health OK: $HEALTH"
|
||||
|
||||
echo "--- Check 2: API login endpoint (expect 401/422) ---"
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -X POST "${API_BASE}/api/v1/auth/login" -H "Content-Type: application/json" -d '{"email":"smoke@test.com","password":"wrong"}')
|
||||
if [ "$HTTP_CODE" != "401" ] && [ "$HTTP_CODE" != "422" ]; then
|
||||
echo "FAIL: login returned HTTP $HTTP_CODE (expected 401 or 422)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Login API OK: HTTP $HTTP_CODE"
|
||||
|
||||
echo "--- Check 3: Web frontend ---"
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${WEB_BASE}/")
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "FAIL: web frontend returned HTTP $HTTP_CODE (expected 200)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Web frontend OK: HTTP $HTTP_CODE"
|
||||
|
||||
echo ""
|
||||
echo "=== ✅ 生产环境健康检查全部通过 ==="
|
||||
echo "Version: ${GITHUB_REF_NAME}"
|
||||
|
||||
- name: Notify CI success
|
||||
if: success()
|
||||
shell: sh
|
||||
|
||||
Executable → Regular
-1
@@ -45,7 +45,6 @@ from packages.application.template.use_cases import (
|
||||
CreateTemplateUseCase,
|
||||
DeleteCategoryUseCase,
|
||||
DeleteTemplateUseCase,
|
||||
GetTemplateUsageUseCase,
|
||||
GetTemplateUseCase,
|
||||
ListCategoriesUseCase,
|
||||
ListTagsUseCase,
|
||||
|
||||
Executable → Regular
+1
-2
@@ -28,7 +28,6 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from video_processing.chroma_key_engine import apply_chroma_key_if_needed
|
||||
from video_processing.color_grade_engine import ColorGradeConfig, ColorGradeEngine
|
||||
from video_processing.ffmpeg_utils import (
|
||||
DEFAULT_FPS,
|
||||
@@ -46,7 +45,7 @@ from video_processing.render_audio import RenderContext, merge_audio_video, mix_
|
||||
from video_processing.render_subtitles import generate_ass_subtitles
|
||||
from video_processing.reverse_engine import ReverseConfig, ReverseEngine
|
||||
from video_processing.speed_engine import SpeedConfig, SpeedEngine
|
||||
from video_processing.sticker_engine import StickerEngine, parse_stickers_from_config
|
||||
from video_processing.sticker_engine import StickerEngine
|
||||
from video_processing.subtitle_generator import generate_ass_from_timeline
|
||||
from video_processing.transition_engine import TransitionEngine
|
||||
from video_processing.trim_engine import TrimConfig, TrimEngine, extract_trim_from_clip_config
|
||||
|
||||
@@ -179,7 +179,7 @@ class AssetAnalyzer:
|
||||
self._video_info = info
|
||||
return info
|
||||
|
||||
def extract_frames(self, count: int = 10, max_frames: int = 30) -> list[np.ndarray]:
|
||||
def extract_frames(self, count: int = 10) -> list[np.ndarray]:
|
||||
"""
|
||||
从视频中均匀抽取帧
|
||||
|
||||
|
||||
Executable → Regular
-2
@@ -461,7 +461,6 @@ def _download_library_assets(
|
||||
asset_library_id: str = "",
|
||||
project_id: str = "",
|
||||
asset_ids: list[str] | None = None,
|
||||
video_extensions: tuple = (".mp4", ".mov", ".avi", ".mkv", ".webm"),
|
||||
strict: bool = True,
|
||||
task_id: str = "",
|
||||
gen_task=None,
|
||||
@@ -480,7 +479,6 @@ def _download_library_assets(
|
||||
asset_library_id: 素材库 ID(可选,与 project_id 二选一)
|
||||
project_id: 项目 ID(可选,与 asset_library_id 二选一)
|
||||
asset_ids: 指定素材 ID 列表,为空则下载全部 ready 视频素材
|
||||
video_extensions: 支持的视频扩展名(保留兼容,当前按 file_type 过滤)
|
||||
strict: 严格模式(默认 True)。
|
||||
True — 任何素材下载失败立即抛 RuntimeError;
|
||||
False — 跳过失败素材,返回成功列表(调用方可通过日志感知失败)。
|
||||
|
||||
Executable → Regular
+1
-1
@@ -13,7 +13,7 @@ uvicorn[standard]==0.32.0
|
||||
pydantic==2.9.0
|
||||
|
||||
# 认证核心
|
||||
pyjwt==2.9.0
|
||||
pyjwt==2.13.0
|
||||
bcrypt==4.2.0
|
||||
|
||||
# Redis
|
||||
|
||||
Reference in New Issue
Block a user