diff --git a/package.json b/package.json index c1bd829..9edcae8 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "test:watch": "vitest", "migrate": "ts-node-esm src/backend/db/migrate.ts", "workflow:run": "ts-node scripts/run-workflow.ts", - "workflow:schedule": "ts-node scripts/schedule-workflow.ts" + "workflow:schedule": "ts-node scripts/schedule-workflow.ts", + "workflow:health-check": "ts-node scripts/check-workflow-health.ts" }, "dependencies": { "express": "^4.18.2", diff --git a/scripts/check-workflow-health.ts b/scripts/check-workflow-health.ts new file mode 100644 index 0000000..fd02adf --- /dev/null +++ b/scripts/check-workflow-health.ts @@ -0,0 +1,58 @@ +#!/usr/bin/env ts-node +import * as path from 'path'; +import { promises as fs } from 'fs'; + +type WorkflowOverallStatus = 'idle' | 'running' | 'blocked' | 'failed' | 'completed'; + +type WorkflowStatusFile = { + overallStatus?: string; +}; + +const DEFAULT_STATUS_PATH = path.join(process.cwd(), 'status', 'workflow-status.json'); +const HEALTHY_STATES = new Set(['running', 'completed', 'idle']); +const UNHEALTHY_STATES = new Set(['failed', 'blocked']); + +function getStatusPathFromArgs(argv: string[]): string { + for (let i = 0; i < argv.length; i += 1) { + if (argv[i] === '--status' && argv[i + 1]) { + return argv[i + 1]; + } + } + return DEFAULT_STATUS_PATH; +} + +async function readOverallStatus(statusPath: string): Promise { + const raw = await fs.readFile(statusPath, 'utf8'); + const parsed = JSON.parse(raw) as WorkflowStatusFile; + return String(parsed.overallStatus ?? '').trim(); +} + +export async function checkWorkflowHealth(statusPath = DEFAULT_STATUS_PATH): Promise { + try { + const overallStatus = await readOverallStatus(statusPath); + + if (HEALTHY_STATES.has(overallStatus as WorkflowOverallStatus)) { + console.log(JSON.stringify({ ok: true, status: overallStatus })); + return 0; + } + + if (UNHEALTHY_STATES.has(overallStatus as WorkflowOverallStatus)) { + console.log(JSON.stringify({ ok: false, status: overallStatus })); + return 1; + } + + console.log(JSON.stringify({ ok: false, status: overallStatus || 'unknown', error: 'unknown_status' })); + return 1; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.log(JSON.stringify({ ok: false, error: 'status_read_failed', message })); + return 1; + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const statusPath = getStatusPathFromArgs(process.argv.slice(2)); + checkWorkflowHealth(statusPath).then((code) => { + process.exitCode = code; + }); +}