feat: add GitHub Pages deployment for development tools and reports

- Add GitHub Pages workflow (release-pages.yml) to deploy Storybook, Knip reports, and Playwright test reports
- Create .pages directory structure for GitHub Pages content
- Add build scripts for generating pages artifacts (build-pages.sh, create-playwright-index.js)
- Update CI workflows to support pages deployment and artifact handling
- Configure Knip and TypeScript for new .pages structure
- Add HTML viewers for Knip and Playwright reports

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
snomiao
2026-02-12 06:40:52 +00:00
parent 553ea63357
commit 2a466af154
14 changed files with 1456 additions and 31 deletions

75
scripts/build-pages.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
# Build or reuse Storybook
echo "[build-pages] Setting up Storybook"
rm -rf ".pages/storybook"
if [ -d "./storybook-static" ] && [ "$(find ./storybook-static -name '*.html' | wc -l)" -gt 0 ]; then
echo "✅ Reusing downloaded Storybook build"
cp -r "./storybook-static" ".pages/storybook"
else
echo "🔨 Building Storybook from source"
pnpm build-storybook && cp -r "storybook-static" ".pages/storybook"
fi
echo "[build-pages] Generating Nx dependency graph"
rm -rf ".pages/nx-graph" && mkdir -p ".pages/nx-graph"
pnpm nx graph --file=".pages/nx-graph/index.html"
# Generate or reuse Vitest test reports
echo "[build-pages] Setting up Vitest test reports"
rm -rf ".pages/vitest-reports" && mkdir -p ".pages/vitest-reports"
if [ -d ".page/vitest-reports" ]; then
echo "✅ Reusing downloaded Vitest reports"
cp -r ".page/vitest-reports/"* ".pages/vitest-reports/" 2>/dev/null || echo "⚠️ No vitest reports to copy"
else
echo "🔨 Generating Vitest reports from source"
pnpm exec vitest \
--reporter=json --outputFile.json=".pages/vitest-reports/results.json" \
--reporter=html --outputFile.html=".pages/vitest-reports/index.html" \
--run
fi
# Set up Playwright test reports if available
echo "[build-pages] Setting up Playwright test reports"
if [ -d ".page/playwright-reports" ]; then
echo "✅ Reusing downloaded Playwright reports"
mkdir -p ".pages/playwright-reports"
cp -r ".page/playwright-reports/"* ".pages/playwright-reports/" 2>/dev/null || echo "⚠️ No playwright reports to copy"
fi
echo "[build-pages] Generating coverage report"
mkdir -p ".pages/coverage"
if pnpm exec vitest --run --coverage --coverage.reporter=html --coverage.reportsDirectory=".pages/coverage"; then
echo "✅ Coverage report completed"
else
echo "⚠️ Coverage report failed, continuing..."
fi
echo "[build-pages] Generating Knip report"
mkdir -p ".pages/knip"
rm -f ".pages/knip/report.md"
if pnpm knip --reporter markdown --no-progress --no-exit-code > ".pages/knip/report.md" 2>/dev/null && [ -s ".pages/knip/report.md" ]; then
echo "✅ Knip report generated at .pages/knip/report.md"
else
echo "⚠️ Knip report failed, creating placeholder..."
cat > ".pages/knip/report.md" <<'EOF'
# Knip report
> ⚠️ Knip report unavailable.
>
> Generation failed during build. See CI logs for details.
EOF
fi
echo "[build-pages] Landing page already exists at .pages/index.html"
echo "[build-pages] Build artifacts ready in ./.pages"
echo "[build-pages] Note: For local dev, you can develop the .pages/index.html using:
pnpm exec vite .pages
"

View File

