191 lines
6.0 KiB
TypeScript
191 lines
6.0 KiB
TypeScript
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<Pipeline[]>(`/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<PipelineRun[]>(
|
|
`/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<RunDetail>(
|
|
`/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<StepLogs>(
|
|
`/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<RecentRun[]>(`/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(),
|
|
}),
|
|
)
|