Luigit
repositories / will

will

owned by admin

src/agent/deploy-client.ts

Raw
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<DeployEventPage>
  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<void>
  },
): 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<string> = new Set([
  'rollback',
  'startup-failed',
  'probation-failed',
])