All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Successful in 2m47s
Build & Deploy / 🏗️ Build (push) Successful in 3m50s
Build & Deploy / 🚀 Deploy (push) Successful in 24s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 5m27s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / 🎭 Lighthouse (push) Successful in 3m34s
Nightly QA / ♿ Accessibility (push) Successful in 5m18s
Nightly QA / 🔗 Links & Deps (push) Successful in 2m42s
Nightly QA / 🔍 Static Analysis (push) Successful in 7m8s
Nightly QA / 🔔 Notify (push) Successful in 1s
214 lines
7.8 KiB
TypeScript
214 lines
7.8 KiB
TypeScript
import axios from 'axios';
|
||
import * as cheerio from 'cheerio';
|
||
import { execSync } from 'child_process';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
/**
|
||
* PageSpeed Test Script
|
||
*
|
||
* 1. Fetches sitemap.xml from the target URL
|
||
* 2. Extracts all URLs
|
||
* 3. Runs Lighthouse CI on those URLs
|
||
*/
|
||
|
||
const targetUrl =
|
||
process.argv.find((arg) => !arg.startsWith('--') && arg.startsWith('http')) ||
|
||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||
process.env.LHCI_URL ||
|
||
'http://localhost:3000';
|
||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; // Default limit to avoid infinite runs
|
||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||
|
||
async function main() {
|
||
console.log(`\n🚀 Starting PageSpeed test for: ${targetUrl}`);
|
||
console.log(`📊 Limit: ${limit} pages\n`);
|
||
|
||
try {
|
||
// 1. Fetch Sitemap
|
||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||
|
||
// We might need to bypass gatekeeper for the sitemap fetch too
|
||
const response = await axios.get(sitemapUrl, {
|
||
headers: {
|
||
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
||
},
|
||
validateStatus: (status) => status < 400,
|
||
});
|
||
|
||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||
let urls = $('url loc')
|
||
.map((i, el) => $(el).text())
|
||
.get();
|
||
|
||
// Cleanup, filter and normalize domains to targetUrl
|
||
const urlPattern = /https?:\/\/[^\/]+/;
|
||
urls = [...new Set(urls)]
|
||
.filter((u) => u.startsWith('http'))
|
||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
||
.sort();
|
||
|
||
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
||
|
||
if (urls.length === 0) {
|
||
console.error('❌ No URLs found in sitemap. Is the site up?');
|
||
process.exit(1);
|
||
}
|
||
|
||
if (urls.length > limit) {
|
||
console.log(
|
||
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
||
);
|
||
// Try to pick a variety: home, some products, some blog posts
|
||
const homeEN = urls.filter((u) => u.endsWith('/en') || u === targetUrl);
|
||
const homeDE = urls.filter((u) => u.endsWith('/de'));
|
||
const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u));
|
||
urls = [...homeEN, ...homeDE, ...others.slice(0, limit - (homeEN.length + homeDE.length))];
|
||
}
|
||
|
||
console.log(`🧪 Pages to be tested:`);
|
||
urls.forEach((u) => console.log(` - ${u}`));
|
||
|
||
// 2. Prepare LHCI command
|
||
// We use --collect.url multiple times
|
||
const urlArgs = urls.map((u) => `--collect.url="${u}"`).join(' ');
|
||
|
||
// Handle authentication for staging/testing
|
||
// Lighthouse can set cookies via --collect.settings.extraHeaders
|
||
const extraHeaders = JSON.stringify({
|
||
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
||
});
|
||
|
||
// Detect Chrome path from Puppeteer installation if not provided
|
||
let chromePath = process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
|
||
if (!chromePath) {
|
||
try {
|
||
console.log('🔍 Attempting to detect Puppeteer Chrome path...');
|
||
const puppeteerInfo = execSync('npx puppeteer browsers latest chrome', {
|
||
encoding: 'utf8',
|
||
});
|
||
console.log(`📦 Puppeteer info: ${puppeteerInfo}`);
|
||
const match = puppeteerInfo.match(/executablePath: (.*)/);
|
||
if (match && match[1]) {
|
||
chromePath = match[1].trim();
|
||
console.log(`✅ Detected Puppeteer Chrome at: ${chromePath}`);
|
||
}
|
||
} catch (e: any) {
|
||
console.warn(`⚠️ Could not detect Puppeteer Chrome path via command: ${e.message}`);
|
||
}
|
||
|
||
// Fallback to known paths if still not found
|
||
if (!chromePath) {
|
||
const fallbacks = [
|
||
'/root/.cache/puppeteer/chrome/linux-145.0.7632.77/chrome-linux64/chrome',
|
||
'/home/runner/.cache/puppeteer/chrome/linux-145.0.7632.77/chrome-linux64/chrome',
|
||
path.join(
|
||
process.cwd(),
|
||
'node_modules',
|
||
'.puppeteer',
|
||
'chrome',
|
||
'linux-145.0.7632.77',
|
||
'chrome-linux64',
|
||
'chrome',
|
||
),
|
||
];
|
||
|
||
for (const fallback of fallbacks) {
|
||
if (fs.existsSync(fallback)) {
|
||
chromePath = fallback;
|
||
console.log(`✅ Found Puppeteer Chrome at fallback: ${chromePath}`);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
console.log(`ℹ️ Using existing Chrome path: ${chromePath}`);
|
||
}
|
||
|
||
if (!chromePath) {
|
||
console.warn('❌ CHROME_PATH is still undefined. Lighthouse might fail.');
|
||
}
|
||
|
||
const chromePathArg = chromePath ? `--collect.chromePath="${chromePath}"` : '';
|
||
|
||
// Clean up old reports
|
||
if (fs.existsSync('.lighthouseci')) {
|
||
fs.rmSync('.lighthouseci', { recursive: true, force: true });
|
||
}
|
||
|
||
// Using a more robust way to execute and capture output
|
||
// We use a puppeteer script to set cookies which is more reliable than extraHeaders for LHCI
|
||
const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --config=config/lighthouserc.json --collect.puppeteerScript="scripts/lhci-puppeteer-setup.js" --collect.settings.chromeFlags="--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage" && npx lhci assert --config=config/lighthouserc.json`;
|
||
|
||
console.log(`💻 Executing LHCI with CHROME_PATH="${chromePath}" and Puppeteer Auth...`);
|
||
|
||
try {
|
||
execSync(lhciCommand, {
|
||
encoding: 'utf8',
|
||
stdio: 'inherit',
|
||
env: { ...process.env, CHROME_PATH: chromePath },
|
||
});
|
||
} catch (err: any) {
|
||
console.warn('⚠️ LHCI assertion finished with warnings or errors.');
|
||
// We continue to show the table even if assertions failed
|
||
}
|
||
|
||
// 3. Summarize Results (Local & Independent)
|
||
const manifestPath = path.join(process.cwd(), '.lighthouseci', 'manifest.json');
|
||
if (fs.existsSync(manifestPath)) {
|
||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||
console.log(`\n📊 PageSpeed Summary (FOSS - Local Report):\n`);
|
||
|
||
const summaryTable = manifest.map((entry: any) => {
|
||
const s = entry.summary;
|
||
return {
|
||
URL: entry.url.replace(targetUrl, ''),
|
||
Perf: Math.round(s.performance * 100),
|
||
Acc: Math.round(s.accessibility * 100),
|
||
BP: Math.round(s['best-practices'] * 100),
|
||
SEO: Math.round(s.seo * 100),
|
||
};
|
||
});
|
||
|
||
console.table(summaryTable);
|
||
|
||
// Calculate Average
|
||
const avg = {
|
||
Perf: Math.round(
|
||
summaryTable.reduce((acc: any, curr: any) => acc + curr.Perf, 0) / summaryTable.length,
|
||
),
|
||
Acc: Math.round(
|
||
summaryTable.reduce((acc: any, curr: any) => acc + curr.Acc, 0) / summaryTable.length,
|
||
),
|
||
BP: Math.round(
|
||
summaryTable.reduce((acc: any, curr: any) => acc + curr.BP, 0) / summaryTable.length,
|
||
),
|
||
SEO: Math.round(
|
||
summaryTable.reduce((acc: any, curr: any) => acc + curr.SEO, 0) / summaryTable.length,
|
||
),
|
||
};
|
||
|
||
console.log(`\n📈 Average Scores:`);
|
||
console.log(` Performance: ${avg.Perf > 90 ? '✅' : '⚠️'} ${avg.Perf}`);
|
||
console.log(` Accessibility: ${avg.Acc > 90 ? '✅' : '⚠️'} ${avg.Acc}`);
|
||
console.log(` Best Practices: ${avg.BP > 90 ? '✅' : '⚠️'} ${avg.BP}`);
|
||
console.log(` SEO: ${avg.SEO > 90 ? '✅' : '⚠️'} ${avg.SEO}`);
|
||
}
|
||
|
||
console.log(`\n✨ PageSpeed tests completed successfully!`);
|
||
} catch (error: any) {
|
||
console.error(`\n❌ Error during PageSpeed test:`);
|
||
if (axios.isAxiosError(error)) {
|
||
console.error(`Status: ${error.response?.status}`);
|
||
console.error(`StatusText: ${error.response?.statusText}`);
|
||
console.error(`URL: ${error.config?.url}`);
|
||
} else {
|
||
console.error(error.message);
|
||
}
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
main();
|