Files
mb-grid-solutions.com/scripts/check-broken-assets.ts
T
mmintel 73cb55ee8d
🚀 Build & Deploy / 🔍 Prepare (push) Successful in 8s
Nightly QA / call-qa-workflow (push) Failing after 1m34s
🚀 Build & Deploy / 🧪 QA (push) Has been cancelled
🚀 Build & Deploy / 🚀 Deploy (push) Has been cancelled
🚀 Build & Deploy / 🏗️ Build (push) Has been cancelled
🚀 Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
🚀 Build & Deploy / 🔔 Notify (push) Has been cancelled
chore(qa): restore reusable workflow with Gitea compatibility fixes
2026-04-25 19:13:49 +02:00

226 lines
7.1 KiB
TypeScript

import puppeteer, { HTTPResponse } from "puppeteer";
import axios from "axios";
import * as cheerio from "cheerio";
const targetUrl =
process.argv.find((arg) => !arg.startsWith("--") && arg.startsWith("http")) ||
process.env.NEXT_PUBLIC_BASE_URL ||
"http://localhost:3000";
const limit = process.env.ASSET_CHECK_LIMIT
? parseInt(process.env.ASSET_CHECK_LIMIT)
: 20;
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid";
async function main() {
console.log(`\n🚀 Starting Strict Asset Integrity Check for: ${targetUrl}`);
console.log(`📊 Limit: ${limit} pages\n`);
// 1. Fetch Sitemap to discover all routes
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
let urls: string[] = [];
try {
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
const response = await axios.get(sitemapUrl, {
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
});
const $ = cheerio.load(response.data, { xmlMode: true });
urls = $("url loc")
.map((i, el) => $(el).text())
.get();
// Normalize to target URL instance
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} target URLs.`);
if (urls.length > limit) {
console.log(
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
);
// Simplify selection: home pages + a slice of the rest
const homeDE = urls.filter((u) => u.endsWith("/de") || u === targetUrl);
const homeEN = urls.filter((u) => u.endsWith("/en"));
const others = urls.filter(
(u) => !homeEN.includes(u) && !homeDE.includes(u),
);
urls = [
...homeDE,
...homeEN,
...others.slice(0, limit - (homeEN.length + homeDE.length)),
];
}
} catch (err: unknown) {
const errorBody = err instanceof Error ? err.message : String(err);
console.error(`❌ Failed to fetch sitemap: ${errorBody}`);
process.exit(1);
}
// 2. Launch Headless Browser
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
const browser = await puppeteer.launch({
headless: true,
executablePath:
process.env.PUPPETEER_EXECUTABLE_PATH ||
process.env.CHROME_PATH ||
undefined,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
],
});
const page = await browser.newPage();
// Inject Gatekeeper session bypassing auth screens
await page.setCookie({
name: "mintel_gatekeeper_session",
value: gatekeeperPassword,
domain: new URL(targetUrl).hostname,
path: "/",
httpOnly: true,
secure: targetUrl.startsWith("https://"),
});
let hasBrokenAssets = false;
let hasConsoleErrors = false;
const brokenAssetsList: Array<{ url: string; status: number; page: string }> =
[];
const consoleErrorsList: Array<{
type: string;
error: string;
page: string;
}> = [];
// Listen for unhandled exceptions natively in the page
page.on("pageerror", (err: unknown) => {
const errorMessage = err instanceof Error ? err.message : String(err);
consoleErrorsList.push({
type: "PAGE_ERROR",
error: errorMessage,
page: page.url(),
});
hasConsoleErrors = true;
});
// Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
page.on("console", (msg: any) => {
const type = msg.type();
if (type === "error" || type === "warn") {
const text = msg.text();
// Exclude common browser extension noise or third party tracker warnings
if (
text.includes("google-analytics") ||
text.includes("googletagmanager") ||
text.includes("SES Removing unpermitted intrinsics") ||
text.includes("Third-party cookie will be blocked") ||
text.includes("Fast Refresh")
)
return;
consoleErrorsList.push({
type: type.toUpperCase(),
error: text,
page: page.url(),
});
hasConsoleErrors = true;
}
});
// Listen to ALL network responses
page.on("response", (response: HTTPResponse) => {
const status = response.status();
// Catch classic 404s and 500s on ANY fetch/image/script
if (
status >= 400 &&
status !== 999 &&
!response.url().includes("google-analytics") &&
!response.url().includes("googletagmanager")
) {
const type = response.request().resourceType();
// We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s.
if (["image", "script", "stylesheet", "fetch", "xhr"].includes(type)) {
brokenAssetsList.push({
url: response.url(),
status: status,
page: page.url(),
});
hasBrokenAssets = true;
}
}
});
// 3. Scan each page
for (let i = 0; i < urls.length; i++) {
const u = urls[i];
console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`);
try {
// Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger
await page.goto(u, { waitUntil: "networkidle0", timeout: 30000 });
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
await page.evaluate(async () => {
await new Promise<void>((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
// Wait a tiny bit more for final lazy loads
await new Promise((r) => setTimeout(r, 1000));
} catch (err: unknown) {
const errorBody = err instanceof Error ? err.message : String(err);
console.error(`⚠️ Timeout or navigation error on ${u}: ${errorBody}`);
// Don't fail the whole script just because one page timed out, but flag it
hasBrokenAssets = true;
}
}
await browser.close();
// 4. Report Results
if (hasBrokenAssets && brokenAssetsList.length > 0) {
console.error(
`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`,
);
console.table(brokenAssetsList);
}
if (hasConsoleErrors && consoleErrorsList.length > 0) {
console.error(`\n❌ FATAL: Console Errors/Warnings detected on the site!`);
console.table(consoleErrorsList);
}
if (hasBrokenAssets || hasConsoleErrors) {
console.error(
`\n🚨 The CI build will now fail to prevent bad code from reaching production.`,
);
process.exit(1);
} else {
console.log(
`\n🎉 SUCCESS: All ${urls.length} pages rendered perfectly with 0 broken images or console errors!`,
);
process.exit(0);
}
}
main();