/** * What a deploy does to alerting. * * Two things, both of which have to happen around the deploy rather than * inside it: * * 1. A deploy is noisy. Restarting a service fails its own health check, so * a deploy without suppression pages the operator about the deploy they * are personally running. The window opens at start and closes at the * end, INCLUDING on failure — a failed deploy that left the window open * would silence that module forever, which is the worst possible way to * get this wrong. * * 2. A module declaring `hooks.health_check.interval` gets a monitor on * first deploy, so watching a module is the default rather than something * to remember. * * Everything here is best-effort: alerting bookkeeping must never be able to * fail a deploy. A missing window means noise; a failed deploy means an outage. */ import { randomUUID } from 'node:crypto'; import { and, eq, isNull } from 'drizzle-orm'; import type { DbClient } from '../../db/client'; import { modules, suppressionWindows } from '../../db/schema'; import type { ModuleManifest } from '../../manifest/schema'; import { ensureMonitorForModule } from './monitors'; /** Open a deploy suppression window for a module. Returns its id, or null. */ export function openDeployWindow(db: DbClient, moduleId: string, now: Date): string | null { try { // Reuse an already-open window rather than stacking them: a redeploy // during a deploy should not need two closes to become audible again. const existing = db .select() .from(suppressionWindows) .where(and(eq(suppressionWindows.scopeModuleId, moduleId), isNull(suppressionWindows.endsAt))) .get(); if (existing) return existing.id; const id = randomUUID(); db.insert(suppressionWindows) .values({ id, source: 'deploy', scopeModuleId: moduleId, startedAt: now }) .run(); return id; } catch { return null; } } /** * Close every open deploy window for a module. * * Called on success AND failure. Closing by module rather than by id means a * crashed deploy that never returned its window id still gets cleaned up on * the next attempt. */ export function closeDeployWindows(db: DbClient, moduleId: string, now: Date): number { try { const open = db .select({ id: suppressionWindows.id }) .from(suppressionWindows) .where(and(eq(suppressionWindows.scopeModuleId, moduleId), isNull(suppressionWindows.endsAt))) .all(); for (const window of open) { db.update(suppressionWindows) .set({ endsAt: now }) .where(eq(suppressionWindows.id, window.id)) .run(); } return open.length; } catch { return 0; } } /** Modules currently inside an open deploy window. */ export function modulesInDeployWindow(db: DbClient): Set { try { return new Set( db .select({ moduleId: suppressionWindows.scopeModuleId }) .from(suppressionWindows) .where(isNull(suppressionWindows.endsAt)) .all() .map((row) => row.moduleId), ); } catch { return new Set(); } } /** * Create the monitor row for a freshly deployed module that declares a * `health_check` hook. * * A row for any such module, not only one whose manifest suggests an interval: * the row carries severity, escalation policy and `lastRunAt`, while whether it * runs comes from the module's effective cadence. Without a row, an operator * setting `health_check_interval` on a module whose author named none would * have nothing to schedule against. */ export function ensureMonitorOnDeploy(db: DbClient, moduleId: string): boolean { try { const module = db.select().from(modules).where(eq(modules.id, moduleId)).get(); if (!module) return false; const manifest = module.manifestData as ModuleManifest; const healthCheck = manifest.hooks?.health_check; if (!healthCheck) return false; return ensureMonitorForModule(db, moduleId, healthCheck.interval) !== null; } catch { return false; } }