feat: environment model + deployment tracking (phase 3a)
- Environment/Deployment XORM models + migration 010 - Full CRUD API: GET/POST/PATCH/DELETE /environments + /deployments - Deployment status update endpoint, publishes deployment.* NATS events - EnvironmentsPage with deploy cards, history accordion, deploy modal - Sidebar Environments nav item between Pipelines and Settings - Repo page deployment status badges (env name + SHA pill per environment) - Environment/Deployment types in types/api.ts + environments.ts query hooks
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
import { api } from '../client'
|
||||
import type { Environment, EnvironmentWithLatest, Deployment, DeployStatus } from '../../types/api'
|
||||
|
||||
// ── Schemas ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const deployStatusSchema = z.enum(['pending', 'in_progress', 'success', 'failure', 'cancelled'])
|
||||
|
||||
const deploymentSchema = z.object({
|
||||
id: z.number(),
|
||||
envId: z.number(),
|
||||
repoId: z.number(),
|
||||
sha: z.string(),
|
||||
ref: z.string(),
|
||||
status: deployStatusSchema,
|
||||
triggeredBy: z.string(),
|
||||
description: z.string(),
|
||||
runId: z.number().nullable(),
|
||||
startedAt: z.string().nullable(),
|
||||
finishedAt: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
|
||||
const environmentSchema = z.object({
|
||||
id: z.number(),
|
||||
repoId: z.number(),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
protectionRules: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
const environmentWithLatestSchema = environmentSchema.extend({
|
||||
latestDeployment: deploymentSchema.nullable(),
|
||||
})
|
||||
|
||||
// ── Queries ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useEnvironments(owner: string, repo: string) {
|
||||
return useQuery({
|
||||
queryKey: ['repos', owner, repo, 'environments'],
|
||||
queryFn: () =>
|
||||
api.get<EnvironmentWithLatest[]>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments`,
|
||||
z.array(environmentWithLatestSchema),
|
||||
),
|
||||
enabled: Boolean(owner && repo),
|
||||
refetchInterval: 15_000,
|
||||
})
|
||||
}
|
||||
|
||||
export function useEnvironment(owner: string, repo: string, envName: string) {
|
||||
return useQuery({
|
||||
queryKey: ['repos', owner, repo, 'environments', envName],
|
||||
queryFn: () =>
|
||||
api.get<EnvironmentWithLatest>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments/${envName}`,
|
||||
environmentWithLatestSchema,
|
||||
),
|
||||
enabled: Boolean(owner && repo && envName),
|
||||
refetchInterval: 10_000,
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeployments(owner: string, repo: string, envName: string, limit = 30) {
|
||||
return useQuery({
|
||||
queryKey: ['repos', owner, repo, 'environments', envName, 'deployments', limit],
|
||||
queryFn: () =>
|
||||
api.get<Deployment[]>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments/${envName}/deployments?limit=${limit}`,
|
||||
z.array(deploymentSchema),
|
||||
),
|
||||
enabled: Boolean(owner && repo && envName),
|
||||
refetchInterval: 10_000,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Mutations ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useCreateEnvironment(owner: string, repo: string) {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (body: { name: string; url?: string; protectionRules?: string }) =>
|
||||
api.post<Environment>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments`,
|
||||
environmentSchema,
|
||||
body,
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateEnvironment(owner: string, repo: string, envName: string) {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (body: { url?: string; protectionRules?: string }) =>
|
||||
api.patch<Environment>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments/${envName}`,
|
||||
environmentSchema,
|
||||
body,
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments'] })
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments', envName] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteEnvironment(owner: string, repo: string) {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (envName: string) =>
|
||||
api.delete<void>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments/${envName}`,
|
||||
z.unknown() as z.ZodType<void>,
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateDeployment(owner: string, repo: string, envName: string) {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (body: { sha: string; ref?: string; description?: string; runId?: number }) =>
|
||||
api.post<Deployment>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments/${envName}/deployments`,
|
||||
deploymentSchema,
|
||||
body,
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments'] })
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments', envName] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateDeploymentStatus(owner: string, repo: string, envName: string) {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ deployId, status, description }: { deployId: number; status: DeployStatus; description?: string }) =>
|
||||
api.patch<Deployment>(
|
||||
`/api/v1/repos/${owner}/${repo}/environments/${envName}/deployments/${deployId}/status`,
|
||||
deploymentSchema,
|
||||
{ status, description },
|
||||
),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments'] })
|
||||
qc.invalidateQueries({ queryKey: ['repos', owner, repo, 'environments', envName] })
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user