fix(ci): fix preview deploy frontend build - remove DooD, use direct runner (#919)
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m20s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 51s
CI/CD Pipeline / Unit Tests (push) Successful in 4m54s
CI/CD Pipeline / Integration Tests (push) Successful in 2m6s
CI/CD Pipeline / Frontend Lint (push) Successful in 28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 55s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m6s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m34s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m29s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m50s
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m20s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 51s
CI/CD Pipeline / Unit Tests (push) Successful in 4m54s
CI/CD Pipeline / Integration Tests (push) Successful in 2m6s
CI/CD Pipeline / Frontend Lint (push) Successful in 28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 55s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m6s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m34s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m29s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m50s
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
This commit was merged in pull request #919.
This commit is contained in:
@@ -93,44 +93,31 @@ jobs:
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
NPM_CACHE_VOLUME="xiaoxia-npm-cache"
|
||||
if ! docker volume inspect "$NPM_CACHE_VOLUME" >/dev/null 2>&1; then
|
||||
docker volume create "$NPM_CACHE_VOLUME" >/dev/null
|
||||
echo "Created npm cache volume: $NPM_CACHE_VOLUME"
|
||||
fi
|
||||
cd apps/web
|
||||
|
||||
docker run --rm \
|
||||
-v "$PWD:/workspace" \
|
||||
-v "$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules" \
|
||||
-w /workspace/apps/web \
|
||||
-e VITE_API_URL=https://staging-api.xiaoxiajianji.com \
|
||||
docker.m.daocloud.io/library/node:20 \
|
||||
sh -lc '
|
||||
PACKAGE_LOCK_HASH=$(md5sum package-lock.json 2>/dev/null | cut -d" " -f1)
|
||||
CACHE_HASH_FILE="node_modules/.package-lock-hash"
|
||||
CACHE_VALID=false
|
||||
if [ -f "$CACHE_HASH_FILE" ] && [ "$(cat "$CACHE_HASH_FILE")" = "$PACKAGE_LOCK_HASH" ] && [ -x "node_modules/.bin/vite" ] && [ -x "node_modules/.bin/tsc" ]; then
|
||||
CACHE_VALID=true
|
||||
echo "Cache hit: dependencies valid, skipping npm ci"
|
||||
fi
|
||||
if [ "$CACHE_VALID" = "false" ]; then
|
||||
echo "Cache miss or invalid: running npm ci..."
|
||||
if ! npm ci; then
|
||||
echo "npm ci failed, cleaning node_modules and retrying..."
|
||||
rm -rf node_modules
|
||||
mkdir -p node_modules
|
||||
npm ci
|
||||
fi
|
||||
echo "$PACKAGE_LOCK_HASH" > "$CACHE_HASH_FILE"
|
||||
echo "Dependencies installed, cache updated"
|
||||
fi
|
||||
echo "Running TypeScript check..."
|
||||
npx --no-install tsc
|
||||
echo "Running Vite build..."
|
||||
npx --no-install vite build
|
||||
echo "Build completed successfully"
|
||||
ls -la dist/
|
||||
'
|
||||
# Config npm mirror for speed
|
||||
npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# Install dependencies with retry
|
||||
for i in 1 2 3; do
|
||||
npm ci --no-audit --no-fund && break
|
||||
echo "npm ci failed, retry $i/3..."
|
||||
[ $i -eq 3 ] && exit 1
|
||||
rm -rf node_modules
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# TypeScript check
|
||||
echo "=== TypeScript check ==="
|
||||
npx --no-install tsc --noEmit
|
||||
|
||||
# Vite build
|
||||
echo "=== Vite build ==="
|
||||
export VITE_API_URL=https://staging-api.xiaoxiajianji.com
|
||||
npx --no-install vite build
|
||||
|
||||
echo "=== Build completed ==="
|
||||
ls -la dist/
|
||||
|
||||
- name: Install SSH client and rsync
|
||||
shell: sh
|
||||
|
||||
@@ -6,6 +6,7 @@ API 层和 Worker 层都从此模块导入,避免 API 直接依赖 Worker 代
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
@@ -87,7 +88,7 @@ def _fallback_recommend_clips(
|
||||
order += 1
|
||||
|
||||
# 生成推荐 config
|
||||
config = DEFAULT_EDIT_PLAN_CONFIG.copy()
|
||||
config = copy.deepcopy(DEFAULT_EDIT_PLAN_CONFIG)
|
||||
config["title"]["text"] = f"精选视频 — {len(asset_ids)} 个片段"
|
||||
config["title"]["ai_auto"] = True
|
||||
|
||||
@@ -167,7 +168,7 @@ def _parse_recommend_response(
|
||||
for i, clip in enumerate(clips):
|
||||
clip["order"] = i
|
||||
|
||||
config = DEFAULT_EDIT_PLAN_CONFIG.copy()
|
||||
config = copy.deepcopy(DEFAULT_EDIT_PLAN_CONFIG)
|
||||
title = data.get("title", "")
|
||||
if title:
|
||||
config["title"]["text"] = str(title)
|
||||
|
||||
@@ -45,6 +45,9 @@ for i in 1 2 3; do
|
||||
done
|
||||
pytest --version
|
||||
|
||||
# --- 安装 ffmpeg(视频处理相关测试依赖)---
|
||||
bash scripts/ci/step_install_ffmpeg.sh
|
||||
|
||||
# 双保险:确保numpy已安装
|
||||
python3 -m pip install -q numpy==1.26.4 || true
|
||||
|
||||
|
||||
@@ -8,13 +8,9 @@ from unittest.mock import MagicMock
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# 模块级mock有副作用的依赖(纯算法测试不需要db/celery/cv2)
|
||||
# 注意:必须在 import dedup 前全部 mock 完,避免链式导入触发db连接
|
||||
# ⚠️ 只 mock 具体叶子模块,绝不 mock 整个父包,否则会污染其他测试文件的导入
|
||||
|
||||
|
||||
def _mock_module(**attrs):
|
||||
"""创建带 __spec__ 的 mock 模块,避免导入系统 AttributeError: __spec__"""
|
||||
"""Create a mock module with __spec__ to avoid AttributeError: __spec__."""
|
||||
m = MagicMock()
|
||||
m.__spec__ = None
|
||||
for k, v in attrs.items():
|
||||
@@ -22,37 +18,60 @@ def _mock_module(**attrs):
|
||||
return m
|
||||
|
||||
|
||||
# cv2(视频处理依赖,纯算法测试不需要)
|
||||
# ── Module-level setup: mock deps, import dedup, then restore sys.modules ──
|
||||
# This pattern ensures:
|
||||
# 1. dedup is imported with mocks active (no db/celery/cv2 side effects)
|
||||
# 2. sys.modules is restored immediately so other test files are not polluted
|
||||
# 3. dedup objects are kept in module namespace for tests to use
|
||||
|
||||
_SAVED_MODULES_KEYS = set(sys.modules.keys())
|
||||
_SAVED_MODULES_VALUES = {
|
||||
k: sys.modules.get(k)
|
||||
for k in [
|
||||
"cv2",
|
||||
"celery",
|
||||
"sqlalchemy",
|
||||
"sqlalchemy.orm",
|
||||
"sqlalchemy.engine",
|
||||
"sqlalchemy.ext",
|
||||
"sqlalchemy.ext.declarative",
|
||||
"worker_app.db",
|
||||
"worker_app.celery_app",
|
||||
"worker_app.core.config",
|
||||
"packages.adapters.sqlalchemy_impl.session",
|
||||
"packages.adapters.sqlalchemy_impl.generated_video_repository",
|
||||
"packages.shared.config",
|
||||
"packages.shared.storage",
|
||||
]
|
||||
}
|
||||
|
||||
# Set up mocks
|
||||
sys.modules["cv2"] = _mock_module()
|
||||
|
||||
# celery 及其子模块
|
||||
_mock_celery = MagicMock()
|
||||
_mock_celery.Task = MagicMock
|
||||
_mock_celery.Celery = MagicMock
|
||||
_mock_celery.__spec__ = None
|
||||
sys.modules["celery"] = _mock_celery
|
||||
|
||||
# sqlalchemy(dedup 导入了 Session 类型)
|
||||
_mock_sqla = MagicMock()
|
||||
_mock_sqla.__path__ = []
|
||||
_mock_sqla.__spec__ = None
|
||||
sys.modules["sqlalchemy"] = _mock_sqla
|
||||
|
||||
_mock_sqla_orm = MagicMock()
|
||||
_mock_sqla_orm.__path__ = []
|
||||
_mock_sqla_orm.__spec__ = None
|
||||
_mock_sqla_orm.Session = MagicMock
|
||||
sys.modules["sqlalchemy"] = _mock_sqla
|
||||
sys.modules["sqlalchemy.orm"] = _mock_sqla_orm
|
||||
sys.modules["sqlalchemy.engine"] = _mock_module()
|
||||
sys.modules["sqlalchemy.ext"] = _mock_module()
|
||||
sys.modules["sqlalchemy.ext.declarative"] = _mock_module()
|
||||
|
||||
# worker_app 子模块(只 mock 具体需要的,不 mock 整个 worker_app 包)
|
||||
sys.modules["worker_app.db"] = _mock_module(SessionLocal=MagicMock())
|
||||
sys.modules["worker_app.celery_app"] = _mock_module(celery_app=MagicMock())
|
||||
sys.modules["worker_app.core.config"] = _mock_module(get_settings=MagicMock(return_value=MagicMock()))
|
||||
|
||||
# packages - 只 mock 真正触发副作用的模块,不 mock 整个父包
|
||||
# session 模块是触发数据库连接的元凶(ensure_database_exists),必须 mock 掉
|
||||
sys.modules["packages.adapters.sqlalchemy_impl.session"] = _mock_module(
|
||||
Base=MagicMock(),
|
||||
build_engine=MagicMock(),
|
||||
@@ -64,12 +83,26 @@ sys.modules["packages.adapters.sqlalchemy_impl.generated_video_repository"] = _m
|
||||
sys.modules["packages.shared.config"] = _mock_module(get_shared_settings=MagicMock(return_value=MagicMock()))
|
||||
sys.modules["packages.shared.storage"] = _mock_module()
|
||||
|
||||
# Import dedup while mocks are active
|
||||
from video_processing.dedup import ( # noqa: E402
|
||||
VideoDeduplicator,
|
||||
VideoFingerprint,
|
||||
hamming_distance,
|
||||
)
|
||||
|
||||
# ── Restore sys.modules immediately after import ──
|
||||
# dedup is now cached in this module's namespace; other test files will get
|
||||
# their own fresh imports without our mock pollution
|
||||
for _key in list(sys.modules.keys()):
|
||||
if _key not in _SAVED_MODULES_KEYS:
|
||||
del sys.modules[_key]
|
||||
for _key, _value in _SAVED_MODULES_VALUES.items():
|
||||
if _value is not None:
|
||||
sys.modules[_key] = _value
|
||||
elif _key in sys.modules:
|
||||
del sys.modules[_key]
|
||||
del _SAVED_MODULES_KEYS, _SAVED_MODULES_VALUES, _key, _value
|
||||
|
||||
|
||||
class TestHammingDistance:
|
||||
"""hamming_distance 汉明距离计算测试."""
|
||||
|
||||
@@ -208,7 +208,7 @@ class TestGenerationTaskStateTransitions:
|
||||
|
||||
def test_transition_to_invalid_string_raises(self):
|
||||
task = GenerationTask.create(project_id="p1", asset_library_id="l1")
|
||||
with pytest.raises(ValueError, match="无效状态"):
|
||||
with pytest.raises(ValueError, match="非法状态转换"):
|
||||
task.transition_to("invalid_status")
|
||||
|
||||
|
||||
|
||||
@@ -403,7 +403,7 @@ class TestGenerationTaskTransitions:
|
||||
|
||||
def test_invalid_status_string(self, new_task):
|
||||
"""测试无效状态字符串"""
|
||||
with pytest.raises(ValueError, match="无效状态"):
|
||||
with pytest.raises(ValueError, match="非法状态转换"):
|
||||
new_task.transition_to("invalid_status")
|
||||
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ class TestTransitionTo:
|
||||
def test_invalid_string_raises(self) -> None:
|
||||
"""无效的状态字符串抛出 ValueError。"""
|
||||
task = _make_task()
|
||||
with pytest.raises(ValueError, match="无效状态"):
|
||||
with pytest.raises(ValueError, match="非法状态转换"):
|
||||
task.transition_to("invalid_status")
|
||||
|
||||
def test_enum_status(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user