Files
blackroad-operating-system/tools/health-check.ts
2025-11-20 12:32:56 -06:00

80 lines
2.5 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { setTimeout as delay } from 'timers/promises';
type ServiceEntry = {
key: string;
name: string;
repoUrl: string;
railwayUrl?: string | null;
cloudflareSubdomain?: string | null;
healthPath?: string;
};
type OsSpec = {
version: string;
services: ServiceEntry[];
};
const SPEC_PATH = path.resolve(__dirname, '..', 'os-spec', 'os-spec.json');
const DEFAULT_TIMEOUT_MS = 8000;
function loadSpec(): OsSpec {
const raw = fs.readFileSync(SPEC_PATH, 'utf-8');
return JSON.parse(raw) as OsSpec;
}
function buildHealthUrl(service: ServiceEntry): string | null {
const base = service.cloudflareSubdomain ?? service.railwayUrl;
if (!base) return null;
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
const pathPart = service.healthPath ?? '/health';
return `${normalizedBase}${pathPart.startsWith('/') ? '' : '/'}${pathPart}`;
}
async function checkHealth(url: string): Promise<{ status: string; detail: string }> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
try {
const response = await fetch(url, { signal: controller.signal });
const bodyText = await response.text();
const statusLabel = response.ok ? 'healthy' : 'unhealthy';
return { status: statusLabel, detail: `${response.status} ${response.statusText}` };
} catch (error) {
const reason = (error as Error).message;
return { status: 'unreachable', detail: reason };
} finally {
clearTimeout(timeout);
}
}
async function main() {
const spec = loadSpec();
const results: Array<{ service: string; target: string; status: string; detail: string }> = [];
for (const service of spec.services) {
const url = buildHealthUrl(service);
if (!url) {
results.push({ service: service.key, target: 'N/A', status: 'skipped', detail: 'No target URL defined' });
continue;
}
const { status, detail } = await checkHealth(url);
results.push({ service: service.key, target: url, status, detail });
await delay(50); // small pause to avoid overwhelming endpoints
}
console.table(results);
const failures = results.filter((row) => row.status !== 'healthy' && row.status !== 'skipped');
if (failures.length > 0) {
console.error(`\nHealth check completed with ${failures.length} issue(s).`);
process.exitCode = 1;
}
}
main().catch((error) => {
console.error('Unexpected error while running health checks', error);
process.exitCode = 1;
});