Skip to content

Route test reports through builds/ directory, generate HTML indexes for unit/E2E results in release#359

Merged
pethers merged 4 commits intomainfrom
copilot/update-release-flow-reports
Feb 20, 2026
Merged

Route test reports through builds/ directory, generate HTML indexes for unit/E2E results in release#359
pethers merged 4 commits intomainfrom
copilot/update-release-flow-reports

Conversation

Copy link
Contributor

Copilot AI commented Feb 20, 2026

Release workflow updates docs/api/ and docs/coverage/ but never populates docs/test-results/ or docs/cypress/ — those directories were created empty. Normal test runs also risk merge conflicts by writing directly to docs/.

Changes

  • vitest.config.js: Coverage output → builds/coverage/, added JSON reporter → builds/test-results/vitest-results.json (configurable via VITEST_JSON_OUTPUT env var)
  • .gitignore: Added builds/ as intermediate report output directory
  • package.json: build:test-reports now copies from builds/{coverage,test-results,cypress}docs/ with empty-directory guards
  • scripts/generate-test-results-html.cjs: Extracted unit test HTML report generator with comprehensive HTML escaping (&, <, >, ", '), try-catch error handling, and NaN-safe duration calculation
  • scripts/generate-e2e-results-html.cjs: Extracted E2E HTML report generator with comprehensive HTML escaping and try-catch error handling
  • release.yml:
    • E2E step captures output via tee builds/cypress/e2e-output.log with set -o pipefail for proper exit code propagation
    • Copies Cypress screenshots to builds/cypress/
    • Invokes extracted scripts to generate docs/test-results/index.html and docs/cypress/index.html
    • Docs hub links now point to explicit index.html paths

Flow

dev/CI:   tests → builds/ (gitignored)
release:  builds/ → docs/ (HTML generation via scripts/) → GitHub Pages + S3

No new dependencies added.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits February 20, 2026 01:29
- Update vitest.config.js: coverage to builds/coverage/, add JSON
  reporter to builds/test-results/vitest-results.json
- Update .gitignore: add builds/ directory
- Update package.json: build:test-reports copies from builds/ to docs/
- Update release.yml: capture E2E output to builds/cypress/,
  generate HTML index pages for unit test results and E2E results,
  fix docs hub links to point to index.html

Co-authored-by: pethers <[email protected]>
Copilot AI changed the title [WIP] Update release flow to include test reports Route test reports through builds/ directory, generate HTML indexes for unit/E2E results in release Feb 20, 2026
Copilot AI requested a review from pethers February 20, 2026 01:35
@pethers pethers requested a review from Copilot February 20, 2026 01:40
@github-actions github-actions bot added dependencies Dependency updates javascript JavaScript code changes workflow GitHub Actions workflows ci-cd CI/CD pipeline changes testing Test coverage size-m Medium change (50-250 lines) labels Feb 20, 2026
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restructures test report generation to route outputs through an intermediate builds/ directory (gitignored) instead of writing directly to docs/, preventing merge conflicts in CI/CD workflows. The release workflow now copies from builds/ to docs/ and generates HTML index pages for unit and E2E test results using inline Node.js scripts.

Changes:

  • Test reports now route through intermediate builds/ directory to avoid merge conflicts
  • Vitest coverage and JSON results output to builds/coverage/ and builds/test-results/
  • E2E test output captured via tee to builds/cypress/e2e-output.log for HTML generation
  • Release workflow generates HTML index pages from test results using inline Node.js scripts
  • Documentation hub links updated to point to explicit index.html paths

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 10 comments.

File Description
vitest.config.js Updated coverage output to builds/coverage/ and added JSON reporter outputting to builds/test-results/vitest-results.json
.gitignore Added builds/ directory to gitignore as intermediate staging area for test reports
package.json Updated build:test-reports script to copy from builds/{coverage,test-results,cypress} to docs/
.github/workflows/release.yml Added E2E output capture with tee, generates HTML indexes from test results using inline Node.js scripts, updated doc hub links to explicit paths

Comment on lines 232 to 235
const tests = (r.assertionResults || []).map(t =>
'<li>' + (t.status === 'passed' ? '✅' : '❌') + ' ' + t.fullName.replace(/</g,'&lt;') + '</li>'
).join('');
return '<details><summary>' + status + ' ' + name + '</summary><ul>' + tests + '</ul></details>';
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name HTML escaping only handles < but not > or &. While > and & are less critical in this context since the content is inside an HTML element (not an attribute), it's still best practice to escape all HTML special characters to prevent potential XSS issues. Consider using the same comprehensive escaping pattern suggested for line 233.

Suggested change
const tests = (r.assertionResults || []).map(t =>
'<li>' + (t.status === 'passed' ? '✅' : '❌') + ' ' + t.fullName.replace(/</g,'&lt;') + '</li>'
).join('');
return '<details><summary>' + status + ' ' + name + '</summary><ul>' + tests + '</ul></details>';
const tests = (r.assertionResults || []).map(t => {
const testStatus = t.status === 'passed' ? '✅' : '';
const escapedFullName = String(t.fullName || '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[c]));
return '<li>' + testStatus + ' ' + escapedFullName + '</li>';
}).join('');
const escapedName = String(name || '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[c]));
return '<details><summary>' + status + ' ' + escapedName + '</summary><ul>' + tests + '</ul></details>';

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Extracted to scripts/generate-test-results-html.cjs with a shared escapeHtml() function that escapes all five HTML special characters (&, <, >, ", ') for both test names and suite names.

package.json Outdated
"test:e2ereportmerge": "echo 'Cypress report merge placeholder - mochawesome-merge if multi-spec reports exist'",
"test:e2ereporthtmlall": "echo 'Cypress HTML report generation placeholder - mochawesome-report-generator'",
"build:test-reports": "mkdir -p docs/test-results docs/cypress && ([ -d coverage ] && cp -r coverage docs/ || echo 'No coverage directory to copy') && echo 'Test reports copied to docs/'",
"build:test-reports": "mkdir -p docs/test-results docs/cypress docs/coverage && ([ -d builds/coverage ] && cp -r builds/coverage/* docs/coverage/ || echo 'No coverage directory to copy') && ([ -d builds/test-results ] && cp -r builds/test-results/* docs/test-results/ || echo 'No test-results directory to copy') && ([ -d builds/cypress ] && cp -r builds/cypress/* docs/cypress/ || echo 'No cypress directory to copy') && echo 'Test reports copied to docs/'",
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy command uses cp -r builds/coverage/* which will fail if the builds/coverage directory exists but is empty, because the shell glob won't match anything. This could cause the entire command chain to fail if set -e is enabled in the shell. Consider using one of these more robust approaches:

  1. Copy the entire directory: [ -d builds/coverage ] && cp -r builds/coverage docs/ || echo '...'
  2. Use find for more control: [ -d builds/coverage ] && find builds/coverage -mindepth 1 -exec cp -r {} docs/coverage/ \; || echo '...'
  3. Add explicit empty check: [ -d builds/coverage ] && [ -n "$(ls -A builds/coverage 2>/dev/null)" ] && cp -r builds/coverage/* docs/coverage/ || echo '...'
Suggested change
"build:test-reports": "mkdir -p docs/test-results docs/cypress docs/coverage && ([ -d builds/coverage ] && cp -r builds/coverage/* docs/coverage/ || echo 'No coverage directory to copy') && ([ -d builds/test-results ] && cp -r builds/test-results/* docs/test-results/ || echo 'No test-results directory to copy') && ([ -d builds/cypress ] && cp -r builds/cypress/* docs/cypress/ || echo 'No cypress directory to copy') && echo 'Test reports copied to docs/'",
"build:test-reports": "mkdir -p docs/test-results docs/cypress docs/coverage && ([ -d builds/coverage ] && [ -n \"$(ls -A builds/coverage 2>/dev/null)\" ] && cp -r builds/coverage/* docs/coverage/ || echo 'No coverage directory to copy') && ([ -d builds/test-results ] && [ -n \"$(ls -A builds/test-results 2>/dev/null)\" ] && cp -r builds/test-results/* docs/test-results/ || echo 'No test-results directory to copy') && ([ -d builds/cypress ] && [ -n \"$(ls -A builds/cypress 2>/dev/null)\" ] && cp -r builds/cypress/* docs/cypress/ || echo 'No cypress directory to copy') && echo 'Test reports copied to docs/'",

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Added [ -n "$(ls -A ... 2>/dev/null)" ] checks to guard against empty directories causing glob failures.

Comment on lines 222 to 239
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('docs/test-results/vitest-results.json', 'utf8'));
const passed = data.numPassedTests || 0;
const failed = data.numFailedTests || 0;
const total = data.numTotalTests || 0;
const duration = ((data.testResults || []).reduce((a, r) => a + (r.endTime - r.startTime), 0) / 1000).toFixed(2);
const suites = (data.testResults || []).map(r => {
const name = r.name.replace(/.*[\\/\\\\]/, '');
const status = r.status === 'passed' ? '✅' : '❌';
const tests = (r.assertionResults || []).map(t =>
'<li>' + (t.status === 'passed' ? '✅' : '❌') + ' ' + t.fullName.replace(/</g,'&lt;') + '</li>'
).join('');
return '<details><summary>' + status + ' ' + name + '</summary><ul>' + tests + '</ul></details>';
}).join('');
const html = '<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"><title>Unit Test Results</title><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:\"Segoe UI\",sans-serif;line-height:1.6;color:#e0e0e0;background:linear-gradient(135deg,#0a0e27,#1a1e3d);min-height:100vh;padding:2rem}.container{max-width:900px;margin:0 auto}h1{color:#00d9ff;margin-bottom:1rem}h2{color:#ffbe0b;margin:1.5rem 0 .5rem}.stats{display:flex;gap:2rem;margin:1rem 0;flex-wrap:wrap}.stat{background:rgba(26,30,61,.6);border:1px solid rgba(0,217,255,.3);border-radius:8px;padding:1rem 1.5rem}.stat .num{font-size:2rem;font-weight:bold}.pass{color:#00ff88}.fail{color:#ff006e}.total{color:#00d9ff}details{background:rgba(26,30,61,.4);border:1px solid rgba(0,217,255,.2);border-radius:4px;margin:.5rem 0;padding:.5rem 1rem}summary{cursor:pointer;font-weight:bold}ul{margin:.5rem 0 .5rem 1.5rem}li{margin:.25rem 0}a{color:#00d9ff}</style></head><body><div class=\"container\"><h1>🧪 Unit Test Results</h1><div class=\"stats\"><div class=\"stat\"><div class=\"num pass\">' + passed + '</div>Passed</div><div class=\"stat\"><div class=\"num fail\">' + failed + '</div>Failed</div><div class=\"stat\"><div class=\"num total\">' + total + '</div>Total</div><div class=\"stat\"><div class=\"num total\">' + duration + 's</div>Duration</div></div><h2>Test Suites</h2>' + suites + '<p style=\"margin-top:2rem\"><a href=\"../index.html\">← Back to Documentation Hub</a></p></div></body></html>';
fs.writeFileSync('docs/test-results/index.html', html);
"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline Node.js script doesn't have explicit error handling. If JSON parsing fails or file operations fail, the error will be printed to stderr but the shell will continue executing (the script won't exit non-zero). Consider wrapping the script in a try-catch or explicitly checking for errors:

node -e "
  try {
    const fs = require('fs');
    const data = JSON.parse(fs.readFileSync('docs/test-results/vitest-results.json', 'utf8'));
    // ... rest of script
  } catch (error) {
    console.error('Failed to generate unit test results HTML:', error.message);
    process.exit(1);
  }
" || echo "⚠️ Failed to generate unit test results HTML"

This ensures the workflow is aware if HTML generation fails.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Extracted to scripts/generate-test-results-html.cjs with try-catch that calls process.exit(1) on failure. The workflow step uses || echo "⚠️ ..." as a fallback.

Comment on lines 255 to 256
fs.writeFileSync('docs/cypress/index.html', html);
"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline Node.js scripts for HTML generation (221-241 for unit tests, 244-258 for E2E tests) are becoming quite large and complex. Consider extracting these to separate script files (e.g., scripts/generate-test-html.js and scripts/generate-e2e-html.js) for better maintainability, testability, and reusability. This would also make error handling, formatting, and future enhancements easier to manage.

Benefits:

  • Easier to test the HTML generation logic
  • Better syntax highlighting and IDE support
  • Simpler to add features (e.g., more sophisticated parsing, better formatting)
  • Can be reused in other contexts (e.g., local development)
  • Clearer separation of concerns in the workflow file
Suggested change
fs.writeFileSync('docs/cypress/index.html', html);
"
if [ -f "docs/cypress/e2e-output.log" ]; then
mkdir -p scripts
cat > scripts/generate-e2e-html.js << 'EOF'
const fs = require('fs');
const path = require('path');
const logPath = path.join('docs', 'cypress', 'e2e-output.log');
const outputDir = path.join('docs', 'cypress');
const outputPath = path.join(outputDir, 'index.html');
if (!fs.existsSync(logPath)) {
console.error('E2E log file not found:', logPath);
process.exit(0);
}
const rawLog = fs.readFileSync(logPath, 'utf8');
const escapeHtml = (str) =>
str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const escapedLog = escapeHtml(rawLog);
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Cypress E2E Test Results</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background-color: #0a0e27;
color: #e0e0e0;
padding: 1.5rem;
}
h1 {
color: #00d9ff;
margin-bottom: 1rem;
}
pre {
background-color: #1a1e3d;
border-radius: 4px;
padding: 1rem;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
</head>
<body>
<h1>Cypress E2E Test Results</h1>
<pre>${escapedLog}</pre>
</body>
</html>`;
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.writeFileSync(outputPath, html, 'utf8');
EOF
node scripts/generate-e2e-html.js

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Extracted to scripts/generate-e2e-results-html.cjs with proper error handling, comprehensive HTML escaping, and clean template literal formatting.

vitest.config.js Outdated
// Reporter configuration - verbose to console, JSON to builds/ for release docs
reporters: [
'verbose',
['json', { outputFile: 'builds/test-results/vitest-results.json' }]
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The directory builds/test-results/ is hardcoded in the JSON reporter configuration. Since the reporters configuration is an array that can include multiple reporters, if someone adds a second JSON reporter in the future, there could be conflicts. Consider using a more descriptive filename or documenting that this path is coupled with the release workflow.

Alternatively, consider making this configurable via an environment variable:

['json', { outputFile: process.env.VITEST_JSON_OUTPUT || 'builds/test-results/vitest-results.json' }]

This would allow for easier testing and flexibility in different environments.

Suggested change
['json', { outputFile: 'builds/test-results/vitest-results.json' }]
['json', { outputFile: process.env.VITEST_JSON_OUTPUT || 'builds/test-results/vitest-results.json' }]

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Now reads process.env.VITEST_JSON_OUTPUT || 'builds/test-results/vitest-results.json'.

const total = data.numTotalTests || 0;
const duration = ((data.testResults || []).reduce((a, r) => a + (r.endTime - r.startTime), 0) / 1000).toFixed(2);
const suites = (data.testResults || []).map(r => {
const name = r.name.replace(/.*[\\/\\\\]/, '');
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern /.*[\\/\\\\]/ is incorrect and will not match directory separators properly. The intent is to match either forward slash or backslash, but the pattern should be /.*[\/\\]/ instead. The current pattern [\\/\\\\] creates a character class containing backslash, forward slash, and two additional backslashes, which is redundant and confusing.

This should be: r.name.replace(/.*[\/\\]/, '') or the more portable: r.name.replace(/.*[\\/]/, '')

Suggested change
const name = r.name.replace(/.*[\\/\\\\]/, '');
const name = r.name.replace(/.*[\\/]/, '');

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Simplified to r.name.replace(/.*[\\/]/, '') in the extracted script.

const passed = data.numPassedTests || 0;
const failed = data.numFailedTests || 0;
const total = data.numTotalTests || 0;
const duration = ((data.testResults || []).reduce((a, r) => a + (r.endTime - r.startTime), 0) / 1000).toFixed(2);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duration calculation will produce NaN if any test result is missing endTime or startTime properties. Consider adding validation and a fallback:

const duration = ((data.testResults || []).reduce((a, r) => a + ((r.endTime || 0) - (r.startTime || 0)), 0) / 1000).toFixed(2);

Or better yet, filter out invalid entries before reducing:
const duration = ((data.testResults || []).filter(r => r.endTime && r.startTime).reduce((a, r) => a + (r.endTime - r.startTime), 0) / 1000).toFixed(2);

Suggested change
const duration = ((data.testResults || []).reduce((a, r) => a + (r.endTime - r.startTime), 0) / 1000).toFixed(2);
const validResults = (data.testResults || []).filter(r => typeof r.endTime === 'number' && typeof r.startTime === 'number');
const durationMs = validResults.reduce((a, r) => a + (r.endTime - r.startTime), 0);
const duration = (durationMs / 1000).toFixed(2);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Now filters with typeof r.endTime === 'number' && typeof r.startTime === 'number' before reducing.

const name = r.name.replace(/.*[\\/\\\\]/, '');
const status = r.status === 'passed' ? '✅' : '❌';
const tests = (r.assertionResults || []).map(t =>
'<li>' + (t.status === 'passed' ? '✅' : '❌') + ' ' + t.fullName.replace(/</g,'&lt;') + '</li>'
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name HTML escaping only handles < but not other dangerous characters like >, &, ", and '. For complete XSS prevention in HTML content, all HTML special characters should be escaped. Consider using:

t.fullName.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m]))

Or at minimum add escaping for > and &:
t.fullName.replace(/[&<>]/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[m]))

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. The extracted escapeHtml() function handles all five characters: &&amp;, <&lt;, >&gt;, "&quot;, '&#39;.

echo "🚀 Running E2E tests for release validation..."
NODE_OPTIONS="--max-old-space-size=4096" xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24 -ac +extension GLX +extension RANDR +render -nolisten tcp" npm run e2e || TEST_EXIT_CODE=$?
mkdir -p builds/cypress
NODE_OPTIONS="--max-old-space-size=4096" xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24 -ac +extension GLX +extension RANDR +render -nolisten tcp" npm run e2e 2>&1 | tee builds/cypress/e2e-output.log; TEST_EXIT_CODE=${PIPESTATUS[0]}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tee command with PIPESTATUS correctly captures the exit code of the npm run e2e command before the pipe. However, there's a subtle issue: if the xvfb-run command itself fails (before npm run e2e even starts), the exit code won't be captured properly. Consider wrapping the entire xvfb-run command to ensure exit codes are preserved:

set -o pipefail
xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24 -ac +extension GLX +extension RANDR +render -nolisten tcp" npm run e2e 2>&1 | tee builds/cypress/e2e-output.log
TEST_EXIT_CODE=$?
exit ${TEST_EXIT_CODE}

Or more concisely with the existing approach, explicitly check if xvfb-run succeeded:

if ! xvfb-run ... npm run e2e 2>&1 | tee builds/cypress/e2e-output.log; then
  TEST_EXIT_CODE=${PIPESTATUS[0]}
  exit ${TEST_EXIT_CODE:-1}
fi
Suggested change
NODE_OPTIONS="--max-old-space-size=4096" xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24 -ac +extension GLX +extension RANDR +render -nolisten tcp" npm run e2e 2>&1 | tee builds/cypress/e2e-output.log; TEST_EXIT_CODE=${PIPESTATUS[0]}
set -o pipefail
NODE_OPTIONS="--max-old-space-size=4096" \
xvfb-run --auto-servernum --server-args="-screen 0 1280x720x24 -ac +extension GLX +extension RANDR +render -nolisten tcp" \
npm run e2e 2>&1 | tee builds/cypress/e2e-output.log
TEST_EXIT_CODE=$?

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Now uses set -o pipefail with $? capture after the pipeline, ensuring failures from both xvfb-run and npm run e2e are properly propagated.

Comment on lines 245 to 257
node -e "
const fs = require('fs');
const log = fs.readFileSync('docs/cypress/e2e-output.log', 'utf8');
const escaped = log.replace(/</g,'&lt;').replace(/>/g,'&gt;');
const passMatch = log.match(/All specs passed!|(\d+) passing/);
const failMatch = log.match(/(\d+) failing/);
const status = failMatch ? '❌ Some tests failed' : (passMatch ? '✅ All tests passed' : 'ℹ️ Test results');
const hasScreenshots = fs.existsSync('docs/cypress/screenshots');
const screenshotLinks = hasScreenshots ? '<h2>Screenshots</h2><p>Available in <a href=\"screenshots/\">screenshots/</a></p>' : '';
const html = '<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"><title>E2E Test Results</title><style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:\"Segoe UI\",sans-serif;line-height:1.6;color:#e0e0e0;background:linear-gradient(135deg,#0a0e27,#1a1e3d);min-height:100vh;padding:2rem}.container{max-width:900px;margin:0 auto}h1{color:#00d9ff;margin-bottom:1rem}h2{color:#ffbe0b;margin:1.5rem 0 .5rem}.status{font-size:1.3rem;margin:1rem 0}pre{background:rgba(0,0,0,.4);border:1px solid rgba(0,217,255,.2);border-radius:8px;padding:1.5rem;overflow-x:auto;font-size:.85rem;max-height:600px;overflow-y:auto}a{color:#00d9ff}</style></head><body><div class=\"container\"><h1>🎭 E2E Test Results (Cypress)</h1><p class=\"status\">' + status + '</p>' + screenshotLinks + '<h2>Test Output</h2><pre>' + escaped + '</pre><p style=\"margin-top:2rem\"><a href=\"../index.html\">← Back to Documentation Hub</a></p></div></body></html>';
fs.writeFileSync('docs/cypress/index.html', html);
"
echo "✅ E2E test results HTML generated at docs/cypress/index.html"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the unit test HTML generation - the inline Node.js script lacks explicit error handling. If JSON parsing or file operations fail, the error won't properly propagate to the workflow. Consider adding try-catch error handling similar to the suggestion for the unit test HTML generation script.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dbd1a42. Extracted to scripts/generate-e2e-results-html.cjs with try-catch and process.exit(1) on failure.

@pethers
Copy link
Member

pethers commented Feb 20, 2026

@copilot apply changes based on the comments in this thread

- Extract inline Node.js HTML generators to scripts/generate-test-results-html.cjs
  and scripts/generate-e2e-results-html.cjs for maintainability and testability
- Add comprehensive HTML escaping (& < > " ') preventing XSS
- Add try-catch error handling with proper process.exit(1)
- Fix NaN duration: filter out results missing endTime/startTime
- Fix regex: simplify path separator matching to /.*[\\/]/
- Use set -o pipefail for proper E2E exit code propagation
- Make vitest JSON output path configurable via VITEST_JSON_OUTPUT env var
- Add empty directory checks in build:test-reports copy commands

Co-authored-by: pethers <[email protected]>
Copy link
Contributor Author

Copilot AI commented Feb 20, 2026

@copilot apply changes based on the comments in this thread

Applied all review feedback in commit dbd1a42:

  • Extracted inline scriptsscripts/generate-test-results-html.cjs and scripts/generate-e2e-results-html.cjs
  • Comprehensive HTML escaping (&, <, >, ", ') for XSS prevention
  • try-catch error handling with process.exit(1) on failure
  • NaN duration fix — filters results missing endTime/startTime
  • Regex fix — simplified [\\/\\\\][\\/]
  • set -o pipefail for proper E2E exit code propagation
  • Configurable vitest JSON path via VITEST_JSON_OUTPUT env var
  • Empty directory checks in build:test-reports copy commands

@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Comment on lines +13 to +26

const inputPath = path.join('docs', 'test-results', 'vitest-results.json');
const outputPath = path.join('docs', 'test-results', 'index.html');

function escapeHtml(str) {
return String(str || '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[c]));
}

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The single quote is escaped as ''' but the canonical implementation in scripts/html-utils.js uses ''' (3-digit form). While both are valid HTML entities, this creates an inconsistency with the established pattern used throughout the codebase. Consider importing and using the escapeHtml function from scripts/html-utils.js instead of reimplementing it, which would ensure consistency and reduce duplication.

Suggested change
const inputPath = path.join('docs', 'test-results', 'vitest-results.json');
const outputPath = path.join('docs', 'test-results', 'index.html');
function escapeHtml(str) {
return String(str || '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[c]));
}
const { escapeHtml } = require('./html-utils');
const inputPath = path.join('docs', 'test-results', 'vitest-results.json');
const outputPath = path.join('docs', 'test-results', 'index.html');

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +26

const logPath = path.join('docs', 'cypress', 'e2e-output.log');
const outputPath = path.join('docs', 'cypress', 'index.html');

function escapeHtml(str) {
return String(str || '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[c]));
}

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The single quote is escaped as ''' but the canonical implementation in scripts/html-utils.js uses ''' (3-digit form). While both are valid HTML entities, this creates an inconsistency with the established pattern used throughout the codebase. Consider importing and using the escapeHtml function from scripts/html-utils.js instead of reimplementing it, which would ensure consistency and reduce duplication.

Suggested change
const logPath = path.join('docs', 'cypress', 'e2e-output.log');
const outputPath = path.join('docs', 'cypress', 'index.html');
function escapeHtml(str) {
return String(str || '').replace(/[&<>"']/g, c => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[c]));
}
const { escapeHtml } = require('./html-utils');
const logPath = path.join('docs', 'cypress', 'e2e-output.log');
const outputPath = path.join('docs', 'cypress', 'index.html');

Copilot uses AI. Check for mistakes.
@pethers pethers marked this pull request as ready for review February 20, 2026 02:22
@pethers pethers merged commit 1403235 into main Feb 20, 2026
21 checks passed
@pethers pethers deleted the copilot/update-release-flow-reports branch February 20, 2026 02:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-cd CI/CD pipeline changes dependencies Dependency updates javascript JavaScript code changes refactor Code refactoring size-m Medium change (50-250 lines) testing Test coverage workflow GitHub Actions workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants