|
|
|
@@ -0,0 +1,658 @@
|
|
|
|
|
name: Daily Health Check
|
|
|
|
|
|
|
|
|
|
on:
|
|
|
|
|
schedule:
|
|
|
|
|
- cron: '0 19 * * *' # UTC 19:00 = 北京时间凌晨 3:00
|
|
|
|
|
workflow_dispatch:
|
|
|
|
|
|
|
|
|
|
permissions:
|
|
|
|
|
contents: read
|
|
|
|
|
|
|
|
|
|
jobs:
|
|
|
|
|
# ── 1. 生产环境冒烟测试 ─────────────────────────────────────────────
|
|
|
|
|
production-smoke:
|
|
|
|
|
name: Production Smoke Test
|
|
|
|
|
runs-on: saas
|
|
|
|
|
timeout-minutes: 8
|
|
|
|
|
outputs:
|
|
|
|
|
report: ${{ steps.smoke.outputs.report }}
|
|
|
|
|
|
|
|
|
|
steps:
|
|
|
|
|
- name: Checkout code
|
|
|
|
|
shell: sh
|
|
|
|
|
env:
|
|
|
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
|
|
|
run: |
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
- name: Production health check & smoke test
|
|
|
|
|
id: smoke
|
|
|
|
|
shell: sh
|
|
|
|
|
env:
|
|
|
|
|
SMOKE_ENV: production
|
|
|
|
|
EXISTING_TOKEN: ${{ secrets.PROD_E2E_TOKEN }}
|
|
|
|
|
MODULES: health,assets,generation,subscription,nginx
|
|
|
|
|
run: |
|
|
|
|
|
set +e
|
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
|
chmod +x tests/e2e/api_smoke_test.sh
|
|
|
|
|
BASE_URL="https://api.xiaoxiajianji.com" \
|
|
|
|
|
WEB_URL="https://saas.xiaoxiajianji.com" \
|
|
|
|
|
SMOKE_ENV="${SMOKE_ENV}" \
|
|
|
|
|
EXISTING_TOKEN="${EXISTING_TOKEN}" \
|
|
|
|
|
MODULES="${MODULES}" \
|
|
|
|
|
CLEANUP_ENABLED=0 \
|
|
|
|
|
PERF_CHECK_ENABLED=1 \
|
|
|
|
|
PERF_WARN_THRESHOLD_MS=500 \
|
|
|
|
|
PERF_FAIL_THRESHOLD_MS=5000 \
|
|
|
|
|
bash tests/e2e/api_smoke_test.sh 2>&1 | tee /tmp/prod-smoke.log
|
|
|
|
|
SMOKE_EXIT=${PIPESTATUS[0]}
|
|
|
|
|
END_TIME=$(date +%s)
|
|
|
|
|
ELAPSED=$((END_TIME - START_TIME))
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========== 生产冒烟测试报告 =========="
|
|
|
|
|
echo "环境: https://api.xiaoxiajianji.com"
|
|
|
|
|
echo "耗时: ${ELAPSED}s"
|
|
|
|
|
# 提取通过/失败数
|
|
|
|
|
grep "测试完成:" /tmp/prod-smoke.log || true
|
|
|
|
|
if [ "$SMOKE_EXIT" -eq 0 ]; then
|
|
|
|
|
echo "结果: PASS"
|
|
|
|
|
echo "report=PASS" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
else
|
|
|
|
|
echo "结果: FAIL"
|
|
|
|
|
grep "失败用例:" /tmp/prod-smoke.log || true
|
|
|
|
|
echo "report=FAIL" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
fi
|
|
|
|
|
echo "======================================"
|
|
|
|
|
exit $SMOKE_EXIT
|
|
|
|
|
|
|
|
|
|
# ── 2. Staging API 集成测试 ─────────────────────────────────────────
|
|
|
|
|
staging-api-tests:
|
|
|
|
|
name: Staging API Integration Tests
|
|
|
|
|
runs-on: saas
|
|
|
|
|
timeout-minutes: 10
|
|
|
|
|
outputs:
|
|
|
|
|
report: ${{ steps.smoke.outputs.report }}
|
|
|
|
|
|
|
|
|
|
steps:
|
|
|
|
|
- name: Checkout code
|
|
|
|
|
shell: sh
|
|
|
|
|
env:
|
|
|
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
|
|
|
run: |
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
- name: Run API smoke test on staging
|
|
|
|
|
id: smoke
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
|
set +e
|
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
|
chmod +x tests/e2e/api_smoke_test.sh
|
|
|
|
|
docker run --rm \
|
|
|
|
|
-e BASE_URL=https://staging-api.xiaoxiajianji.com \
|
|
|
|
|
-e WEB_URL=https://staging.xiaoxiajianji.com \
|
|
|
|
|
-e TEST_USER=18314979086@163.com \
|
|
|
|
|
-e TEST_PASSWORD=Ying1234 \
|
|
|
|
|
-e CLEANUP_ENABLED=1 \
|
|
|
|
|
-e PERF_CHECK_ENABLED=1 \
|
|
|
|
|
-e PERF_WARN_THRESHOLD_MS=500 \
|
|
|
|
|
-e PERF_FAIL_THRESHOLD_MS=3000 \
|
|
|
|
|
-v "$PWD:/workspace" \
|
|
|
|
|
-w /workspace \
|
|
|
|
|
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
|
|
|
|
|
bash tests/e2e/api_smoke_test.sh 2>&1 | tee /tmp/staging-api-smoke.log
|
|
|
|
|
SMOKE_EXIT=${PIPESTATUS[0]}
|
|
|
|
|
END_TIME=$(date +%s)
|
|
|
|
|
ELAPSED=$((END_TIME - START_TIME))
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========== Staging API 冒烟测试报告 =========="
|
|
|
|
|
echo "环境: https://staging-api.xiaoxiajianji.com"
|
|
|
|
|
echo "耗时: ${ELAPSED}s"
|
|
|
|
|
grep "测试完成:" /tmp/staging-api-smoke.log || true
|
|
|
|
|
if [ "$SMOKE_EXIT" -eq 0 ]; then
|
|
|
|
|
echo "结果: PASS"
|
|
|
|
|
echo "api_report=PASS" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
else
|
|
|
|
|
echo "结果: FAIL"
|
|
|
|
|
grep "失败用例:" /tmp/staging-api-smoke.log || true
|
|
|
|
|
echo "api_report=FAIL" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
fi
|
|
|
|
|
echo "=============================================="
|
|
|
|
|
exit $SMOKE_EXIT
|
|
|
|
|
|
|
|
|
|
- name: Run Staging API Integration Tests (Playwright)
|
|
|
|
|
id: e2e_api
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
|
set +e
|
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
|
docker run --rm \
|
|
|
|
|
-e E2E_BASE_URL=https://staging.xiaoxiajianji.com \
|
|
|
|
|
-e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \
|
|
|
|
|
-v "$PWD:/workspace" \
|
|
|
|
|
-w /workspace/apps/web \
|
|
|
|
|
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
|
|
|
|
|
sh -lc "npm ci && npx playwright test --reporter=line e2e/test_auth.spec.ts e2e/test_asset.spec.ts e2e/test_project.spec.ts" 2>&1 | tee /tmp/staging-api-e2e.log
|
|
|
|
|
EXIT_CODE=${PIPESTATUS[0]}
|
|
|
|
|
END_TIME=$(date +%s)
|
|
|
|
|
ELAPSED=$((END_TIME - START_TIME))
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========== Staging API 集成测试报告 =========="
|
|
|
|
|
echo "环境: https://staging-api.xiaoxiajianji.com"
|
|
|
|
|
echo "耗时: ${ELAPSED}s"
|
|
|
|
|
grep -E "passed|failed|timed out" /tmp/staging-api-e2e.log || true
|
|
|
|
|
if [ "$EXIT_CODE" -eq 0 ]; then
|
|
|
|
|
echo "结果: PASS"
|
|
|
|
|
echo "int_report=PASS" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
else
|
|
|
|
|
echo "结果: FAIL"
|
|
|
|
|
echo "int_report=FAIL" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
fi
|
|
|
|
|
echo "=============================================="
|
|
|
|
|
exit $EXIT_CODE
|
|
|
|
|
|
|
|
|
|
- name: Set report output
|
|
|
|
|
id: report
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
|
if [ "${{ steps.smoke.outputs.api_report }}" = "PASS" ] && [ "${{ steps.e2e_api.outputs.int_report }}" = "PASS" ]; then
|
|
|
|
|
echo "report=PASS" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
else
|
|
|
|
|
echo "report=FAIL" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 3. Staging 浏览器 E2E ──────────────────────────────────────────
|
|
|
|
|
staging-e2e:
|
|
|
|
|
name: Staging Browser E2E
|
|
|
|
|
runs-on: saas
|
|
|
|
|
timeout-minutes: 15
|
|
|
|
|
outputs:
|
|
|
|
|
report: ${{ steps.smoke.outputs.report }}
|
|
|
|
|
|
|
|
|
|
steps:
|
|
|
|
|
- name: Checkout code
|
|
|
|
|
shell: sh
|
|
|
|
|
env:
|
|
|
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
|
|
|
run: |
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
- name: Run Playwright E2E on staging
|
|
|
|
|
id: e2e
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
|
set +e
|
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
|
docker run --rm --ipc=host \
|
|
|
|
|
-e E2E_BASE_URL=https://staging.xiaoxiajianji.com \
|
|
|
|
|
-e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \
|
|
|
|
|
-e E2E_BROWSER_CHANNEL=chromium \
|
|
|
|
|
-e PLAYWRIGHT_HEADLESS=1 \
|
|
|
|
|
-v "$PWD:/workspace" \
|
|
|
|
|
-w /workspace/apps/web \
|
|
|
|
|
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
|
|
|
|
|
sh -lc 'npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts' 2>&1 | tee /tmp/staging-e2e.log
|
|
|
|
|
EXIT_CODE=${PIPESTATUS[0]}
|
|
|
|
|
END_TIME=$(date +%s)
|
|
|
|
|
ELAPSED=$((END_TIME - START_TIME))
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========== Staging E2E 测试报告 =========="
|
|
|
|
|
echo "环境: https://staging.xiaoxiajianji.com"
|
|
|
|
|
echo "耗时: ${ELAPSED}s"
|
|
|
|
|
grep -E "passed|failed|timed out" /tmp/staging-e2e.log || true
|
|
|
|
|
if [ "$EXIT_CODE" -eq 0 ]; then
|
|
|
|
|
echo "结果: PASS"
|
|
|
|
|
echo "report=PASS" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
else
|
|
|
|
|
echo "结果: FAIL"
|
|
|
|
|
echo "report=FAIL" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
fi
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
exit $EXIT_CODE
|
|
|
|
|
|
|
|
|
|
# ── 4. 性能基线巡检 ────────────────────────────────────────────────
|
|
|
|
|
performance-check:
|
|
|
|
|
name: Performance Baseline Check
|
|
|
|
|
runs-on: saas
|
|
|
|
|
timeout-minutes: 8
|
|
|
|
|
outputs:
|
|
|
|
|
report: ${{ steps.report.outputs.report }}
|
|
|
|
|
|
|
|
|
|
steps:
|
|
|
|
|
- name: Run performance baseline checks
|
|
|
|
|
id: perf
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
|
set +e
|
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
echo " 性能基线巡检 - Staging API"
|
|
|
|
|
echo " 目标: https://staging-api.xiaoxiajianji.com"
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
TOTAL=0
|
|
|
|
|
PASS=0
|
|
|
|
|
FAIL=0
|
|
|
|
|
WARN=0
|
|
|
|
|
WARN_LIST=""
|
|
|
|
|
FAIL_LIST=""
|
|
|
|
|
|
|
|
|
|
# 核心接口配置: 名称|路径|方法|阈值(ms)|失败阈值(ms)
|
|
|
|
|
# 核心接口(core): 500ms
|
|
|
|
|
# 普通接口(normal): 1000ms
|
|
|
|
|
# 重操作接口(heavy): 3000ms
|
|
|
|
|
ENDPOINTS="
|
|
|
|
|
登录|/api/v1/auth/login|POST|500|3000
|
|
|
|
|
获取当前用户|/api/v1/auth/me|GET|500|3000
|
|
|
|
|
项目列表|/api/v1/projects|GET|500|3000
|
|
|
|
|
素材列表|/api/v1/assets|GET|500|3000
|
|
|
|
|
模板列表|/api/v1/templates|GET|500|3000
|
|
|
|
|
剪辑计划列表|/api/v1/edit-plans|GET|500|3000
|
|
|
|
|
生成任务列表|/api/v1/generation/tasks|GET|500|3000
|
|
|
|
|
订阅信息|/api/v1/subscription/current|GET|500|3000
|
|
|
|
|
音色列表|/api/v1/voices|GET|1000|5000
|
|
|
|
|
健康检查|/health|GET|200|1000
|
|
|
|
|
"
|
|
|
|
|
|
|
|
|
|
# 先登录获取 token
|
|
|
|
|
echo "--- 准备: 获取测试 Token ---"
|
|
|
|
|
AUTH_RESP=$(curl -s -w "\n%{http_code}" -X POST \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d '{"email":"18314979086@163.com","password":"Ying1234"}' \
|
|
|
|
|
"https://staging-api.xiaoxiajianji.com/api/v1/auth/login" \
|
|
|
|
|
--max-time 10 2>&1)
|
|
|
|
|
AUTH_CODE=$(echo "$AUTH_RESP" | tail -1)
|
|
|
|
|
AUTH_BODY=$(echo "$AUTH_RESP" | sed '$d')
|
|
|
|
|
|
|
|
|
|
if [ "$AUTH_CODE" = "200" ]; then
|
|
|
|
|
TOKEN=$(echo "$AUTH_BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token',''))" 2>/dev/null)
|
|
|
|
|
if [ -n "$TOKEN" ]; then
|
|
|
|
|
echo "Token 获取成功"
|
|
|
|
|
else
|
|
|
|
|
echo "Token 解析失败,部分接口可能无法测试"
|
|
|
|
|
TOKEN=""
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo "登录失败 (HTTP $AUTH_CODE),部分接口将跳过鉴权测试"
|
|
|
|
|
TOKEN=""
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "--- 开始性能测试 ---"
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
echo "$ENDPOINTS" | while IFS='|' read -r name path method warn_ms fail_ms; do
|
|
|
|
|
[ -z "$name" ] && continue
|
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
|
|
|
|
|
|
|
|
# 构建 curl 命令
|
|
|
|
|
CURL_ARGS="-s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30"
|
|
|
|
|
if [ "$method" = "POST" ]; then
|
|
|
|
|
CURL_ARGS="$CURL_ARGS -X POST -H 'Content-Type: application/json' -d '{\"email\":\"18314979086@163.com\",\"password\":\"Ying1234\"}'"
|
|
|
|
|
fi
|
|
|
|
|
if [ -n "$TOKEN" ] && [ "$name" != "健康检查" ]; then
|
|
|
|
|
CURL_ARGS="$CURL_ARGS -H 'Authorization: Bearer $TOKEN'"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# 执行请求
|
|
|
|
|
RESP=$(eval curl $CURL_ARGS "https://staging-api.xiaoxiajianji.com${path}" 2>&1)
|
|
|
|
|
HTTP_CODE=$(echo "$RESP" | awk '{print $1}')
|
|
|
|
|
TIME_TOTAL=$(echo "$RESP" | awk '{print $2}')
|
|
|
|
|
ELAPSED_MS=$(python3 -c "print(int(float('${TIME_TOTAL:-0}') * 1000))" 2>/dev/null || echo "0")
|
|
|
|
|
|
|
|
|
|
if [ "$HTTP_CODE" -ge 500 ] 2>/dev/null; then
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
FAIL_LIST="$FAIL_LIST\n ❌ $name - HTTP $HTTP_CODE (${ELAPSED_MS}ms)"
|
|
|
|
|
echo "❌ $name - HTTP $HTTP_CODE - ${ELAPSED_MS}ms (FAIL)"
|
|
|
|
|
elif [ "$ELAPSED_MS" -ge "$fail_ms" ] 2>/dev/null; then
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
FAIL_LIST="$FAIL_LIST\n ❌ $name - ${ELAPSED_MS}ms > ${fail_ms}ms"
|
|
|
|
|
echo "❌ $name - ${ELAPSED_MS}ms > ${fail_ms}ms (FAIL)"
|
|
|
|
|
elif [ "$ELAPSED_MS" -ge "$warn_ms" ] 2>/dev/null; then
|
|
|
|
|
WARN=$((WARN + 1))
|
|
|
|
|
WARN_LIST="$WARN_LIST\n ⚠️ $name - ${ELAPSED_MS}ms > ${warn_ms}ms"
|
|
|
|
|
echo "⚠️ $name - ${ELAPSED_MS}ms (WARN, threshold: ${warn_ms}ms)"
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
else
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
echo "✅ $name - ${ELAPSED_MS}ms (OK, threshold: ${warn_ms}ms)"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# 由于 while 在子 shell 中执行,用文件传递结果
|
|
|
|
|
# 重新跑一次用文件计数方式
|
|
|
|
|
echo ""
|
|
|
|
|
echo "--- 汇总性能数据 ---"
|
|
|
|
|
|
|
|
|
|
END_TIME=$(date +%s)
|
|
|
|
|
ELAPSED=$((END_TIME - START_TIME))
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========== 性能基线巡检报告 =========="
|
|
|
|
|
echo "环境: https://staging-api.xiaoxiajianji.com"
|
|
|
|
|
echo "耗时: ${ELAPSED}s"
|
|
|
|
|
echo "======================================"
|
|
|
|
|
|
|
|
|
|
- name: Generate performance report
|
|
|
|
|
id: report
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
|
set +e
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
echo " 性能基线巡检 - 详细报告"
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
|
|
|
|
|
TOTAL=0
|
|
|
|
|
PASS=0
|
|
|
|
|
FAIL=0
|
|
|
|
|
WARN=0
|
|
|
|
|
RESULTS=""
|
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
|
|
|
|
|
|
# 先登录获取 token
|
|
|
|
|
AUTH_RESP=$(curl -s -w "\n%{http_code}" -X POST \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d '{"email":"18314979086@163.com","password":"Ying1234"}' \
|
|
|
|
|
"https://staging-api.xiaoxiajianji.com/api/v1/auth/login" \
|
|
|
|
|
--max-time 10 2>&1)
|
|
|
|
|
AUTH_CODE=$(echo "$AUTH_RESP" | tail -1)
|
|
|
|
|
AUTH_BODY=$(echo "$AUTH_RESP" | sed '$d')
|
|
|
|
|
TOKEN=""
|
|
|
|
|
if [ "$AUTH_CODE" = "200" ]; then
|
|
|
|
|
TOKEN=$(echo "$AUTH_BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token',''))" 2>/dev/null || echo "")
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
run_perf_test() {
|
|
|
|
|
local name="$1" path="$2" method="$3" warn_ms="$4" fail_ms="$5"
|
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
|
|
|
|
|
|
|
|
local CURL_ARGS="-s -o /dev/null -w '%{http_code} %{time_total}' --max-time 30"
|
|
|
|
|
if [ "$method" = "POST" ]; then
|
|
|
|
|
CURL_ARGS="$CURL_ARGS -X POST -H 'Content-Type: application/json' -d '{\"email\":\"18314979086@163.com\",\"password\":\"Ying1234\"}'"
|
|
|
|
|
fi
|
|
|
|
|
if [ -n "$TOKEN" ] && [ "$name" != "健康检查" ]; then
|
|
|
|
|
CURL_ARGS="$CURL_ARGS -H 'Authorization: Bearer $TOKEN'"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
local RESP=$(eval curl $CURL_ARGS "https://staging-api.xiaoxiajianji.com${path}" 2>&1)
|
|
|
|
|
local HTTP_CODE=$(echo "$RESP" | awk '{print $1}')
|
|
|
|
|
local TIME_TOTAL=$(echo "$RESP" | awk '{print $2}')
|
|
|
|
|
local ELAPSED_MS=$(python3 -c "print(int(float('${TIME_TOTAL:-0}') * 1000))" 2>/dev/null || echo "0")
|
|
|
|
|
|
|
|
|
|
if echo "$HTTP_CODE" | grep -q "^[5]"; then
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
RESULTS="$RESULTS\n ❌ $name - HTTP $HTTP_CODE (${ELAPSED_MS}ms)"
|
|
|
|
|
echo "❌ $name - HTTP $HTTP_CODE - ${ELAPSED_MS}ms [FAIL]"
|
|
|
|
|
return 1
|
|
|
|
|
elif [ "$ELAPSED_MS" -ge "$fail_ms" ] 2>/dev/null; then
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
RESULTS="$RESULTS\n ❌ $name - ${ELAPSED_MS}ms > ${fail_ms}ms [FAIL]"
|
|
|
|
|
echo "❌ $name - ${ELAPSED_MS}ms > ${fail_ms}ms [FAIL]"
|
|
|
|
|
return 1
|
|
|
|
|
elif [ "$ELAPSED_MS" -ge "$warn_ms" ] 2>/dev/null; then
|
|
|
|
|
WARN=$((WARN + 1))
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
RESULTS="$RESULTS\n ⚠️ $name - ${ELAPSED_MS}ms (阈值: ${warn_ms}ms) [WARN]"
|
|
|
|
|
echo "⚠️ $name - ${ELAPSED_MS}ms > 阈值 ${warn_ms}ms [WARN]"
|
|
|
|
|
return 0
|
|
|
|
|
else
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
RESULTS="$RESULTS\n ✅ $name - ${ELAPSED_MS}ms (阈值: ${warn_ms}ms) [OK]"
|
|
|
|
|
echo "✅ $name - ${ELAPSED_MS}ms (阈值: ${warn_ms}ms) [OK]"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=== 核心接口 (阈值: 500ms / 3000ms) ==="
|
|
|
|
|
run_perf_test "登录" "/api/v1/auth/login" "POST" 500 3000 || true
|
|
|
|
|
run_perf_test "获取当前用户" "/api/v1/auth/me" "GET" 500 3000 || true
|
|
|
|
|
run_perf_test "项目列表" "/api/v1/projects" "GET" 500 3000 || true
|
|
|
|
|
run_perf_test "素材列表" "/api/v1/assets" "GET" 500 3000 || true
|
|
|
|
|
run_perf_test "模板列表" "/api/v1/templates" "GET" 500 3000 || true
|
|
|
|
|
run_perf_test "剪辑计划列表" "/api/v1/edit-plans" "GET" 500 3000 || true
|
|
|
|
|
run_perf_test "生成任务列表" "/api/v1/generation/tasks" "GET" 500 3000 || true
|
|
|
|
|
run_perf_test "订阅信息" "/api/v1/subscription/current" "GET" 500 3000 || true
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=== 普通接口 (阈值: 1000ms / 5000ms) ==="
|
|
|
|
|
run_perf_test "音色列表" "/api/v1/voices" "GET" 1000 5000 || true
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=== 基础接口 (阈值: 200ms / 1000ms) ==="
|
|
|
|
|
run_perf_test "健康检查" "/health" "GET" 200 1000 || true
|
|
|
|
|
|
|
|
|
|
END_TIME=$(date +%s)
|
|
|
|
|
ELAPSED=$((END_TIME - START_TIME))
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "========== 性能基线巡检报告 =========="
|
|
|
|
|
echo "环境: https://staging-api.xiaoxiajianji.com"
|
|
|
|
|
echo "总接口: ${TOTAL}"
|
|
|
|
|
echo "通过: ${PASS}"
|
|
|
|
|
echo "失败: ${FAIL}"
|
|
|
|
|
echo "警告: ${WARN}"
|
|
|
|
|
echo "耗时: ${ELAPSED}s"
|
|
|
|
|
echo "======================================"
|
|
|
|
|
|
|
|
|
|
# 写入结果文件供 report job 使用
|
|
|
|
|
echo "${TOTAL}" > /tmp/perf_total
|
|
|
|
|
echo "${PASS}" > /tmp/perf_pass
|
|
|
|
|
echo "${FAIL}" > /tmp/perf_fail
|
|
|
|
|
echo "${WARN}" > /tmp/perf_warn
|
|
|
|
|
echo "${ELAPSED}" > /tmp/perf_elapsed
|
|
|
|
|
|
|
|
|
|
if [ "$FAIL" -gt 0 ]; then
|
|
|
|
|
echo "report=FAIL" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
echo "perf_detail=fail:${FAIL}:warn:${WARN}" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
exit 1
|
|
|
|
|
else
|
|
|
|
|
echo "report=PASS" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
if [ "$WARN" -gt 0 ]; then
|
|
|
|
|
echo "perf_detail=pass:warn:${WARN}" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
else
|
|
|
|
|
echo "perf_detail=pass" >> "${GITHUB_OUTPUT}"
|
|
|
|
|
fi
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── 5. 每日巡检汇总报告 ────────────────────────────────────────────
|
|
|
|
|
daily-report:
|
|
|
|
|
name: Daily Check Report
|
|
|
|
|
runs-on: saas
|
|
|
|
|
timeout-minutes: 2
|
|
|
|
|
if: always()
|
|
|
|
|
needs:
|
|
|
|
|
- production-smoke
|
|
|
|
|
- staging-api-tests
|
|
|
|
|
- staging-e2e
|
|
|
|
|
- performance-check
|
|
|
|
|
|
|
|
|
|
steps:
|
|
|
|
|
- name: Print summary report
|
|
|
|
|
shell: sh
|
|
|
|
|
run: |
|
|
|
|
|
echo ""
|
|
|
|
|
echo "╔══════════════════════════════════════════════════════╗"
|
|
|
|
|
echo "║ 每日巡检报告 ║"
|
|
|
|
|
echo "╠══════════════════════════════════════════════════════╣"
|
|
|
|
|
|
|
|
|
|
# 获取各 job 状态
|
|
|
|
|
PROD_STATUS="${{ needs.production-smoke.result }}"
|
|
|
|
|
STAGING_API_STATUS="${{ needs.staging-api-tests.result }}"
|
|
|
|
|
STAGING_E2E_STATUS="${{ needs.staging-e2e.result }}"
|
|
|
|
|
PERF_STATUS="${{ needs.performance-check.result }}"
|
|
|
|
|
|
|
|
|
|
format_result() {
|
|
|
|
|
if [ "$1" = "success" ]; then
|
|
|
|
|
echo "✅ PASS"
|
|
|
|
|
elif [ "$1" = "failure" ]; then
|
|
|
|
|
echo "❌ FAIL"
|
|
|
|
|
elif [ "$1" = "skipped" ]; then
|
|
|
|
|
echo "⏭️ SKIP"
|
|
|
|
|
else
|
|
|
|
|
echo "❓ UNKNOWN ($1)"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
echo "║"
|
|
|
|
|
echo "║ 生产冒烟测试: $(format_result "$PROD_STATUS")"
|
|
|
|
|
echo "║ Staging API: $(format_result "$STAGING_API_STATUS")"
|
|
|
|
|
echo "║ Staging E2E: $(format_result "$STAGING_E2E_STATUS")"
|
|
|
|
|
echo "║ 性能基线巡检: $(format_result "$PERF_STATUS")"
|
|
|
|
|
echo "║"
|
|
|
|
|
echo "║ 巡检时间: $(date '+%Y-%m-%d %H:%M:%S UTC')"
|
|
|
|
|
echo "║"
|
|
|
|
|
|
|
|
|
|
# 判断整体状态
|
|
|
|
|
ALL_PASS=true
|
|
|
|
|
FAILED_ITEMS=""
|
|
|
|
|
for status_name in "$PROD_STATUS:生产冒烟" "$STAGING_API_STATUS:Staging API" "$STAGING_E2E_STATUS:Staging E2E" "$PERF_STATUS:性能基线"; do
|
|
|
|
|
STATUS=$(echo "$status_name" | cut -d: -f1)
|
|
|
|
|
NAME=$(echo "$status_name" | cut -d: -f2)
|
|
|
|
|
if [ "$STATUS" != "success" ] && [ "$STATUS" != "skipped" ]; then
|
|
|
|
|
ALL_PASS=false
|
|
|
|
|
FAILED_ITEMS="$FAILED_ITEMS $NAME"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
echo "╠══════════════════════════════════════════════════════╣"
|
|
|
|
|
if [ "$ALL_PASS" = "true" ]; then
|
|
|
|
|
echo "║ 整体状态: ✅ 全部通过 ║"
|
|
|
|
|
else
|
|
|
|
|
echo "║ 整体状态: ❌ 存在失败 ║"
|
|
|
|
|
echo "║ 失败项: ${FAILED_ITEMS} ║"
|
|
|
|
|
fi
|
|
|
|
|
echo "╚══════════════════════════════════════════════════════╝"
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
# 如果有失败项,以非零退出码结束(方便 Gitea 标记流水线失败)
|
|
|
|
|
if [ "$ALL_PASS" = "false" ]; then
|
|
|
|
|
echo "⚠️ 部分巡检项失败,请检查上方日志获取详细信息。"
|
|
|
|
|
# 不 exit 1,因为我们用了 always(),保持 report job 成功,
|
|
|
|
|
# 但其他失败的 job 已经让整体流水线标记为失败
|
|
|
|
|
fi
|