139 lines
3.7 KiB
JavaScript
139 lines
3.7 KiB
JavaScript
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);
|
|
});
|