ci: 架构精简 - 合并4个workflow为2个,抽取公共步骤为脚本 (#605)
CI/CD Pipeline / Build Staging API Image (push) Failing after 24s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 29s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 29s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1412h26m44s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 1412h26m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 1412h26m46s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1412h27m12s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1412h27m14s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1412h27m15s
CI/CD Pipeline / Build Production API Image (push) Failing after 1412h27m15s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1412h27m17s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Failing after 1412h59m20s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1412h58m51s
CI/CD Pipeline / Build Staging API Image (push) Failing after 24s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 29s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 29s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1412h26m44s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 1412h26m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 1412h26m46s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1412h27m12s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1412h27m14s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1412h27m15s
CI/CD Pipeline / Build Production API Image (push) Failing after 1412h27m15s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1412h27m17s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Failing after 1412h59m20s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1412h58m51s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #605.
This commit is contained in:
Regular → Executable
Regular → Executable
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/bin/bash
|
||||
# CI Integration Tests Job 主脚本
|
||||
# 包含:依赖安装、ffmpeg安装、Redis启动、PG启动、迁移、测试、清理、覆盖率
|
||||
set -eu
|
||||
|
||||
echo "=== CI Integration Tests 开始 ==="
|
||||
|
||||
# --- 安装依赖 ---
|
||||
echo ""
|
||||
echo "=== 安装 Python 依赖 ==="
|
||||
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
|
||||
python3 -m pip install -q pytest-rerunfailures
|
||||
pytest --version
|
||||
|
||||
# --- 安装 ffmpeg ---
|
||||
echo ""
|
||||
echo "=== 安装 ffmpeg ==="
|
||||
bash scripts/ci/step_install_ffmpeg.sh
|
||||
|
||||
# --- 启动 Redis ---
|
||||
echo ""
|
||||
echo "=== 启动 Redis ==="
|
||||
REDIS_CONTAINER="ci-redis-${GITHUB_RUN_ID:-$$}"
|
||||
docker rm -f "$REDIS_CONTAINER" 2>/dev/null || true
|
||||
docker run -d --name "$REDIS_CONTAINER" \
|
||||
-P \
|
||||
--health-cmd "redis-cli ping" \
|
||||
--health-interval 2s \
|
||||
--health-timeout 2s \
|
||||
--health-retries 10 \
|
||||
redis:7-alpine
|
||||
REDIS_PORT=$(docker port "$REDIS_CONTAINER" 6379/tcp | cut -d: -f2)
|
||||
echo "Redis port: $REDIS_PORT"
|
||||
export REDIS_URL="redis://127.0.0.1:$REDIS_PORT/0"
|
||||
for i in $(seq 1 15); do
|
||||
if docker inspect --format='{{.State.Health.Status}}' "$REDIS_CONTAINER" 2>/dev/null | grep -q healthy; then
|
||||
echo "Redis is ready on port $REDIS_PORT"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for Redis... ($i/15)"
|
||||
sleep 2
|
||||
done
|
||||
docker inspect --format='{{.State.Health.Status}}' "$REDIS_CONTAINER" | grep -q healthy
|
||||
|
||||
# --- 启动 PostgreSQL ---
|
||||
echo ""
|
||||
echo "=== 启动 PostgreSQL ==="
|
||||
PG_CONTAINER="ci-pg-${GITHUB_RUN_ID:-$$}"
|
||||
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
|
||||
docker run -d --name "$PG_CONTAINER" \
|
||||
--shm-size=256m \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=xiaoxia_saas \
|
||||
-P \
|
||||
--health-cmd "pg_isready -U postgres" \
|
||||
--health-interval 5s \
|
||||
--health-timeout 5s \
|
||||
--health-retries 12 \
|
||||
postgres:16
|
||||
PG_PORT=$(docker port "$PG_CONTAINER" 5432/tcp | cut -d: -f2)
|
||||
echo "PostgreSQL port: $PG_PORT"
|
||||
export DATABASE_URL="postgresql+psycopg://postgres:postgres@127.0.0.1:$PG_PORT/xiaoxia_saas"
|
||||
for i in $(seq 1 30); do
|
||||
if docker inspect --format='{{.State.Health.Status}}' "$PG_CONTAINER" 2>/dev/null | grep -q healthy; then
|
||||
echo "PostgreSQL is ready on port $PG_PORT"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for PostgreSQL... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
docker inspect --format='{{.State.Health.Status}}' "$PG_CONTAINER" | grep -q healthy
|
||||
|
||||
# --- 执行迁移 ---
|
||||
echo ""
|
||||
echo "=== 执行 Alembic 迁移 ==="
|
||||
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m alembic upgrade head
|
||||
echo "✅ 迁移完成"
|
||||
|
||||
# --- 运行集成测试 ---
|
||||
echo ""
|
||||
echo "=== 运行集成测试 ==="
|
||||
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run \
|
||||
--source=apps/api/app,packages \
|
||||
--omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \
|
||||
--branch \
|
||||
-m pytest tests/integration -q --timeout=60 -x --reruns 2 --reruns-delay 1 -m "not performance"
|
||||
python3 -m coverage report --show-missing
|
||||
python3 -m coverage xml -o coverage.xml
|
||||
python3 -m coverage report --fail-under=40 > /dev/null
|
||||
echo "✅ 集成测试通过"
|
||||
|
||||
# --- API 性能基线测试(仅告警) ---
|
||||
echo ""
|
||||
echo "=== API 性能基线测试(仅告警) ==="
|
||||
set +e
|
||||
PERF_OUTPUT=$(mktemp)
|
||||
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/integration/test_api_performance.py \
|
||||
-v --timeout=120 -p no:cacheprovider 2>&1 | tee "$PERF_OUTPUT"
|
||||
echo ""
|
||||
echo "=== 性能测试摘要 ==="
|
||||
grep "PERF_STATS:" "$PERF_OUTPUT" || echo "PERF_STATS: 未找到统计数据"
|
||||
grep "PERF_RESULT:" "$PERF_OUTPUT" || echo "PERF_RESULT: 未找到详细结果"
|
||||
TOTAL=$(grep -c "PERF_RESULT:" "$PERF_OUTPUT" || echo 0)
|
||||
PASSED=$(grep "PERF_RESULT: PASS" "$PERF_OUTPUT" | wc -l)
|
||||
FAILED=$(grep "PERF_RESULT: FAIL" "$PERF_OUTPUT" | wc -l)
|
||||
echo ""
|
||||
echo "性能测试结果: $PASSED/$TOTAL 通过, $FAILED 未达标"
|
||||
if [ "$FAILED" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "⚠️ 警告: $FAILED 个接口性能未达标"
|
||||
fi
|
||||
rm -f "$PERF_OUTPUT"
|
||||
set -e
|
||||
|
||||
# --- 清理 ---
|
||||
echo ""
|
||||
echo "=== 清理容器 ==="
|
||||
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
|
||||
docker rm -f "$REDIS_CONTAINER" 2>/dev/null || true
|
||||
echo "✅ 清理完成"
|
||||
|
||||
# --- 覆盖率汇总 ---
|
||||
echo ""
|
||||
echo "=== 覆盖率汇总 ==="
|
||||
set +e
|
||||
python3 scripts/ci_coverage_summary.py
|
||||
set -e
|
||||
|
||||
echo ""
|
||||
echo "=== CI Integration Tests 全部通过 ✅ ==="
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# CI Unit Tests Job 主脚本
|
||||
# 包含:依赖安装、增量测试选择、覆盖率测试、diff覆盖率门禁
|
||||
set -eu
|
||||
|
||||
JOB_NAME="${1:-Unit Tests}"
|
||||
|
||||
echo "=== CI Unit Tests 开始 ==="
|
||||
|
||||
# --- 安装依赖 ---
|
||||
echo ""
|
||||
echo "=== 安装 Python 依赖 ==="
|
||||
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
|
||||
|
||||
# --- 增量测试选择(仅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) ==="
|
||||
|
||||
if [ "$UNIT_TEST_MODE" = "incremental" ]; then
|
||||
echo "=== 增量测试模式 ==="
|
||||
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run \
|
||||
--source=apps/api/app,packages \
|
||||
--omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \
|
||||
--branch \
|
||||
-m pytest $SELECTED_TEST_FILES -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" python3 -m coverage run \
|
||||
--source=apps/api/app,packages \
|
||||
--omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \
|
||||
--branch \
|
||||
-m pytest tests/unit -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 全部通过 ✅ ==="
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
#!/bin/bash
|
||||
# CI Validate Job 主脚本:代码质量全量检查
|
||||
# 包含:密钥扫描、格式检查、类型检查、安全扫描、依赖漏洞检查、死代码检测、Alembic迁移验证
|
||||
set -eu
|
||||
|
||||
echo "=== CI Validate: 开始全量代码质量检查 ==="
|
||||
|
||||
# --- 密钥检测 ---
|
||||
echo ""
|
||||
echo "=== [1/8] Secret detection (detect-secrets) ==="
|
||||
python3 -m pip install -q detect-secrets
|
||||
detect-secrets --version
|
||||
|
||||
detect-secrets scan \
|
||||
--all-files \
|
||||
--exclude-files '(^|/)(tests|test|e2e|__tests__|spec|docs|node_modules|site-packages|migrations|alembic|.gitea|.git|.pytest_cache|.next|dist|build)/' \
|
||||
--exclude-files '\.(md|rst|txt|lock|example|sample|min\.js|min\.css|spec\.ts|test\.ts|test\.py)$' \
|
||||
--exclude-files '(package-lock|yarn\.lock|poetry\.lock|Pipfile\.lock)$' \
|
||||
--disable-plugin Base64HighEntropyString \
|
||||
--disable-plugin HexHighEntropyString \
|
||||
--disable-plugin BasicAuthDetector \
|
||||
--disable-plugin KeywordDetector \
|
||||
--disable-plugin IPPublicDetector \
|
||||
> /tmp/secrets-scan.json 2>&1
|
||||
|
||||
FOUND=$(python3 -c "
|
||||
import json
|
||||
try:
|
||||
with open('/tmp/secrets-scan.json') as f:
|
||||
data = json.load(f)
|
||||
results = data.get('results', {})
|
||||
total = sum(len(v) for v in results.values())
|
||||
print(total)
|
||||
except Exception:
|
||||
print('error')
|
||||
")
|
||||
|
||||
echo "Secrets detected: $FOUND"
|
||||
if [ "$FOUND" != "0" ] && [ "$FOUND" != "error" ]; then
|
||||
echo ""
|
||||
echo "=== Secret details ==="
|
||||
python3 -c "
|
||||
import json
|
||||
with open('/tmp/secrets-scan.json') as f:
|
||||
data = json.load(f)
|
||||
for fpath, items in data.get('results', {}).items():
|
||||
for item in items:
|
||||
line = item.get('line_number', '?')
|
||||
stype = item.get('type', '?')
|
||||
hashed = item.get('hashed_secret', '')[:16]
|
||||
print(f' {fpath}:{line} [{stype}] {hashed}...')
|
||||
"
|
||||
echo ""
|
||||
echo "ERROR: Potential secrets detected in code!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Secret scan passed"
|
||||
|
||||
# --- 增量/全量模式判断 ---
|
||||
echo ""
|
||||
echo "=== [2/8] Code quality checks ==="
|
||||
SCAN_MODE="full"
|
||||
CHANGED_PY_FILES=""
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && [ -n "${GITHUB_REF_NAME:-}" ] && [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
|
||||
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=100"
|
||||
set +e
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL")
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
set -e
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
CHANGED_PY_FILES=$(echo "$BODY" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
files = json.load(sys.stdin)
|
||||
py_files = [f['filename'] for f in files if f['filename'].endswith('.py') and f['status'] != 'removed']
|
||||
print(' '.join(py_files))
|
||||
except Exception:
|
||||
print('')
|
||||
")
|
||||
if [ -n "$CHANGED_PY_FILES" ]; then
|
||||
SCAN_MODE="incremental"
|
||||
echo "Incremental mode: $(echo "$CHANGED_PY_FILES" | wc -w) Python files changed"
|
||||
else
|
||||
SCAN_MODE="skip_py"
|
||||
echo "No Python files changed in this PR"
|
||||
fi
|
||||
else
|
||||
echo "WARN: API returned HTTP $HTTP_CODE, falling back to full scan"
|
||||
fi
|
||||
else
|
||||
echo "Full scan mode (not a PR event)"
|
||||
fi
|
||||
|
||||
if [ "$SCAN_MODE" = "incremental" ]; then
|
||||
python3 -m compileall -q $CHANGED_PY_FILES
|
||||
python3 -m black --check --fast $CHANGED_PY_FILES
|
||||
python3 -m isort --check-only $CHANGED_PY_FILES
|
||||
RUFF_FILES=$(echo "$CHANGED_PY_FILES" | tr ' ' '\n' | grep -v '^scripts/' | tr '\n' ' ')
|
||||
if [ -n "$RUFF_FILES" ]; then
|
||||
python3 -m ruff check $RUFF_FILES --statistics
|
||||
else
|
||||
echo "No ruff-checkable files changed, skipping"
|
||||
fi
|
||||
elif [ "$SCAN_MODE" = "skip_py" ]; then
|
||||
echo "No Python files changed - skipping Python lint checks"
|
||||
else
|
||||
echo "Full scan mode"
|
||||
python3 -m compileall -q alembic apps packages tests scripts
|
||||
python3 -m black --check --fast alembic apps packages tests scripts
|
||||
python3 -m isort --check-only alembic apps packages tests scripts
|
||||
python3 -m ruff check apps packages tests --statistics
|
||||
fi
|
||||
echo "✅ Code quality checks passed"
|
||||
|
||||
# --- Mypy 类型检查 ---
|
||||
echo ""
|
||||
echo "=== [3/8] Type check (mypy) ==="
|
||||
bash scripts/ci/mypy_check.sh
|
||||
echo "✅ Mypy type check passed"
|
||||
|
||||
# --- Bandit 安全扫描 ---
|
||||
echo ""
|
||||
echo "=== [4/8] Security scan (bandit) ==="
|
||||
bandit -r apps packages -q -ll
|
||||
echo "✅ Bandit security scan passed"
|
||||
|
||||
# --- Pip-audit 依赖漏洞扫描(仅告警) ---
|
||||
echo ""
|
||||
echo "=== [5/8] Python dependency vulnerability scan (pip-audit, advisory only) ==="
|
||||
python3 -m pip install -q pip-audit
|
||||
pip-audit --version
|
||||
EXIT_CODE=0
|
||||
for req_file in requirements.txt requirements-base.txt requirements-dev.txt; do
|
||||
if [ -f "$req_file" ]; then
|
||||
echo "--- Scanning $req_file ---"
|
||||
pip-audit -r "$req_file" --desc on 2>&1 | head -40 || EXIT_CODE=$?
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
echo "pip-audit scan completed (advisory mode - warnings only, not blocking CI)"
|
||||
|
||||
# --- Vulture 死代码检测(仅告警) ---
|
||||
echo ""
|
||||
echo "=== [6/8] Dead code detection (vulture, advisory only) ==="
|
||||
set +e
|
||||
python3 -m pip install -q vulture
|
||||
vulture --version
|
||||
echo "告警模式,不阻断CI。置信度>=90%建议尽快确认。"
|
||||
echo ""
|
||||
vulture apps packages scripts \
|
||||
--exclude "tests,test,migrations,.gitea,docs,node_modules,site-packages,*/test_*.py,*/conftest.py" \
|
||||
--min-confidence 70 \
|
||||
2>&1 | sort -t'(' -k2 -rn | head -80
|
||||
echo ""
|
||||
echo "=== vulture scan summary ==="
|
||||
echo "发现潜在死代码(可能包含框架装饰器注册的函数,为误报)"
|
||||
echo "建议:定期人工审查高置信度(>=90%)条目"
|
||||
set -e
|
||||
|
||||
# --- Release 脚本语法校验 ---
|
||||
echo ""
|
||||
echo "=== [7/8] Release scripts syntax validation ==="
|
||||
bash -n scripts/backup_postgres.sh
|
||||
bash -n scripts/restore_postgres_plan.sh
|
||||
bash -n scripts/init_production_env.sh
|
||||
echo "✅ Release scripts syntax OK"
|
||||
|
||||
# --- Alembic 迁移验证 ---
|
||||
echo ""
|
||||
echo "=== [8/8] Alembic migrations validation (with isolated PG) ==="
|
||||
PG_CONTAINER=ci-pg-validate-${GITHUB_RUN_ID:-$$}
|
||||
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
|
||||
docker run -d --name "$PG_CONTAINER" \
|
||||
--shm-size=256m \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=xiaoxia_saas \
|
||||
-P \
|
||||
--health-cmd "pg_isready -U postgres" \
|
||||
--health-interval 3s \
|
||||
--health-timeout 3s \
|
||||
--health-retries 20 \
|
||||
postgres:16-alpine
|
||||
PG_PORT=$(docker port "$PG_CONTAINER" 5432/tcp | cut -d: -f2)
|
||||
echo "PostgreSQL port: $PG_PORT"
|
||||
export DATABASE_URL=postgresql+psycopg://postgres:postgres@127.0.0.1:$PG_PORT/xiaoxia_saas
|
||||
for i in $(seq 1 30); do
|
||||
if docker inspect --format='{{.State.Health.Status}}' "$PG_CONTAINER" 2>/dev/null | grep -q healthy; then
|
||||
echo "PostgreSQL is ready on port $PG_PORT"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for PostgreSQL... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
docker inspect --format='{{.State.Health.Status}}' "$PG_CONTAINER" | grep -q healthy
|
||||
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m alembic upgrade head
|
||||
echo "✅ Alembic migrations applied successfully"
|
||||
docker rm -f "$PG_CONTAINER" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "=== CI Validate: 所有检查通过 ✅ ==="
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:Checkout 代码(带重试)
|
||||
# 用法:直接 source 或调用,需要 GITHUB_TOKEN 环境变量
|
||||
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
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:前端依赖安装(在 docker node 容器中运行)
|
||||
# 用法:step_frontend_install.sh [模式]
|
||||
# 模式: full (默认) - 完整安装所有依赖
|
||||
# vitest - 同full(保持接口兼容)
|
||||
set -eu
|
||||
|
||||
MODE="${1:-full}"
|
||||
|
||||
echo "=== 前端依赖安装开始 (模式: $MODE) ==="
|
||||
|
||||
docker run --rm \
|
||||
-v "$PWD:/workspace" \
|
||||
-w /workspace/apps/web \
|
||||
docker.m.daocloud.io/library/node:20 \
|
||||
sh -lc "npm ci --no-audit --no-fund"
|
||||
|
||||
echo "=== 前端依赖安装完成 ==="
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:前端命令执行(在 docker node 容器中运行)
|
||||
# 用法:step_frontend_run.sh "要执行的命令"
|
||||
set -eu
|
||||
|
||||
CMD="${1:-echo 'no command'}"
|
||||
|
||||
docker run --rm \
|
||||
-v "$PWD:/workspace" \
|
||||
-w /workspace/apps/web \
|
||||
docker.m.daocloud.io/library/node:20 \
|
||||
sh -lc "$CMD"
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:安装 ffmpeg
|
||||
set +e
|
||||
if command -v ffmpeg > /dev/null 2>&1; then
|
||||
echo "ffmpeg already installed: $(ffmpeg -version | head -1)"
|
||||
exit 0
|
||||
fi
|
||||
if command -v apt-get > /dev/null 2>&1; then
|
||||
apt-get update -qq && apt-get install -y -qq ffmpeg
|
||||
elif command -v yum > /dev/null 2>&1; then
|
||||
yum install -y -q epel-release 2>/dev/null
|
||||
yum install -y -q ffmpeg 2>/dev/null
|
||||
if [ $? -ne 0 ] && command -v dnf > /dev/null 2>&1; then
|
||||
dnf install -y -q --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm 2>/dev/null
|
||||
dnf install -y -q ffmpeg 2>/dev/null
|
||||
fi
|
||||
elif command -v dnf > /dev/null 2>&1; then
|
||||
dnf install -y -q ffmpeg 2>/dev/null
|
||||
fi
|
||||
if command -v ffmpeg > /dev/null 2>&1; then
|
||||
echo "ffmpeg installed successfully: $(ffmpeg -version | head -1)"
|
||||
else
|
||||
echo "Warning: ffmpeg installation failed or not available, some tests may be skipped"
|
||||
fi
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:Job 结束计时统计
|
||||
set +eu
|
||||
if [ -n "$JOB_START_TIME" ]; then
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - JOB_START_TIME))
|
||||
MINS=$((DURATION / 60))
|
||||
SECS=$((DURATION % 60))
|
||||
echo "JOB_DURATION_SECONDS=$DURATION" >> $GITHUB_ENV
|
||||
echo "=== Job Duration: ${MINS}m${SECS}s ==="
|
||||
else
|
||||
echo "JOB_DURATION_SECONDS=0" >> $GITHUB_ENV
|
||||
echo "=== Job Duration: unknown ==="
|
||||
fi
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:Job 开始计时
|
||||
echo "JOB_START_TIME=$(date +%s)" >> $GITHUB_ENV
|
||||
echo "Job started at $(date)"
|
||||
Reference in New Issue
Block a user