deploy fix bug

This commit is contained in:
ZuoZuo 2026-04-07 13:19:50 +08:00
parent 53629519f5
commit 4cd2d4e31c
7 changed files with 215 additions and 47 deletions

View File

@ -136,14 +136,14 @@ type ReleaseRunStep struct {
ServiceName string `gorm:"size:32;index;not null" json:"service_name"`
PreviousReleaseID string `gorm:"size:128;index" json:"previous_release_id"`
Status string `gorm:"size:32;index;not null" json:"status"`
DeploymentID uint `gorm:"index" json:"deployment_id"`
DeploymentID *uint `gorm:"index" json:"deployment_id"`
Deployment *Deployment `json:"deployment,omitempty"`
ErrorMessage string `gorm:"type:text" json:"error_message"`
LogExcerpt string `gorm:"type:text" json:"log_excerpt"`
StartedAt *time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at"`
RollbackStatus string `gorm:"size:32;index" json:"rollback_status"`
RollbackDeploymentID uint `gorm:"index" json:"rollback_deployment_id"`
RollbackDeploymentID *uint `gorm:"index" json:"rollback_deployment_id"`
RollbackDeployment *Deployment `json:"rollback_deployment,omitempty"`
RollbackErrorMessage string `gorm:"type:text" json:"rollback_error_message"`
RollbackLogExcerpt string `gorm:"type:text" json:"rollback_log_excerpt"`

View File

@ -455,7 +455,7 @@ func (m *Manager) ProcessReleaseRun(ctx context.Context, runID uint) error {
return err
}
step.Status = model.DeploymentSuccess
step.DeploymentID = deployment.ID
step.DeploymentID = &deployment.ID
completedSteps = append(completedSteps, step)
}
@ -642,6 +642,7 @@ func (m *Manager) ProcessDeployment(ctx context.Context, deploymentID uint) erro
targetFinishedAt := time.Now()
if err != nil {
errorMessage := summarizeDeploymentError(err, output)
_ = m.db.WithContext(ctx).Model(&model.DeploymentTarget{}).
Where("id = ?", target.ID).
Updates(map[string]any{
@ -654,10 +655,10 @@ func (m *Manager) ProcessDeployment(ctx context.Context, deploymentID uint) erro
Where("id = ?", deploymentID).
Updates(map[string]any{
"status": model.DeploymentFailed,
"error_message": err.Error(),
"error_message": errorMessage,
"finished_at": &targetFinishedAt,
}).Error
return err
return errors.New(errorMessage)
}
updates := map[string]any{
@ -911,6 +912,26 @@ func summarizeBuildError(err error, output string) string {
return err.Error()
}
func summarizeDeploymentError(err error, output string) string {
lines := strings.Split(output, "\n")
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" || line == err.Error() {
continue
}
if line == "Traceback (most recent call last):" || strings.HasPrefix(line, "File ") {
continue
}
if strings.HasPrefix(line, "RuntimeError:") {
line = strings.TrimSpace(strings.TrimPrefix(line, "RuntimeError:"))
}
if line != "" {
return line
}
}
return err.Error()
}
func deploymentLogExcerpt(deployment *model.Deployment) string {
if deployment == nil {
return ""

View File

@ -0,0 +1,3 @@
// TODO: Replace this fallback with the authenticated user once identity is wired end-to-end,
// then restore operator-related fields in the UI.
export const defaultOperator = 'platform-admin'

View File

@ -26,16 +26,15 @@ import { useEffect, useMemo, useState } from 'react'
import { api } from '@/api/client'
import StatusChip from '@/components/StatusChip'
import { defaultOperator } from '@/constants/operator'
import type { BuildJob, CatalogService } from '@/types'
type FormState = {
service_name: string
release_id: string
branch: string
operator: string
}
const defaultOperator = 'platform-admin'
const activeBuildStatuses = new Set(['queued', 'running'])
function formatDateTime(value?: string) {
@ -56,8 +55,7 @@ export default function BuildsPage() {
const [form, setForm] = useState<FormState>({
service_name: '',
release_id: '',
branch: 'main',
operator: defaultOperator
branch: 'main'
})
const selectedService = useMemo(
@ -77,8 +75,7 @@ export default function BuildsPage() {
setForm({
service_name: first?.name ?? '',
release_id: '',
branch: first?.default_branch ?? 'main',
operator: defaultOperator
branch: first?.default_branch ?? 'main'
})
}
@ -141,7 +138,7 @@ export default function BuildsPage() {
const createBuild = async () => {
try {
const created = await api.createBuild(form)
const created = await api.createBuild({ ...form, operator: defaultOperator })
setOpen(false)
resetForm(services)
setMessage('构建任务已创建,已打开实时构建日志')
@ -267,11 +264,6 @@ export default function BuildsPage() {
value={form.branch}
onChange={(event) => setForm((prev) => ({ ...prev, branch: event.target.value }))}
/>
<TextField
label="操作人"
value={form.operator}
onChange={(event) => setForm((prev) => ({ ...prev, operator: event.target.value }))}
/>
<TextField label="仓库" value={selectedService?.repo ?? ''} slotProps={{ input: { readOnly: true } }} />
<Typography variant="body2" color="text.secondary">
`git fetch / checkout / pull`

View File

@ -1,8 +1,11 @@
import {
Alert,
Box,
Button,
Card,
CardContent,
Chip,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
@ -24,32 +27,31 @@ import { useNavigate } from 'react-router-dom'
import { api } from '@/api/client'
import StatusChip from '@/components/StatusChip'
import type { Deployment, ReleaseRun } from '@/types'
import { defaultOperator } from '@/constants/operator'
import type { Deployment, DeploymentTarget, ReleaseRun } from '@/types'
type DeploymentFormState = {
service_name: string
release_id: string
operation: string
operator: string
}
type ReleaseRunFormState = {
release_id: string
operator: string
}
const emptyDeploymentForm: DeploymentFormState = {
service_name: 'user',
release_id: '',
operation: 'deploy',
operator: 'platform-admin'
operation: 'deploy'
}
const emptyReleaseRunForm: ReleaseRunFormState = {
release_id: '',
operator: 'platform-admin'
release_id: ''
}
const activeDeploymentStatuses = new Set(['queued', 'running'])
function stepSummary(run: ReleaseRun) {
return (run.steps ?? []).map((step) => ({
key: `${run.id}-${step.sequence}`,
@ -60,14 +62,29 @@ function stepSummary(run: ReleaseRun) {
}))
}
function formatDateTime(value?: string) {
if (!value) return '-'
return new Date(value).toLocaleString()
}
function targetLabel(target: DeploymentTarget) {
const hostName = target.host?.name || `host-${target.host_id}`
const privateIP = target.host?.private_ip
return privateIP ? `${hostName} · ${privateIP}` : hostName
}
export default function DeploymentsPage() {
const navigate = useNavigate()
const [deployments, setDeployments] = useState<Deployment[]>([])
const [releaseRuns, setReleaseRuns] = useState<ReleaseRun[]>([])
const [deploymentOpen, setDeploymentOpen] = useState(false)
const [releaseRunOpen, setReleaseRunOpen] = useState(false)
const [detailOpen, setDetailOpen] = useState(false)
const [deploymentForm, setDeploymentForm] = useState<DeploymentFormState>(emptyDeploymentForm)
const [releaseRunForm, setReleaseRunForm] = useState<ReleaseRunFormState>(emptyReleaseRunForm)
const [selectedDeploymentID, setSelectedDeploymentID] = useState<number | null>(null)
const [selectedDeployment, setSelectedDeployment] = useState<Deployment | null>(null)
const [detailLoading, setDetailLoading] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState('')
@ -86,7 +103,7 @@ export default function DeploymentsPage() {
const createDeployment = async () => {
try {
await api.createDeployment(deploymentForm)
await api.createDeployment({ ...deploymentForm, operator: defaultOperator })
setDeploymentOpen(false)
setDeploymentForm(emptyDeploymentForm)
setMessage('单服务部署任务已创建')
@ -98,7 +115,7 @@ export default function DeploymentsPage() {
const createReleaseRun = async () => {
try {
await api.createReleaseRun(releaseRunForm)
await api.createReleaseRun({ ...releaseRunForm, operator: defaultOperator })
setReleaseRunOpen(false)
setReleaseRunForm(emptyReleaseRunForm)
setMessage('总发布任务已创建,将按 user -> pay -> gateway 执行')
@ -110,7 +127,7 @@ export default function DeploymentsPage() {
const rollback = async (row: Deployment) => {
try {
await api.rollbackDeployment(row.id, { release_id: row.release_id, operator: 'platform-admin' })
await api.rollbackDeployment(row.id, { release_id: row.release_id, operator: defaultOperator })
setMessage('回滚任务已创建')
await load()
} catch (err) {
@ -118,6 +135,27 @@ export default function DeploymentsPage() {
}
}
const loadDeploymentDetail = async (id: number, silent = false) => {
if (!silent) {
setDetailLoading(true)
}
try {
const data = await api.getDeployment(id)
setSelectedDeployment(data)
} catch (err) {
setError(err instanceof Error ? err.message : '加载部署详情失败')
} finally {
setDetailLoading(false)
}
}
const openDeploymentDetail = (row: Deployment) => {
setSelectedDeploymentID(row.id)
setSelectedDeployment(row)
setDetailOpen(true)
void loadDeploymentDetail(row.id)
}
return (
<Stack spacing={3}>
<Card>
@ -161,7 +199,6 @@ export default function DeploymentsPage() {
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
@ -195,7 +232,6 @@ export default function DeploymentsPage() {
))}
</Stack>
</TableCell>
<TableCell>{run.operator}</TableCell>
<TableCell sx={{ maxWidth: 320 }}>
<Typography variant="body2" color="text.secondary">
{run.error_message || '-'}
@ -228,7 +264,7 @@ export default function DeploymentsPage() {
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
@ -242,11 +278,20 @@ export default function DeploymentsPage() {
<TableCell>
<StatusChip status={row.status} />
</TableCell>
<TableCell>{row.operator}</TableCell>
<TableCell sx={{ maxWidth: 360 }}>
<Typography variant="body2" color="text.secondary">
{row.error_message || '-'}
</Typography>
</TableCell>
<TableCell align="right">
<Stack direction="row" spacing={1} justifyContent="flex-end">
<Button size="small" onClick={() => openDeploymentDetail(row)}>
</Button>
<Button size="small" disabled={!row.release_id} onClick={() => void rollback(row)}>
</Button>
</Stack>
</TableCell>
</TableRow>
))}
@ -266,11 +311,6 @@ export default function DeploymentsPage() {
value={releaseRunForm.release_id}
onChange={(e) => setReleaseRunForm((prev) => ({ ...prev, release_id: e.target.value }))}
/>
<TextField
label="操作人"
value={releaseRunForm.operator}
onChange={(e) => setReleaseRunForm((prev) => ({ ...prev, operator: e.target.value }))}
/>
</Stack>
</DialogContent>
<DialogActions>
@ -310,11 +350,6 @@ export default function DeploymentsPage() {
value={deploymentForm.release_id}
onChange={(e) => setDeploymentForm((prev) => ({ ...prev, release_id: e.target.value }))}
/>
<TextField
label="操作人"
value={deploymentForm.operator}
onChange={(e) => setDeploymentForm((prev) => ({ ...prev, operator: e.target.value }))}
/>
</Stack>
</DialogContent>
<DialogActions>
@ -325,6 +360,121 @@ export default function DeploymentsPage() {
</DialogActions>
</Dialog>
<Dialog open={detailOpen} onClose={() => setDetailOpen(false)} fullWidth maxWidth="lg">
<DialogTitle></DialogTitle>
<DialogContent dividers>
{detailLoading && !selectedDeployment ? (
<Stack alignItems="center" justifyContent="center" sx={{ py: 8 }}>
<CircularProgress size={28} />
</Stack>
) : selectedDeployment ? (
<Stack spacing={2}>
<Stack direction={{ xs: 'column', lg: 'row' }} spacing={1.5} justifyContent="space-between">
<Stack spacing={1}>
<Typography variant="h6">{`${selectedDeployment.service_name} · ${selectedDeployment.release_id || '-'}`}</Typography>
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap">
<Chip size="small" variant="outlined" label={`任务 #${selectedDeployment.id}`} />
<Chip size="small" variant="outlined" label={`操作 ${selectedDeployment.operation}`} />
<Chip
size="small"
variant="outlined"
label={`开始 ${formatDateTime(selectedDeployment.started_at || selectedDeployment.created_at)}`}
/>
<Chip size="small" variant="outlined" label={`结束 ${formatDateTime(selectedDeployment.finished_at)}`} />
</Stack>
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<StatusChip status={selectedDeployment.status} />
<Button
variant="outlined"
onClick={() => selectedDeploymentID && void loadDeploymentDetail(selectedDeploymentID, true)}
>
</Button>
</Stack>
</Stack>
{activeDeploymentStatuses.has(selectedDeployment.status) ? (
<Alert severity="info" variant="outlined">
</Alert>
) : null}
{selectedDeployment.error_message ? (
<Alert severity="error" variant="filled">
{selectedDeployment.error_message}
</Alert>
) : null}
{(selectedDeployment.targets ?? []).length === 0 ? (
<Alert severity="info" variant="outlined">
</Alert>
) : (
<Stack spacing={1.5}>
{(selectedDeployment.targets ?? []).map((target) => (
<Box
key={target.id}
sx={{
border: '1px solid rgba(53,80,161,0.1)',
borderRadius: 3,
overflow: 'hidden',
backgroundColor: 'rgba(53,80,161,0.02)'
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={1}
justifyContent="space-between"
alignItems={{ sm: 'center' }}
sx={{ px: 2, py: 1.5, borderBottom: '1px solid rgba(53,80,161,0.08)' }}
>
<Stack spacing={0.5}>
<Typography variant="subtitle2">{targetLabel(target)}</Typography>
<Typography variant="body2" color="text.secondary">
{`开始 ${formatDateTime(target.started_at)} · 结束 ${formatDateTime(target.finished_at)}`}
</Typography>
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Chip size="small" variant="outlined" label={`步骤 ${target.step || '-'}`} />
<StatusChip status={target.status} />
</Stack>
</Stack>
<Box
component="pre"
sx={{
m: 0,
px: 2,
py: 1.5,
maxHeight: 320,
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontSize: 13,
lineHeight: 1.7,
fontFamily: '"SFMono-Regular", "Roboto Mono", "Menlo", monospace',
backgroundColor: 'rgba(16,24,40,0.98)',
color: 'rgb(231, 239, 255)'
}}
>
{target.log_excerpt || '无日志输出'}
</Box>
</Box>
))}
</Stack>
)}
</Stack>
) : (
<Alert severity="warning" variant="outlined">
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setDetailOpen(false)}></Button>
</DialogActions>
</Dialog>
<Snackbar open={Boolean(message)} autoHideDuration={3000} onClose={() => setMessage('')}>
<Alert severity="success" variant="filled">
{message}

View File

@ -637,7 +637,6 @@ export default function ReleaseRunDetailPage() {
</Box>
<Stack direction="row" spacing={1} useFlexGap flexWrap="wrap" alignItems="flex-start">
<StatusChip status={run.status} />
<Chip variant="outlined" label={`操作人 ${run.operator || '-'}`} />
<Chip variant="outlined" label={`成功阶段 ${summary.succeeded}`} />
<Chip variant="outlined" label={`失败阶段 ${summary.failed}`} />
<Chip variant="outlined" label={`已回滚 ${summary.rollbacked}`} />

View File

@ -73,6 +73,8 @@ export interface DeploymentTarget {
status: string
step: string
log_excerpt: string
started_at?: string
finished_at?: string
host?: Host
service_instance?: ServiceInstance
}
@ -87,6 +89,7 @@ export interface Deployment {
trigger_source: string
error_message: string
created_at?: string
started_at?: string
finished_at?: string
targets?: DeploymentTarget[]
}
@ -98,11 +101,11 @@ export interface ReleaseRunStep {
service_name: string
previous_release_id: string
status: string
deployment_id: number
deployment_id: number | null
error_message: string
log_excerpt: string
rollback_status: string
rollback_deployment_id: number
rollback_deployment_id: number | null
rollback_error_message: string
rollback_log_excerpt: string
created_at?: string