feat(server): 支持稀疏检出路径并完善部署执行队列

- 在部署DTO中添加sparseCheckoutPaths字段支持稀疏检出路径
- 数据模型Deployment新增稀疏检出路径字段及相关数据库映射
- 部署创建时支持设置稀疏检出路径字段
- 部署重试接口实现,支持复制原始部署记录并加入执行队列
- 新增流水线模板初始化与基于模板创建流水线接口
- 优化应用初始化流程,确保执行队列和流水线模板正确加载
- 添加启动日志,提示执行队列初始化完成
This commit is contained in:
2025-12-12 23:21:26 +08:00
parent 73240d94b1
commit 9897bd04c2
22 changed files with 1307 additions and 136 deletions

View File

@@ -1,13 +1,32 @@
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';
const app = new Koa();
// 初始化应用
async function initializeApp() {
// 初始化流水线模板
await initializePipelineTemplates();
initMiddlewares(app);
// 初始化执行队列
const executionQueue = ExecutionQueue.getInstance();
await executionQueue.initialize();
const PORT = process.env.PORT || 3001;
const app = new Koa();
app.listen(PORT, () => {
initMiddlewares(app);
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);
});

View File

@@ -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>;

View File

@@ -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()
};
}
}

View File

@@ -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) {

View File

@@ -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')

View File

@@ -885,6 +885,7 @@ export const DeploymentScalarFieldEnum = {
commitHash: 'commitHash',
commitMessage: 'commitMessage',
buildLog: 'buildLog',
sparseCheckoutPaths: 'sparseCheckoutPaths',
startedAt: 'startedAt',
finishedAt: 'finishedAt',
valid: 'valid',

View File

@@ -142,6 +142,7 @@ export const DeploymentScalarFieldEnum = {
commitHash: 'commitHash',
commitMessage: 'commitMessage',
buildLog: 'buildLog',
sparseCheckoutPaths: 'sparseCheckoutPaths',
startedAt: 'startedAt',
finishedAt: 'finishedAt',
valid: 'valid',

View File

@@ -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'>

View 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
};
}
}

View 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;
}
}

View File

@@ -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.

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
import { PipelineRunner } from './pipeline-runner';
export { PipelineRunner };

View 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;
}

View 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;
}
}

View File

@@ -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 }) => (

View File

@@ -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}

View File

@@ -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}

View File

@@ -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();

View File

@@ -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项目
}

View File

@@ -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;