repositories / will
will
owned by admin
src/agent/deploy-client.ts
Rawimport 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',
])