Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d3f71c503 | |||
| 83d58bff88 | |||
| 15585710ef | |||
| ffd99ffeb0 |
+72
-57
@@ -158,57 +158,72 @@ jobs:
|
||||
python3 scripts/check_migration_safety.py --allow-medium-risk
|
||||
fi
|
||||
|
||||
- name: Debug coverage paths
|
||||
shell: sh
|
||||
run: |
|
||||
set +e
|
||||
echo "=== PWD ==="
|
||||
pwd
|
||||
echo "=== check source dirs ==="
|
||||
ls -d apps/api/app packages
|
||||
echo "=== python import check ==="
|
||||
python3 - <<'PY'
|
||||
import sys, os
|
||||
os.environ["PYTHONPATH"] = f"{os.getcwd()}/apps/api:{os.getcwd()}"
|
||||
sys.path.insert(0, f"{os.getcwd()}/apps/api")
|
||||
sys.path.insert(0, os.getcwd())
|
||||
print(f"cwd: {os.getcwd()}")
|
||||
print(f"sys.path[:5]: {sys.path[:5]}")
|
||||
try:
|
||||
import app
|
||||
print(f"app.__file__: {app.__file__}")
|
||||
except Exception as e:
|
||||
print(f"import app failed: {e}")
|
||||
try:
|
||||
import packages
|
||||
print(f"packages.__file__: {packages.__file__}")
|
||||
except Exception as e:
|
||||
print(f"import packages failed: {e}")
|
||||
PY
|
||||
echo "=== coverage debug ==="
|
||||
python3 - <<'PY'
|
||||
import os, sys
|
||||
sys.path.insert(0, f"{os.getcwd()}/apps/api")
|
||||
sys.path.insert(0, os.getcwd())
|
||||
import coverage
|
||||
cov = coverage.Coverage(source=["apps/api/app", "packages"])
|
||||
print(f"source: {cov.config.source}")
|
||||
for src in cov.config.source or []:
|
||||
abspath = os.path.abspath(src)
|
||||
print(f" {src} -> {abspath} exists={os.path.exists(src)}")
|
||||
if os.path.isdir(src):
|
||||
pyfiles = []
|
||||
for root, dirs, files in os.walk(src):
|
||||
for f in files:
|
||||
if f.endswith('.py'):
|
||||
pyfiles.append(os.path.join(root, f))
|
||||
print(f" .py files: {len(pyfiles)}")
|
||||
PY
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: host
|
||||
timeout-minutes: 8
|
||||
|
||||
- name: Run unit tests
|
||||
env:
|
||||
USE_IN_MEMORY_DB: "true"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
shell: sh
|
||||
env:
|
||||
USE_IN_MEMORY_DB: "true"
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
python3 - <<'PY'
|
||||
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
|
||||
raise
|
||||
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, '.')
|
||||
PY
|
||||
|
||||
- name: Install dependencies
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
python3 -m pip install -q -r requirements-base.txt
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
python3 -m pip install -q -r requirements-dev.txt
|
||||
pytest --version
|
||||
|
||||
- name: Run unit tests with coverage
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run \
|
||||
@@ -220,16 +235,16 @@ jobs:
|
||||
python3 -m coverage xml -o coverage.xml
|
||||
python3 -m coverage report --fail-under=60 > /dev/null
|
||||
|
||||
- name: Build summary
|
||||
if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main'
|
||||
- name: CI failure notification
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
CI_WEBHOOK_URL: ${{ secrets.CI_WEBHOOK_URL }}
|
||||
run: |
|
||||
set -eu
|
||||
echo "Build completed successfully!"
|
||||
echo "Branch: ${GITHUB_REF_NAME}"
|
||||
echo "Commit: ${GITHUB_SHA}"
|
||||
# 输出最终覆盖率
|
||||
python3 scripts/ci_coverage_summary.py
|
||||
set +e
|
||||
FAILED_JOB="Unit Tests" python3 scripts/ci_notify_failure.py
|
||||
|
||||
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
@@ -578,7 +593,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
needs: [validate, frontend-lint]
|
||||
|
||||
if: github.ref_name == 'main' || github.ref_name == 'develop' || startsWith(github.ref_name, 'feature/')
|
||||
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -30,7 +30,8 @@ fi
|
||||
# ---- Registry 配置 ----
|
||||
REGISTRY="${REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}"
|
||||
CACHE_REGISTRY="${CACHE_REGISTRY:-git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas}"
|
||||
CACHE_TAG="${CACHE_TAG:-release}"
|
||||
# 主缓存 tag:develop 分支构建时写入,所有分支读取
|
||||
CACHE_TAG_PRIMARY="${CACHE_TAG:-develop}"
|
||||
|
||||
API_IMAGE="xiaoxia-saas-api:$VERSION"
|
||||
WORKER_IMAGE="xiaoxia-saas-worker:$VERSION"
|
||||
@@ -45,6 +46,7 @@ REGISTRY_WEB="${REGISTRY}/xiaoxia-saas-web:$VERSION"
|
||||
|
||||
USE_CACHE=0
|
||||
USE_PUSH=0
|
||||
CACHE_WRITE=0
|
||||
|
||||
# 检查 buildx 和 Registry 认证
|
||||
if docker buildx version >/dev/null 2>&1; then
|
||||
@@ -54,7 +56,10 @@ if docker buildx version >/dev/null 2>&1; then
|
||||
docker buildx use default 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "=== Building API image ==="
|
||||
# ---- 缓存读写策略(按分支隔离)----
|
||||
# 默认只读不写,防止 feature 分支污染主缓存
|
||||
# 只有 develop/main 分支才写回缓存
|
||||
BRANCH_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_BRANCH:-unknown}}"
|
||||
if [ "$USE_CACHE" -eq 1 ]; then
|
||||
docker buildx build \
|
||||
--build-arg APP_VERSION="$VERSION" \
|
||||
@@ -68,6 +73,52 @@ else
|
||||
docker build --pull=false --build-arg APP_VERSION="$VERSION" -f infra/docker/api.Dockerfile -t "$API_IMAGE" -t "$API_LATEST" .
|
||||
fi
|
||||
|
||||
build_with_cache() {
|
||||
# usage: build_with_cache <image_name> <dockerfile> <extra_args...>
|
||||
IMG_NAME="$1"
|
||||
DOCKERFILE="$2"
|
||||
shift 2
|
||||
EXTRA_ARGS="$*"
|
||||
|
||||
CACHE_FROM="type=registry,ref=${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY},ignore-error=true"
|
||||
|
||||
if [ "$CACHE_WRITE" -eq 1 ]; then
|
||||
CACHE_TO="type=registry,ref=${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY},mode=max"
|
||||
echo " cache: read+write from ${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY}"
|
||||
else
|
||||
CACHE_TO=""
|
||||
echo " cache: read-only from ${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY}"
|
||||
fi
|
||||
|
||||
if [ "$USE_CACHE" -eq 1 ]; then
|
||||
if [ -n "$CACHE_TO" ]; then
|
||||
docker buildx build \
|
||||
$EXTRA_ARGS \
|
||||
--cache-from "$CACHE_FROM" \
|
||||
--cache-to "$CACHE_TO" \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "$IMG_NAME:$VERSION" \
|
||||
--load \
|
||||
.
|
||||
else
|
||||
docker buildx build \
|
||||
$EXTRA_ARGS \
|
||||
--cache-from "$CACHE_FROM" \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "$IMG_NAME:$VERSION" \
|
||||
--load \
|
||||
.
|
||||
fi
|
||||
else
|
||||
docker build --pull=false $EXTRA_ARGS -f "$DOCKERFILE" -t "$IMG_NAME:$VERSION" .
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Building API image ==="
|
||||
build_with_cache "api" "infra/docker/api.Dockerfile" \
|
||||
"--build-arg APP_VERSION=$VERSION"
|
||||
docker tag "$API_IMAGE" "$API_LATEST"
|
||||
|
||||
echo "=== Building Worker image ==="
|
||||
if [ "$USE_CACHE" -eq 1 ]; then
|
||||
docker buildx build \
|
||||
@@ -83,9 +134,16 @@ else
|
||||
fi
|
||||
|
||||
echo "=== Building Web image (with buildx cache) ==="
|
||||
# 先构建前端产物
|
||||
# 先构建前端产物(使用持久化 npm 缓存卷)
|
||||
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
|
||||
|
||||
docker run --rm \
|
||||
-v "$PWD:/workspace" \
|
||||
-v "$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules" \
|
||||
-w /workspace/apps/web \
|
||||
docker.m.daocloud.io/library/node:20 \
|
||||
sh -lc "npm ci && npm run build"
|
||||
|
||||
@@ -79,7 +79,7 @@ class TestProbeVideoInfoTimeout:
|
||||
"""正常情况应解析 ffprobe JSON 输出。"""
|
||||
fake_output = """
|
||||
{
|
||||
"streams": [{"width": 1920, "height": 1080, "r_frame_rate": "30/1", "duration": "10.5"}],
|
||||
"streams": [{"width": 1920, "height": 1080, "codec_type": "video", "r_frame_rate": "30/1", "duration": "10.5"}],
|
||||
"format": {"duration": "10.5"}
|
||||
}
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user