192 lines
8.0 KiB
JavaScript
192 lines
8.0 KiB
JavaScript
#!/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);
|
|
});
|