@@ -0,0 +1,290 @@
#!/usr/bin/env node
/**
* Creates an index page for Playwright test reports with test statistics
* Reads JSON reports from each browser and creates a landing page with cards
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const reportsDir = path.join(__dirname, '..', '.pages', 'playwright-reports')
function getTestStats(reportPath) {
try {
const reportJsonPath = path.join(reportPath, 'report.json')
if (!fs.existsSync(reportJsonPath)) {
console.warn(`No report.json found at ${reportJsonPath}`)
return null
}
const reportData = JSON.parse(fs.readFileSync(reportJsonPath, 'utf-8'))
let passed = 0
let failed = 0
let skipped = 0
let flaky = 0
// Parse Playwright JSON report format
if (reportData.suites) {
const countResults = (suites) => {
for (const suite of suites) {
if (suite.specs) {
for (const spec of suite.specs) {
if (!spec.tests || spec.tests.length === 0) continue
const test = spec.tests[0]
const results = test.results || []
// Check if test is flaky (has both pass and fail results)
const hasPass = results.some((r) => r.status === 'passed')
const hasFail = results.some((r) => r.status === 'failed')
if (hasPass && hasFail) {
flaky++
} else if (results.some((r) => r.status === 'passed')) {
passed++
} else if (results.some((r) => r.status === 'failed')) {
failed++
} else if (results.some((r) => r.status === 'skipped')) {
skipped++
}
}
}
if (suite.suites) {
countResults(suite.suites)
}
}
}
countResults(reportData.suites)
}
return { passed, failed, skipped, flaky }
} catch (error) {
console.error(`Error reading report at ${reportPath}:`, error.message)
return null
}
}
function generateIndexHtml(browsers) {
const cards = browsers
.map((browser) => {
const { name, stats } = browser
if (!stats) return ''
const total = stats.passed + stats.failed + stats.skipped + stats.flaky
const passRate = total > 0 ? ((stats.passed / total) * 100).toFixed(1) : 0
return `
<a href="./${name}/index.html" class="card">
<div class="card-header">
<h2>${name}</h2>
<span class="pass-rate ${stats.failed > 0 ? 'has-failures' : ''}">${passRate}%</span>
</div>
<div class="stats">
<div class="stat passed">
<span class="label">Passed</span>
<span class="value">${stats.passed}</span>
</div>
<div class="stat failed">
<span class="label">Failed</span>
<span class="value">${stats.failed}</span>
</div>
<div class="stat skipped">
<span class="label">Skipped</span>
<span class="value">${stats.skipped}</span>
</div>
<div class="stat flaky">
<span class="label">Flaky</span>
<span class="value">${stats.flaky}</span>
</div>
</div>
</a>
`
})
.join('')
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playwright E2E Test Reports</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
display: block;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #f0f0f0;
}
.card h2 {
color: #333;
font-size: 1.5rem;
text-transform: capitalize;
}
.pass-rate {
background: #10b981;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: bold;
font-size: 1.1rem;
}
.pass-rate.has-failures {
background: #ef4444;
}
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat .label {
font-size: 0.875rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat .value {
font-size: 1.75rem;
font-weight: bold;
}
.stat.passed .value {
color: #10b981;
}
.stat.failed .value {
color: #ef4444;
}
.stat.skipped .value {
color: #f59e0b;
}
.stat.flaky .value {
color: #8b5cf6;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.cards-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🎭 Playwright E2E Test Reports</h1>
<div class="cards-grid">
${cards}
</div>
</div>
</body>
</html>`
}
function main() {
if (!fs.existsSync(reportsDir)) {
console.log(
'No playwright reports directory found, skipping index creation'
)
return
}
const browsers = []
const browserDirs = fs.readdirSync(reportsDir, { withFileTypes: true })
for (const dirent of browserDirs) {
if (dirent.isDirectory()) {
const browserName = dirent.name
const browserPath = path.join(reportsDir, browserName)
const stats = getTestStats(browserPath)
if (stats) {
browsers.push({ name: browserName, stats })
console.log(`✓ Found report for ${browserName}:`, stats)
}
}
}
if (browsers.length === 0) {
console.warn('No valid browser reports found')
return
}
const html = generateIndexHtml(browsers)
const indexPath = path.join(reportsDir, 'index.html')
fs.writeFileSync(indexPath, html, 'utf-8')
console.log(`✓ Created index page at ${indexPath}`)
}
main()