import { writeFileSync } from 'node:fs'; const baseUrl = process.env.BASE_URL?.replace(/\/$/, ''); const expectedReleaseId = process.env.EXPECTED_RELEASE_ID; const smokeWrite = process.env.SMOKE_WRITE === 'true'; const outputPath = process.env.SMOKE_RESULT_PATH; const timeoutMs = Number(process.env.SMOKE_TIMEOUT_MS ?? 5000); const retryAttempts = Number(process.env.SMOKE_RETRY_ATTEMPTS ?? 12); const retryDelayMs = Number(process.env.SMOKE_RETRY_DELAY_MS ?? 5000); const retryableStatuses = new Set([404, 408, 425, 429, 500, 502, 503, 504]); if (!baseUrl) { console.error('BASE_URL is required for smoke checks'); process.exit(2); } function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function requestOnce(path, options = {}) { const response = await fetch(`${baseUrl}${path}`, { ...options, headers: { 'content-type': 'application/json', ...options.headers, }, signal: AbortSignal.timeout(timeoutMs), }); const text = await response.text(); let body; try { body = text ? JSON.parse(text) : undefined; } catch { body = text; } if (!response.ok) { const error = new Error(`${path} returned ${response.status}: ${text}`); error.status = response.status; throw error; } return body; } async function request(path, options = {}) { let lastError; for (let attempt = 1; attempt <= retryAttempts; attempt += 1) { try { return await requestOnce(path, options); } catch (error) { lastError = error; const status = error?.status; const retryableStatus = typeof status === 'number' && retryableStatuses.has(status); const retryableNetworkError = error?.name === 'TimeoutError' || error?.cause?.code; if (attempt >= retryAttempts || (!retryableStatus && !retryableNetworkError)) { throw error; } await sleep(retryDelayMs); } } throw lastError; } function assertRelease(body) { if (!expectedReleaseId) return; const actual = body?.metadata?.releaseId; if (actual !== expectedReleaseId) { throw new Error(`service reports release ${actual}, expected ${expectedReleaseId}`); } } async function run() { const startedAt = Date.now(); const checks = []; const live = await request('/health/live'); assertRelease(live); checks.push({ name: 'web_live', status: 'ok' }); const ready = await request('/health/ready'); assertRelease(ready); checks.push({ name: 'web_ready', status: 'ok' }); const registry = await request('/api/videos'); if (!registry.stats || !Array.isArray(registry.videos)) { throw new Error('Video registry API returned an invalid shape'); } checks.push({ name: 'registry_read', status: 'ok' }); if (smokeWrite) { await request('/api/videos', { method: 'POST', body: JSON.stringify({ title: `Smoke video ${Date.now()}`, owner: 'Deploy smoke', sourceUrl: 'https://example.com/smoke', status: 'draft', }), }); checks.push({ name: 'registry_write', status: 'ok' }); } const result = { status: 'ok', baseUrl, expectedReleaseId, checks, durationMs: Date.now() - startedAt, }; if (outputPath) { writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`); } console.log(JSON.stringify({ event: 'smoke_check_success', ...result })); } run().catch((error) => { const result = { status: 'error', baseUrl, expectedReleaseId, error: error instanceof Error ? error.message : String(error), }; if (outputPath) { writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`); } console.error(JSON.stringify({ event: 'smoke_check_failure', ...result })); process.exit(1); });