import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { z } from 'zod' import { api } from '../client' import type { Pipeline, PipelineRun, RunDetail, StepLogs } from '../../types/api' // ── Zod schemas ─────────────────────────────────────────────────────────────── const runStatusSchema = z.enum(['queued', 'running', 'succeeded', 'failed', 'cancelled']) const pipelineSchema = z.object({ id: z.number(), repoId: z.number(), name: z.string(), filePath: z.string(), createdAt: z.string(), updatedAt: z.string(), }) const runSchema = z.object({ id: z.number(), pipelineId: z.number(), repoId: z.number(), triggerRef: z.string(), triggerSha: z.string(), triggeredBy: z.string(), status: runStatusSchema, startedAt: z.string().nullable(), finishedAt: z.string().nullable(), createdAt: z.string(), }) const stepSchema = z.object({ id: z.number(), jobId: z.number(), seq: z.number(), name: z.string(), runCmd: z.string(), usesAction: z.string(), status: runStatusSchema, exitCode: z.number(), startedAt: z.string().nullable(), finishedAt: z.string().nullable(), }) const jobSchema = z.object({ id: z.number(), runId: z.number(), name: z.string(), image: z.string(), needs: z.string(), status: runStatusSchema, startedAt: z.string().nullable(), finishedAt: z.string().nullable(), createdAt: z.string(), steps: z.array(stepSchema), }) const runDetailSchema = runSchema.extend({ jobs: z.array(jobSchema), }) const stepLogSchema = z.object({ id: z.number(), stepId: z.number(), chunkIndex: z.number(), content: z.string(), createdAt: z.string(), }) const stepLogsSchema = z.array( stepSchema.extend({ logs: z.array(stepLogSchema) }), ) // ── Queries ─────────────────────────────────────────────────────────────────── /** Pipeline definitions for a repo. */ export function usePipelines(owner: string, repo: string) { return useQuery({ queryKey: ['repos', owner, repo, 'pipelines'], queryFn: () => api.get(`/api/v1/repos/${owner}/${repo}/pipelines`, z.array(pipelineSchema)), enabled: Boolean(owner && repo), }) } /** Pipeline runs for a repo, newest first. */ export function useRuns(owner: string, repo: string, limit = 30) { return useQuery({ queryKey: ['repos', owner, repo, 'runs', limit], queryFn: () => api.get( `/api/v1/repos/${owner}/${repo}/runs?limit=${limit}`, z.array(runSchema), ), enabled: Boolean(owner && repo), refetchInterval: 8_000, // poll while runs may be active }) } /** Run detail: run + jobs (each with steps). */ export function useRunDetail(owner: string, repo: string, runId: number) { return useQuery({ queryKey: ['repos', owner, repo, 'runs', runId], queryFn: () => api.get( `/api/v1/repos/${owner}/${repo}/runs/${runId}`, runDetailSchema, ), enabled: Boolean(owner && repo && runId), refetchInterval: (query) => { const status = query.state.data?.status return status === 'running' || status === 'queued' ? 3_000 : false }, }) } /** Step-level log chunks for a job. */ export function useJobLogs(owner: string, repo: string, runId: number, jobId: number) { return useQuery({ queryKey: ['repos', owner, repo, 'runs', runId, 'jobs', jobId, 'logs'], queryFn: () => api.get( `/api/v1/repos/${owner}/${repo}/runs/${runId}/jobs/${jobId}/logs`, stepLogsSchema, ), enabled: Boolean(owner && repo && runId && jobId), refetchInterval: (query) => { // Keep polling only while the job may still be running const hasRunning = query.state.data?.some(s => s.status === 'running') return hasRunning ? 2_000 : false }, }) } /** Recent pipeline runs across all repos owned by the current user. */ export function useRecentRuns(limit = 20) { return useQuery({ queryKey: ['pipelines', 'runs', limit], queryFn: () => api.get(`/api/v1/pipelines/runs?limit=${limit}`, recentRunSchema), refetchInterval: 10_000, }) } // ── Mutations ───────────────────────────────────────────────────────────────── export function useCancelRun(owner: string, repo: string) { const qc = useQueryClient() return useMutation({ mutationFn: (runId: number) => api.post(`/api/v1/repos/${owner}/${repo}/runs/${runId}/cancel`, z.unknown(), undefined), onSuccess: (_data, runId) => { qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs'] }) qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs', runId] }) qc.invalidateQueries({ queryKey: ['pipelines', 'runs'] }) }, }) } export function useRetryJob(owner: string, repo: string, runId: number) { const qc = useQueryClient() return useMutation({ mutationFn: (jobId: number) => api.post( `/api/v1/repos/${owner}/${repo}/runs/${runId}/jobs/${jobId}/retry`, z.unknown(), undefined, ), onSuccess: () => { qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs', runId] }) qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'runs'] }) qc.invalidateQueries({ queryKey: ['pipelines', 'runs'] }) }, }) } // ── Cross-repo recent run type ───────────────────────────────────────────────── // Returned by GET /api/v1/pipelines/runs — extends PipelineRun with repo context. export interface RecentRun extends PipelineRun { repoName: string ownerName: string } const recentRunSchema = z.array( runSchema.extend({ repoName: z.string(), ownerName: z.string(), }), )