cd8f162b17
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
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 / Validate - Migration (alembic) (push) Successful in 1m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m35s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m54s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m32s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m8s
CI/CD Pipeline / Integration Tests (push) Successful in 1m47s
CI/CD Pipeline / Unit Tests (push) Failing after 7m53s
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 / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m48s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
168 lines
5.8 KiB
Bash
Executable File
168 lines
5.8 KiB
Bash
Executable File
#!/bin/bash
|
||
# CI Unit Tests Job 主脚本
|
||
# 包含:依赖安装、增量测试选择、覆盖率测试、diff覆盖率门禁
|
||
set -eu
|
||
|
||
JOB_NAME="${1:-Unit Tests}"
|
||
|
||
echo "=== CI Unit Tests 开始 ==="
|
||
|
||
# --- 安装依赖 ---
|
||
echo ""
|
||
echo "=== 安装 Python 依赖 ==="
|
||
# pip install 带重试(网络不稳定时自动重试)
|
||
for i in 1 2 3; do
|
||
python3 -m pip install -q -r requirements-base.txt && break
|
||
echo "pip install requirements-base.txt 失败,重试 $i/3..."
|
||
[ $i -eq 3 ] && exit 1
|
||
sleep 5
|
||
done
|
||
for i in 1 2 3; do
|
||
python3 -m pip install -q -r requirements.txt && break
|
||
echo "pip install requirements.txt 失败,重试 $i/3..."
|
||
[ $i -eq 3 ] && exit 1
|
||
sleep 5
|
||
done
|
||
for i in 1 2 3; do
|
||
python3 -m pip install -q -r requirements-dev.txt && break
|
||
echo "pip install requirements-dev.txt 失败,重试 $i/3..."
|
||
[ $i -eq 3 ] && exit 1
|
||
sleep 5
|
||
done
|
||
pytest --version
|
||
|
||
# 双保险:确保numpy已安装
|
||
echo "=== 验证 numpy 安装 ==="
|
||
SKIP_NUMPY_TESTS=0
|
||
python3 -m pip install numpy==1.26.4 || {
|
||
echo "❌ numpy 首次安装失败,尝试不使用缓存重新安装..."
|
||
python3 -m pip install --no-cache-dir numpy==1.26.4 || {
|
||
echo "⚠️ numpy 安装失败,跳过需要 numpy 的测试"
|
||
SKIP_NUMPY_TESTS=1
|
||
}
|
||
}
|
||
if [ "$SKIP_NUMPY_TESTS" = "0" ]; then
|
||
python3 -c "import numpy; print(f'✅ numpy {numpy.__version__} 安装成功')" || {
|
||
echo "⚠️ numpy 导入失败,跳过需要 numpy 的测试"
|
||
SKIP_NUMPY_TESTS=1
|
||
}
|
||
fi
|
||
|
||
# --- 增量测试选择(仅PR) ---
|
||
UNIT_TEST_MODE="full"
|
||
SELECTED_TEST_FILES="tests/unit"
|
||
|
||
if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && [ -n "${GITHUB_TOKEN:-}" ]; then
|
||
echo ""
|
||
echo "=== 增量测试选择 ==="
|
||
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
|
||
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300"
|
||
CHANGED_FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin) if f['status'] != 'removed']")
|
||
echo "改动文件数: $(echo "$CHANGED_FILES" | grep -c . || echo 0)"
|
||
set +e
|
||
CHANGED_FILES="$CHANGED_FILES" \
|
||
SELECTED_TESTS_OUTPUT=/tmp/selected_tests.txt \
|
||
python3 scripts/ci/select_unit_tests.py
|
||
SELECT_EXIT=$?
|
||
set -e
|
||
if [ $SELECT_EXIT -eq 0 ]; then
|
||
UNIT_TEST_MODE="incremental"
|
||
TEST_FILES=$(cat /tmp/selected_tests.txt | tr '\n' ' ')
|
||
SELECTED_TEST_FILES="$TEST_FILES"
|
||
echo "增量模式: $(cat /tmp/selected_tests.txt | wc -l) 个测试文件"
|
||
else
|
||
echo "全量模式"
|
||
fi
|
||
fi
|
||
|
||
# --- 运行单元测试 + 覆盖率 ---
|
||
echo ""
|
||
echo "=== 运行单元测试 (模式: $UNIT_TEST_MODE) ==="
|
||
|
||
# 如果 numpy 不可用,跳过依赖 numpy 的测试文件
|
||
NUMPY_IGNORE=""
|
||
if [ "${SKIP_NUMPY_TESTS:-0}" = "1" ]; then
|
||
echo "⚠️ SKIP_NUMPY_TESTS=1,将跳过依赖 numpy 的测试"
|
||
NUMPY_IGNORE="--ignore=tests/unit/test_dedup_engine.py"
|
||
fi
|
||
|
||
if [ "$UNIT_TEST_MODE" = "incremental" ]; then
|
||
echo "=== 增量测试模式 ==="
|
||
PYTHONPATH="$PWD/apps/api:$PWD/apps/worker:$PWD/packages:$PWD" python3 -m coverage run \
|
||
--source=apps/api/app,packages \
|
||
--omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \
|
||
--branch \
|
||
-m pytest $SELECTED_TEST_FILES $NUMPY_IGNORE -q
|
||
python3 -m coverage report --show-missing
|
||
python3 -m coverage xml -o coverage.xml
|
||
python3 -m coverage report --fail-under=10 > /dev/null || true
|
||
else
|
||
PYTHONPATH="$PWD/apps/api:$PWD/apps/worker:$PWD/packages:$PWD" python3 -m coverage run \
|
||
--source=apps/api/app,packages \
|
||
--omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \
|
||
--branch \
|
||
-m pytest tests/unit $NUMPY_IGNORE -q
|
||
python3 -m coverage report --show-missing
|
||
python3 -m coverage xml -o coverage.xml
|
||
python3 -m coverage report --fail-under=65 > /dev/null
|
||
fi
|
||
|
||
# --- Diff 覆盖率检查(仅PR) ---
|
||
if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && [ -n "${GITHUB_TOKEN:-}" ]; then
|
||
echo ""
|
||
echo "=== Diff 覆盖率检查 ==="
|
||
BASE_BRANCH="${GITHUB_BASE_REF:-develop}"
|
||
echo "Base branch: $BASE_BRANCH"
|
||
|
||
PR_CODE_DIR="/tmp/pr-code-$$"
|
||
mkdir -p "$PR_CODE_DIR"
|
||
# 备份PR代码(含coverage.xml,diff-cover需要用到
|
||
find . -maxdepth 1 -mindepth 1 ! -name 'diff_coverage.html' -exec cp -r {} "$PR_CODE_DIR/" \;
|
||
rm -rf .git
|
||
git init > /dev/null 2>&1
|
||
git remote add origin https://git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas.git > /dev/null 2>&1
|
||
git config user.email "ci@local"
|
||
git config user.name "CI"
|
||
git fetch origin "$BASE_BRANCH" --depth=200
|
||
# 先清理工作目录,避免未跟踪文件导致checkout失败
|
||
find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
|
||
git checkout -b ci-pr-branch "origin/$BASE_BRANCH" > /dev/null 2>&1
|
||
# 清除base分支源码,用PR代码覆盖
|
||
find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
|
||
cp -r "$PR_CODE_DIR"/. .
|
||
rm -rf "$PR_CODE_DIR"
|
||
git add -A > /dev/null 2>&1
|
||
git commit -m "ci-tmp" > /dev/null 2>&1
|
||
|
||
if [ "$UNIT_TEST_MODE" = "incremental" ]; then
|
||
THRESHOLD=40
|
||
echo "增量测试模式,增量覆盖率门槛: ${THRESHOLD}%"
|
||
else
|
||
THRESHOLD=60
|
||
echo "全量测试模式,增量覆盖率门槛: ${THRESHOLD}%"
|
||
fi
|
||
|
||
set +e
|
||
python3 -m diff_cover.diff_cover_tool coverage.xml \
|
||
--compare-branch="origin/$BASE_BRANCH" \
|
||
--fail-under=$THRESHOLD \
|
||
--html-report diff_coverage.html \
|
||
2>&1
|
||
DIFF_EXIT=$?
|
||
set -e
|
||
if [ $DIFF_EXIT -ne 0 ]; then
|
||
echo ""
|
||
echo "❌ 增量覆盖率未达到门槛 (${THRESHOLD}%)"
|
||
echo " 请为改动的代码添加单元测试后再提交"
|
||
echo ""
|
||
echo "=== 覆盖率报告 ==="
|
||
python3 -m diff_cover.diff_cover_tool coverage.xml \
|
||
--compare-branch="origin/$BASE_BRANCH" 2>&1 | tail -30
|
||
exit 1
|
||
fi
|
||
echo "✅ 增量覆盖率达标"
|
||
fi
|
||
|
||
echo ""
|
||
echo "=== CI Unit Tests 全部通过 ✅ ==="
|