Files
xiaoxia-saas/scripts/ci/run_unit_tests.sh
xiaoxia 37f62e5ad0
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m15s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m51s
AI Code Review / AI Code Review (pull_request) Successful in 6m56s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m52s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m57s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m25s
CI/CD Pipeline / Validate - Style (push) Successful in 5m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m36s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m40s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Validate - Security (push) Successful in 10m25s
CI/CD Pipeline / Unit Tests (push) Successful in 12m18s
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 / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 23m29s
fix(alembic): 107迁移down_revision修正为106_system_settings,修复CI alembic验证失败 (#2252)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-10-10 01:48:15 +08:00

247 lines
9.4 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# CI Unit Tests Job 主脚本
# 包含:依赖缓存、增量测试选择、覆盖率测试、diff覆盖率门禁
set -eu
JOB_NAME="${1:-Unit Tests}"
echo "=== CI Unit Tests 开始 ==="
# --- Python 版本选择(必须 >= 3.11,代码使用 datetime.UTC)---
if ! python3 -c "import sys; assert sys.version_info >= (3, 11)" 2>/dev/null; then
for cand in python3.12 python3.11 /opt/python3.12/bin/python3; do
if command -v "$cand" >/dev/null 2>&1 && "$cand" -c "import sys; assert sys.version_info >= (3, 11)" 2>/dev/null; then
PY3_DIR="$(dirname "$(command -v "$cand")")"
export PATH="$PY3_DIR:$PATH"
hash -r
echo "✅ python3 版本过低,改用 $cand ($("$cand" -c "import sys; print(sys.version.split()[0])"))"
break
fi
done
if ! python3 -c "import sys; assert sys.version_info >= (3, 11)" 2>/dev/null; then
echo "❌ 未找到 Python >= 3.11,本项目要求 Python 3.11+(使用 datetime.UTC)" >&2
exit 1
fi
fi
PYVER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
echo "使用 Python 版本: $(python3 --version)"
# --- 依赖缓存检查(按 Python 版本区分缓存文件,避免跨版本复用)---
# 如果 requirements 文件未变化且依赖已安装,跳过 pip install(持久 runner 优化)
REQ_HASH_FILE="/tmp/.ci_unit_tests_req_hash_py${PYVER}"
CURRENT_REQ_HASH=""
if [ -f requirements-base.txt ] && [ -f requirements.txt ] && [ -f requirements-dev.txt ]; then
CURRENT_REQ_HASH=$(cat requirements-base.txt requirements.txt requirements-dev.txt | md5sum | cut -d' ' -f1)
fi
SKIP_PIP_INSTALL=false
if [ -n "$CURRENT_REQ_HASH" ] && [ -f "$REQ_HASH_FILE" ]; then
CACHED_HASH=$(cat "$REQ_HASH_FILE")
if [ "$CACHED_HASH" = "$CURRENT_REQ_HASH" ]; then
# 验证关键包是否还在
if python3 -c "import pytest; import celery" 2>/dev/null; then
echo "✅ 依赖无变化 (hash=$CURRENT_REQ_HASH),跳过 pip install"
SKIP_PIP_INSTALL=true
else
echo "⚠️ 依赖 hash 匹配但关键包缺失,重新安装"
fi
fi
fi
# --- 安装依赖 ---
if [ "$SKIP_PIP_INSTALL" = "false" ]; then
echo ""
echo "=== 安装 Python 依赖 ==="
# pip install 带重试(网络不稳定时自动重试),合并为一次调用减少开销
for i in 1 2 3; do
python3 -m pip install -q -r requirements-base.txt -r requirements.txt -r requirements-dev.txt && break
echo "pip install 失败,重试 $i/3..."
[ $i -eq 3 ] && exit 1
sleep 5
done
# 保存 hash 标记
if [ -n "$CURRENT_REQ_HASH" ]; then
echo "$CURRENT_REQ_HASH" > "$REQ_HASH_FILE"
fi
fi
pytest --version
# 双保险:确保numpy已安装
echo "=== 验证 numpy 安装 ==="
SKIP_NUMPY_TESTS=0
if python3 -c "import numpy; assert numpy.__version__ == '1.26.4'" 2>/dev/null; then
echo "✅ numpy 1.26.4 已就绪(缓存命中)"
else
echo "需要安装 numpy 1.26.4..."
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
}
}
fi
if [ "$SKIP_NUMPY_TESTS" = "0" ]; then
python3 -c "import numpy; print(f'✅ numpy {numpy.__version__} 就绪')" || {
echo "⚠️ numpy 导入失败,跳过需要 numpy 的测试"
SKIP_NUMPY_TESTS=1
}
fi
# --- 增量测试选择(PR + push 均支持) ---
UNIT_TEST_MODE="full"
SELECTED_TEST_FILES="tests/unit"
IS_PULL_REQUEST=false
IS_PUSH=false
[ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && IS_PULL_REQUEST=true
[ "${GITHUB_EVENT_NAME:-}" = "push" ] && IS_PUSH=true
if ($IS_PULL_REQUEST || $IS_PUSH) && [ -n "${GITHUB_TOKEN:-}" ]; then
echo ""
echo "=== 增量测试选择 ==="
CHANGED_FILES=""
if $IS_PULL_REQUEST; then
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']")
elif $IS_PUSH && [ -n "${GITHUB_SHA:-}" ]; then
# Push 事件:通过 GitHub API 获取本次 push 改动的文件
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}"
RESPONSE=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3.diff" "$API_URL" 2>/dev/null || echo "")
if [ -n "$RESPONSE" ]; then
CHANGED_FILES=$(echo "$RESPONSE" | grep '^diff --git' | sed 's|diff --git a/\(.*\) b/.*|\1|' || echo "")
fi
# 备用方案:获取 previous commit SHA 再查 API
if [ -z "$CHANGED_FILES" ]; then
PREV_SHA=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits?sha=${GITHUB_SHA}&per_page=2" \
| python3 -c "import sys,json; commits=json.load(sys.stdin); print(commits[1]['sha'] if len(commits)>1 else '')" 2>/dev/null || echo "")
if [ -n "$PREV_SHA" ]; then
COMPARE_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/compare/${PREV_SHA}...${GITHUB_SHA}"
CHANGED_FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$COMPARE_URL" \
| python3 -c "import sys,json; data=json.load(sys.stdin); [print(f['filename']) for f in data.get('files',[]) if f['status'] != 'removed']" 2>/dev/null || echo "")
fi
fi
fi
echo "改动文件数: $(echo "$CHANGED_FILES" | grep -c . || echo 0)"
if [ -n "$CHANGED_FILES" ]; then
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
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 全部通过 ✅ ==="