feat(server): 支持稀疏检出路径并完善部署执行队列
- 在部署DTO中添加sparseCheckoutPaths字段支持稀疏检出路径 - 数据模型Deployment新增稀疏检出路径字段及相关数据库映射 - 部署创建时支持设置稀疏检出路径字段 - 部署重试接口实现,支持复制原始部署记录并加入执行队列 - 新增流水线模板初始化与基于模板创建流水线接口 - 优化应用初始化流程,确保执行队列和流水线模板正确加载 - 添加启动日志,提示执行队列初始化完成
This commit is contained in:
@@ -108,6 +108,7 @@ function DeployModal({
|
||||
commitHash: selectedCommit.sha,
|
||||
commitMessage: selectedCommit.commit.message,
|
||||
env: env,
|
||||
sparseCheckoutPaths: values.sparseCheckoutPaths,
|
||||
});
|
||||
|
||||
Message.success('部署任务已创建');
|
||||
@@ -196,6 +197,17 @@ function DeployModal({
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="稀疏检出路径(用于monorepo项目,每行一个路径)"
|
||||
field="sparseCheckoutPaths"
|
||||
tooltip="在monorepo项目中,指定需要检出的目录路径,每行一个路径。留空则检出整个仓库。"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={`例如:\n/packages/frontend\n/packages/backend`}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="mb-2 font-medium text-gray-700">环境变量</div>
|
||||
<Form.List field="envVars">
|
||||
{(fields, { add, remove }) => (
|
||||
|
||||
@@ -34,6 +34,7 @@ function DeployRecordItem({
|
||||
const config = envMap[env] || { color: 'gray', text: env };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Menu,
|
||||
Message,
|
||||
Modal,
|
||||
Select,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
IconMore,
|
||||
IconPlayArrow,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import {
|
||||
@@ -37,7 +39,7 @@ import {
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
import type { Deployment, Pipeline, Project, Step } from '../types';
|
||||
@@ -55,8 +57,6 @@ interface PipelineWithEnabled extends Pipeline {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function ProjectDetailPage() {
|
||||
const [detail, setDetail] = useState<Project | null>();
|
||||
|
||||
@@ -82,7 +82,24 @@ function ProjectDetailPage() {
|
||||
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
||||
const [deployModalVisible, setDeployModalVisible] = useState(false);
|
||||
|
||||
// 流水线模板相关状态
|
||||
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
||||
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
||||
|
||||
const { id } = useParams();
|
||||
|
||||
// 获取可用的流水线模板
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
const templateData = await detailService.getPipelineTemplates();
|
||||
setTemplates(templateData);
|
||||
} catch (error) {
|
||||
console.error('获取流水线模板失败:', error);
|
||||
Message.error('获取流水线模板失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (id) {
|
||||
const project = await detailService.getProject(id);
|
||||
@@ -134,6 +151,28 @@ function ProjectDetailPage() {
|
||||
return record.buildLog.split('\n');
|
||||
};
|
||||
|
||||
// 定期轮询部署记录以更新状态和日志
|
||||
useAsyncEffect(async () => {
|
||||
const interval = setInterval(async () => {
|
||||
if (id) {
|
||||
try {
|
||||
const records = await detailService.getDeployments(Number(id));
|
||||
setDeployRecords(records);
|
||||
|
||||
// 如果当前选中的记录正在运行,则更新选中记录
|
||||
const selectedRecord = records.find((r: Deployment) => r.id === selectedRecordId);
|
||||
if (selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending')) {
|
||||
// 保持当前选中状态,但更新数据
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('轮询部署记录失败:', error);
|
||||
}
|
||||
}
|
||||
}, 3000); // 每3秒轮询一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [id, selectedRecordId]);
|
||||
|
||||
// 触发部署
|
||||
const handleDeploy = () => {
|
||||
setDeployModalVisible(true);
|
||||
@@ -144,6 +183,8 @@ function ProjectDetailPage() {
|
||||
setEditingPipeline(null);
|
||||
pipelineForm.resetFields();
|
||||
setPipelineModalVisible(true);
|
||||
setIsCreatingFromTemplate(false); // 默认不是从模板创建
|
||||
setSelectedTemplateId(null);
|
||||
};
|
||||
|
||||
// 编辑流水线
|
||||
@@ -295,6 +336,32 @@ function ProjectDetailPage() {
|
||||
),
|
||||
);
|
||||
Message.success('流水线更新成功');
|
||||
} else if (isCreatingFromTemplate && selectedTemplateId) {
|
||||
// 基于模板创建新流水线
|
||||
const newPipeline = await detailService.createPipelineFromTemplate(
|
||||
selectedTemplateId,
|
||||
Number(id),
|
||||
values.name,
|
||||
values.description || ''
|
||||
);
|
||||
|
||||
// 更新本地状态 - 需要转换步骤数据结构
|
||||
const transformedSteps = newPipeline.steps?.map(step => ({
|
||||
...step,
|
||||
enabled: step.valid === 1
|
||||
})) || [];
|
||||
|
||||
const pipelineWithDefaults = {
|
||||
...newPipeline,
|
||||
description: newPipeline.description || '',
|
||||
enabled: newPipeline.valid === 1,
|
||||
steps: transformedSteps,
|
||||
};
|
||||
|
||||
setPipelines((prev) => [...prev, pipelineWithDefaults]);
|
||||
// 自动选中新创建的流水线
|
||||
setSelectedPipelineId(newPipeline.id);
|
||||
Message.success('基于模板创建流水线成功');
|
||||
} else {
|
||||
// 创建新流水线
|
||||
const newPipeline = await detailService.createPipeline({
|
||||
@@ -316,6 +383,8 @@ function ProjectDetailPage() {
|
||||
Message.success('流水线创建成功');
|
||||
}
|
||||
setPipelineModalVisible(false);
|
||||
setIsCreatingFromTemplate(false);
|
||||
setSelectedTemplateId(null);
|
||||
} catch (error) {
|
||||
console.error('保存流水线失败:', error);
|
||||
Message.error('保存流水线失败');
|
||||
@@ -494,6 +563,23 @@ function ProjectDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 添加重新执行部署的函数
|
||||
const handleRetryDeployment = async (deploymentId: number) => {
|
||||
try {
|
||||
await detailService.retryDeployment(deploymentId);
|
||||
Message.success('重新执行任务已创建');
|
||||
|
||||
// 刷新部署记录
|
||||
if (id) {
|
||||
const records = await detailService.getDeployments(Number(id));
|
||||
setDeployRecords(records);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新执行部署失败:', error);
|
||||
Message.error('重新执行部署失败');
|
||||
}
|
||||
};
|
||||
|
||||
const selectedRecord = deployRecords.find(
|
||||
(record) => record.id === selectedRecordId,
|
||||
);
|
||||
@@ -512,17 +598,20 @@ function ProjectDetailPage() {
|
||||
};
|
||||
|
||||
// 渲染部署记录项
|
||||
const renderDeployRecordItem = (item: Deployment, _index: number) => {
|
||||
const isSelected = item.id === selectedRecordId;
|
||||
return (
|
||||
<DeployRecordItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={isSelected}
|
||||
onSelect={setSelectedRecordId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const renderDeployRecordItem = (item: Deployment) => (
|
||||
<DeployRecordItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={selectedRecordId === item.id}
|
||||
onSelect={setSelectedRecordId}
|
||||
onRetry={handleRetryDeployment} // 传递重新执行函数
|
||||
/>
|
||||
);
|
||||
|
||||
// 获取选中的流水线
|
||||
const selectedPipeline = pipelines.find(
|
||||
(pipeline) => pipeline.id === selectedPipelineId,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 flex flex-col h-full">
|
||||
@@ -541,7 +630,7 @@ function ProjectDetailPage() {
|
||||
<Tabs
|
||||
type="line"
|
||||
size="large"
|
||||
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-content_.arco-tabs-pane]:h-full"
|
||||
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
||||
>
|
||||
<Tabs.TabPane title="部署记录" key="deployRecords">
|
||||
<div className="grid grid-cols-5 gap-6 h-full">
|
||||
@@ -588,6 +677,16 @@ function ProjectDetailPage() {
|
||||
</div>
|
||||
{selectedRecord && (
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedRecord.status === 'failed' && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconRefresh />}
|
||||
size="small"
|
||||
onClick={() => handleRetryDeployment(selectedRecord.id)}
|
||||
>
|
||||
重新执行
|
||||
</Button>
|
||||
)}
|
||||
{renderStatusTag(selectedRecord.status)}
|
||||
</div>
|
||||
)}
|
||||
@@ -700,43 +799,36 @@ function ProjectDetailPage() {
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
position="bottom"
|
||||
position="br"
|
||||
trigger="click"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconMore />}
|
||||
className="text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md p-1 transition-all duration-200"
|
||||
<button
|
||||
className="p-1 hover:bg-gray-100 rounded cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<IconMore />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div>{pipeline.description}</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span>
|
||||
共 {pipeline.steps?.length || 0} 个步骤
|
||||
</span>
|
||||
<span>
|
||||
{new Date(
|
||||
pipeline.updatedAt,
|
||||
).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<Typography.Text type="secondary">
|
||||
{pipeline.description}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||
<span>
|
||||
{pipeline.steps?.length || 0} 个步骤
|
||||
</span>
|
||||
<span>{pipeline.updatedAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{pipelines.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<Empty description="暂无流水线" />
|
||||
<Typography.Text type="secondary">
|
||||
点击上方"新建流水线"按钮开始创建
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -768,7 +860,6 @@ function ProjectDetailPage() {
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconPlus />}
|
||||
size="small"
|
||||
onClick={() => handleAddStep(selectedPipelineId)}
|
||||
>
|
||||
@@ -776,21 +867,17 @@ function ProjectDetailPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 h-full overflow-y-auto">
|
||||
<div className="p-4 flex-1 overflow-hidden">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={
|
||||
selectedPipeline.steps?.map(
|
||||
(step) => step.id,
|
||||
) || []
|
||||
}
|
||||
items={selectedPipeline.steps?.map(step => step.id) || []}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
{selectedPipeline.steps?.map((step, index) => (
|
||||
<PipelineStepItem
|
||||
key={step.id}
|
||||
@@ -825,77 +912,147 @@ function ProjectDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 新建/编辑流水线模态框 */}
|
||||
<Modal
|
||||
title={editingPipeline ? '编辑流水线' : '新建流水线'}
|
||||
visible={pipelineModalVisible}
|
||||
onOk={handleSavePipeline}
|
||||
onCancel={() => setPipelineModalVisible(false)}
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<Form form={pipelineForm} layout="vertical">
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="流水线名称"
|
||||
rules={[{ required: true, message: '请输入流水线名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
rules={[{ required: true, message: '请输入流水线描述' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="描述这个流水线的用途和特点..."
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑步骤模态框 */}
|
||||
<Modal
|
||||
title={editingStep ? '编辑流水线步骤' : '添加流水线步骤'}
|
||||
visible={editModalVisible}
|
||||
onOk={handleSaveStep}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="步骤名称"
|
||||
rules={[{ required: true, message: '请输入步骤名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:安装依赖、运行测试、构建项目..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="script"
|
||||
label="Shell 脚本"
|
||||
rules={[{ required: true, message: '请输入脚本内容' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="例如:npm install npm test npm run build"
|
||||
rows={8}
|
||||
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="bg-blue-50 p-3 rounded text-sm">
|
||||
<Typography.Text type="secondary">
|
||||
<strong>可用环境变量:</strong>
|
||||
<br />• $PROJECT_NAME - 项目名称
|
||||
<br />• $BUILD_NUMBER - 构建编号
|
||||
<br />• $REGISTRY - 镜像仓库地址
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 新建/编辑流水线模态框 */}
|
||||
<Modal
|
||||
title={editingPipeline ? '编辑流水线' : '新建流水线'}
|
||||
visible={pipelineModalVisible}
|
||||
onOk={handleSavePipeline}
|
||||
onCancel={() => {
|
||||
setPipelineModalVisible(false);
|
||||
setIsCreatingFromTemplate(false);
|
||||
setSelectedTemplateId(null);
|
||||
}}
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<Form form={pipelineForm} layout="vertical">
|
||||
{!editingPipeline && templates.length > 0 && (
|
||||
<Form.Item label="创建方式">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type={isCreatingFromTemplate ? 'default' : 'primary'}
|
||||
onClick={() => setIsCreatingFromTemplate(false)}
|
||||
>
|
||||
自定义创建
|
||||
</Button>
|
||||
<Button
|
||||
type={isCreatingFromTemplate ? 'primary' : 'default'}
|
||||
onClick={() => setIsCreatingFromTemplate(true)}
|
||||
>
|
||||
使用模板创建
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{isCreatingFromTemplate && templates.length > 0 ? (
|
||||
<>
|
||||
<Form.Item
|
||||
field="templateId"
|
||||
label="选择模板"
|
||||
rules={[{ required: true, message: '请选择模板' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择流水线模板"
|
||||
onChange={(value) => setSelectedTemplateId(value)}
|
||||
value={selectedTemplateId ?? undefined}
|
||||
>
|
||||
{templates.map((template) => (
|
||||
<Select.Option key={template.id} value={template.id}>
|
||||
<div>
|
||||
<div>{template.name}</div>
|
||||
<div className="text-xs text-gray-500">{template.description}</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{selectedTemplateId && (
|
||||
<>
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="流水线名称"
|
||||
rules={[{ required: true, message: '请输入流水线名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="描述这个流水线的用途和特点..."
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="流水线名称"
|
||||
rules={[{ required: true, message: '请输入流水线名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="描述这个流水线的用途和特点..."
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑步骤模态框 */}
|
||||
<Modal
|
||||
title={editingStep ? '编辑流水线步骤' : '添加流水线步骤'}
|
||||
visible={editModalVisible}
|
||||
onOk={handleSaveStep}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="步骤名称"
|
||||
rules={[{ required: true, message: '请输入步骤名称' }]}
|
||||
>
|
||||
<Input placeholder="例如:安装依赖、运行测试、构建项目..." />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="script"
|
||||
label="Shell 脚本"
|
||||
rules={[{ required: true, message: '请输入脚本内容' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="例如:npm install npm test npm run build"
|
||||
rows={8}
|
||||
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="bg-blue-50 p-3 rounded text-sm">
|
||||
<Typography.Text type="secondary">
|
||||
<strong>可用环境变量:</strong>
|
||||
<br />• $PROJECT_NAME - 项目名称
|
||||
<br />• $BUILD_NUMBER - 构建编号
|
||||
<br />• $REGISTRY - 镜像仓库地址
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<DeployModal
|
||||
visible={deployModalVisible}
|
||||
onCancel={() => setDeployModalVisible(false)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type APIResponse, net } from '@shared';
|
||||
import type { Branch, Commit, Deployment, Pipeline, Project, Step } from '../types';
|
||||
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types';
|
||||
|
||||
class DetailService {
|
||||
async getProject(id: string) {
|
||||
@@ -17,6 +17,14 @@ class DetailService {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取可用的流水线模板
|
||||
async getPipelineTemplates() {
|
||||
const { data } = await net.request<APIResponse<{id: number, name: string, description: string}[]>>({
|
||||
url: '/api/pipelines/templates',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取项目的部署记录
|
||||
async getDeployments(projectId: number) {
|
||||
const { data } = await net.request<any>({
|
||||
@@ -46,6 +54,26 @@ class DetailService {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 基于模板创建流水线
|
||||
async createPipelineFromTemplate(
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
name: string,
|
||||
description?: string
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
url: '/api/pipelines/from-template',
|
||||
method: 'POST',
|
||||
data: {
|
||||
templateId,
|
||||
projectId,
|
||||
name,
|
||||
description
|
||||
},
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 更新流水线
|
||||
async updatePipeline(
|
||||
id: number,
|
||||
@@ -122,6 +150,7 @@ class DetailService {
|
||||
|
||||
// 删除步骤
|
||||
async deleteStep(id: number) {
|
||||
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
||||
const { data } = await net.request<APIResponse<null>>({
|
||||
url: `/api/steps/${id}`,
|
||||
method: 'DELETE',
|
||||
@@ -146,14 +175,7 @@ class DetailService {
|
||||
}
|
||||
|
||||
// 创建部署
|
||||
async createDeployment(deployment: {
|
||||
projectId: number;
|
||||
pipelineId: number;
|
||||
branch: string;
|
||||
commitHash: string;
|
||||
commitMessage: string;
|
||||
env?: string;
|
||||
}) {
|
||||
async createDeployment(deployment: CreateDeploymentRequest) {
|
||||
const { data } = await net.request<APIResponse<Deployment>>({
|
||||
url: '/api/deployments',
|
||||
method: 'POST',
|
||||
@@ -161,6 +183,15 @@ class DetailService {
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 重新执行部署
|
||||
async retryDeployment(deploymentId: number) {
|
||||
const { data } = await net.request<APIResponse<Deployment>>({
|
||||
url: `/api/deployments/${deploymentId}/retry`,
|
||||
method: 'POST',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export const detailService = new DetailService();
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface Deployment {
|
||||
commitHash?: string;
|
||||
commitMessage?: string;
|
||||
buildLog?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
valid: number;
|
||||
@@ -90,3 +91,14 @@ export interface Branch {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// 创建部署请求的类型定义
|
||||
export interface CreateDeploymentRequest {
|
||||
projectId: number;
|
||||
pipelineId: number;
|
||||
branch: string;
|
||||
commitHash: string;
|
||||
commitMessage: string;
|
||||
env?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
}
|
||||
|
||||
@@ -19,6 +19,16 @@ class Net {
|
||||
},
|
||||
(error) => {
|
||||
console.log('error', error);
|
||||
// 对于DELETE请求返回204状态码的情况,视为成功
|
||||
if (error.response && error.response.status === 204 && error.config.method === 'delete') {
|
||||
// 创建一个模拟的成功响应
|
||||
return Promise.resolve({
|
||||
...error.response,
|
||||
data: error.response.data || null,
|
||||
status: 200, // 将204转换为200,避免被当作错误处理
|
||||
});
|
||||
}
|
||||
|
||||
if (error.status === 401 && error.config.url !== '/api/auth/info') {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user