Luigit
repositories / will

will

owned by admin

src/agent/ciwatch.ts

Raw
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<LuciRun[]>
}

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 }
}