#!/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 LIST_URL = `${BASE_URL}/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage`; const DETAIL_URL = `${BASE_URL}/releases/deployments/DEP-2026-050?tenant=demo-prod®ions=us-east&environments=stage`; 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-releases-deployments-check.json'); const EXPECTED_SCOPE = { tenant: 'demo-prod', regions: 'us-east', environments: 'stage', }; 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)'}`); } } const returnTo = parsed.searchParams.get('returnTo'); if (returnTo) { const parsedReturnTo = new URL(returnTo, BASE_URL); for (const [key, expectedValue] of Object.entries(expectedScope)) { const actualValue = parsedReturnTo.searchParams.get(key); if (actualValue !== expectedValue) { issues.push(`returnTo from ${label} expected ${key}=${expectedValue} but got ${actualValue ?? '(missing)'}`); } } } return issues; } async function seedAuthenticatedPage(browser, authReport) { const context = await createAuthenticatedContext(browser, authReport, { statePath: STATE_PATH, contextOptions: { acceptDownloads: true, }, }); const page = await context.newPage(); await page.goto(LIST_URL, { waitUntil: 'networkidle', timeout: 30_000 }); return { context, page }; } 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, page } = await seedAuthenticatedPage(browser, authReport); const result = { checkedAtUtc: new Date().toISOString(), listUrl: page.url(), listHeading: await page.locator('h1').first().textContent(), releaseVersionAnchors: await page.locator('tbody tr td:nth-child(2) a').count(), firstDeploymentHref: '', detailUrl: '', detailHeading: '', reloadedDetailUrl: '', reloadedDetailHeading: '', reloadedDetailButtons: [], replayUrl: '', evidenceUrl: '', evidenceHeading: null, evidenceWorkspaceHref: '', evidenceWorkspaceUrl: '', proofChainsHref: '', proofChainsUrl: '', copyHashStatus: '', artifactViewUrl: '', artifactViewStatus: '', artifactDownloadSuggestedFilename: '', logsDownloadSuggestedFilename: '', detailActionStatus: '', scopeIssues: [], }; const headerActions = page.locator('.deployment-detail .header-actions button'); result.firstDeploymentHref = (await page.locator('tbody a.deployment-link').first().getAttribute('href')) ?? ''; await page.goto(DETAIL_URL, { waitUntil: 'networkidle', timeout: 30_000 }); result.detailUrl = page.url(); result.detailHeading = (await page.locator('h1').first().textContent()) ?? ''; process.stdout.write(`[live-releases-deployments-check] detail ${result.detailUrl} :: ${result.detailHeading}\n`); await headerActions.nth(1).click(); await page.waitForTimeout(1_000); result.replayUrl = page.url(); await page.goto(result.detailUrl, { waitUntil: 'networkidle', timeout: 30_000 }); result.reloadedDetailUrl = page.url(); result.reloadedDetailHeading = (await page.locator('h1').first().textContent().catch(() => '')) ?? ''; result.reloadedDetailButtons = await page.locator('button').allTextContents(); process.stdout.write( `[live-releases-deployments-check] reloaded ${result.reloadedDetailUrl} :: ${result.reloadedDetailHeading} :: ${result.reloadedDetailButtons.join(' | ')}\n`, ); await headerActions.nth(0).click(); await page.waitForTimeout(1_000); result.evidenceUrl = page.url(); result.evidenceHeading = await page.locator('.tab-content h3').first().textContent().catch(() => null); result.evidenceWorkspaceHref = (await page.locator('.evidence-info a').getAttribute('href')) ?? ''; await page.goto(new URL(result.evidenceWorkspaceHref, BASE_URL).toString(), { waitUntil: 'networkidle', timeout: 30_000 }); result.evidenceWorkspaceUrl = page.url(); await page.goto(result.detailUrl, { waitUntil: 'networkidle', timeout: 30_000 }); await headerActions.nth(0).click(); await page.waitForTimeout(1_000); result.proofChainsHref = (await page.locator('.rekor-link').getAttribute('href')) ?? ''; await page.goto(new URL(result.proofChainsHref, BASE_URL).toString(), { waitUntil: 'networkidle', timeout: 30_000 }); result.proofChainsUrl = page.url(); result.scopeIssues.push( ...collectScopeIssues(result.replayUrl, EXPECTED_SCOPE, 'replayUrl'), ...collectScopeIssues(result.evidenceWorkspaceUrl, EXPECTED_SCOPE, 'evidenceWorkspaceUrl'), ...collectScopeIssues(result.proofChainsUrl, EXPECTED_SCOPE, 'proofChainsUrl'), ); await page.goto(result.detailUrl, { waitUntil: 'networkidle', timeout: 30_000 }); await page.getByRole('button', { name: 'Artifacts' }).click(); await page.getByTitle('Copy full hash').first().click(); await page.waitForTimeout(500); result.copyHashStatus = (await page.locator('.deployment-detail .action-status').first().textContent().catch(() => ''))?.trim() ?? ''; const popupPromise = page.waitForEvent('popup', { timeout: 5_000 }).catch(() => null); await page.getByRole('button', { name: 'View' }).first().click(); const popup = await popupPromise; result.artifactViewUrl = popup?.url() ?? ''; result.artifactViewStatus = (await page.locator('.deployment-detail .action-status').first().textContent().catch(() => ''))?.trim() ?? ''; await popup?.close(); const artifactDownloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Download' }).first().click(); const artifactDownload = await artifactDownloadPromise; result.artifactDownloadSuggestedFilename = artifactDownload.suggestedFilename(); await page.getByRole('button', { name: 'Logs' }).click(); const logsDownloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Download' }).click(); const logsDownload = await logsDownloadPromise; result.logsDownloadSuggestedFilename = logsDownload.suggestedFilename(); result.detailActionStatus = (await page.locator('.deployment-detail .action-status').first().textContent().catch(() => ''))?.trim() ?? ''; writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, 'utf8'); await context.close(); process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); if (result.scopeIssues.length > 0) { throw new Error(result.scopeIssues.join('; ')); } } finally { await browser.close(); } } main().catch((error) => { process.stderr.write(`[live-releases-deployments-check] ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); });