#!/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-hotfix-action-check.json'); const EXPECTED_SCOPE = { tenant: 'demo-prod', regions: 'us-east', environments: 'stage', timeWindow: '7d', }; const HOTFIX_LIST_URL = new URL('/releases/hotfixes', BASE_URL); const HOTFIX_CREATE_URL = new URL('/releases/hotfixes/new', BASE_URL); for (const [key, value] of Object.entries(EXPECTED_SCOPE)) { HOTFIX_LIST_URL.searchParams.set(key, value); HOTFIX_CREATE_URL.searchParams.set(key, value); } function collectScopeIssues(url, expectedScope, label) { const issues = []; const parsed = new URL(url); for (const [key, expectedValue] of Object.entries(expectedScope)) { const actualValue = parsed.searchParams.get(key); if (actualValue !== expectedValue) { issues.push(`${label} expected ${key}=${expectedValue} but got ${actualValue ?? '(missing)'}`); } } return issues; } function shouldIgnoreRequestFailure(request) { const url = request.url(); if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) { return true; } const error = request.failure()?.errorText ?? ''; return error.includes('net::ERR_ABORTED'); } 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, contextOptions: { acceptDownloads: false, }, }); const page = await context.newPage(); const runtimeIssues = []; const failedActions = []; page.on('console', (message) => { if (message.type() === 'error') { runtimeIssues.push(`console:${message.text()}`); } }); page.on('pageerror', (error) => { runtimeIssues.push(`pageerror:${error.message}`); }); page.on('response', (response) => { const url = response.url(); if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) { return; } if (response.status() >= 400) { runtimeIssues.push(`response:${response.status()}:${response.request().method()}:${url}`); } }); page.on('requestfailed', (request) => { if (shouldIgnoreRequestFailure(request)) { return; } runtimeIssues.push(`requestfailed:${request.method()}:${request.url()}:${request.failure()?.errorText ?? 'unknown'}`); }); const result = { checkedAtUtc: new Date().toISOString(), hotfixListUrl: '', hotfixListHeading: '', hotfixReviewUrl: '', hotfixReviewHeading: '', hotfixCreateUrl: '', hotfixCreateHeading: '', failedActionCount: 0, failedActions, runtimeIssueCount: 0, runtimeIssues, scopeIssues: [], }; await page.goto(HOTFIX_LIST_URL.toString(), { waitUntil: 'networkidle', timeout: 30_000 }); result.hotfixListUrl = page.url(); result.hotfixListHeading = (await page.locator('h1').first().textContent().catch(() => '')) ?? ''; await Promise.all([ page.waitForURL(/\/releases\/hotfixes\/platform-bundle-1-3-1-hotfix1/, { timeout: 10_000 }), page.getByRole('link', { name: 'Review' }).click(), ]); await page.waitForLoadState('networkidle'); result.hotfixReviewUrl = page.url(); result.hotfixReviewHeading = (await page.locator('h1').first().textContent().catch(() => '')) ?? ''; result.scopeIssues.push(...collectScopeIssues(result.hotfixReviewUrl, EXPECTED_SCOPE, 'hotfixReviewUrl')); if (!new URL(result.hotfixReviewUrl).pathname.endsWith('/releases/hotfixes/platform-bundle-1-3-1-hotfix1')) { failedActions.push(`Review expected /releases/hotfixes/platform-bundle-1-3-1-hotfix1 but landed on ${result.hotfixReviewUrl}`); } if (!result.hotfixReviewHeading.includes('platform-bundle-1-3-1-hotfix1')) { failedActions.push(`Review expected hotfix detail heading but found "${result.hotfixReviewHeading}"`); } await page.goto(HOTFIX_CREATE_URL.toString(), { waitUntil: 'networkidle', timeout: 30_000 }); result.hotfixCreateUrl = page.url(); result.hotfixCreateHeading = (await page.locator('h1').first().textContent().catch(() => '')) ?? ''; result.scopeIssues.push(...collectScopeIssues(result.hotfixCreateUrl, EXPECTED_SCOPE, 'hotfixCreateUrl')); const createUrl = new URL(result.hotfixCreateUrl); if (createUrl.pathname !== '/releases/versions/new') { failedActions.push(`Create Hotfix expected /releases/versions/new but landed on ${result.hotfixCreateUrl}`); } if (createUrl.searchParams.get('type') !== 'hotfix' || createUrl.searchParams.get('hotfixLane') !== 'true') { failedActions.push(`Create Hotfix expected type=hotfix and hotfixLane=true but landed on ${result.hotfixCreateUrl}`); } if (result.hotfixCreateHeading !== 'Create Release Version') { failedActions.push(`Create Hotfix expected "Create Release Version" heading but found "${result.hotfixCreateHeading}"`); } result.failedActionCount = failedActions.length; result.runtimeIssueCount = runtimeIssues.length + result.scopeIssues.length; result.runtimeIssues.push(...result.scopeIssues); writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, 'utf8'); await context.close(); process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); if (result.failedActionCount > 0 || result.runtimeIssueCount > 0) { throw new Error(`hotfix action check failed: failedActionCount=${result.failedActionCount} runtimeIssueCount=${result.runtimeIssueCount}`); } } finally { await browser.close(); } } main().catch((error) => { process.stderr.write(`[live-hotfix-action-check] ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); });