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, () => {
log.info('APP', 'Server started at port %d', 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;
}
}