ab6717eeae
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
- push 事件也启用增量测试选择(仅跑改动相关的测试文件) - 持久 runner 依赖 hash 缓存(requirements 无变化时跳过 pip install) - 3 次独立 pip install 合并为 1 次 - numpy 缓存检查(已安装则跳过) - 预计节省 5-10 分钟
227 lines
8.4 KiB
Bash
Executable File
227 lines
8.4 KiB
Bash
Executable File
#!/bin/bash
|
||
# CI Unit Tests Job 主脚本
|
||
# 包含:依赖缓存、增量测试选择、覆盖率测试、diff覆盖率门禁
|
||
set -eu
|
||
|
||
JOB_NAME="${1:-Unit Tests}"
|
||
|
||
echo "=== CI Unit Tests 开始 ==="
|
||
|
||
# --- 依赖缓存检查 ---
|
||
# 如果 requirements 文件未变化且依赖已安装,跳过 pip install(持久 runner 优化)
|
||
REQ_HASH_FILE="/tmp/.ci_unit_tests_req_hash"
|
||
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 全部通过 ✅ ==="
|