feat: add Woodpecker CI configuration for tiered testing
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user