feat(ci): add deep quality assertions (html, security, links, spelling)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 2m0s
Build & Deploy / 🏗️ Build (push) Successful in 2m49s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Smoke Test (push) Successful in 57s
Build & Deploy / ♿ WCAG (push) Successful in 2m29s
Build & Deploy / 🛡️ Quality Gates (push) Failing after 3m42s
Build & Deploy / 📸 Visual Diff (push) Failing after 6m6s
Build & Deploy / ⚡ Lighthouse (push) Successful in 10m55s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 2m0s
Build & Deploy / 🏗️ Build (push) Successful in 2m49s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🧪 Smoke Test (push) Successful in 57s
Build & Deploy / ♿ WCAG (push) Successful in 2m29s
Build & Deploy / 🛡️ Quality Gates (push) Failing after 3m42s
Build & Deploy / 📸 Visual Diff (push) Failing after 6m6s
Build & Deploy / ⚡ Lighthouse (push) Successful in 10m55s
Build & Deploy / 🔔 Notify (push) Successful in 3s
This commit is contained in:
82
scripts/check-html.ts
Normal file
82
scripts/check-html.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20;
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🚀 Starting HTML Validation for: ${targetUrl}`);
|
||||
console.log(`📊 Limit: ${limit} pages\n`);
|
||||
|
||||
try {
|
||||
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
||||
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
||||
|
||||
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();
|
||||
|
||||
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.`,
|
||||
);
|
||||
const home = urls.filter((u) => u.endsWith('/de') || u.endsWith('/en') || u === targetUrl);
|
||||
const others = urls.filter((u) => !home.includes(u));
|
||||
urls = [...home, ...others.slice(0, limit - home.length)];
|
||||
}
|
||||
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log(`📥 Fetching HTML for ${urls.length} pages...`);
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const u = urls[i];
|
||||
const res = await axios.get(u, {
|
||||
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
||||
});
|
||||
const filename = `page-${i}.html`;
|
||||
fs.writeFileSync(path.join(outputDir, filename), res.data);
|
||||
}
|
||||
|
||||
console.log(`\n💻 Executing html-validate...`);
|
||||
try {
|
||||
execSync(`npx html-validate .htmlvalidate-tmp/*.html`, { stdio: 'inherit' });
|
||||
console.log(`✅ HTML Validation passed perfectly!`);
|
||||
} catch (e) {
|
||||
console.error(`❌ HTML Validation found issues.`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`\n❌ Error during HTML Validation:`, error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
const outputDir = path.join(process.cwd(), '.htmlvalidate-tmp');
|
||||
if (fs.existsSync(outputDir)) fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
26
scripts/check-links.sh
Normal file
26
scripts/check-links.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Auto-provision Lychee Rust Binary if missing
|
||||
if [ ! -f ./lychee ]; then
|
||||
echo "📥 Downloading Lychee Link Checker (v0.15.1)..."
|
||||
curl -sSLo lychee.tar.gz https://github.com/lycheeverse/lychee/releases/download/v0.15.1/lychee-v0.15.1-x86_64-unknown-linux-gnu.tar.gz
|
||||
tar -xzf lychee.tar.gz lychee
|
||||
rm lychee.tar.gz
|
||||
chmod +x ./lychee
|
||||
fi
|
||||
|
||||
echo "🚀 Starting Deep Link Assessment (Lychee)..."
|
||||
|
||||
# Exclude localhost, mintel.me (internal infrastructure), and common placeholder domains
|
||||
# Scan markdown files and component files for hardcoded dead links
|
||||
./lychee \
|
||||
--exclude "localhost" \
|
||||
--exclude "127.0.0.1" \
|
||||
--exclude "mintel\.me" \
|
||||
--exclude "example\.com" \
|
||||
--exclude-mail \
|
||||
--accept 200,204,401,403 \
|
||||
"./content/**/*.mdx" "./content/**/*.md" "./app/**/*.tsx" "./components/**/*.tsx"
|
||||
|
||||
echo "✅ All project source links are alive and healthy!"
|
||||
54
scripts/check-security.ts
Normal file
54
scripts/check-security.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
||||
|
||||
const requiredHeaders = [
|
||||
'strict-transport-security',
|
||||
'x-frame-options',
|
||||
'x-content-type-options',
|
||||
'referrer-policy',
|
||||
'content-security-policy',
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log(`\n🛡️ Starting Security Headers Scan for: ${targetUrl}\n`);
|
||||
try {
|
||||
const response = await axios.head(targetUrl, {
|
||||
headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
|
||||
const headers = response.headers;
|
||||
let allPassed = true;
|
||||
|
||||
const results = requiredHeaders.map((header) => {
|
||||
const present = !!headers[header];
|
||||
if (!present) allPassed = false;
|
||||
return {
|
||||
Header: header,
|
||||
Status: present ? '✅ Present' : '❌ Missing',
|
||||
Value: present
|
||||
? headers[header].length > 50
|
||||
? headers[header].substring(0, 47) + '...'
|
||||
: headers[header]
|
||||
: 'N/A',
|
||||
};
|
||||
});
|
||||
|
||||
console.table(results);
|
||||
|
||||
if (allPassed) {
|
||||
console.log(`\n✅ All required security headers are correctly configured!\n`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`\n❌ Missing critical security headers. Please update next.config.mjs!\n`);
|
||||
process.exit(process.env.CI ? 1 : 0); // Don't crash local dev hard if missing, but crash CI
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Failed to scan headers: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user