From 98bc2afa6efab21fed0eeb2ad78f1a20b353e330 Mon Sep 17 00:00:00 2001 From: XiaoXia Bot Date: Thu, 16 Jul 2026 09:55:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=BF=87=E6=BB=A4=E5=AE=9E=E7=8E=B0=20-=20?= =?UTF-8?q?=E7=94=A8=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F+=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4if=E7=A1=AE=E4=BF=9D=E7=9C=9F=E8=B7=B3=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原方案step内exit 0无法跳过后续步骤 - 改用IS_FRONTEND_ONLY环境变量 + 每个后端步骤if条件 - 覆盖validate/unit-tests/integration-tests三个后端job - 纯前端PR只跑Frontend Lint,3-5分钟完成 --- .gitea/workflows/ci-cd.yml | 154 ++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index b322adb28..3a0f5ef0b 100755 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -83,16 +83,63 @@ jobs: fi echo "🔧 包含后端/公共变更,继续完整检查" + - name: Detect frontend-only change + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + echo "IS_FRONTEND_ONLY=false" >> $GITHUB_ENV + + if [ "${GITHUB_EVENT_NAME:-}" != "pull_request" ]; then + echo "非PR模式,继续执行完整CI" + exit 0 + fi + + PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||') + API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${{PR_NUMBER}}/files?limit=300" + + set +e + FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | grep -o '"filename": *"[^"]*"' | sed 's/"filename": *"//;s/"$//') + set -e + + if [ -z "$FILES" ]; then + echo "无法获取变更文件列表,继续执行完整CI" + exit 0 + fi + + FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) + BACKEND_COUNT=$(echo "$FILES" | grep -cv '^apps/web/' || true) + TOTAL_COUNT=$(echo "$FILES" | grep -v '^$' | wc -l) + + echo "变更文件: $TOTAL_COUNT 个 (前端: $FRONTEND_COUNT, 后端/公共: $BACKEND_COUNT)" + + if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then + echo "✅ 纯前端改动,跳过后端检查" + echo "IS_FRONTEND_ONLY=true" >> $GITHUB_ENV + else + echo "🔧 包含后端/公共变更,执行完整CI" + fi + - name: Skip notice (frontend-only) + if: env.IS_FRONTEND_ONLY == 'true' + shell: sh + run: | + echo "==========================================" + echo " 纯前端改动,跳过后端相关检查" + echo " 仅执行 Frontend Lint" + echo "==========================================" + - name: Record job start time + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu - echo "JOB_START_TIME=$(date +%s)" >> $GITHUB_ENV + echo "JOB_START_TIME=$(date +%s) >> $GITHUB_ENV echo "Job started at $(date)" ' - name: Verify CI environment + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -104,6 +151,7 @@ jobs: ' - name: Install dependencies + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -125,6 +173,7 @@ jobs: ' - name: Secret detection (detect-secrets) + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: "set -eu\necho \"=== Installing detect-secrets ===\"\npython3 -m pip install -q detect-secrets\ndetect-secrets --version\necho \"\"\necho \"=== Running secret scan ===\"\ndetect-secrets scan \\\n --all-files \\\n --exclude-files '(^|/)(tests|test|e2e|__tests__|spec|docs|node_modules|site-packages|migrations|alembic|.gitea|.git|.pytest_cache|.next|dist|build)/' \\\n --exclude-files '\\.(md|rst|txt|lock|example|sample|min\\.js|min\\.css|spec\\.ts|test\\.ts|test\\.py)$' \\\n --exclude-files '(package-lock|yarn\\.lock|poetry\\.lock|Pipfile\\.lock)$' \\\n --disable-plugin Base64HighEntropyString \\\n --disable-plugin HexHighEntropyString \\\n --disable-plugin BasicAuthDetector \\\n --disable-plugin KeywordDetector \\\n --disable-plugin IPPublicDetector \\\n 2>&1 | tee /tmp/secrets-scan.json\n\nFOUND=$(python3 -c \"\nimport json\ntry:\n with open('/tmp/secrets-scan.json') as f:\n data = json.load(f)\n results = data.get('results', {})\n total = sum(len(v) for\ \ v in results.values())\n print(total)\nexcept Exception:\n print('error')\n\")\necho \"\"\necho \"Secrets detected: $FOUND\"\nif [ \"$FOUND\" != \"0\" ] && [ \"$FOUND\" != \"error\" ]; then\n echo \"\"\n echo \"=== Secret details ===\"\n python3 -c \"\nimport json\nwith open('/tmp/secrets-scan.json') as f:\n data = json.load(f)\nfor fpath, items in data.get('results', {}).items():\n for item in items:\n line = item.get('line_number', '?')\n stype = item.get('type', '?')\n hashed = item.get('hashed_secret', '')[:16]\n print(f' {fpath}:{line} [{stype}] {hashed}...')\n\"\n echo \"\"\n echo \"ERROR: Potential secrets detected in code!\"\n echo \"If these are false positives, add exclusions in the CI workflow.\"\n exit 1\nfi\necho \"Secret scan completed - no secrets detected\"\n" @@ -134,6 +183,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: "set -eu\nSCAN_MODE=\"full\"\nCHANGED_PY_FILES=\"\"\n\nif [ \"${GITHUB_EVENT_NAME:-}\" = \"pull_request\" ] && [ -n \"${GITHUB_REF_NAME:-}\" ]; then\n echo \"PR mode (#${GITHUB_REF_NAME}) - fetching changed files from API\"\n\n PR_NUMBER=$(echo \"$GITHUB_REF\" | sed 's|refs/pull/||; s|/.*||')\n API_URL=\"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=100\"\n\n set +e\n RESPONSE=$(curl -s -w \"\\n%{http_code}\" -H \"Authorization: token ${GITHUB_TOKEN}\" \"${API_URL}\")\n HTTP_CODE=$(echo \"$RESPONSE\" | tail -n1)\n BODY=$(echo \"$RESPONSE\" | sed '$d')\n set -e\n\n if [ \"$HTTP_CODE\" = \"200\" ]; then\n CHANGED_PY_FILES=$(echo \"$BODY\" | python3 -c \"\nimport json, sys\ntry:\n files = json.load(sys.stdin)\n py_files = [f['filename'] for f in files\n if f['filename'].endswith('.py') and f['status'] != 'removed']\n print(' '.join(py_files))\nexcept Exception:\n print('')\n\")\n if [ -n \"$CHANGED_PY_FILES\" ]; then\n SCAN_MODE=\"incremental\"\n FILE_COUNT=$(echo \"$CHANGED_PY_FILES\" | wc -w)\n echo \"Changed Python files: ${FILE_COUNT}\"\n echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^$'\n else\n SCAN_MODE=\"skip_py\"\n echo \"No Python files changed in this PR\"\n fi\n else\n echo \"WARN: API returned HTTP $HTTP_CODE, falling back to full scan\"\n fi\nelse\n echo \"Full scan mode (not a PR event)\"\nfi\n\necho \"SCAN_MODE=$SCAN_MODE\" >> $GITHUB_ENV\necho \"CHANGED_PY_FILES=$CHANGED_PY_FILES\" >> $GITHUB_ENV\n" - name: Run code quality checks + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: "set -eu\n\nif [ \"$SCAN_MODE\" = \"incremental\" ]; then\n echo \"=== Incremental scan mode ===\"\n\n python3 -m compileall -q $CHANGED_PY_FILES\n\n python3 -m black --check --fast $CHANGED_PY_FILES\n\n python3 -m isort --check-only $CHANGED_PY_FILES\n\n RUFF_FILES=$(echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^scripts/' | tr '\\n' ' ')\n if [ -n \"$RUFF_FILES\" ]; then\n python3 -m ruff check $RUFF_FILES --statistics\n else\n echo \"No ruff-checkable files changed, skipping\"\n fi\n\nelif [ \"$SCAN_MODE\" = \"skip_py\" ]; then\n echo \"No Python files changed - skipping Python lint checks\"\n\nelse\n echo \"=== Full scan mode ===\"\n\n python3 -m compileall -q alembic apps packages tests scripts\n\n python3 -m black --check --fast alembic apps packages tests scripts\n\n python3 -m isort --check-only alembic apps packages tests scripts\n\n python3 -m ruff check apps packages tests --statistics\nfi\n" - name: Type check (mypy, hard gate) @@ -141,6 +191,7 @@ jobs: shell: sh run: "bash scripts/ci/mypy_check.sh" - name: Run security scan (bandit) + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -155,6 +206,7 @@ jobs: shell: sh run: "set +e\necho \"=== Installing vulture ===\"\npython3 -m pip install -q vulture\nvulture --version\necho \"\"\necho \"=== Running vulture dead code scan (confidence >= 70%) ===\"\necho \"告警模式,不阻断CI。置信度>=90%建议尽快确认。\"\necho \"\"\n# 按置信度从高到低输出,便于优先查看高价值条目\nvulture apps packages scripts \\\n --exclude \"tests,test,migrations,.gitea,docs,node_modules,site-packages,*/test_*.py,*/conftest.py\" \\\n --min-confidence 70 \\\n 2>&1 | sort -t'(' -k2 -rn | head -80\nEXIT_CODE=$?\necho \"\"\necho \"=== vulture scan summary ===\"\nif [ \"$EXIT_CODE\" != \"0\" ]; then\n echo \"发现潜在死代码(可能包含框架装饰器注册的函数,为误报)\"\n echo \"建议:定期人工审查高置信度(>=90%)条目\"\nelse\n echo \"未发现明显死代码 ✅\"\nfi\nexit 0\n" - name: Validate release scripts syntax + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -166,6 +218,7 @@ jobs: ' - name: Validate Alembic migrations + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -179,6 +232,7 @@ jobs: ' - name: Check migration safety + if: env.IS_FRONTEND_ONLY != 'true' shell: sh env: GITHUB_TOKEN: ${{ github.token }} @@ -215,7 +269,53 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: "set -eu\npython3 - <<'PY'\nimport io, os, tarfile, time, urllib.request, urllib.error\nurl = f\"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz\"\nrequest = urllib.request.Request(url, headers={\"Authorization\": f\"token {os.environ['GITHUB_TOKEN']}\"})\nlast_err = None\nfor attempt in range(5):\n try:\n with urllib.request.urlopen(request, timeout=120) as response:\n archive = response.read()\n break\n except urllib.error.HTTPError as e:\n last_err = e\n if e.code >= 500 and attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...\")\n time.sleep(wait)\n continue\n raise\n except Exception as e:\n last_err = e\n if attempt < 4:\n wait = 2 ** attempt\n print(f\"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...\"\ )\n time.sleep(wait)\n continue\n raise\nelse:\n raise last_err\nwith tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:\n root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'\n for member in tar.getmembers():\n name = member.name\n if name == root_prefix[:-1]:\n continue\n if name.startswith(root_prefix):\n member.name = name[len(root_prefix):]\n if member.name:\n tar.extract(member, '.')\nPY\n" + - name: Detect frontend-only change + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + echo "IS_FRONTEND_ONLY=false" >> $GITHUB_ENV + + if [ "${GITHUB_EVENT_NAME:-}" != "pull_request" ]; then + echo "非PR模式,继续执行完整CI" + exit 0 + fi + + PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||') + API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${{PR_NUMBER}}/files?limit=300" + + set +e + FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | grep -o '"filename": *"[^"]*"' | sed 's/"filename": *"//;s/"$//') + set -e + + if [ -z "$FILES" ]; then + echo "无法获取变更文件列表,继续执行完整CI" + exit 0 + fi + + FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) + BACKEND_COUNT=$(echo "$FILES" | grep -cv '^apps/web/' || true) + TOTAL_COUNT=$(echo "$FILES" | grep -v '^$' | wc -l) + + echo "变更文件: $TOTAL_COUNT 个 (前端: $FRONTEND_COUNT, 后端/公共: $BACKEND_COUNT)" + + if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then + echo "✅ 纯前端改动,跳过后端检查" + echo "IS_FRONTEND_ONLY=true" >> $GITHUB_ENV + else + echo "🔧 包含后端/公共变更,执行完整CI" + fi + - name: Skip notice (frontend-only) + if: env.IS_FRONTEND_ONLY == 'true' + shell: sh + run: | + echo "==========================================" + echo " 纯前端改动,跳过后端相关检查" + echo " 仅执行 Frontend Lint" + echo "==========================================" + - name: Record job start time + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -225,9 +325,11 @@ jobs: ' - name: Install ffmpeg + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: "set +e\nif command -v ffmpeg > /dev/null 2>&1; then\n echo \"ffmpeg already installed: $(ffmpeg -version | head -1)\"\n exit 0\nfi\nif command -v apt-get > /dev/null 2>&1; then\n apt-get update -qq && apt-get install -y -qq ffmpeg\nelif command -v yum > /dev/null 2>&1; then\n yum install -y -q epel-release 2>/dev/null\n yum install -y -q ffmpeg 2>/dev/null\n if [ $? -ne 0 ] && command -v dnf > /dev/null 2>&1; then\n dnf install -y -q --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm 2>/dev/null\n dnf install -y -q ffmpeg 2>/dev/null\n fi\nelif command -v dnf > /dev/null 2>&1; then\n dnf install -y -q ffmpeg 2>/dev/null\nfi\nif command -v ffmpeg > /dev/null 2>&1; then\n echo \"ffmpeg installed successfully: $(ffmpeg -version | head -1)\"\nelse\n echo \"Warning: ffmpeg installation failed or not available, some tests may be skipped\"\nfi\n" - name: Install dependencies + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -241,6 +343,7 @@ jobs: ' - name: Run unit tests with coverage + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: "set -eu\nPYTHONPATH=\"$PWD/apps/api:$PWD\" python3 -m coverage run \\\n --source=apps/api/app,packages \\\n --omit=\"*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*\" \\\n --branch \\\n -m pytest tests/unit -q\npython3 -m coverage report --show-missing\npython3 -m coverage xml -o coverage.xml\npython3 -m coverage report --fail-under=60 > /dev/null\n" - name: CI failure notification @@ -325,7 +428,53 @@ jobs: fi echo "🔧 包含后端/公共变更,继续完整检查" + - name: Detect frontend-only change + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + echo "IS_FRONTEND_ONLY=false" >> $GITHUB_ENV + + if [ "${GITHUB_EVENT_NAME:-}" != "pull_request" ]; then + echo "非PR模式,继续执行完整CI" + exit 0 + fi + + PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||') + API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${{PR_NUMBER}}/files?limit=300" + + set +e + FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | grep -o '"filename": *"[^"]*"' | sed 's/"filename": *"//;s/"$//') + set -e + + if [ -z "$FILES" ]; then + echo "无法获取变更文件列表,继续执行完整CI" + exit 0 + fi + + FRONTEND_COUNT=$(echo "$FILES" | grep -c '^apps/web/' || true) + BACKEND_COUNT=$(echo "$FILES" | grep -cv '^apps/web/' || true) + TOTAL_COUNT=$(echo "$FILES" | grep -v '^$' | wc -l) + + echo "变更文件: $TOTAL_COUNT 个 (前端: $FRONTEND_COUNT, 后端/公共: $BACKEND_COUNT)" + + if [ "$BACKEND_COUNT" = "0" ] && [ "$FRONTEND_COUNT" -gt "0" ]; then + echo "✅ 纯前端改动,跳过后端检查" + echo "IS_FRONTEND_ONLY=true" >> $GITHUB_ENV + else + echo "🔧 包含后端/公共变更,执行完整CI" + fi + - name: Skip notice (frontend-only) + if: env.IS_FRONTEND_ONLY == 'true' + shell: sh + run: | + echo "==========================================" + echo " 纯前端改动,跳过后端相关检查" + echo " 仅执行 Frontend Lint" + echo "==========================================" + - name: Record job start time + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -346,6 +495,7 @@ jobs: ' - name: Install dependencies + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: 'set -eu @@ -359,6 +509,7 @@ jobs: ' - name: Install ffmpeg + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: "set +e\nif command -v ffmpeg > /dev/null 2>&1; then\n echo \"ffmpeg already installed: $(ffmpeg -version | head -1)\"\n exit 0\nfi\nif command -v apt-get > /dev/null 2>&1; then\n apt-get update -qq && apt-get install -y -qq ffmpeg\nelif command -v yum > /dev/null 2>&1; then\n yum install -y -q epel-release 2>/dev/null\n yum install -y -q ffmpeg 2>/dev/null\n if [ $? -ne 0 ] && command -v dnf > /dev/null 2>&1; then\n dnf install -y -q --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm 2>/dev/null\n dnf install -y -q ffmpeg 2>/dev/null\n fi\nelif command -v dnf > /dev/null 2>&1; then\n dnf install -y -q ffmpeg 2>/dev/null\nfi\nif command -v ffmpeg > /dev/null 2>&1; then\n echo \"ffmpeg installed successfully: $(ffmpeg -version | head -1)\"\nelse\n echo \"Warning: ffmpeg installation failed or not available, some tests may be skipped\"\nfi\n" - name: Start Redis @@ -376,6 +527,7 @@ jobs: ' - name: Run integration tests + if: env.IS_FRONTEND_ONLY != 'true' shell: sh run: "set -eu\npython3 -m pip install -q pytest-rerunfailures\nPYTHONPATH=\"$PWD/apps/api:$PWD\" python3 -m coverage run --append \\\n --source=apps/api/app,packages \\\n --omit=\"*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*\" \\\n --branch \\\n -m pytest tests/integration -q --timeout=60 -x --reruns 2 --reruns-delay 1 -m \"not performance\"\npython3 -m coverage report --show-missing\npython3 -m coverage xml -o coverage.xml\npython3 -m coverage report --fail-under=40 > /dev/null # 集成测试覆盖率门槛较低,核心目标是功能验证\n" - name: Run API performance baseline tests