feat(server): 支持稀疏检出路径并完善部署执行队列
- 在部署DTO中添加sparseCheckoutPaths字段支持稀疏检出路径 - 数据模型Deployment新增稀疏检出路径字段及相关数据库映射 - 部署创建时支持设置稀疏检出路径字段 - 部署重试接口实现,支持复制原始部署记录并加入执行队列 - 新增流水线模板初始化与基于模板创建流水线接口 - 优化应用初始化流程,确保执行队列和流水线模板正确加载 - 添加启动日志,提示执行队列初始化完成
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
import Koa from 'koa';
|
||||
import { initMiddlewares } from './middlewares/index.ts';
|
||||
import { log } from './libs/logger.ts';
|
||||
import { ExecutionQueue } from './libs/execution-queue.ts';
|
||||
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
||||
|
||||
// 初始化应用
|
||||
async function initializeApp() {
|
||||
// 初始化流水线模板
|
||||
await initializePipelineTemplates();
|
||||
|
||||
// 初始化执行队列
|
||||
const executionQueue = ExecutionQueue.getInstance();
|
||||
await executionQueue.initialize();
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
@@ -10,4 +21,12 @@ const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
log.info('APP', 'Server started at port %d', PORT);
|
||||
log.info('QUEUE', 'Execution queue initialized');
|
||||
});
|
||||
}
|
||||
|
||||
// 启动应用
|
||||
initializeApp().catch(error => {
|
||||
console.error('Failed to start application:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ export const createDeploymentSchema = z.object({
|
||||
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
||||
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
||||
env: z.string().optional(),
|
||||
sparseCheckoutPaths: z.string().optional(), // 添加稀疏检出路径字段
|
||||
});
|
||||
|
||||
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Prisma } from '../../generated/client.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import type { Context } from 'koa';
|
||||
import { listDeploymentsQuerySchema, createDeploymentSchema } from './dto.ts';
|
||||
import { ExecutionQueue } from '../../libs/execution-queue.ts';
|
||||
|
||||
@Controller('/deployments')
|
||||
export class DeploymentController {
|
||||
@@ -50,12 +51,69 @@ export class DeploymentController {
|
||||
},
|
||||
pipelineId: body.pipelineId,
|
||||
env: body.env || 'dev',
|
||||
sparseCheckoutPaths: body.sparseCheckoutPaths || '', // 添加稀疏检出路径
|
||||
buildLog: '',
|
||||
createdBy: 'system', // TODO: get from user
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 将新创建的部署任务添加到执行队列
|
||||
const executionQueue = ExecutionQueue.getInstance();
|
||||
await executionQueue.addTask(result.id, result.pipelineId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 添加重新执行部署的接口
|
||||
@Post('/:id/retry')
|
||||
async retry(ctx: Context) {
|
||||
const { id } = ctx.params;
|
||||
|
||||
// 获取原始部署记录
|
||||
const originalDeployment = await prisma.deployment.findUnique({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
if (!originalDeployment) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
code: 404,
|
||||
message: '部署记录不存在',
|
||||
data: null,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个新的部署记录,复制原始记录的信息
|
||||
const newDeployment = await prisma.deployment.create({
|
||||
data: {
|
||||
branch: originalDeployment.branch,
|
||||
commitHash: originalDeployment.commitHash,
|
||||
commitMessage: originalDeployment.commitMessage,
|
||||
status: 'pending',
|
||||
projectId: originalDeployment.projectId,
|
||||
pipelineId: originalDeployment.pipelineId,
|
||||
env: originalDeployment.env,
|
||||
sparseCheckoutPaths: originalDeployment.sparseCheckoutPaths,
|
||||
buildLog: '',
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 将新创建的部署任务添加到执行队列
|
||||
const executionQueue = ExecutionQueue.getInstance();
|
||||
await executionQueue.addTask(newDeployment.id, newDeployment.pipelineId);
|
||||
|
||||
ctx.body = {
|
||||
code: 0,
|
||||
message: '重新执行任务已创建',
|
||||
data: newDeployment,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { log } from '../../libs/logger.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { getAvailableTemplates, createPipelineFromTemplate } from '../../libs/pipeline-template.ts';
|
||||
import {
|
||||
createPipelineSchema,
|
||||
updatePipelineSchema,
|
||||
@@ -43,6 +44,18 @@ export class PipelineController {
|
||||
return pipelines;
|
||||
}
|
||||
|
||||
// GET /api/pipelines/templates - 获取可用的流水线模板
|
||||
@Get('/templates')
|
||||
async getTemplates(ctx: Context) {
|
||||
try {
|
||||
const templates = await getAvailableTemplates();
|
||||
return templates;
|
||||
} catch (error) {
|
||||
console.error('Failed to get templates:', error);
|
||||
throw new BusinessError('获取模板失败', 3002, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/pipelines/:id - 获取单个流水线
|
||||
@Get('/:id')
|
||||
async get(ctx: Context) {
|
||||
@@ -92,6 +105,60 @@ export class PipelineController {
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
// POST /api/pipelines/from-template - 基于模板创建流水线
|
||||
@Post('/from-template')
|
||||
async createFromTemplate(ctx: Context) {
|
||||
try {
|
||||
const { templateId, projectId, name, description } = ctx.request.body as {
|
||||
templateId: number;
|
||||
projectId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// 验证必要参数
|
||||
if (!templateId || !projectId || !name) {
|
||||
throw new BusinessError('缺少必要参数', 3003, 400);
|
||||
}
|
||||
|
||||
// 基于模板创建流水线
|
||||
const newPipelineId = await createPipelineFromTemplate(
|
||||
templateId,
|
||||
projectId,
|
||||
name,
|
||||
description || ''
|
||||
);
|
||||
|
||||
// 返回新创建的流水线
|
||||
const pipeline = await prisma.pipeline.findUnique({
|
||||
where: { id: newPipelineId },
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!pipeline) {
|
||||
throw new BusinessError('创建流水线失败', 3004, 500);
|
||||
}
|
||||
|
||||
log.info('pipeline', 'Created pipeline from template: %s', pipeline.name);
|
||||
return pipeline;
|
||||
} catch (error) {
|
||||
console.error('Failed to create pipeline from template:', error);
|
||||
if (error instanceof BusinessError) {
|
||||
throw error;
|
||||
}
|
||||
throw new BusinessError('基于模板创建流水线失败', 3005, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/pipelines/:id - 更新流水线
|
||||
@Put('/:id')
|
||||
async update(ctx: Context) {
|
||||
|
||||
@@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
|
||||
"clientVersion": "7.0.0",
|
||||
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513",
|
||||
"activeProvider": "sqlite",
|
||||
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n // Relations\n deployments Deployment[]\n pipelines Pipeline[]\n\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n username String\n login String\n email String\n avatar_url String?\n active Boolean @default(true)\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String @default(\"system\")\n updatedBy String @default(\"system\")\n}\n\nmodel Pipeline {\n id Int @id @default(autoincrement())\n name String\n description String?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n // Relations\n projectId Int?\n Project Project? @relation(fields: [projectId], references: [id])\n steps Step[]\n}\n\nmodel Step {\n id Int @id @default(autoincrement())\n name String\n order Int\n script String // 执行的脚本命令\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n pipelineId Int\n pipeline Pipeline @relation(fields: [pipelineId], references: [id])\n}\n\nmodel Deployment {\n id Int @id @default(autoincrement())\n branch String\n env String?\n status String // pending, running, success, failed, cancelled\n commitHash String?\n commitMessage String?\n buildLog String?\n startedAt DateTime @default(now())\n finishedAt DateTime?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n projectId Int\n Project Project? @relation(fields: [projectId], references: [id])\n pipelineId Int\n}\n",
|
||||
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n // Relations\n deployments Deployment[]\n pipelines Pipeline[]\n\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n username String\n login String\n email String\n avatar_url String?\n active Boolean @default(true)\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String @default(\"system\")\n updatedBy String @default(\"system\")\n}\n\nmodel Pipeline {\n id Int @id @default(autoincrement())\n name String\n description String?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n // Relations\n projectId Int?\n Project Project? @relation(fields: [projectId], references: [id])\n steps Step[]\n}\n\nmodel Step {\n id Int @id @default(autoincrement())\n name String\n order Int\n script String // 执行的脚本命令\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n pipelineId Int\n pipeline Pipeline @relation(fields: [pipelineId], references: [id])\n}\n\nmodel Deployment {\n id Int @id @default(autoincrement())\n branch String\n env String?\n status String // pending, running, success, failed, cancelled\n commitHash String?\n commitMessage String?\n buildLog String?\n sparseCheckoutPaths String? // 稀疏检出路径,用于monorepo项目\n startedAt DateTime @default(now())\n finishedAt DateTime?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n projectId Int\n Project Project? @relation(fields: [projectId], references: [id])\n pipelineId Int\n}\n",
|
||||
"runtimeDataModel": {
|
||||
"models": {},
|
||||
"enums": {},
|
||||
@@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"Project\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"repository\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deployments\",\"kind\":\"object\",\"type\":\"Deployment\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelines\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToProject\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"avatar_url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Pipeline\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"PipelineToProject\"},{\"name\":\"steps\",\"kind\":\"object\",\"type\":\"Step\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Step\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"order\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"script\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"pipeline\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Deployment\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"branch\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"env\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitMessage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"buildLog\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"startedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"finishedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"Project\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"repository\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deployments\",\"kind\":\"object\",\"type\":\"Deployment\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelines\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToProject\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"avatar_url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Pipeline\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"PipelineToProject\"},{\"name\":\"steps\",\"kind\":\"object\",\"type\":\"Step\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Step\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"order\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"script\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"pipeline\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Deployment\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"branch\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"env\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitMessage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"buildLog\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sparseCheckoutPaths\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"startedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"finishedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||
|
||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
|
||||
@@ -885,6 +885,7 @@ export const DeploymentScalarFieldEnum = {
|
||||
commitHash: 'commitHash',
|
||||
commitMessage: 'commitMessage',
|
||||
buildLog: 'buildLog',
|
||||
sparseCheckoutPaths: 'sparseCheckoutPaths',
|
||||
startedAt: 'startedAt',
|
||||
finishedAt: 'finishedAt',
|
||||
valid: 'valid',
|
||||
|
||||
@@ -142,6 +142,7 @@ export const DeploymentScalarFieldEnum = {
|
||||
commitHash: 'commitHash',
|
||||
commitMessage: 'commitMessage',
|
||||
buildLog: 'buildLog',
|
||||
sparseCheckoutPaths: 'sparseCheckoutPaths',
|
||||
startedAt: 'startedAt',
|
||||
finishedAt: 'finishedAt',
|
||||
valid: 'valid',
|
||||
|
||||
@@ -48,6 +48,7 @@ export type DeploymentMinAggregateOutputType = {
|
||||
commitHash: string | null
|
||||
commitMessage: string | null
|
||||
buildLog: string | null
|
||||
sparseCheckoutPaths: string | null
|
||||
startedAt: Date | null
|
||||
finishedAt: Date | null
|
||||
valid: number | null
|
||||
@@ -67,6 +68,7 @@ export type DeploymentMaxAggregateOutputType = {
|
||||
commitHash: string | null
|
||||
commitMessage: string | null
|
||||
buildLog: string | null
|
||||
sparseCheckoutPaths: string | null
|
||||
startedAt: Date | null
|
||||
finishedAt: Date | null
|
||||
valid: number | null
|
||||
@@ -86,6 +88,7 @@ export type DeploymentCountAggregateOutputType = {
|
||||
commitHash: number
|
||||
commitMessage: number
|
||||
buildLog: number
|
||||
sparseCheckoutPaths: number
|
||||
startedAt: number
|
||||
finishedAt: number
|
||||
valid: number
|
||||
@@ -121,6 +124,7 @@ export type DeploymentMinAggregateInputType = {
|
||||
commitHash?: true
|
||||
commitMessage?: true
|
||||
buildLog?: true
|
||||
sparseCheckoutPaths?: true
|
||||
startedAt?: true
|
||||
finishedAt?: true
|
||||
valid?: true
|
||||
@@ -140,6 +144,7 @@ export type DeploymentMaxAggregateInputType = {
|
||||
commitHash?: true
|
||||
commitMessage?: true
|
||||
buildLog?: true
|
||||
sparseCheckoutPaths?: true
|
||||
startedAt?: true
|
||||
finishedAt?: true
|
||||
valid?: true
|
||||
@@ -159,6 +164,7 @@ export type DeploymentCountAggregateInputType = {
|
||||
commitHash?: true
|
||||
commitMessage?: true
|
||||
buildLog?: true
|
||||
sparseCheckoutPaths?: true
|
||||
startedAt?: true
|
||||
finishedAt?: true
|
||||
valid?: true
|
||||
@@ -265,6 +271,7 @@ export type DeploymentGroupByOutputType = {
|
||||
commitHash: string | null
|
||||
commitMessage: string | null
|
||||
buildLog: string | null
|
||||
sparseCheckoutPaths: string | null
|
||||
startedAt: Date
|
||||
finishedAt: Date | null
|
||||
valid: number
|
||||
@@ -307,6 +314,7 @@ export type DeploymentWhereInput = {
|
||||
commitHash?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
commitMessage?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
buildLog?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
sparseCheckoutPaths?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
startedAt?: Prisma.DateTimeFilter<"Deployment"> | Date | string
|
||||
finishedAt?: Prisma.DateTimeNullableFilter<"Deployment"> | Date | string | null
|
||||
valid?: Prisma.IntFilter<"Deployment"> | number
|
||||
@@ -327,6 +335,7 @@ export type DeploymentOrderByWithRelationInput = {
|
||||
commitHash?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
commitMessage?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
buildLog?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
sparseCheckoutPaths?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
startedAt?: Prisma.SortOrder
|
||||
finishedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
valid?: Prisma.SortOrder
|
||||
@@ -350,6 +359,7 @@ export type DeploymentWhereUniqueInput = Prisma.AtLeast<{
|
||||
commitHash?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
commitMessage?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
buildLog?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
sparseCheckoutPaths?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
startedAt?: Prisma.DateTimeFilter<"Deployment"> | Date | string
|
||||
finishedAt?: Prisma.DateTimeNullableFilter<"Deployment"> | Date | string | null
|
||||
valid?: Prisma.IntFilter<"Deployment"> | number
|
||||
@@ -370,6 +380,7 @@ export type DeploymentOrderByWithAggregationInput = {
|
||||
commitHash?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
commitMessage?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
buildLog?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
sparseCheckoutPaths?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
startedAt?: Prisma.SortOrder
|
||||
finishedAt?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
valid?: Prisma.SortOrder
|
||||
@@ -397,6 +408,7 @@ export type DeploymentScalarWhereWithAggregatesInput = {
|
||||
commitHash?: Prisma.StringNullableWithAggregatesFilter<"Deployment"> | string | null
|
||||
commitMessage?: Prisma.StringNullableWithAggregatesFilter<"Deployment"> | string | null
|
||||
buildLog?: Prisma.StringNullableWithAggregatesFilter<"Deployment"> | string | null
|
||||
sparseCheckoutPaths?: Prisma.StringNullableWithAggregatesFilter<"Deployment"> | string | null
|
||||
startedAt?: Prisma.DateTimeWithAggregatesFilter<"Deployment"> | Date | string
|
||||
finishedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"Deployment"> | Date | string | null
|
||||
valid?: Prisma.IntWithAggregatesFilter<"Deployment"> | number
|
||||
@@ -415,6 +427,7 @@ export type DeploymentCreateInput = {
|
||||
commitHash?: string | null
|
||||
commitMessage?: string | null
|
||||
buildLog?: string | null
|
||||
sparseCheckoutPaths?: string | null
|
||||
startedAt?: Date | string
|
||||
finishedAt?: Date | string | null
|
||||
valid?: number
|
||||
@@ -434,6 +447,7 @@ export type DeploymentUncheckedCreateInput = {
|
||||
commitHash?: string | null
|
||||
commitMessage?: string | null
|
||||
buildLog?: string | null
|
||||
sparseCheckoutPaths?: string | null
|
||||
startedAt?: Date | string
|
||||
finishedAt?: Date | string | null
|
||||
valid?: number
|
||||
@@ -452,6 +466,7 @@ export type DeploymentUpdateInput = {
|
||||
commitHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
commitMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
buildLog?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sparseCheckoutPaths?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
startedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
finishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
@@ -471,6 +486,7 @@ export type DeploymentUncheckedUpdateInput = {
|
||||
commitHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
commitMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
buildLog?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sparseCheckoutPaths?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
startedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
finishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
@@ -490,6 +506,7 @@ export type DeploymentCreateManyInput = {
|
||||
commitHash?: string | null
|
||||
commitMessage?: string | null
|
||||
buildLog?: string | null
|
||||
sparseCheckoutPaths?: string | null
|
||||
startedAt?: Date | string
|
||||
finishedAt?: Date | string | null
|
||||
valid?: number
|
||||
@@ -508,6 +525,7 @@ export type DeploymentUpdateManyMutationInput = {
|
||||
commitHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
commitMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
buildLog?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sparseCheckoutPaths?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
startedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
finishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
@@ -526,6 +544,7 @@ export type DeploymentUncheckedUpdateManyInput = {
|
||||
commitHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
commitMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
buildLog?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sparseCheckoutPaths?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
startedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
finishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
@@ -555,6 +574,7 @@ export type DeploymentCountOrderByAggregateInput = {
|
||||
commitHash?: Prisma.SortOrder
|
||||
commitMessage?: Prisma.SortOrder
|
||||
buildLog?: Prisma.SortOrder
|
||||
sparseCheckoutPaths?: Prisma.SortOrder
|
||||
startedAt?: Prisma.SortOrder
|
||||
finishedAt?: Prisma.SortOrder
|
||||
valid?: Prisma.SortOrder
|
||||
@@ -581,6 +601,7 @@ export type DeploymentMaxOrderByAggregateInput = {
|
||||
commitHash?: Prisma.SortOrder
|
||||
commitMessage?: Prisma.SortOrder
|
||||
buildLog?: Prisma.SortOrder
|
||||
sparseCheckoutPaths?: Prisma.SortOrder
|
||||
startedAt?: Prisma.SortOrder
|
||||
finishedAt?: Prisma.SortOrder
|
||||
valid?: Prisma.SortOrder
|
||||
@@ -600,6 +621,7 @@ export type DeploymentMinOrderByAggregateInput = {
|
||||
commitHash?: Prisma.SortOrder
|
||||
commitMessage?: Prisma.SortOrder
|
||||
buildLog?: Prisma.SortOrder
|
||||
sparseCheckoutPaths?: Prisma.SortOrder
|
||||
startedAt?: Prisma.SortOrder
|
||||
finishedAt?: Prisma.SortOrder
|
||||
valid?: Prisma.SortOrder
|
||||
@@ -671,6 +693,7 @@ export type DeploymentCreateWithoutProjectInput = {
|
||||
commitHash?: string | null
|
||||
commitMessage?: string | null
|
||||
buildLog?: string | null
|
||||
sparseCheckoutPaths?: string | null
|
||||
startedAt?: Date | string
|
||||
finishedAt?: Date | string | null
|
||||
valid?: number
|
||||
@@ -689,6 +712,7 @@ export type DeploymentUncheckedCreateWithoutProjectInput = {
|
||||
commitHash?: string | null
|
||||
commitMessage?: string | null
|
||||
buildLog?: string | null
|
||||
sparseCheckoutPaths?: string | null
|
||||
startedAt?: Date | string
|
||||
finishedAt?: Date | string | null
|
||||
valid?: number
|
||||
@@ -735,6 +759,7 @@ export type DeploymentScalarWhereInput = {
|
||||
commitHash?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
commitMessage?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
buildLog?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
sparseCheckoutPaths?: Prisma.StringNullableFilter<"Deployment"> | string | null
|
||||
startedAt?: Prisma.DateTimeFilter<"Deployment"> | Date | string
|
||||
finishedAt?: Prisma.DateTimeNullableFilter<"Deployment"> | Date | string | null
|
||||
valid?: Prisma.IntFilter<"Deployment"> | number
|
||||
@@ -754,6 +779,7 @@ export type DeploymentCreateManyProjectInput = {
|
||||
commitHash?: string | null
|
||||
commitMessage?: string | null
|
||||
buildLog?: string | null
|
||||
sparseCheckoutPaths?: string | null
|
||||
startedAt?: Date | string
|
||||
finishedAt?: Date | string | null
|
||||
valid?: number
|
||||
@@ -771,6 +797,7 @@ export type DeploymentUpdateWithoutProjectInput = {
|
||||
commitHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
commitMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
buildLog?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sparseCheckoutPaths?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
startedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
finishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
@@ -789,6 +816,7 @@ export type DeploymentUncheckedUpdateWithoutProjectInput = {
|
||||
commitHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
commitMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
buildLog?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sparseCheckoutPaths?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
startedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
finishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
@@ -807,6 +835,7 @@ export type DeploymentUncheckedUpdateManyWithoutProjectInput = {
|
||||
commitHash?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
commitMessage?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
buildLog?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
sparseCheckoutPaths?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
startedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
finishedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
@@ -827,6 +856,7 @@ export type DeploymentSelect<ExtArgs extends runtime.Types.Extensions.InternalAr
|
||||
commitHash?: boolean
|
||||
commitMessage?: boolean
|
||||
buildLog?: boolean
|
||||
sparseCheckoutPaths?: boolean
|
||||
startedAt?: boolean
|
||||
finishedAt?: boolean
|
||||
valid?: boolean
|
||||
@@ -847,6 +877,7 @@ export type DeploymentSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Ex
|
||||
commitHash?: boolean
|
||||
commitMessage?: boolean
|
||||
buildLog?: boolean
|
||||
sparseCheckoutPaths?: boolean
|
||||
startedAt?: boolean
|
||||
finishedAt?: boolean
|
||||
valid?: boolean
|
||||
@@ -867,6 +898,7 @@ export type DeploymentSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Ex
|
||||
commitHash?: boolean
|
||||
commitMessage?: boolean
|
||||
buildLog?: boolean
|
||||
sparseCheckoutPaths?: boolean
|
||||
startedAt?: boolean
|
||||
finishedAt?: boolean
|
||||
valid?: boolean
|
||||
@@ -887,6 +919,7 @@ export type DeploymentSelectScalar = {
|
||||
commitHash?: boolean
|
||||
commitMessage?: boolean
|
||||
buildLog?: boolean
|
||||
sparseCheckoutPaths?: boolean
|
||||
startedAt?: boolean
|
||||
finishedAt?: boolean
|
||||
valid?: boolean
|
||||
@@ -898,7 +931,7 @@ export type DeploymentSelectScalar = {
|
||||
pipelineId?: boolean
|
||||
}
|
||||
|
||||
export type DeploymentOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "branch" | "env" | "status" | "commitHash" | "commitMessage" | "buildLog" | "startedAt" | "finishedAt" | "valid" | "createdAt" | "updatedAt" | "createdBy" | "updatedBy" | "projectId" | "pipelineId", ExtArgs["result"]["deployment"]>
|
||||
export type DeploymentOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "branch" | "env" | "status" | "commitHash" | "commitMessage" | "buildLog" | "sparseCheckoutPaths" | "startedAt" | "finishedAt" | "valid" | "createdAt" | "updatedAt" | "createdBy" | "updatedBy" | "projectId" | "pipelineId", ExtArgs["result"]["deployment"]>
|
||||
export type DeploymentInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
Project?: boolean | Prisma.Deployment$ProjectArgs<ExtArgs>
|
||||
}
|
||||
@@ -922,6 +955,7 @@ export type $DeploymentPayload<ExtArgs extends runtime.Types.Extensions.Internal
|
||||
commitHash: string | null
|
||||
commitMessage: string | null
|
||||
buildLog: string | null
|
||||
sparseCheckoutPaths: string | null
|
||||
startedAt: Date
|
||||
finishedAt: Date | null
|
||||
valid: number
|
||||
@@ -1362,6 +1396,7 @@ export interface DeploymentFieldRefs {
|
||||
readonly commitHash: Prisma.FieldRef<"Deployment", 'String'>
|
||||
readonly commitMessage: Prisma.FieldRef<"Deployment", 'String'>
|
||||
readonly buildLog: Prisma.FieldRef<"Deployment", 'String'>
|
||||
readonly sparseCheckoutPaths: Prisma.FieldRef<"Deployment", 'String'>
|
||||
readonly startedAt: Prisma.FieldRef<"Deployment", 'DateTime'>
|
||||
readonly finishedAt: Prisma.FieldRef<"Deployment", 'DateTime'>
|
||||
readonly valid: Prisma.FieldRef<"Deployment", 'Int'>
|
||||
|
||||
233
apps/server/libs/execution-queue.ts
Normal file
233
apps/server/libs/execution-queue.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { PipelineRunner } from '../runners/index.ts';
|
||||
import { prisma } from './prisma.ts';
|
||||
|
||||
// 存储正在运行的部署任务
|
||||
const runningDeployments = new Set<number>();
|
||||
|
||||
// 存储待执行的任务队列
|
||||
const pendingQueue: Array<{
|
||||
deploymentId: number;
|
||||
pipelineId: number;
|
||||
}> = [];
|
||||
|
||||
// 定时器ID
|
||||
let pollingTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
// 轮询间隔(毫秒)
|
||||
const POLLING_INTERVAL = 30000; // 30秒
|
||||
|
||||
/**
|
||||
* 执行队列管理器
|
||||
*/
|
||||
export class ExecutionQueue {
|
||||
private static instance: ExecutionQueue;
|
||||
private isProcessing = false;
|
||||
private isPolling = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 获取执行队列的单例实例
|
||||
*/
|
||||
public static getInstance(): ExecutionQueue {
|
||||
if (!ExecutionQueue.instance) {
|
||||
ExecutionQueue.instance = new ExecutionQueue();
|
||||
}
|
||||
return ExecutionQueue.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化执行队列,包括恢复未完成的任务
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
console.log('Initializing execution queue...');
|
||||
// 恢复未完成的任务
|
||||
await this.recoverPendingDeployments();
|
||||
|
||||
// 启动定时轮询
|
||||
this.startPolling();
|
||||
|
||||
console.log('Execution queue initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库中恢复未完成的部署任务
|
||||
*/
|
||||
private async recoverPendingDeployments(): Promise<void> {
|
||||
try {
|
||||
console.log('Recovering pending deployments from database...');
|
||||
|
||||
// 查询数据库中状态为pending的部署任务
|
||||
const pendingDeployments = await prisma.deployment.findMany({
|
||||
where: {
|
||||
status: 'pending',
|
||||
valid: 1
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
pipelineId: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Found ${pendingDeployments.length} pending deployments`);
|
||||
|
||||
// 将这些任务添加到执行队列中
|
||||
for (const deployment of pendingDeployments) {
|
||||
await this.addTask(deployment.id, deployment.pipelineId);
|
||||
}
|
||||
|
||||
console.log('Pending deployments recovery completed');
|
||||
} catch (error) {
|
||||
console.error('Failed to recover pending deployments:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时轮询机制
|
||||
*/
|
||||
private startPolling(): void {
|
||||
if (this.isPolling) {
|
||||
console.log('Polling is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPolling = true;
|
||||
console.log(`Starting polling with interval ${POLLING_INTERVAL}ms`);
|
||||
|
||||
// 立即执行一次检查
|
||||
this.checkPendingDeployments();
|
||||
|
||||
// 设置定时器定期检查
|
||||
pollingTimer = setInterval(() => {
|
||||
this.checkPendingDeployments();
|
||||
}, POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时轮询机制
|
||||
*/
|
||||
public stopPolling(): void {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
pollingTimer = null;
|
||||
this.isPolling = false;
|
||||
console.log('Polling stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库中的待处理部署任务
|
||||
*/
|
||||
private async checkPendingDeployments(): Promise<void> {
|
||||
try {
|
||||
console.log('Checking for pending deployments in database...');
|
||||
|
||||
// 查询数据库中状态为pending的部署任务
|
||||
const pendingDeployments = await prisma.deployment.findMany({
|
||||
where: {
|
||||
status: 'pending',
|
||||
valid: 1
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
pipelineId: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Found ${pendingDeployments.length} pending deployments in polling`);
|
||||
|
||||
// 检查这些任务是否已经在队列中,如果没有则添加
|
||||
for (const deployment of pendingDeployments) {
|
||||
// 检查是否已经在运行队列中
|
||||
if (!runningDeployments.has(deployment.id)) {
|
||||
console.log(`Adding deployment ${deployment.id} to queue from polling`);
|
||||
await this.addTask(deployment.id, deployment.pipelineId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check pending deployments:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将部署任务添加到执行队列
|
||||
* @param deploymentId 部署ID
|
||||
* @param pipelineId 流水线ID
|
||||
*/
|
||||
public async addTask(deploymentId: number, pipelineId: number): Promise<void> {
|
||||
// 检查是否已经在运行队列中
|
||||
if (runningDeployments.has(deploymentId)) {
|
||||
console.log(`Deployment ${deploymentId} is already queued or running`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到运行队列
|
||||
runningDeployments.add(deploymentId);
|
||||
|
||||
// 添加到待执行队列
|
||||
pendingQueue.push({ deploymentId, pipelineId });
|
||||
|
||||
// 开始处理队列(如果尚未开始)
|
||||
if (!this.isProcessing) {
|
||||
this.processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理执行队列中的任务
|
||||
*/
|
||||
private async processQueue(): Promise<void> {
|
||||
this.isProcessing = true;
|
||||
|
||||
while (pendingQueue.length > 0) {
|
||||
const task = pendingQueue.shift();
|
||||
|
||||
if (task) {
|
||||
try {
|
||||
// 执行流水线
|
||||
await this.executePipeline(task.deploymentId, task.pipelineId);
|
||||
} catch (error) {
|
||||
console.error('执行流水线失败:', error);
|
||||
// 这里可以添加更多的错误处理逻辑
|
||||
} finally {
|
||||
// 从运行队列中移除
|
||||
runningDeployments.delete(task.deploymentId);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加一个小延迟以避免过度占用资源
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流水线
|
||||
* @param deploymentId 部署ID
|
||||
* @param pipelineId 流水线ID
|
||||
*/
|
||||
private async executePipeline(deploymentId: number, pipelineId: number): Promise<void> {
|
||||
try {
|
||||
const runner = new PipelineRunner(deploymentId);
|
||||
await runner.run(pipelineId);
|
||||
} catch (error) {
|
||||
console.error('执行流水线失败:', error);
|
||||
// 错误处理可以在这里添加,比如更新部署状态为失败
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列状态
|
||||
*/
|
||||
public getQueueStatus(): {
|
||||
pendingCount: number;
|
||||
runningCount: number;
|
||||
} {
|
||||
return {
|
||||
pendingCount: pendingQueue.length,
|
||||
runningCount: runningDeployments.size
|
||||
};
|
||||
}
|
||||
}
|
||||
247
apps/server/libs/pipeline-template.ts
Normal file
247
apps/server/libs/pipeline-template.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { prisma } from './prisma.ts';
|
||||
|
||||
// 默认流水线模板
|
||||
export interface PipelineTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
steps: Array<{
|
||||
name: string;
|
||||
order: number;
|
||||
script: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 系统默认的流水线模板
|
||||
export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
||||
{
|
||||
name: 'Git Clone Pipeline',
|
||||
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
||||
steps: [
|
||||
{
|
||||
name: 'Clone Repository',
|
||||
order: 0,
|
||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
||||
},
|
||||
{
|
||||
name: 'Install Dependencies',
|
||||
order: 1,
|
||||
script: '# 安装项目依赖\nnpm install',
|
||||
},
|
||||
{
|
||||
name: 'Run Tests',
|
||||
order: 2,
|
||||
script: '# 运行测试\nnpm test',
|
||||
},
|
||||
{
|
||||
name: 'Build Project',
|
||||
order: 3,
|
||||
script: '# 构建项目\nnpm run build',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Sparse Checkout Pipeline',
|
||||
description: '稀疏检出流水线,适用于monorepo项目,只获取指定目录的代码',
|
||||
steps: [
|
||||
{
|
||||
name: 'Sparse Checkout Repository',
|
||||
order: 0,
|
||||
script: '# 进行稀疏检出指定目录的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit config core.sparseCheckout true\necho "$SPARSE_CHECKOUT_PATHS" > .git/info/sparse-checkout\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
||||
},
|
||||
{
|
||||
name: 'Install Dependencies',
|
||||
order: 1,
|
||||
script: '# 安装项目依赖\nnpm install',
|
||||
},
|
||||
{
|
||||
name: 'Run Tests',
|
||||
order: 2,
|
||||
script: '# 运行测试\nnpm test',
|
||||
},
|
||||
{
|
||||
name: 'Build Project',
|
||||
order: 3,
|
||||
script: '# 构建项目\nnpm run build',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Simple Deploy Pipeline',
|
||||
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
||||
steps: [
|
||||
{
|
||||
name: 'Clone Repository',
|
||||
order: 0,
|
||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD',
|
||||
},
|
||||
{
|
||||
name: 'Build and Deploy',
|
||||
order: 1,
|
||||
script: '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 初始化系统默认流水线模板
|
||||
*/
|
||||
export async function initializePipelineTemplates(): Promise<void> {
|
||||
console.log('Initializing pipeline templates...');
|
||||
|
||||
try {
|
||||
// 检查是否已经存在模板流水线
|
||||
const existingTemplates = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
name: {
|
||||
in: DEFAULT_PIPELINE_TEMPLATES.map(template => template.name)
|
||||
},
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有现有的模板,则创建默认模板
|
||||
if (existingTemplates.length === 0) {
|
||||
console.log('Creating default pipeline templates...');
|
||||
|
||||
for (const template of DEFAULT_PIPELINE_TEMPLATES) {
|
||||
// 创建模板流水线(使用负数ID表示模板)
|
||||
const pipeline = await prisma.pipeline.create({
|
||||
data: {
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
projectId: null // 模板不属于任何特定项目
|
||||
}
|
||||
});
|
||||
|
||||
// 创建模板步骤
|
||||
for (const step of template.steps) {
|
||||
await prisma.step.create({
|
||||
data: {
|
||||
name: step.name,
|
||||
order: step.order,
|
||||
script: step.script,
|
||||
pipelineId: pipeline.id,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Created template: ${template.name}`);
|
||||
}
|
||||
} else {
|
||||
console.log('Pipeline templates already exist, skipping initialization');
|
||||
}
|
||||
|
||||
console.log('Pipeline templates initialization completed');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize pipeline templates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的流水线模板
|
||||
*/
|
||||
export async function getAvailableTemplates(): Promise<Array<{id: number, name: string, description: string}>> {
|
||||
try {
|
||||
const templates = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
projectId: null, // 模板流水线没有关联的项目
|
||||
valid: 1
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true
|
||||
}
|
||||
});
|
||||
|
||||
// 处理可能为null的description字段
|
||||
return templates.map(template => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description || ''
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to get pipeline templates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于模板创建新的流水线
|
||||
* @param templateId 模板ID
|
||||
* @param projectId 项目ID
|
||||
* @param pipelineName 新流水线名称
|
||||
* @param pipelineDescription 新流水线描述
|
||||
*/
|
||||
export async function createPipelineFromTemplate(
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
pipelineName: string,
|
||||
pipelineDescription: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
// 获取模板流水线及其步骤
|
||||
const templatePipeline = await prisma.pipeline.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
projectId: null, // 确保是模板流水线
|
||||
valid: 1
|
||||
},
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!templatePipeline) {
|
||||
throw new Error(`Template with id ${templateId} not found`);
|
||||
}
|
||||
|
||||
// 创建新的流水线
|
||||
const newPipeline = await prisma.pipeline.create({
|
||||
data: {
|
||||
name: pipelineName,
|
||||
description: pipelineDescription,
|
||||
projectId: projectId,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
|
||||
// 复制模板步骤到新流水线
|
||||
for (const templateStep of templatePipeline.steps) {
|
||||
await prisma.step.create({
|
||||
data: {
|
||||
name: templateStep.name,
|
||||
order: templateStep.order,
|
||||
script: templateStep.script,
|
||||
pipelineId: newPipeline.id,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Created pipeline from template ${templateId}: ${newPipeline.name}`);
|
||||
return newPipeline.id;
|
||||
} catch (error) {
|
||||
console.error('Failed to create pipeline from template:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Session } from './session.ts';
|
||||
import { CORS } from './cors.ts';
|
||||
import { HttpLogger } from './logger.ts';
|
||||
import type Koa from 'koa';
|
||||
import { Authorization } from './Authorization.ts';
|
||||
import { Authorization } from './authorization.ts';
|
||||
|
||||
/**
|
||||
* 初始化中间件
|
||||
|
||||
Binary file not shown.
@@ -79,6 +79,7 @@ model Deployment {
|
||||
commitHash String?
|
||||
commitMessage String?
|
||||
buildLog String?
|
||||
sparseCheckoutPaths String? // 稀疏检出路径,用于monorepo项目
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
valid Int @default(1)
|
||||
|
||||
3
apps/server/runners/index.ts
Normal file
3
apps/server/runners/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PipelineRunner } from './pipeline-runner';
|
||||
|
||||
export { PipelineRunner };
|
||||
28
apps/server/runners/mq-interface.ts
Normal file
28
apps/server/runners/mq-interface.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// MQ集成接口设计 (暂不实现)
|
||||
// 该接口用于将来通过消息队列触发流水线执行
|
||||
|
||||
export interface MQPipelineMessage {
|
||||
deploymentId: number;
|
||||
pipelineId: number;
|
||||
// 其他可能需要的参数
|
||||
triggerUser?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface MQRunnerInterface {
|
||||
/**
|
||||
* 发送流水线执行消息到MQ
|
||||
* @param message 流水线执行消息
|
||||
*/
|
||||
sendPipelineExecutionMessage(message: MQPipelineMessage): Promise<void>;
|
||||
|
||||
/**
|
||||
* 监听MQ消息并执行流水线
|
||||
*/
|
||||
listenForPipelineMessages(): void;
|
||||
|
||||
/**
|
||||
* 停止监听MQ消息
|
||||
*/
|
||||
stopListening(): void;
|
||||
}
|
||||
254
apps/server/runners/pipeline-runner.ts
Normal file
254
apps/server/runners/pipeline-runner.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { $ } from 'zx';
|
||||
import { prisma } from '../libs/prisma.ts';
|
||||
import type { Step } from '../generated/client.ts';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export class PipelineRunner {
|
||||
private deploymentId: number;
|
||||
private workspace: string;
|
||||
|
||||
constructor(deploymentId: number) {
|
||||
this.deploymentId = deploymentId;
|
||||
// 从环境变量获取工作空间路径,默认为/tmp/foka-ci/workspace
|
||||
this.workspace = process.env.PIPELINE_WORKSPACE || '/tmp/foka-ci/workspace';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流水线
|
||||
* @param pipelineId 流水线ID
|
||||
*/
|
||||
async run(pipelineId: number): Promise<void> {
|
||||
// 获取流水线及其步骤
|
||||
const pipeline = await prisma.pipeline.findUnique({
|
||||
where: { id: pipelineId },
|
||||
include: {
|
||||
steps: { where: { valid: 1 }, orderBy: { order: 'asc' } },
|
||||
Project: true // 同时获取关联的项目信息
|
||||
}
|
||||
});
|
||||
|
||||
if (!pipeline) {
|
||||
throw new Error(`Pipeline with id ${pipelineId} not found`);
|
||||
}
|
||||
|
||||
// 获取部署信息
|
||||
const deployment = await prisma.deployment.findUnique({
|
||||
where: { id: this.deploymentId }
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(`Deployment with id ${this.deploymentId} not found`);
|
||||
}
|
||||
|
||||
// 确保工作空间目录存在
|
||||
await this.ensureWorkspace();
|
||||
|
||||
// 创建项目目录(在工作空间内)
|
||||
const projectDir = path.join(this.workspace, `project-${pipelineId}`);
|
||||
await this.ensureProjectDirectory(projectDir);
|
||||
|
||||
// 更新部署状态为running
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: { status: 'running' }
|
||||
});
|
||||
|
||||
let logs = '';
|
||||
let hasError = false;
|
||||
|
||||
try {
|
||||
// 依次执行每个步骤
|
||||
for (const [index, step] of pipeline.steps.entries()) {
|
||||
// 准备环境变量
|
||||
const envVars = this.prepareEnvironmentVariables(pipeline, deployment, projectDir);
|
||||
|
||||
// 记录开始执行步骤的日志,包含脚本内容(合并为一行,并用括号括起脚本内容)
|
||||
const startLog = `[${new Date().toISOString()}] 开始执行步骤 ${index + 1}/${pipeline.steps.length}: ${step.name}\n`;
|
||||
logs += startLog;
|
||||
|
||||
// 实时更新日志
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: { buildLog: logs }
|
||||
});
|
||||
|
||||
// 执行步骤(传递环境变量和项目目录)
|
||||
const stepLog = await this.executeStep(step, envVars, projectDir);
|
||||
logs += stepLog + '\n';
|
||||
|
||||
// 记录步骤执行完成的日志
|
||||
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
|
||||
logs += endLog;
|
||||
|
||||
// 实时更新日志
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: { buildLog: logs }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
hasError = true;
|
||||
logs += `[${new Date().toISOString()}] Error: ${(error as Error).message}\n`;
|
||||
|
||||
// 记录错误日志
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: {
|
||||
buildLog: logs,
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
// 更新最终状态
|
||||
if (!hasError) {
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: {
|
||||
buildLog: logs,
|
||||
status: 'success',
|
||||
finishedAt: new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备环境变量
|
||||
* @param pipeline 流水线信息
|
||||
* @param deployment 部署信息
|
||||
* @param projectDir 项目目录路径
|
||||
*/
|
||||
private prepareEnvironmentVariables(pipeline: any, deployment: any, projectDir: string): Record<string, string> {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
// 项目相关信息
|
||||
if (pipeline.Project) {
|
||||
envVars.REPOSITORY_URL = pipeline.Project.repository || '';
|
||||
envVars.PROJECT_NAME = pipeline.Project.name || '';
|
||||
}
|
||||
|
||||
// 部署相关信息
|
||||
envVars.BRANCH_NAME = deployment.branch || '';
|
||||
envVars.COMMIT_HASH = deployment.commitHash || '';
|
||||
|
||||
// 稀疏检出路径(如果有配置的话)
|
||||
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || '';
|
||||
|
||||
// 工作空间路径和项目路径
|
||||
envVars.WORKSPACE = this.workspace;
|
||||
envVars.PROJECT_DIR = projectDir;
|
||||
|
||||
return envVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为日志添加时间戳前缀
|
||||
* @param message 日志消息
|
||||
* @param isError 是否为错误日志
|
||||
* @returns 带时间戳的日志消息
|
||||
*/
|
||||
private addTimestamp(message: string, isError = false): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
if (isError) {
|
||||
return `[${timestamp}] [ERROR] ${message}`;
|
||||
}
|
||||
return `[${timestamp}] ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为多行日志添加时间戳前缀
|
||||
* @param content 多行日志内容
|
||||
* @param isError 是否为错误日志
|
||||
* @returns 带时间戳的多行日志消息
|
||||
*/
|
||||
private addTimestampToLines(content: string, isError = false): string {
|
||||
if (!content) return '';
|
||||
|
||||
return content.split('\n')
|
||||
.filter(line => line.trim() !== '')
|
||||
.map(line => this.addTimestamp(line, isError))
|
||||
.join('\n') + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保工作空间目录存在
|
||||
*/
|
||||
private async ensureWorkspace(): Promise<void> {
|
||||
try {
|
||||
// 检查目录是否存在,如果不存在则创建
|
||||
if (!fs.existsSync(this.workspace)) {
|
||||
// 创建目录包括所有必要的父目录
|
||||
fs.mkdirSync(this.workspace, { recursive: true });
|
||||
}
|
||||
|
||||
// 检查目录是否可写
|
||||
fs.accessSync(this.workspace, fs.constants.W_OK);
|
||||
} catch (error) {
|
||||
throw new Error(`无法访问或创建工作空间目录 "${this.workspace}": ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保项目目录存在
|
||||
* @param projectDir 项目目录路径
|
||||
*/
|
||||
private async ensureProjectDirectory(projectDir: string): Promise<void> {
|
||||
try {
|
||||
// 检查目录是否存在,如果不存在则创建
|
||||
if (!fs.existsSync(projectDir)) {
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 检查目录是否可写
|
||||
fs.accessSync(projectDir, fs.constants.W_OK);
|
||||
} catch (error) {
|
||||
throw new Error(`无法访问或创建项目目录 "${projectDir}": ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个步骤
|
||||
* @param step 步骤对象
|
||||
* @param envVars 环境变量
|
||||
* @param projectDir 项目目录路径
|
||||
*/
|
||||
private async executeStep(step: Step, envVars: Record<string, string>, projectDir: string): Promise<string> {
|
||||
let logs = '';
|
||||
|
||||
try {
|
||||
// 添加步骤开始执行的时间戳
|
||||
logs += this.addTimestamp(`开始执行步骤 "${step.name}"`) + '\n';
|
||||
|
||||
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
||||
const script = step.script;
|
||||
|
||||
// 通过bash -c执行脚本,确保环境变量能被正确解析
|
||||
const result = await $({
|
||||
cwd: projectDir,
|
||||
env: { ...process.env, ...envVars }
|
||||
})`bash -c ${script}`;
|
||||
|
||||
if (result.stdout) {
|
||||
// 为stdout中的每一行添加时间戳
|
||||
logs += this.addTimestampToLines(result.stdout);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
// 为stderr中的每一行添加时间戳和错误标记
|
||||
logs += this.addTimestampToLines(result.stderr, true);
|
||||
}
|
||||
|
||||
// 添加步骤执行完成的时间戳
|
||||
logs += this.addTimestamp(`步骤 "${step.name}" 执行完成`) + '\n';
|
||||
} catch (error) {
|
||||
logs += this.addTimestamp(`Error executing step "${step.name}": ${(error as Error).message}`) + '\n';
|
||||
throw error;
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
const renderDeployRecordItem = (item: Deployment) => (
|
||||
<DeployRecordItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={isSelected}
|
||||
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">
|
||||
<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} 个步骤
|
||||
{pipeline.steps?.length || 0} 个步骤
|
||||
</span>
|
||||
<span>
|
||||
{new Date(
|
||||
pipeline.updatedAt,
|
||||
).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<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,16 +912,67 @@ function ProjectDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 新建/编辑流水线模态框 */}
|
||||
<Modal
|
||||
title={editingPipeline ? '编辑流水线' : '新建流水线'}
|
||||
visible={pipelineModalVisible}
|
||||
onOk={handleSavePipeline}
|
||||
onCancel={() => setPipelineModalVisible(false)}
|
||||
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="流水线名称"
|
||||
@@ -845,13 +983,35 @@ function ProjectDetailPage() {
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="流水线描述"
|
||||
rules={[{ required: true, message: '请输入流水线描述' }]}
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -892,9 +1052,6 @@ function ProjectDetailPage() {
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<DeployModal
|
||||
visible={deployModalVisible}
|
||||
|
||||
@@ -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