From b86ca1f4280deffa53ccc98ac3515ce0c2f9c102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Fri, 13 Mar 2026 11:52:32 +0800 Subject: [PATCH] feat: add Woodpecker CI configuration for tiered testing --- .woodpecker/test-tiered-simple.yml | 50 +++++++++++++ .woodpecker/test-tiered.yml | 102 ++++++++++++++++++++++++++ e2e/scripts/generate-report.js | 99 +++++++++++++++++++++++++ scripts/validate-woodpecker-config.js | 59 +++++++++++++++ 4 files changed, 310 insertions(+) create mode 100644 .woodpecker/test-tiered-simple.yml create mode 100644 .woodpecker/test-tiered.yml create mode 100644 e2e/scripts/generate-report.js create mode 100644 scripts/validate-woodpecker-config.js diff --git a/.woodpecker/test-tiered-simple.yml b/.woodpecker/test-tiered-simple.yml new file mode 100644 index 0000000..8f92b10 --- /dev/null +++ b/.woodpecker/test-tiered-simple.yml @@ -0,0 +1,50 @@ +when: + event: + - push + - pull_request + +pipeline: + test-tier-fast: + image: mcr.microsoft.com/playwright:v1.42.0-jammy + environment: + TEST_TIER: fast + CI: true + commands: + - cd e2e + - npm ci + - npx playwright install --with-deps + - npm run test:tier:fast + when: + branch: + - main + - develop + - feat-dynamic + + test-tier-standard: + image: mcr.microsoft.com/playwright:v1.42.0-jammy + environment: + TEST_TIER: standard + CI: true + commands: + - cd e2e + - npm ci + - npx playwright install --with-deps + - npm run test:tier:standard + when: + branch: + - main + - develop + + test-tier-deep: + image: mcr.microsoft.com/playwright:v1.42.0-jammy + environment: + TEST_TIER: deep + CI: true + commands: + - cd e2e + - npm ci + - npx playwright install --with-deps + - npm run test:tier:deep + when: + branch: + - main \ No newline at end of file diff --git a/.woodpecker/test-tiered.yml b/.woodpecker/test-tiered.yml new file mode 100644 index 0000000..1d8eb0a --- /dev/null +++ b/.woodpecker/test-tiered.yml @@ -0,0 +1,102 @@ +when: + event: + - push + - pull_request + - tag + +pipeline: + setup: + image: node:20-alpine + commands: + - node -v + - npm -v + - npm ci + - cd e2e && npm ci + + test-tier-fast: + image: mcr.microsoft.com/playwright:v1.42.0-jammy + environment: + TEST_TIER: fast + CI: true + commands: + - cd e2e + - npx playwright install --with-deps + - npm run test:tier:fast + depends_on: + - setup + + test-tier-standard: + image: mcr.microsoft.com/playwright:v1.42.0-jammy + environment: + TEST_TIER: standard + CI: true + commands: + - cd e2e + - npx playwright install --with-deps + - npm run test:tier:standard + depends_on: + - test-tier-fast + when: + status: + - success + + test-tier-deep: + image: mcr.microsoft.com/playwright:v1.42.0-jammy + environment: + TEST_TIER: deep + CI: true + commands: + - cd e2e + - npx playwright install --with-deps + - npm run test:tier:deep + depends_on: + - test-tier-standard + when: + status: + - success + + generate-report: + image: node:20-alpine + commands: + - cd e2e + - node scripts/generate-report.js + depends_on: + - test-tier-fast + - test-tier-standard + - test-tier-deep + + upload-artifacts: + image: plugins/s3 + settings: + bucket: test-reports + source: e2e/test-results/** + target: /${CI_REPO}/${CI_BUILD_NUMBER}/ + path_style: true + depends_on: + - generate-report + when: + status: + - success + - failure + + notify: + image: plugins/webhook + settings: + urls: + from_secret: webhook_url + content_type: application/json + template: | + { + "repo": "{{ repo.name }}", + "build": "{{ build.number }}", + "status": "{{ build.status }}", + "message": "{{ build.message }}", + "author": "{{ commit.author }}", + "link": "{{ build.link }}" + } + depends_on: + - upload-artifacts + when: + status: + - success + - failure \ No newline at end of file diff --git a/e2e/scripts/generate-report.js b/e2e/scripts/generate-report.js new file mode 100644 index 0000000..1c73841 --- /dev/null +++ b/e2e/scripts/generate-report.js @@ -0,0 +1,99 @@ +const fs = require('fs'); +const path = require('path'); + +const resultsDir = 'test-results'; +const reportDir = 'test-results'; + +console.log('📊 生成测试报告...'); + +if (!fs.existsSync(resultsDir)) { + console.log('❌ 测试结果目录不存在'); + process.exit(1); +} + +const jsonFiles = fs.readdirSync(resultsDir) + .filter(file => file.endsWith('.json') && file.includes('-results.json')); + +if (jsonFiles.length === 0) { + console.log('❌ 未找到测试结果文件'); + process.exit(1); +} + +console.log(`📁 找到 ${jsonFiles.length} 个测试结果文件`); + +const allResults = []; +for (const file of jsonFiles) { + const filePath = path.join(resultsDir, file); + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + + if (data.suites) { + for (const suite of data.suites) { + for (const spec of suite.suites) { + for (const test of spec.tests) { + const result = test.results[0]; + allResults.push({ + testId: `${spec.file}::${test.title}`, + file: spec.file, + title: test.title, + status: result.status, + duration: result.duration, + tier: file.includes('fast') ? 'fast' : file.includes('deep') ? 'deep' : 'standard', + }); + } + } + } + } +} + +const report = { + timestamp: new Date().toISOString(), + total: { + name: 'total', + total: allResults.length, + passed: allResults.filter(r => r.status === 'passed').length, + failed: allResults.filter(r => r.status === 'failed').length, + skipped: allResults.filter(r => r.status === 'skipped').length, + duration: allResults.reduce((sum, r) => sum + r.duration, 0), + }, + tiers: { + fast: allResults.filter(r => r.tier === 'fast').reduce((acc, r) => ({ + name: 'fast', + total: acc.total + 1, + passed: acc.passed + (r.status === 'passed' ? 1 : 0), + failed: acc.failed + (r.status === 'failed' ? 1 : 0), + duration: acc.duration + r.duration, + }), { name: 'fast', total: 0, passed: 0, failed: 0, duration: 0 }), + standard: allResults.filter(r => r.tier === 'standard').reduce((acc, r) => ({ + name: 'standard', + total: acc.total + 1, + passed: acc.passed + (r.status === 'passed' ? 1 : 0), + failed: acc.failed + (r.status === 'failed' ? 1 : 0), + duration: acc.duration + r.duration, + }), { name: 'standard', total: 0, passed: 0, failed: 0, duration: 0 }), + deep: allResults.filter(r => r.tier === 'deep').reduce((acc, r) => ({ + name: 'deep', + total: acc.total + 1, + passed: acc.passed + (r.status === 'passed' ? 1 : 0), + failed: acc.failed + (r.status === 'failed' ? 1 : 0), + duration: acc.duration + r.duration, + }), { name: 'deep', total: 0, passed: 0, failed: 0, duration: 0 }), + }, + failedTests: allResults.filter(r => r.status === 'failed'), + slowTests: allResults.filter(r => r.duration > 120000), +}; + +report.total.avgDuration = report.total.duration / report.total.total; + +const reportPath = path.join(reportDir, 'ci-report.json'); +fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); + +console.log('✅ 测试报告生成完成'); +console.log(` 总测试数: ${report.total.total}`); +console.log(` 通过: ${report.total.passed}`); +console.log(` 失败: ${report.total.failed}`); +console.log(` 总耗时: ${(report.total.duration / 1000).toFixed(2)}s`); + +if (report.total.failed > 0) { + console.log(`\n❌ 发现 ${report.total.failed} 个失败测试`); + process.exit(1); +} \ No newline at end of file diff --git a/scripts/validate-woodpecker-config.js b/scripts/validate-woodpecker-config.js new file mode 100644 index 0000000..3ad9c06 --- /dev/null +++ b/scripts/validate-woodpecker-config.js @@ -0,0 +1,59 @@ +const fs = require('fs'); +const path = require('path'); + +console.log('🔍 验证Woodpecker CI配置...'); + +const configFiles = [ + '.woodpecker/test-tiered.yml', + '.woodpecker/test-tiered-simple.yml', +]; + +let allValid = true; + +for (const configFile of configFiles) { + const filePath = path.join(__dirname, '..', configFile); + + if (!fs.existsSync(filePath)) { + console.log(`❌ 配置文件不存在: ${configFile}`); + allValid = false; + continue; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + + if (content.includes('when:') && content.includes('pipeline:')) { + console.log(`✅ ${configFile} - 配置格式正确`); + } else { + console.log(`❌ ${configFile} - 配置格式错误`); + allValid = false; + } + + if (content.includes('TEST_TIER')) { + console.log(`✅ ${configFile} - 包含分层测试环境变量`); + } else { + console.log(`❌ ${configFile} - 缺少分层测试环境变量`); + allValid = false; + } + + if (content.includes('depends_on')) { + console.log(`✅ ${configFile} - 包含任务依赖配置`); + } else { + console.log(`⚠️ ${configFile} - 未配置任务依赖`); + } +} + +const reportScript = path.join(__dirname, '..', 'e2e/scripts/generate-report.js'); +if (fs.existsSync(reportScript)) { + console.log(`✅ 测试报告脚本存在`); +} else { + console.log(`❌ 测试报告脚本不存在`); + allValid = false; +} + +if (allValid) { + console.log('\n✅ 所有配置验证通过'); + process.exit(0); +} else { + console.log('\n❌ 部分配置验证失败'); + process.exit(1); +} \ No newline at end of file