import type { DeploymentEventMirror } from '../shared/events.ts' import { createLogger } from '../shared/log.ts' import type { ServiceClient } from './service-client.ts' const log = createLogger({ component: 'deploy' }) /** * Deployment platform client (contract dependency, spec AC24). * The real API is not implemented yet; the agent fails closed with a clear * error until WILL_DEPLOY_API_URL is configured. */ export interface DeployEventPage { events: (DeploymentEventMirror & { cursor: string })[] nextCursor?: string | undefined } export interface DeployPlatformClient { fetchEvents(after?: string): Promise rollbackPrevious(): Promise<{ ok: boolean; revision?: string; error?: string }> } export function createDeployClient(baseUrl: string): DeployPlatformClient { const base = baseUrl.replace(/\/$/, '') return { async fetchEvents(after) { const url = `${base}/v1/events${after !== undefined ? `?after=${encodeURIComponent(after)}` : ''}` const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }) if (!res.ok) throw new Error(`deploy events failed: ${res.status}`) return (await res.json()) as DeployEventPage }, async rollbackPrevious() { const res = await fetch(`${base}/v1/rollback`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason: 'agent-declared semantic failure' }), signal: AbortSignal.timeout(30_000), }) const body = (await res.json().catch(() => ({}))) as { revision?: string; error?: string } if (!res.ok) return { ok: false, error: body.error ?? `rollback failed: ${res.status}` } return { ok: true, ...(body.revision !== undefined ? { revision: body.revision } : {}) } }, } } /** Cursor-based polling (spec: deployment history is platform-authoritative). */ export async function pollDeployEvents( client: DeployPlatformClient, opts: { cursor?: string | undefined service: ServiceClient onEvent: (event: DeploymentEventMirror) => void | Promise }, ): Promise<{ cursor?: string | undefined; imported: number }> { const page = await client.fetchEvents(opts.cursor) for (const event of page.events) { await opts.service.ingestDeploy([event]) await opts.onEvent(event) } if (page.nextCursor !== undefined || page.events.length > 0) { log.info('deploy events imported', { count: page.events.length }) } return { cursor: page.nextCursor ?? opts.cursor, imported: page.events.length } } export const FAILURE_EVENT_KINDS: ReadonlySet = new Set([ 'rollback', 'startup-failed', 'probation-failed', ])