import { createLogger } from '../shared/log.ts' import type { WakeQueue } from './queue-store.ts' import type { AgentState } from './state.ts' const log = createLogger({ component: 'ciwatch' }) /** * Luci CI watch (spec AC16/AC23): poll pushed commits' runs and enqueue a * ci_failure wake when a required run fails before deployment. * Contract: GET {base}/api/v1/repos/{repo}/runs -> { runs: [...] }. * The JSON endpoint is a required Luci extension (deploy/contract.md); * polling is disabled until WILL_LUCI_BASE_URL is configured. */ export interface LuciRun { id: string revision: string status: 'running' | 'success' | 'failed' jobs?: { name: string; status: string }[] } export interface LuciClient { listRuns(): Promise } export function createLuciClient(baseUrl: string, repo: string): LuciClient { return { async listRuns() { const res = await fetch( `${baseUrl.replace(/\/$/, '')}/api/v1/repos/${encodeURIComponent(repo)}/runs`, { signal: AbortSignal.timeout(10_000), }, ) if (!res.ok) throw new Error(`luci runs failed: ${res.status}`) const body = (await res.json()) as { runs?: LuciRun[] } return body.runs ?? [] }, } } /** Poll once; enqueue ci_failure wakes for newly-failed runs of tracked pushes. */ export async function pollCi( client: LuciClient, state: AgentState, queue: WakeQueue, ): Promise<{ wakes: number }> { const runs = await client.listRuns() const tracked = new Set(state.data.pushedShas) const handled = new Set(state.data.handledCiRuns) let wakes = 0 for (const run of runs) { if (run.status !== 'failed' || handled.has(run.id)) continue if (!tracked.has(run.revision)) continue await queue.enqueue('ci_failure', { sha: run.revision, job: run.jobs?.find((j) => j.status === 'failed')?.name ?? 'unknown', }) wakes++ } if (wakes > 0) { const newHandled = runs.filter((r) => r.status === 'failed').map((r) => r.id) await state.update((d) => { d.handledCiRuns = [...new Set([...d.handledCiRuns, ...newHandled])].slice(-500) }) } log.debug('ci poll', { runs: runs.length, wakes }) return { wakes } }