Restore scratch setup bootstrap and live frontdoor sweep

This commit is contained in:
master
2026-03-09 01:42:24 +02:00
parent abda749ffd
commit c9686edf07
13 changed files with 766 additions and 63 deletions

View File

@@ -3,15 +3,25 @@ const { join } = require('path');
const linuxArchivePath = ['.cache', 'chromium', 'chrome-linux64', 'chrome'];
const windowsArchivePath = ['.cache', 'chromium', 'chrome-win64', 'chrome.exe'];
const macArchivePath = [
'.cache',
'chromium',
'chrome-mac',
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
];
const macArchivePath = [
'.cache',
'chromium',
'chrome-mac',
'Chromium.app',
'Contents',
'MacOS',
'Chromium'
];
function sortChromiumDirectories(entries) {
return [...entries].sort((left, right) => {
const leftMatch = /chromium-?(\d+)/i.exec(left);
const rightMatch = /chromium-?(\d+)/i.exec(right);
const leftValue = leftMatch ? Number.parseInt(leftMatch[1], 10) : Number.NEGATIVE_INFINITY;
const rightValue = rightMatch ? Number.parseInt(rightMatch[1], 10) : Number.NEGATIVE_INFINITY;
return rightValue - leftValue;
});
}
function expandVersionedArchives(rootDir = join(__dirname, '..')) {
const base = join(rootDir, '.cache', 'chromium');
@@ -91,19 +101,22 @@ function candidatePaths(rootDir = join(__dirname, '..')) {
const { env } = process;
const playwrightBase = join(rootDir, 'node_modules', 'playwright-core', '.local-browsers');
const homePlaywrightBase = env.HOME ? join(env.HOME, '.cache', 'ms-playwright') : null;
const windowsLocalPlaywrightBase = env.LOCALAPPDATA ? join(env.LOCALAPPDATA, 'ms-playwright') : null;
const windowsProfilePlaywrightBase = env.USERPROFILE ? join(env.USERPROFILE, 'AppData', 'Local', 'ms-playwright') : null;
const playwrightCacheBases = [homePlaywrightBase, windowsLocalPlaywrightBase, windowsProfilePlaywrightBase].filter(Boolean);
let playwrightChromium = [];
try {
if (existsSync(playwrightBase)) {
playwrightChromium = readdirSync(playwrightBase)
.filter((d) => d.startsWith('chromium-'))
const chromiumEntries = sortChromiumDirectories(
readdirSync(playwrightBase).filter((d) => d.startsWith('chromium-'))
);
playwrightChromium = chromiumEntries
.map((d) => join(playwrightBase, d, 'chrome-linux', 'chrome'))
.concat(
readdirSync(playwrightBase)
.filter((d) => d.startsWith('chromium-'))
.map((d) => join(playwrightBase, d, 'chrome-win', 'chrome.exe')),
readdirSync(playwrightBase)
.filter((d) => d.startsWith('chromium-'))
.map((d) => join(playwrightBase, d, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'))
chromiumEntries.map((d) => join(playwrightBase, d, 'chrome-win', 'chrome.exe')),
chromiumEntries.map((d) => join(playwrightBase, d, 'chrome-win64', 'chrome.exe')),
chromiumEntries.map((d) => join(playwrightBase, d, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'))
);
}
} catch {
@@ -112,14 +125,23 @@ function candidatePaths(rootDir = join(__dirname, '..')) {
let homeChromium = [];
try {
if (homePlaywrightBase && existsSync(homePlaywrightBase)) {
homeChromium = readdirSync(homePlaywrightBase)
.filter((d) => d.startsWith('chromium'))
.flatMap((d) => [
join(homePlaywrightBase, d, 'chrome-linux', 'chrome'),
join(homePlaywrightBase, d, 'chrome-win', 'chrome.exe'),
join(homePlaywrightBase, d, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
]);
for (const cacheBase of playwrightCacheBases) {
if (!existsSync(cacheBase)) {
continue;
}
const chromiumEntries = sortChromiumDirectories(
readdirSync(cacheBase).filter((d) => d.startsWith('chromium'))
);
homeChromium.push(
...chromiumEntries.flatMap((d) => [
join(cacheBase, d, 'chrome-linux', 'chrome'),
join(cacheBase, d, 'chrome-win', 'chrome.exe'),
join(cacheBase, d, 'chrome-win64', 'chrome.exe'),
join(cacheBase, d, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
])
);
}
} catch {
homeChromium = [];

View File

@@ -211,6 +211,37 @@ export async function authenticateFrontdoor(options = {}) {
return report;
}
export function getSessionStorageEntries(authReport) {
return Array.isArray(authReport?.storage?.sessionStorageEntries)
? authReport.storage.sessionStorageEntries.filter(
(entry) => Array.isArray(entry) && typeof entry[0] === 'string' && typeof entry[1] === 'string',
)
: [];
}
export async function addSessionStorageInitScript(context, authReport) {
const sessionEntries = getSessionStorageEntries(authReport);
await context.addInitScript((entries) => {
sessionStorage.clear();
for (const [key, value] of entries) {
if (typeof key === 'string' && typeof value === 'string') {
sessionStorage.setItem(key, value);
}
}
}, sessionEntries);
}
export async function createAuthenticatedContext(browser, authReport, options = {}) {
const context = await browser.newContext({
ignoreHTTPSErrors: true,
storageState: options.statePath || DEFAULT_STATE_PATH,
...options.contextOptions,
});
await addSessionStorageInitScript(context, authReport);
return context;
}
async function main() {
const report = await authenticateFrontdoor();
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);

View File

@@ -0,0 +1,417 @@
#!/usr/bin/env node
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const webRoot = path.resolve(__dirname, '..');
const outputDirectory = path.join(webRoot, 'output', 'playwright');
const BASE_URL = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local';
const STATE_PATH = path.join(outputDirectory, 'live-frontdoor-auth-state.json');
const REPORT_PATH = path.join(outputDirectory, 'live-frontdoor-auth-report.json');
const RESULT_PATH = path.join(outputDirectory, 'live-frontdoor-canonical-route-sweep.json');
const scopedPrefixes = ['/mission-control', '/releases', '/security', '/evidence', '/ops'];
const defaultScopeEntries = [
['tenant', 'demo-prod'],
['regions', 'us-east'],
['environments', 'stage'],
['timeWindow', '7d'],
];
const canonicalRoutes = [
'/mission-control/board',
'/mission-control/alerts',
'/mission-control/activity',
'/releases',
'/releases/overview',
'/releases/versions',
'/releases/versions/new',
'/releases/runs',
'/releases/approvals',
'/releases/promotion-queue',
'/releases/hotfixes',
'/releases/hotfixes/new',
'/releases/environments',
'/releases/deployments',
'/security',
'/security/posture',
'/security/triage',
'/security/advisories-vex',
'/security/disposition',
'/security/supply-chain-data',
'/security/supply-chain-data/graph',
'/security/sbom-lake',
'/security/reachability',
'/security/reports',
'/evidence',
'/evidence/overview',
'/evidence/capsules',
'/evidence/verify-replay',
'/evidence/proofs',
'/evidence/exports',
'/evidence/audit-log',
'/ops',
'/ops/operations',
'/ops/operations/jobs-queues',
'/ops/operations/feeds-airgap',
'/ops/operations/data-integrity',
'/ops/operations/system-health',
'/ops/operations/health-slo',
'/ops/operations/jobengine',
'/ops/operations/scheduler',
'/ops/operations/quotas',
'/ops/operations/offline-kit',
'/ops/operations/dead-letter',
'/ops/operations/aoc',
'/ops/operations/doctor',
'/ops/operations/signals',
'/ops/operations/packs',
'/ops/operations/ai-runs',
'/ops/operations/notifications',
'/ops/operations/status',
'/ops/integrations',
'/ops/integrations/onboarding',
'/ops/integrations/registries',
'/ops/integrations/scm',
'/ops/integrations/ci',
'/ops/integrations/runtime-hosts',
'/ops/integrations/advisory-vex-sources',
'/ops/integrations/secrets',
'/ops/integrations/notifications',
'/ops/integrations/sbom-sources',
'/ops/integrations/activity',
'/ops/policy',
'/ops/policy/overview',
'/ops/policy/baselines',
'/ops/policy/gates',
'/ops/policy/simulation',
'/ops/policy/waivers',
'/ops/policy/risk-budget',
'/ops/policy/trust-weights',
'/ops/policy/staleness',
'/ops/policy/sealed-mode',
'/ops/policy/profiles',
'/ops/policy/validator',
'/ops/policy/audit',
'/ops/platform-setup',
'/ops/platform-setup/regions-environments',
'/ops/platform-setup/promotion-paths',
'/ops/platform-setup/workflows-gates',
'/ops/platform-setup/release-templates',
'/ops/platform-setup/policy-bindings',
'/ops/platform-setup/gate-profiles',
'/ops/platform-setup/defaults-guardrails',
'/ops/platform-setup/trust-signing',
'/setup',
'/setup/integrations',
'/setup/integrations/advisory-vex-sources',
'/setup/integrations/secrets',
'/setup/identity-access',
'/setup/tenant-branding',
'/setup/notifications',
'/setup/usage',
'/setup/system',
'/setup/trust-signing',
'/setup/topology',
'/setup/topology/overview',
'/setup/topology/map',
'/setup/topology/regions',
'/setup/topology/environments',
'/setup/topology/targets',
'/setup/topology/hosts',
'/setup/topology/agents',
'/setup/topology/connectivity',
'/setup/topology/runtime-drift',
'/setup/topology/promotion-graph',
'/setup/topology/workflows',
'/setup/topology/gate-profiles',
];
const strictRouteExpectations = {
'/security/advisories-vex': {
title: /Advisories/i,
texts: ['Security / Advisories & VEX', 'Providers'],
},
'/security/sbom-lake': {
title: /SBOM Lake/i,
texts: ['SBOM Lake', 'Attestation Coverage Metrics'],
},
'/security/reachability': {
title: /Reachability/i,
texts: ['Reachability', 'Proof of Exposure'],
},
'/setup/trust-signing': {
title: /Trust/i,
texts: ['Trust Management'],
},
'/setup/integrations': {
title: /Integrations/i,
texts: ['Integrations', 'External system connectors'],
},
'/setup/integrations/advisory-vex-sources': {
title: /Advisory & VEX Sources/i,
texts: ['Integrations', 'FeedMirror Integrations'],
},
'/setup/integrations/secrets': {
title: /Secrets/i,
texts: ['Integrations', 'RepoSource Integrations'],
},
'/ops/policy': {
title: /Policy/i,
texts: ['Policy Decisioning Studio', 'One operator shell for policy, VEX, and release gates'],
},
'/ops/policy/overview': {
title: /Policy/i,
texts: ['Policy Decisioning Studio', 'One operator shell for policy, VEX, and release gates'],
},
'/ops/policy/risk-budget': {
title: /Policy/i,
texts: ['Policy Decisioning Studio', 'Risk Budget Overview'],
},
};
const allowedFinalPaths = {
'/releases': ['/releases/deployments'],
'/releases/promotion-queue': ['/releases/promotions'],
'/ops/policy': ['/ops/policy/overview'],
'/ops/policy/audit': ['/ops/policy/audit/policy'],
'/ops/platform-setup/trust-signing': ['/setup/trust-signing'],
'/setup/topology': ['/setup/topology/overview'],
};
function buildRouteUrl(routePath) {
const url = new URL(routePath, BASE_URL);
if (scopedPrefixes.some((prefix) => routePath.startsWith(prefix))) {
for (const [key, value] of defaultScopeEntries) {
if (!url.searchParams.has(key)) {
url.searchParams.set(key, value);
}
}
}
return url.toString();
}
function trimText(value, maxLength = 300) {
const normalized = value.replace(/\s+/g, ' ').trim();
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
}
function shouldIgnoreUrl(url) {
return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url) || url.startsWith('data:');
}
async function collectHeadings(page) {
return page.locator('h1, main h1, main h2, h2').evaluateAll((elements) =>
elements
.map((element) => (element.textContent || '').replace(/\s+/g, ' ').trim())
.filter((text, index, values) => text.length > 0 && values.indexOf(text) === index),
).catch(() => []);
}
async function collectVisibleProblemTexts(page) {
return page.locator([
'[role="alert"]',
'.alert',
'.banner',
'.status-banner',
'.degraded-banner',
'.warning-banner',
'.error-banner',
'.empty-state',
'.error-state',
].join(', ')).evaluateAll((elements) =>
elements
.map((element) => (element.textContent || '').replace(/\s+/g, ' ').trim())
.filter((text) =>
text.length > 0 && /(failed|unable|error|warning|degraded|unavailable|timed out|timeout|no results)/i.test(text),
),
).catch(() => []);
}
async function collectVisibleActions(page) {
return page.locator('main a[href], main button').evaluateAll((elements) =>
elements
.map((element) => {
const tagName = element.tagName.toLowerCase();
const text = (element.textContent || '').replace(/\s+/g, ' ').trim();
const href = tagName === 'a' ? element.getAttribute('href') || '' : '';
return {
tagName,
text,
href,
};
})
.filter((entry) => entry.text.length > 0 || entry.href.length > 0)
.slice(0, 20),
).catch(() => []);
}
function collectExpectationFailures(routePath, title, headings, bodyText) {
const expectation = strictRouteExpectations[routePath];
if (!expectation) {
return [];
}
const failures = [];
if (!expectation.title.test(title)) {
failures.push(`title mismatch: expected ${expectation.title}`);
}
for (const text of expectation.texts) {
const present = headings.some((heading) => heading.includes(text)) || bodyText.includes(text);
if (!present) {
failures.push(`missing text: ${text}`);
}
}
return failures;
}
function finalPathMatchesRoute(routePath, finalUrl) {
const finalPath = new URL(finalUrl).pathname;
if (finalPath === routePath) {
return true;
}
return allowedFinalPaths[routePath]?.includes(finalPath) ?? false;
}
async function inspectRoute(context, routePath) {
const page = await context.newPage();
const consoleErrors = [];
const requestFailures = [];
const responseErrors = [];
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(trimText(message.text(), 500));
}
});
page.on('requestfailed', (request) => {
const url = request.url();
if (shouldIgnoreUrl(url)) {
return;
}
requestFailures.push({
method: request.method(),
url,
error: request.failure()?.errorText ?? 'unknown',
});
});
page.on('response', (response) => {
const url = response.url();
if (shouldIgnoreUrl(url)) {
return;
}
if (response.status() >= 400) {
responseErrors.push({
status: response.status(),
method: response.request().method(),
url,
});
}
});
const targetUrl = buildRouteUrl(routePath);
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
await page.waitForTimeout(1_500);
const finalUrl = page.url();
const title = await page.title().catch(() => '');
const headings = await collectHeadings(page);
const bodyText = trimText(await page.locator('body').innerText().catch(() => ''), 1_200);
const problemTexts = await collectVisibleProblemTexts(page);
const visibleActions = await collectVisibleActions(page);
const finalPathMatches = finalPathMatchesRoute(routePath, finalUrl);
const expectationFailures = collectExpectationFailures(routePath, title, headings, bodyText);
const record = {
routePath,
targetUrl,
finalUrl,
title,
headings,
problemTexts,
visibleActions,
consoleErrors,
requestFailures,
responseErrors,
finalPathMatches,
expectationFailures,
passed: finalPathMatches
&& expectationFailures.length === 0
&& consoleErrors.length === 0
&& requestFailures.length === 0
&& responseErrors.length === 0
&& problemTexts.length === 0,
};
await page.close();
return record;
}
async function main() {
mkdirSync(outputDirectory, { recursive: true });
await authenticateFrontdoor({
baseUrl: BASE_URL,
statePath: STATE_PATH,
reportPath: REPORT_PATH,
headless: true,
});
const authReport = JSON.parse(readFileSync(REPORT_PATH, 'utf8'));
const browser = await chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'] });
try {
const context = await createAuthenticatedContext(browser, authReport, { statePath: STATE_PATH });
const routes = [];
for (const routePath of canonicalRoutes) {
const record = await inspectRoute(context, routePath);
routes.push(record);
const status = record.passed ? 'PASS' : 'FAIL';
process.stdout.write(`[live-frontdoor-canonical-route-sweep] ${status} ${routePath} -> ${record.finalUrl}\n`);
}
await context.close();
const summary = {
checkedAtUtc: new Date().toISOString(),
baseUrl: BASE_URL,
totalRoutes: routes.length,
passedRoutes: routes.filter((route) => route.passed).length,
failedRoutes: routes.filter((route) => !route.passed).map((route) => route.routePath),
routes,
};
writeFileSync(RESULT_PATH, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
if (summary.failedRoutes.length > 0) {
process.exitCode = 1;
}
} finally {
await browser.close();
}
}
main().catch((error) => {
process.stderr.write(`[live-frontdoor-canonical-route-sweep] ${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
});

View File

@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
import { authenticateFrontdoor } from './live-frontdoor-auth.mjs';
import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -417,20 +417,7 @@ async function main() {
headless: true,
args: ['--disable-dev-shm-usage'],
});
const context = await browser.newContext({
ignoreHTTPSErrors: true,
storageState: statePath,
});
const sessionEntries = Array.isArray(authReport.storage?.sessionStorageEntries)
? authReport.storage.sessionStorageEntries
: [];
await context.addInitScript((entries) => {
for (const [key, value] of entries) {
if (typeof key === 'string' && typeof value === 'string') {
sessionStorage.setItem(key, value);
}
}
}, sessionEntries);
const context = await createAuthenticatedContext(browser, authReport, { statePath });
const report = {
generatedAtUtc: new Date().toISOString(),

View File

@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
import { chromium } from 'playwright';
import { authenticateFrontdoor } from './live-frontdoor-auth.mjs';
import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -21,23 +21,14 @@ const REPORT_PATH = path.join(outputDirectory, 'live-frontdoor-auth-report.json'
const RESULT_PATH = path.join(outputDirectory, 'live-releases-deployments-check.json');
async function seedAuthenticatedPage(browser, authReport) {
const context = await browser.newContext({
ignoreHTTPSErrors: true,
acceptDownloads: true,
storageState: STATE_PATH,
const context = await createAuthenticatedContext(browser, authReport, {
statePath: STATE_PATH,
contextOptions: {
acceptDownloads: true,
},
});
const page = await context.newPage();
await page.goto(`${BASE_URL}/welcome`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.evaluate((storage) => {
sessionStorage.clear();
for (const [key, value] of storage.sessionStorageEntries ?? []) {
if (typeof key === 'string' && typeof value === 'string') {
sessionStorage.setItem(key, value);
}
}
}, authReport.storage);
await page.goto(LIST_URL, { waitUntil: 'networkidle', timeout: 30_000 });
return { context, page };
}