Files
ForgeBucket/frontend/src/api/queries/environments.ts
T
erangel1 24bf4706e1 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
2026-05-11 21:20:12 +02:00

158 lines
5.5 KiB
TypeScript

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] })
},
})
}