chore(qa): restore reusable workflow with Gitea compatibility fixes
Some checks failed
🚀 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
Some checks failed
🚀 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
This commit is contained in:
@@ -1,38 +1,40 @@
|
||||
import puppeteer, { HTTPResponse } from 'puppeteer';
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
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.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';
|
||||
"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`;
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`;
|
||||
let urls: string[] = [];
|
||||
|
||||
try {
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
const response = await axios.get(sitemapUrl, {
|
||||
headers: { Cookie: `mb_gatekeeper_session=${gatekeeperPassword}` },
|
||||
headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` },
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.data, { xmlMode: true });
|
||||
urls = $('url loc')
|
||||
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(/\/$/, '')))
|
||||
.filter((u) => u.startsWith("http"))
|
||||
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, "")))
|
||||
.sort();
|
||||
|
||||
console.log(`✅ Found ${urls.length} target URLs.`);
|
||||
@@ -42,10 +44,16 @@ async function main() {
|
||||
`⚠️ 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))];
|
||||
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);
|
||||
@@ -57,32 +65,44 @@ async function main() {
|
||||
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'],
|
||||
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: 'mb_gatekeeper_session',
|
||||
name: "mintel_gatekeeper_session",
|
||||
value: gatekeeperPassword,
|
||||
domain: new URL(targetUrl).hostname,
|
||||
path: '/',
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: targetUrl.startsWith('https://'),
|
||||
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 }> = [];
|
||||
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) => {
|
||||
page.on("pageerror", (err: unknown) => {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
consoleErrorsList.push({
|
||||
type: 'PAGE_ERROR',
|
||||
type: "PAGE_ERROR",
|
||||
error: errorMessage,
|
||||
page: page.url(),
|
||||
});
|
||||
@@ -91,18 +111,18 @@ async function main() {
|
||||
|
||||
// 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) => {
|
||||
page.on("console", (msg: any) => {
|
||||
const type = msg.type();
|
||||
if (type === 'error' || type === 'warn') {
|
||||
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')
|
||||
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;
|
||||
|
||||
@@ -116,19 +136,19 @@ async function main() {
|
||||
});
|
||||
|
||||
// Listen to ALL network responses
|
||||
page.on('response', (response: HTTPResponse) => {
|
||||
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')
|
||||
!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)) {
|
||||
if (["image", "script", "stylesheet", "fetch", "xhr"].includes(type)) {
|
||||
brokenAssetsList.push({
|
||||
url: response.url(),
|
||||
status: status,
|
||||
@@ -145,7 +165,7 @@ async function main() {
|
||||
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 });
|
||||
await page.goto(u, { waitUntil: "networkidle0", timeout: 30000 });
|
||||
|
||||
// Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images
|
||||
await page.evaluate(async () => {
|
||||
@@ -178,7 +198,9 @@ async function main() {
|
||||
|
||||
// 4. Report Results
|
||||
if (hasBrokenAssets && brokenAssetsList.length > 0) {
|
||||
console.error(`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`);
|
||||
console.error(
|
||||
`\n❌ FATAL: Broken assets (404/500) detected heavily on the site!`,
|
||||
);
|
||||
console.table(brokenAssetsList);
|
||||
}
|
||||
|
||||
@@ -188,7 +210,9 @@ async function main() {
|
||||
}
|
||||
|
||||
if (hasBrokenAssets || hasConsoleErrors) {
|
||||
console.error(`\n🚨 The CI build will now fail to prevent bad code from reaching production.`);
|
||||
console.error(
|
||||
`\n🚨 The CI build will now fail to prevent bad code from reaching production.`,
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user