feat(project): add workspace directory configuration and management (#1)
- Add projectDir field to Project model for workspace directory management - Implement workspace directory creation, validation and Git initialization - Add workspace status query endpoint with directory info and Git status - Create GitManager for Git repository operations (clone, branch, commit info) - Add PathValidator for secure path validation and traversal attack prevention - Implement execution queue with concurrency control for build tasks - Refactor project list UI to remove edit/delete actions from cards - Add project settings tab in detail page with edit/delete functionality - Add icons to all tabs (History, Code, Settings) - Implement time formatting with dayjs in YYYY-MM-DD HH:mm:ss format - Display all timestamps using browser's local timezone - Update PipelineRunner to use workspace directory for command execution - Add workspace status card showing directory path, size, Git info - Enhance CreateProjectModal with repository URL validation Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { projectDirSchema } from '../../libs/path-validator.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建项目验证架构
|
* 创建项目验证架构
|
||||||
@@ -15,6 +16,8 @@ export const createProjectSchema = z.object({
|
|||||||
repository: z.string({
|
repository: z.string({
|
||||||
message: '仓库地址必须是字符串',
|
message: '仓库地址必须是字符串',
|
||||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }),
|
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }),
|
||||||
|
|
||||||
|
projectDir: projectDirSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import {prisma} from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
|
import { GitManager } from '../../libs/git-manager.ts';
|
||||||
import {
|
import {
|
||||||
createProjectSchema,
|
createProjectSchema,
|
||||||
updateProjectSchema,
|
updateProjectSchema,
|
||||||
@@ -37,7 +38,7 @@ export class ProjectController {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -47,7 +48,7 @@ export class ProjectController {
|
|||||||
limit: query?.limit || 10,
|
limit: query?.limit || 10,
|
||||||
total,
|
total,
|
||||||
totalPages: Math.ceil(total / (query?.limit || 10)),
|
totalPages: Math.ceil(total / (query?.limit || 10)),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +68,48 @@ export class ProjectController {
|
|||||||
throw new BusinessError('项目不存在', 1002, 404);
|
throw new BusinessError('项目不存在', 1002, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return project;
|
// 获取工作目录状态信息
|
||||||
|
let workspaceStatus = null;
|
||||||
|
if (project.projectDir) {
|
||||||
|
try {
|
||||||
|
const status = await GitManager.checkWorkspaceStatus(
|
||||||
|
project.projectDir,
|
||||||
|
);
|
||||||
|
let size = 0;
|
||||||
|
let gitInfo = null;
|
||||||
|
|
||||||
|
if (status.exists && !status.isEmpty) {
|
||||||
|
size = await GitManager.getDirectorySize(project.projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.hasGit) {
|
||||||
|
gitInfo = await GitManager.getGitInfo(project.projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceStatus = {
|
||||||
|
...status,
|
||||||
|
size,
|
||||||
|
gitInfo,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'project',
|
||||||
|
'Failed to get workspace status for project %s: %s',
|
||||||
|
project.name,
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
// 即使获取状态失败,也返回项目信息
|
||||||
|
workspaceStatus = {
|
||||||
|
status: 'error',
|
||||||
|
error: (error as Error).message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
workspaceStatus,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/projects - 创建项目
|
// POST /api/projects - 创建项目
|
||||||
@@ -75,18 +117,36 @@ export class ProjectController {
|
|||||||
async create(ctx: Context) {
|
async create(ctx: Context) {
|
||||||
const validatedData = createProjectSchema.parse(ctx.request.body);
|
const validatedData = createProjectSchema.parse(ctx.request.body);
|
||||||
|
|
||||||
|
// 检查工作目录是否已被其他项目使用
|
||||||
|
const existingProject = await prisma.project.findFirst({
|
||||||
|
where: {
|
||||||
|
projectDir: validatedData.projectDir,
|
||||||
|
valid: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingProject) {
|
||||||
|
throw new BusinessError('该工作目录已被其他项目使用', 1003, 400);
|
||||||
|
}
|
||||||
|
|
||||||
const project = await prisma.project.create({
|
const project = await prisma.project.create({
|
||||||
data: {
|
data: {
|
||||||
name: validatedData.name,
|
name: validatedData.name,
|
||||||
description: validatedData.description || '',
|
description: validatedData.description || '',
|
||||||
repository: validatedData.repository,
|
repository: validatedData.repository,
|
||||||
|
projectDir: validatedData.projectDir,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('project', 'Created new project: %s', project.name);
|
log.info(
|
||||||
|
'project',
|
||||||
|
'Created new project: %s with projectDir: %s',
|
||||||
|
project.name,
|
||||||
|
project.projectDir,
|
||||||
|
);
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
|
|||||||
"clientVersion": "7.0.0",
|
"clientVersion": "7.0.0",
|
||||||
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513",
|
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513",
|
||||||
"activeProvider": "sqlite",
|
"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 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",
|
"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 projectDir String @unique // 项目工作目录路径(必填)\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": {
|
"runtimeDataModel": {
|
||||||
"models": {},
|
"models": {},
|
||||||
"enums": {},
|
"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\":\"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\":{}}")
|
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\":\"projectDir\",\"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> {
|
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||||
const { Buffer } = await import('node:buffer')
|
const { Buffer } = await import('node:buffer')
|
||||||
|
|||||||
@@ -819,6 +819,7 @@ export const ProjectScalarFieldEnum = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
repository: 'repository',
|
repository: 'repository',
|
||||||
|
projectDir: 'projectDir',
|
||||||
valid: 'valid',
|
valid: 'valid',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export const ProjectScalarFieldEnum = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
repository: 'repository',
|
repository: 'repository',
|
||||||
|
projectDir: 'projectDir',
|
||||||
valid: 'valid',
|
valid: 'valid',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type ProjectMinAggregateOutputType = {
|
|||||||
name: string | null
|
name: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
repository: string | null
|
repository: string | null
|
||||||
|
projectDir: string | null
|
||||||
valid: number | null
|
valid: number | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
@@ -53,6 +54,7 @@ export type ProjectMaxAggregateOutputType = {
|
|||||||
name: string | null
|
name: string | null
|
||||||
description: string | null
|
description: string | null
|
||||||
repository: string | null
|
repository: string | null
|
||||||
|
projectDir: string | null
|
||||||
valid: number | null
|
valid: number | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
@@ -65,6 +67,7 @@ export type ProjectCountAggregateOutputType = {
|
|||||||
name: number
|
name: number
|
||||||
description: number
|
description: number
|
||||||
repository: number
|
repository: number
|
||||||
|
projectDir: number
|
||||||
valid: number
|
valid: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
@@ -89,6 +92,7 @@ export type ProjectMinAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
repository?: true
|
repository?: true
|
||||||
|
projectDir?: true
|
||||||
valid?: true
|
valid?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -101,6 +105,7 @@ export type ProjectMaxAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
repository?: true
|
repository?: true
|
||||||
|
projectDir?: true
|
||||||
valid?: true
|
valid?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -113,6 +118,7 @@ export type ProjectCountAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
description?: true
|
description?: true
|
||||||
repository?: true
|
repository?: true
|
||||||
|
projectDir?: true
|
||||||
valid?: true
|
valid?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
@@ -212,6 +218,7 @@ export type ProjectGroupByOutputType = {
|
|||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid: number
|
valid: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@@ -247,6 +254,7 @@ export type ProjectWhereInput = {
|
|||||||
name?: Prisma.StringFilter<"Project"> | string
|
name?: Prisma.StringFilter<"Project"> | string
|
||||||
description?: Prisma.StringNullableFilter<"Project"> | string | null
|
description?: Prisma.StringNullableFilter<"Project"> | string | null
|
||||||
repository?: Prisma.StringFilter<"Project"> | string
|
repository?: Prisma.StringFilter<"Project"> | string
|
||||||
|
projectDir?: Prisma.StringFilter<"Project"> | string
|
||||||
valid?: Prisma.IntFilter<"Project"> | number
|
valid?: Prisma.IntFilter<"Project"> | number
|
||||||
createdAt?: Prisma.DateTimeFilter<"Project"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Project"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Project"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Project"> | Date | string
|
||||||
@@ -261,6 +269,7 @@ export type ProjectOrderByWithRelationInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
repository?: Prisma.SortOrder
|
repository?: Prisma.SortOrder
|
||||||
|
projectDir?: Prisma.SortOrder
|
||||||
valid?: Prisma.SortOrder
|
valid?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -272,6 +281,7 @@ export type ProjectOrderByWithRelationInput = {
|
|||||||
|
|
||||||
export type ProjectWhereUniqueInput = Prisma.AtLeast<{
|
export type ProjectWhereUniqueInput = Prisma.AtLeast<{
|
||||||
id?: number
|
id?: number
|
||||||
|
projectDir?: string
|
||||||
AND?: Prisma.ProjectWhereInput | Prisma.ProjectWhereInput[]
|
AND?: Prisma.ProjectWhereInput | Prisma.ProjectWhereInput[]
|
||||||
OR?: Prisma.ProjectWhereInput[]
|
OR?: Prisma.ProjectWhereInput[]
|
||||||
NOT?: Prisma.ProjectWhereInput | Prisma.ProjectWhereInput[]
|
NOT?: Prisma.ProjectWhereInput | Prisma.ProjectWhereInput[]
|
||||||
@@ -285,13 +295,14 @@ export type ProjectWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
updatedBy?: Prisma.StringFilter<"Project"> | string
|
updatedBy?: Prisma.StringFilter<"Project"> | string
|
||||||
deployments?: Prisma.DeploymentListRelationFilter
|
deployments?: Prisma.DeploymentListRelationFilter
|
||||||
pipelines?: Prisma.PipelineListRelationFilter
|
pipelines?: Prisma.PipelineListRelationFilter
|
||||||
}, "id">
|
}, "id" | "projectDir">
|
||||||
|
|
||||||
export type ProjectOrderByWithAggregationInput = {
|
export type ProjectOrderByWithAggregationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
description?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||||
repository?: Prisma.SortOrder
|
repository?: Prisma.SortOrder
|
||||||
|
projectDir?: Prisma.SortOrder
|
||||||
valid?: Prisma.SortOrder
|
valid?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -312,6 +323,7 @@ export type ProjectScalarWhereWithAggregatesInput = {
|
|||||||
name?: Prisma.StringWithAggregatesFilter<"Project"> | string
|
name?: Prisma.StringWithAggregatesFilter<"Project"> | string
|
||||||
description?: Prisma.StringNullableWithAggregatesFilter<"Project"> | string | null
|
description?: Prisma.StringNullableWithAggregatesFilter<"Project"> | string | null
|
||||||
repository?: Prisma.StringWithAggregatesFilter<"Project"> | string
|
repository?: Prisma.StringWithAggregatesFilter<"Project"> | string
|
||||||
|
projectDir?: Prisma.StringWithAggregatesFilter<"Project"> | string
|
||||||
valid?: Prisma.IntWithAggregatesFilter<"Project"> | number
|
valid?: Prisma.IntWithAggregatesFilter<"Project"> | number
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Project"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Project"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Project"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Project"> | Date | string
|
||||||
@@ -323,6 +335,7 @@ export type ProjectCreateInput = {
|
|||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid?: number
|
valid?: number
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -337,6 +350,7 @@ export type ProjectUncheckedCreateInput = {
|
|||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid?: number
|
valid?: number
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -350,6 +364,7 @@ export type ProjectUpdateInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -364,6 +379,7 @@ export type ProjectUncheckedUpdateInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -378,6 +394,7 @@ export type ProjectCreateManyInput = {
|
|||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid?: number
|
valid?: number
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -389,6 +406,7 @@ export type ProjectUpdateManyMutationInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -401,6 +419,7 @@ export type ProjectUncheckedUpdateManyInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -413,6 +432,7 @@ export type ProjectCountOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
repository?: Prisma.SortOrder
|
repository?: Prisma.SortOrder
|
||||||
|
projectDir?: Prisma.SortOrder
|
||||||
valid?: Prisma.SortOrder
|
valid?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -430,6 +450,7 @@ export type ProjectMaxOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
repository?: Prisma.SortOrder
|
repository?: Prisma.SortOrder
|
||||||
|
projectDir?: Prisma.SortOrder
|
||||||
valid?: Prisma.SortOrder
|
valid?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -442,6 +463,7 @@ export type ProjectMinOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
description?: Prisma.SortOrder
|
description?: Prisma.SortOrder
|
||||||
repository?: Prisma.SortOrder
|
repository?: Prisma.SortOrder
|
||||||
|
projectDir?: Prisma.SortOrder
|
||||||
valid?: Prisma.SortOrder
|
valid?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
@@ -515,6 +537,7 @@ export type ProjectCreateWithoutPipelinesInput = {
|
|||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid?: number
|
valid?: number
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -528,6 +551,7 @@ export type ProjectUncheckedCreateWithoutPipelinesInput = {
|
|||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid?: number
|
valid?: number
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -556,6 +580,7 @@ export type ProjectUpdateWithoutPipelinesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -569,6 +594,7 @@ export type ProjectUncheckedUpdateWithoutPipelinesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -581,6 +607,7 @@ export type ProjectCreateWithoutDeploymentsInput = {
|
|||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid?: number
|
valid?: number
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -594,6 +621,7 @@ export type ProjectUncheckedCreateWithoutDeploymentsInput = {
|
|||||||
name: string
|
name: string
|
||||||
description?: string | null
|
description?: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid?: number
|
valid?: number
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
@@ -622,6 +650,7 @@ export type ProjectUpdateWithoutDeploymentsInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -635,6 +664,7 @@ export type ProjectUncheckedUpdateWithoutDeploymentsInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
repository?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
projectDir?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
valid?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
@@ -688,6 +718,7 @@ export type ProjectSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
repository?: boolean
|
repository?: boolean
|
||||||
|
projectDir?: boolean
|
||||||
valid?: boolean
|
valid?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -703,6 +734,7 @@ export type ProjectSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
repository?: boolean
|
repository?: boolean
|
||||||
|
projectDir?: boolean
|
||||||
valid?: boolean
|
valid?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -715,6 +747,7 @@ export type ProjectSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
repository?: boolean
|
repository?: boolean
|
||||||
|
projectDir?: boolean
|
||||||
valid?: boolean
|
valid?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -727,6 +760,7 @@ export type ProjectSelectScalar = {
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
description?: boolean
|
description?: boolean
|
||||||
repository?: boolean
|
repository?: boolean
|
||||||
|
projectDir?: boolean
|
||||||
valid?: boolean
|
valid?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
@@ -734,7 +768,7 @@ export type ProjectSelectScalar = {
|
|||||||
updatedBy?: boolean
|
updatedBy?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "description" | "repository" | "valid" | "createdAt" | "updatedAt" | "createdBy" | "updatedBy", ExtArgs["result"]["project"]>
|
export type ProjectOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "description" | "repository" | "projectDir" | "valid" | "createdAt" | "updatedAt" | "createdBy" | "updatedBy", ExtArgs["result"]["project"]>
|
||||||
export type ProjectInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type ProjectInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
deployments?: boolean | Prisma.Project$deploymentsArgs<ExtArgs>
|
deployments?: boolean | Prisma.Project$deploymentsArgs<ExtArgs>
|
||||||
pipelines?: boolean | Prisma.Project$pipelinesArgs<ExtArgs>
|
pipelines?: boolean | Prisma.Project$pipelinesArgs<ExtArgs>
|
||||||
@@ -754,6 +788,7 @@ export type $ProjectPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
name: string
|
name: string
|
||||||
description: string | null
|
description: string | null
|
||||||
repository: string
|
repository: string
|
||||||
|
projectDir: string
|
||||||
valid: number
|
valid: number
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
@@ -1188,6 +1223,7 @@ export interface ProjectFieldRefs {
|
|||||||
readonly name: Prisma.FieldRef<"Project", 'String'>
|
readonly name: Prisma.FieldRef<"Project", 'String'>
|
||||||
readonly description: Prisma.FieldRef<"Project", 'String'>
|
readonly description: Prisma.FieldRef<"Project", 'String'>
|
||||||
readonly repository: Prisma.FieldRef<"Project", 'String'>
|
readonly repository: Prisma.FieldRef<"Project", 'String'>
|
||||||
|
readonly projectDir: Prisma.FieldRef<"Project", 'String'>
|
||||||
readonly valid: Prisma.FieldRef<"Project", 'Int'>
|
readonly valid: Prisma.FieldRef<"Project", 'Int'>
|
||||||
readonly createdAt: Prisma.FieldRef<"Project", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"Project", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"Project", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"Project", 'DateTime'>
|
||||||
|
|||||||
@@ -61,12 +61,12 @@ export class ExecutionQueue {
|
|||||||
const pendingDeployments = await prisma.deployment.findMany({
|
const pendingDeployments = await prisma.deployment.findMany({
|
||||||
where: {
|
where: {
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
pipelineId: true
|
pipelineId: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Found ${pendingDeployments.length} pending deployments`);
|
console.log(`Found ${pendingDeployments.length} pending deployments`);
|
||||||
@@ -126,21 +126,25 @@ export class ExecutionQueue {
|
|||||||
const pendingDeployments = await prisma.deployment.findMany({
|
const pendingDeployments = await prisma.deployment.findMany({
|
||||||
where: {
|
where: {
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
pipelineId: true
|
pipelineId: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Found ${pendingDeployments.length} pending deployments in polling`);
|
console.log(
|
||||||
|
`Found ${pendingDeployments.length} pending deployments in polling`,
|
||||||
|
);
|
||||||
|
|
||||||
// 检查这些任务是否已经在队列中,如果没有则添加
|
// 检查这些任务是否已经在队列中,如果没有则添加
|
||||||
for (const deployment of pendingDeployments) {
|
for (const deployment of pendingDeployments) {
|
||||||
// 检查是否已经在运行队列中
|
// 检查是否已经在运行队列中
|
||||||
if (!runningDeployments.has(deployment.id)) {
|
if (!runningDeployments.has(deployment.id)) {
|
||||||
console.log(`Adding deployment ${deployment.id} to queue from polling`);
|
console.log(
|
||||||
|
`Adding deployment ${deployment.id} to queue from polling`,
|
||||||
|
);
|
||||||
await this.addTask(deployment.id, deployment.pipelineId);
|
await this.addTask(deployment.id, deployment.pipelineId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +158,10 @@ export class ExecutionQueue {
|
|||||||
* @param deploymentId 部署ID
|
* @param deploymentId 部署ID
|
||||||
* @param pipelineId 流水线ID
|
* @param pipelineId 流水线ID
|
||||||
*/
|
*/
|
||||||
public async addTask(deploymentId: number, pipelineId: number): Promise<void> {
|
public async addTask(
|
||||||
|
deploymentId: number,
|
||||||
|
pipelineId: number,
|
||||||
|
): Promise<void> {
|
||||||
// 检查是否已经在运行队列中
|
// 检查是否已经在运行队列中
|
||||||
if (runningDeployments.has(deploymentId)) {
|
if (runningDeployments.has(deploymentId)) {
|
||||||
console.log(`Deployment ${deploymentId} is already queued or running`);
|
console.log(`Deployment ${deploymentId} is already queued or running`);
|
||||||
@@ -196,7 +203,7 @@ export class ExecutionQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加一个小延迟以避免过度占用资源
|
// 添加一个小延迟以避免过度占用资源
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
@@ -207,9 +214,35 @@ export class ExecutionQueue {
|
|||||||
* @param deploymentId 部署ID
|
* @param deploymentId 部署ID
|
||||||
* @param pipelineId 流水线ID
|
* @param pipelineId 流水线ID
|
||||||
*/
|
*/
|
||||||
private async executePipeline(deploymentId: number, pipelineId: number): Promise<void> {
|
private async executePipeline(
|
||||||
|
deploymentId: number,
|
||||||
|
pipelineId: number,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const runner = new PipelineRunner(deploymentId);
|
// 获取部署信息以获取项目和 projectDir
|
||||||
|
const deployment = await prisma.deployment.findUnique({
|
||||||
|
where: { id: deploymentId },
|
||||||
|
include: {
|
||||||
|
Project: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deployment || !deployment.Project) {
|
||||||
|
throw new Error(
|
||||||
|
`Deployment ${deploymentId} or associated project not found`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deployment.Project.projectDir) {
|
||||||
|
throw new Error(
|
||||||
|
`项目 "${deployment.Project.name}" 未配置工作目录,无法执行流水线`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = new PipelineRunner(
|
||||||
|
deploymentId,
|
||||||
|
deployment.Project.projectDir,
|
||||||
|
);
|
||||||
await runner.run(pipelineId);
|
await runner.run(pipelineId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('执行流水线失败:', error);
|
console.error('执行流水线失败:', error);
|
||||||
@@ -227,7 +260,7 @@ export class ExecutionQueue {
|
|||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
pendingCount: pendingQueue.length,
|
pendingCount: pendingQueue.length,
|
||||||
runningCount: runningDeployments.size
|
runningCount: runningDeployments.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
280
apps/server/libs/git-manager.ts
Normal file
280
apps/server/libs/git-manager.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* Git 管理器
|
||||||
|
* 封装 Git 操作:克隆、更新、分支切换等
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { $ } from 'zx';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { log } from './logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作目录状态
|
||||||
|
*/
|
||||||
|
export const WorkspaceDirStatus = {
|
||||||
|
NOT_CREATED: 'not_created', // 目录不存在
|
||||||
|
EMPTY: 'empty', // 目录存在但为空
|
||||||
|
NO_GIT: 'no_git', // 目录存在但不是 Git 仓库
|
||||||
|
READY: 'ready', // 目录存在且包含 Git 仓库
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type WorkspaceDirStatus =
|
||||||
|
(typeof WorkspaceDirStatus)[keyof typeof WorkspaceDirStatus];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作目录状态信息
|
||||||
|
*/
|
||||||
|
export interface WorkspaceStatus {
|
||||||
|
status: WorkspaceDirStatus;
|
||||||
|
exists: boolean;
|
||||||
|
isEmpty?: boolean;
|
||||||
|
hasGit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git仓库信息
|
||||||
|
*/
|
||||||
|
export interface GitInfo {
|
||||||
|
branch?: string;
|
||||||
|
lastCommit?: string;
|
||||||
|
lastCommitMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git管理器类
|
||||||
|
*/
|
||||||
|
export class GitManager {
|
||||||
|
static readonly TAG = 'GitManager';
|
||||||
|
/**
|
||||||
|
* 检查工作目录状态
|
||||||
|
*/
|
||||||
|
static async checkWorkspaceStatus(dirPath: string): Promise<WorkspaceStatus> {
|
||||||
|
try {
|
||||||
|
// 检查目录是否存在
|
||||||
|
const stats = await fs.stat(dirPath);
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
return {
|
||||||
|
status: WorkspaceDirStatus.NOT_CREATED,
|
||||||
|
exists: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查目录是否为空
|
||||||
|
const files = await fs.readdir(dirPath);
|
||||||
|
if (files.length === 0) {
|
||||||
|
return {
|
||||||
|
status: WorkspaceDirStatus.EMPTY,
|
||||||
|
exists: true,
|
||||||
|
isEmpty: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含 .git 目录
|
||||||
|
const gitDir = path.join(dirPath, '.git');
|
||||||
|
try {
|
||||||
|
const gitStats = await fs.stat(gitDir);
|
||||||
|
if (gitStats.isDirectory()) {
|
||||||
|
return {
|
||||||
|
status: WorkspaceDirStatus.READY,
|
||||||
|
exists: true,
|
||||||
|
isEmpty: false,
|
||||||
|
hasGit: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
status: WorkspaceDirStatus.NO_GIT,
|
||||||
|
exists: true,
|
||||||
|
isEmpty: false,
|
||||||
|
hasGit: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: WorkspaceDirStatus.NO_GIT,
|
||||||
|
exists: true,
|
||||||
|
isEmpty: false,
|
||||||
|
hasGit: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||||
|
return {
|
||||||
|
status: WorkspaceDirStatus.NOT_CREATED,
|
||||||
|
exists: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 克隆仓库到指定目录
|
||||||
|
* @param repoUrl 仓库URL
|
||||||
|
* @param dirPath 目标目录
|
||||||
|
* @param branch 分支名
|
||||||
|
* @param token Gitea access token(可选)
|
||||||
|
*/
|
||||||
|
static async cloneRepository(
|
||||||
|
repoUrl: string,
|
||||||
|
dirPath: string,
|
||||||
|
branch: string,
|
||||||
|
token?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
log.info(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Cloning repository: %s to %s (branch: %s)',
|
||||||
|
repoUrl,
|
||||||
|
dirPath,
|
||||||
|
branch,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果提供了token,嵌入到URL中
|
||||||
|
let cloneUrl = repoUrl;
|
||||||
|
if (token) {
|
||||||
|
const url = new URL(repoUrl);
|
||||||
|
url.username = token;
|
||||||
|
cloneUrl = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 zx 执行 git clone(浅克隆)
|
||||||
|
$.verbose = false; // 禁止打印敏感信息
|
||||||
|
await $`git clone --depth 1 --branch ${branch} ${cloneUrl} ${dirPath}`;
|
||||||
|
$.verbose = true;
|
||||||
|
|
||||||
|
log.info(GitManager.TAG, 'Repository cloned successfully: %s', dirPath);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Failed to clone repository: %s to %s, error: %s',
|
||||||
|
repoUrl,
|
||||||
|
dirPath,
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
throw new Error(`克隆仓库失败: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新已存在的仓库
|
||||||
|
* @param dirPath 仓库目录
|
||||||
|
* @param branch 目标分支
|
||||||
|
*/
|
||||||
|
static async updateRepository(
|
||||||
|
dirPath: string,
|
||||||
|
branch: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
log.info(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Updating repository: %s (branch: %s)',
|
||||||
|
dirPath,
|
||||||
|
branch,
|
||||||
|
);
|
||||||
|
|
||||||
|
$.verbose = false;
|
||||||
|
// 切换到仓库目录
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(dirPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取最新代码
|
||||||
|
await $`git fetch --depth 1 origin ${branch}`;
|
||||||
|
// 切换到目标分支
|
||||||
|
await $`git checkout ${branch}`;
|
||||||
|
// 拉取最新代码
|
||||||
|
await $`git pull origin ${branch}`;
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Repository updated successfully: %s (branch: %s)',
|
||||||
|
dirPath,
|
||||||
|
branch,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
$.verbose = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Failed to update repository: %s (branch: %s), error: %s',
|
||||||
|
dirPath,
|
||||||
|
branch,
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
throw new Error(`更新仓库失败: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Git仓库信息
|
||||||
|
*/
|
||||||
|
static async getGitInfo(dirPath: string): Promise<GitInfo> {
|
||||||
|
try {
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(dirPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$.verbose = false;
|
||||||
|
const branchResult = await $`git branch --show-current`;
|
||||||
|
const commitResult = await $`git rev-parse --short HEAD`;
|
||||||
|
const messageResult = await $`git log -1 --pretty=%B`;
|
||||||
|
$.verbose = true;
|
||||||
|
|
||||||
|
return {
|
||||||
|
branch: branchResult.stdout.trim(),
|
||||||
|
lastCommit: commitResult.stdout.trim(),
|
||||||
|
lastCommitMessage: messageResult.stdout.trim(),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Failed to get git info: %s, error: %s',
|
||||||
|
dirPath,
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建目录(递归)
|
||||||
|
*/
|
||||||
|
static async ensureDirectory(dirPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
log.info(GitManager.TAG, 'Directory created: %s', dirPath);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Failed to create directory: %s, error: %s',
|
||||||
|
dirPath,
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
throw new Error(`创建目录失败: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取目录大小
|
||||||
|
*/
|
||||||
|
static async getDirectorySize(dirPath: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await $`du -sb ${dirPath}`;
|
||||||
|
const size = Number.parseInt(stdout.split('\t')[0], 10);
|
||||||
|
return size;
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
GitManager.TAG,
|
||||||
|
'Failed to get directory size: %s, error: %s',
|
||||||
|
dirPath,
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
apps/server/libs/path-validator.ts
Normal file
67
apps/server/libs/path-validator.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 路径验证工具
|
||||||
|
* 用于验证项目工作目录路径的合法性
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目目录路径验证schema
|
||||||
|
*/
|
||||||
|
export const projectDirSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1, '工作目录路径不能为空')
|
||||||
|
.refine(path.isAbsolute, '工作目录路径必须是绝对路径')
|
||||||
|
.refine((v) => !v.includes('..'), '不能包含路径遍历字符')
|
||||||
|
.refine((v) => !v.includes('~'), '不能包含用户目录符号')
|
||||||
|
.refine((v) => !/[<>:"|?*\x00-\x1f]/.test(v), '包含非法字符')
|
||||||
|
.refine((v) => path.normalize(v) === v, '路径格式不规范');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证路径格式
|
||||||
|
* @param dirPath 待验证的路径
|
||||||
|
* @returns 验证结果
|
||||||
|
*/
|
||||||
|
export function validateProjectDir(dirPath: string): {
|
||||||
|
valid: boolean;
|
||||||
|
error?: string;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
projectDirSchema.parse(dirPath);
|
||||||
|
return { valid: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return { valid: false, error: error.issues[0].message };
|
||||||
|
}
|
||||||
|
return { valid: false, error: '路径验证失败' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径是否为绝对路径
|
||||||
|
*/
|
||||||
|
export function isAbsolutePath(dirPath: string): boolean {
|
||||||
|
return path.isAbsolute(dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径是否包含非法字符
|
||||||
|
*/
|
||||||
|
export function hasIllegalCharacters(dirPath: string): boolean {
|
||||||
|
return /[<>:"|?*\x00-\x1f]/.test(dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查路径是否包含路径遍历
|
||||||
|
*/
|
||||||
|
export function hasPathTraversal(dirPath: string): boolean {
|
||||||
|
return dirPath.includes('..') || dirPath.includes('~');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化路径
|
||||||
|
*/
|
||||||
|
export function normalizePath(dirPath: string): string {
|
||||||
|
return path.normalize(dirPath);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -15,6 +15,7 @@ model Project {
|
|||||||
name String
|
name String
|
||||||
description String?
|
description String?
|
||||||
repository String
|
repository String
|
||||||
|
projectDir String @unique // 项目工作目录路径(必填)
|
||||||
// Relations
|
// Relations
|
||||||
deployments Deployment[]
|
deployments Deployment[]
|
||||||
pipelines Pipeline[]
|
pipelines Pipeline[]
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import { $ } from 'zx';
|
import { $ } from 'zx';
|
||||||
import { prisma } from '../libs/prisma.ts';
|
import { prisma } from '../libs/prisma.ts';
|
||||||
import type { Step } from '../generated/client.ts';
|
import type { Step } from '../generated/client.ts';
|
||||||
import fs from 'node:fs';
|
import { GitManager, WorkspaceDirStatus } from '../libs/git-manager.ts';
|
||||||
import path from 'node:path';
|
import { log } from '../libs/logger.ts';
|
||||||
|
|
||||||
export class PipelineRunner {
|
export class PipelineRunner {
|
||||||
|
private readonly TAG = 'PipelineRunner';
|
||||||
private deploymentId: number;
|
private deploymentId: number;
|
||||||
private workspace: string;
|
private projectDir: string;
|
||||||
|
|
||||||
constructor(deploymentId: number) {
|
constructor(deploymentId: number, projectDir: string) {
|
||||||
this.deploymentId = deploymentId;
|
this.deploymentId = deploymentId;
|
||||||
// 从环境变量获取工作空间路径,默认为/tmp/foka-ci/workspace
|
|
||||||
this.workspace = process.env.PIPELINE_WORKSPACE || '/tmp/foka-ci/workspace';
|
if (!projectDir) {
|
||||||
|
throw new Error('项目工作目录未配置,无法执行流水线');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.projectDir = projectDir;
|
||||||
|
log.info(
|
||||||
|
this.TAG,
|
||||||
|
'PipelineRunner initialized with projectDir: %s',
|
||||||
|
this.projectDir,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,8 +34,8 @@ export class PipelineRunner {
|
|||||||
where: { id: pipelineId },
|
where: { id: pipelineId },
|
||||||
include: {
|
include: {
|
||||||
steps: { where: { valid: 1 }, orderBy: { order: 'asc' } },
|
steps: { where: { valid: 1 }, orderBy: { order: 'asc' } },
|
||||||
Project: true // 同时获取关联的项目信息
|
Project: true, // 同时获取关联的项目信息
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
@@ -34,47 +44,43 @@ export class PipelineRunner {
|
|||||||
|
|
||||||
// 获取部署信息
|
// 获取部署信息
|
||||||
const deployment = await prisma.deployment.findUnique({
|
const deployment = await prisma.deployment.findUnique({
|
||||||
where: { id: this.deploymentId }
|
where: { id: this.deploymentId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deployment) {
|
if (!deployment) {
|
||||||
throw new Error(`Deployment with id ${this.deploymentId} not found`);
|
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 logs = '';
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 准备工作目录(检查、克隆或更新)
|
||||||
|
logs += await this.prepareWorkspace(pipeline.Project, deployment.branch);
|
||||||
|
|
||||||
|
// 更新部署状态为running
|
||||||
|
await prisma.deployment.update({
|
||||||
|
where: { id: this.deploymentId },
|
||||||
|
data: { status: 'running', buildLog: logs },
|
||||||
|
});
|
||||||
|
|
||||||
// 依次执行每个步骤
|
// 依次执行每个步骤
|
||||||
for (const [index, step] of pipeline.steps.entries()) {
|
for (const [index, step] of pipeline.steps.entries()) {
|
||||||
// 准备环境变量
|
// 准备环境变量
|
||||||
const envVars = this.prepareEnvironmentVariables(pipeline, deployment, projectDir);
|
const envVars = this.prepareEnvironmentVariables(pipeline, deployment);
|
||||||
|
|
||||||
// 记录开始执行步骤的日志,包含脚本内容(合并为一行,并用括号括起脚本内容)
|
// 记录开始执行步骤的日志
|
||||||
const startLog = `[${new Date().toISOString()}] 开始执行步骤 ${index + 1}/${pipeline.steps.length}: ${step.name}\n`;
|
const startLog = `[${new Date().toISOString()}] 开始执行步骤 ${index + 1}/${pipeline.steps.length}: ${step.name}\n`;
|
||||||
logs += startLog;
|
logs += startLog;
|
||||||
|
|
||||||
// 实时更新日志
|
// 实时更新日志
|
||||||
await prisma.deployment.update({
|
await prisma.deployment.update({
|
||||||
where: { id: this.deploymentId },
|
where: { id: this.deploymentId },
|
||||||
data: { buildLog: logs }
|
data: { buildLog: logs },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 执行步骤(传递环境变量和项目目录)
|
// 执行步骤
|
||||||
const stepLog = await this.executeStep(step, envVars, projectDir);
|
const stepLog = await this.executeStep(step, envVars);
|
||||||
logs += stepLog + '\n';
|
logs += stepLog + '\n';
|
||||||
|
|
||||||
// 记录步骤执行完成的日志
|
// 记录步骤执行完成的日志
|
||||||
@@ -84,35 +90,107 @@ export class PipelineRunner {
|
|||||||
// 实时更新日志
|
// 实时更新日志
|
||||||
await prisma.deployment.update({
|
await prisma.deployment.update({
|
||||||
where: { id: this.deploymentId },
|
where: { id: this.deploymentId },
|
||||||
data: { buildLog: logs }
|
data: { buildLog: logs },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hasError = true;
|
hasError = true;
|
||||||
logs += `[${new Date().toISOString()}] Error: ${(error as Error).message}\n`;
|
const errorMsg = `[${new Date().toISOString()}] Error: ${(error as Error).message}\n`;
|
||||||
|
logs += errorMsg;
|
||||||
|
|
||||||
|
log.error(
|
||||||
|
this.TAG,
|
||||||
|
'Pipeline execution failed: %s',
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
|
||||||
// 记录错误日志
|
// 记录错误日志
|
||||||
await prisma.deployment.update({
|
await prisma.deployment.update({
|
||||||
where: { id: this.deploymentId },
|
where: { id: this.deploymentId },
|
||||||
data: {
|
data: {
|
||||||
buildLog: logs,
|
buildLog: logs,
|
||||||
status: 'failed'
|
status: 'failed',
|
||||||
}
|
finishedAt: new Date(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
}
|
||||||
// 更新最终状态
|
|
||||||
if (!hasError) {
|
// 更新最终状态
|
||||||
await prisma.deployment.update({
|
if (!hasError) {
|
||||||
where: { id: this.deploymentId },
|
await prisma.deployment.update({
|
||||||
data: {
|
where: { id: this.deploymentId },
|
||||||
buildLog: logs,
|
data: {
|
||||||
status: 'success',
|
buildLog: logs,
|
||||||
finishedAt: new Date()
|
status: 'success',
|
||||||
}
|
finishedAt: new Date(),
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备工作目录:检查状态、克隆或更新代码
|
||||||
|
* @param project 项目信息
|
||||||
|
* @param branch 目标分支
|
||||||
|
* @returns 准备过程的日志
|
||||||
|
*/
|
||||||
|
private async prepareWorkspace(
|
||||||
|
project: any,
|
||||||
|
branch: string,
|
||||||
|
): Promise<string> {
|
||||||
|
let logs = '';
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logs += `[${timestamp}] 检查工作目录状态: ${this.projectDir}\n`;
|
||||||
|
|
||||||
|
// 检查工作目录状态
|
||||||
|
const status = await GitManager.checkWorkspaceStatus(this.projectDir);
|
||||||
|
logs += `[${new Date().toISOString()}] 工作目录状态: ${status.status}\n`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.status === WorkspaceDirStatus.NOT_CREATED ||
|
||||||
|
status.status === WorkspaceDirStatus.EMPTY
|
||||||
|
) {
|
||||||
|
// 目录不存在或为空,需要克隆
|
||||||
|
logs += `[${new Date().toISOString()}] 工作目录不存在或为空,开始克隆仓库\n`;
|
||||||
|
|
||||||
|
// 确保父目录存在
|
||||||
|
await GitManager.ensureDirectory(this.projectDir);
|
||||||
|
|
||||||
|
// 克隆仓库(注意:如果需要认证,token 应该从环境变量或配置中获取)
|
||||||
|
await GitManager.cloneRepository(
|
||||||
|
project.repository,
|
||||||
|
this.projectDir,
|
||||||
|
branch,
|
||||||
|
// TODO: 添加 token 支持
|
||||||
|
);
|
||||||
|
|
||||||
|
logs += `[${new Date().toISOString()}] 仓库克隆成功\n`;
|
||||||
|
} else if (status.status === WorkspaceDirStatus.NO_GIT) {
|
||||||
|
// 目录存在但不是 Git 仓库
|
||||||
|
throw new Error(
|
||||||
|
`工作目录 ${this.projectDir} 已存在但不是 Git 仓库,请检查配置`,
|
||||||
|
);
|
||||||
|
} else if (status.status === WorkspaceDirStatus.READY) {
|
||||||
|
// 已存在 Git 仓库,更新代码
|
||||||
|
logs += `[${new Date().toISOString()}] 工作目录已存在 Git 仓库,开始更新代码\n`;
|
||||||
|
await GitManager.updateRepository(this.projectDir, branch);
|
||||||
|
logs += `[${new Date().toISOString()}] 代码更新成功\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return logs;
|
||||||
|
} catch (error) {
|
||||||
|
const errorLog = `[${new Date().toISOString()}] 准备工作目录失败: ${(error as Error).message}\n`;
|
||||||
|
logs += errorLog;
|
||||||
|
log.error(
|
||||||
|
this.TAG,
|
||||||
|
'Failed to prepare workspace: %s',
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
throw new Error(`准备工作目录失败: ${(error as Error).message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +198,11 @@ export class PipelineRunner {
|
|||||||
* 准备环境变量
|
* 准备环境变量
|
||||||
* @param pipeline 流水线信息
|
* @param pipeline 流水线信息
|
||||||
* @param deployment 部署信息
|
* @param deployment 部署信息
|
||||||
* @param projectDir 项目目录路径
|
|
||||||
*/
|
*/
|
||||||
private prepareEnvironmentVariables(pipeline: any, deployment: any, projectDir: string): Record<string, string> {
|
private prepareEnvironmentVariables(
|
||||||
|
pipeline: any,
|
||||||
|
deployment: any,
|
||||||
|
): Record<string, string> {
|
||||||
const envVars: Record<string, string> = {};
|
const envVars: Record<string, string> = {};
|
||||||
|
|
||||||
// 项目相关信息
|
// 项目相关信息
|
||||||
@@ -138,9 +218,9 @@ export class PipelineRunner {
|
|||||||
// 稀疏检出路径(如果有配置的话)
|
// 稀疏检出路径(如果有配置的话)
|
||||||
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || '';
|
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || '';
|
||||||
|
|
||||||
// 工作空间路径和项目路径
|
// 工作空间路径(使用配置的项目目录)
|
||||||
envVars.WORKSPACE = this.workspace;
|
envVars.WORKSPACE = this.projectDir;
|
||||||
envVars.PROJECT_DIR = projectDir;
|
envVars.PROJECT_DIR = this.projectDir;
|
||||||
|
|
||||||
return envVars;
|
return envVars;
|
||||||
}
|
}
|
||||||
@@ -168,68 +248,37 @@ export class PipelineRunner {
|
|||||||
private addTimestampToLines(content: string, isError = false): string {
|
private addTimestampToLines(content: string, isError = false): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
return content.split('\n')
|
return (
|
||||||
.filter(line => line.trim() !== '')
|
content
|
||||||
.map(line => this.addTimestamp(line, isError))
|
.split('\n')
|
||||||
.join('\n') + '\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 step 步骤对象
|
||||||
* @param envVars 环境变量
|
* @param envVars 环境变量
|
||||||
* @param projectDir 项目目录路径
|
|
||||||
*/
|
*/
|
||||||
private async executeStep(step: Step, envVars: Record<string, string>, projectDir: string): Promise<string> {
|
private async executeStep(
|
||||||
|
step: Step,
|
||||||
|
envVars: Record<string, string>,
|
||||||
|
): Promise<string> {
|
||||||
let logs = '';
|
let logs = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 添加步骤开始执行的时间戳
|
// 添加步骤开始执行的时间戳
|
||||||
logs += this.addTimestamp(`开始执行步骤 "${step.name}"`) + '\n';
|
logs += this.addTimestamp(`执行脚本: ${step.script}`) + '\n';
|
||||||
|
|
||||||
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
||||||
const script = step.script;
|
const script = step.script;
|
||||||
|
|
||||||
// 通过bash -c执行脚本,确保环境变量能被正确解析
|
// 通过bash -c执行脚本,确保环境变量能被正确解析
|
||||||
const result = await $({
|
const result = await $({
|
||||||
cwd: projectDir,
|
cwd: this.projectDir,
|
||||||
env: { ...process.env, ...envVars }
|
env: { ...process.env, ...envVars },
|
||||||
})`bash -c ${script}`;
|
})`bash -c ${script}`;
|
||||||
|
|
||||||
if (result.stdout) {
|
if (result.stdout) {
|
||||||
@@ -242,10 +291,11 @@ export class PipelineRunner {
|
|||||||
logs += this.addTimestampToLines(result.stderr, true);
|
logs += this.addTimestampToLines(result.stderr, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加步骤执行完成的时间戳
|
logs += this.addTimestamp(`步骤执行完成`) + '\n';
|
||||||
logs += this.addTimestamp(`步骤 "${step.name}" 执行完成`) + '\n';
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logs += this.addTimestamp(`Error executing step "${step.name}": ${(error as Error).message}`) + '\n';
|
const errorMsg = `Error executing step "${step.name}": ${(error as Error).message}`;
|
||||||
|
logs += this.addTimestamp(errorMsg, true) + '\n';
|
||||||
|
log.error(this.TAG, errorMsg);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.8.0",
|
"react-router": "^7.8.0",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
|
import { formatDateTime } from '../../../../utils/time';
|
||||||
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import type { Branch, Commit, Pipeline } from '../../types';
|
import type { Branch, Commit, Pipeline } from '../../types';
|
||||||
@@ -182,7 +183,7 @@ function DeployModal({
|
|||||||
{commit.sha.substring(0, 7)}
|
{commit.sha.substring(0, 7)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-500 text-xs">
|
<span className="text-gray-500 text-xs">
|
||||||
{new Date(commit.commit.author.date).toLocaleString()}
|
{formatDateTime(commit.commit.author.date)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-600 text-sm truncate">
|
<div className="text-gray-600 text-sm truncate">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { List, Space, Tag } from '@arco-design/web-react';
|
import { List, Space, Tag } from '@arco-design/web-react';
|
||||||
|
import { formatDateTime } from '../../../../utils/time';
|
||||||
import type { Deployment } from '../../types';
|
import type { Deployment } from '../../types';
|
||||||
|
|
||||||
interface DeployRecordItemProps {
|
interface DeployRecordItemProps {
|
||||||
@@ -76,7 +77,7 @@ function DeployRecordItem({
|
|||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
执行时间:{' '}
|
执行时间:{' '}
|
||||||
<span className="font-medium text-gray-700">
|
<span className="font-medium text-gray-700">
|
||||||
{item.createdAt}
|
{formatDateTime(item.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Descriptions,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Empty,
|
Empty,
|
||||||
Form,
|
Form,
|
||||||
@@ -10,19 +11,24 @@ import {
|
|||||||
Message,
|
Message,
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
|
IconCode,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
|
IconFolder,
|
||||||
|
IconHistory,
|
||||||
IconMore,
|
IconMore,
|
||||||
IconPlayArrow,
|
IconPlayArrow,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
|
IconSettings,
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
@@ -40,9 +46,10 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
import type { Deployment, Pipeline, Project, Step } from '../types';
|
import { formatDateTime } from '../../../utils/time';
|
||||||
|
import type { Deployment, Pipeline, Project, Step, WorkspaceDirStatus, WorkspaceStatus } from '../types';
|
||||||
import DeployModal from './components/DeployModal';
|
import DeployModal from './components/DeployModal';
|
||||||
import DeployRecordItem from './components/DeployRecordItem';
|
import DeployRecordItem from './components/DeployRecordItem';
|
||||||
import PipelineStepItem from './components/PipelineStepItem';
|
import PipelineStepItem from './components/PipelineStepItem';
|
||||||
@@ -59,6 +66,7 @@ interface PipelineWithEnabled extends Pipeline {
|
|||||||
|
|
||||||
function ProjectDetailPage() {
|
function ProjectDetailPage() {
|
||||||
const [detail, setDetail] = useState<Project | null>();
|
const [detail, setDetail] = useState<Project | null>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 拖拽传感器配置
|
// 拖拽传感器配置
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
@@ -87,6 +95,10 @@ function ProjectDetailPage() {
|
|||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
||||||
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
||||||
|
|
||||||
|
// 项目设置相关状态
|
||||||
|
const [projectEditModalVisible, setProjectEditModalVisible] = useState(false);
|
||||||
|
const [projectForm] = Form.useForm();
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
// 获取可用的流水线模板
|
// 获取可用的流水线模板
|
||||||
@@ -580,6 +592,56 @@ function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 项目设置相关函数
|
||||||
|
const handleEditProject = () => {
|
||||||
|
if (detail) {
|
||||||
|
projectForm.setFieldsValue({
|
||||||
|
name: detail.name,
|
||||||
|
description: detail.description,
|
||||||
|
repository: detail.repository,
|
||||||
|
});
|
||||||
|
setProjectEditModalVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProjectEditSuccess = async () => {
|
||||||
|
try {
|
||||||
|
const values = await projectForm.validate();
|
||||||
|
await detailService.updateProject(Number(id), values);
|
||||||
|
Message.success('项目更新成功');
|
||||||
|
setProjectEditModalVisible(false);
|
||||||
|
|
||||||
|
// 刷新项目详情
|
||||||
|
if (id) {
|
||||||
|
const projectDetail = await detailService.getProjectDetail(Number(id));
|
||||||
|
setDetail(projectDetail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新项目失败:', error);
|
||||||
|
Message.error('更新项目失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteProject = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '删除项目',
|
||||||
|
content: `确定要删除项目 "${detail?.name}" 吗?删除后将无法恢复。`,
|
||||||
|
okButtonProps: {
|
||||||
|
status: 'danger',
|
||||||
|
},
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await detailService.deleteProject(Number(id));
|
||||||
|
Message.success('项目删除成功');
|
||||||
|
navigate('/project');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除项目失败:', error);
|
||||||
|
Message.error('删除项目失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const selectedRecord = deployRecords.find(
|
const selectedRecord = deployRecords.find(
|
||||||
(record) => record.id === selectedRecordId,
|
(record) => record.id === selectedRecordId,
|
||||||
);
|
);
|
||||||
@@ -613,6 +675,74 @@ function ProjectDetailPage() {
|
|||||||
(pipeline) => pipeline.id === selectedPipelineId,
|
(pipeline) => pipeline.id === selectedPipelineId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取工作目录状态标签
|
||||||
|
const getWorkspaceStatusTag = (status: string): { text: string; color: string } => {
|
||||||
|
const statusMap: Record<string, { text: string; color: string }> = {
|
||||||
|
not_created: { text: '未创建', color: 'gray' },
|
||||||
|
empty: { text: '空目录', color: 'orange' },
|
||||||
|
no_git: { text: '无Git仓库', color: 'orange' },
|
||||||
|
ready: { text: '就绪', color: 'green' },
|
||||||
|
};
|
||||||
|
return statusMap[status] || { text: '未知', color: 'gray' };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染工作目录状态卡片
|
||||||
|
const renderWorkspaceStatus = () => {
|
||||||
|
if (!detail?.workspaceStatus) return null;
|
||||||
|
|
||||||
|
const { workspaceStatus } = detail;
|
||||||
|
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-6" title={<Space><IconFolder />工作目录状态</Space>}>
|
||||||
|
<Descriptions
|
||||||
|
column={2}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: '目录路径',
|
||||||
|
value: detail.projectDir,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '状态',
|
||||||
|
value: <Tag color={statusInfo.color}>{statusInfo.text}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '目录大小',
|
||||||
|
value: workspaceStatus.size ? formatSize(workspaceStatus.size) : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '当前分支',
|
||||||
|
value: workspaceStatus.gitInfo?.branch || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '最后提交',
|
||||||
|
value: workspaceStatus.gitInfo?.lastCommit ? (
|
||||||
|
<Space direction="vertical" size="mini">
|
||||||
|
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
) : '-',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{workspaceStatus.error && (
|
||||||
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
||||||
|
<Typography.Text type="danger">{workspaceStatus.error}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 flex flex-col h-full">
|
<div className="p-6 flex flex-col h-full">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
@@ -626,13 +756,14 @@ function ProjectDetailPage() {
|
|||||||
部署
|
部署
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md flex-1 flex flex-col overflow-hidden">
|
<div className="bg-white p-6 rounded-lg shadow-md flex-1 flex flex-col overflow-hidden">
|
||||||
<Tabs
|
<Tabs
|
||||||
type="line"
|
type="line"
|
||||||
size="large"
|
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-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">
|
<Tabs.TabPane title={<Space><IconHistory />部署记录</Space>} key="deployRecords">
|
||||||
<div className="grid grid-cols-5 gap-6 h-full">
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
{/* 左侧部署记录列表 */}
|
{/* 左侧部署记录列表 */}
|
||||||
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
||||||
@@ -671,7 +802,7 @@ function ProjectDetailPage() {
|
|||||||
{selectedRecord && (
|
{selectedRecord && (
|
||||||
<Typography.Text type="secondary" className="text-sm">
|
<Typography.Text type="secondary" className="text-sm">
|
||||||
{selectedRecord.branch} · {selectedRecord.env} ·{' '}
|
{selectedRecord.branch} · {selectedRecord.env} ·{' '}
|
||||||
{selectedRecord.createdAt}
|
{formatDateTime(selectedRecord.createdAt)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -707,7 +838,7 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane title="流水线" key="pipeline">
|
<Tabs.TabPane title={<Space><IconCode />流水线</Space>} key="pipeline">
|
||||||
<div className="grid grid-cols-5 gap-6 h-full">
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
{/* 左侧流水线列表 */}
|
{/* 左侧流水线列表 */}
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
@@ -823,7 +954,7 @@ function ProjectDetailPage() {
|
|||||||
<span>
|
<span>
|
||||||
{pipeline.steps?.length || 0} 个步骤
|
{pipeline.steps?.length || 0} 个步骤
|
||||||
</span>
|
</span>
|
||||||
<span>{pipeline.updatedAt}</span>
|
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -913,6 +1044,50 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
{/* 项目设置标签页 */}
|
||||||
|
<Tabs.TabPane key="settings" title={<Space><IconSettings />项目设置</Space>}>
|
||||||
|
<div className="p-6">
|
||||||
|
<Card title="项目信息" className="mb-4">
|
||||||
|
<Descriptions
|
||||||
|
column={1}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: '项目名称',
|
||||||
|
value: detail?.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '项目描述',
|
||||||
|
value: detail?.description || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Git 仓库',
|
||||||
|
value: detail?.repository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '工作目录',
|
||||||
|
value: detail?.projectDir || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
value: formatDateTime(detail?.createdAt),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 flex gap-2">
|
||||||
|
<Button type="primary" onClick={handleEditProject}>
|
||||||
|
编辑项目
|
||||||
|
</Button>
|
||||||
|
<Button status="danger" onClick={handleDeleteProject}>
|
||||||
|
删除项目
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 工作目录状态 */}
|
||||||
|
{renderWorkspaceStatus()}
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1053,6 +1228,47 @@ function ProjectDetailPage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 编辑项目模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="编辑项目"
|
||||||
|
visible={projectEditModalVisible}
|
||||||
|
onOk={handleProjectEditSuccess}
|
||||||
|
onCancel={() => setProjectEditModalVisible(false)}
|
||||||
|
style={{ width: 500 }}
|
||||||
|
>
|
||||||
|
<Form form={projectForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
field="name"
|
||||||
|
label="项目名称"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入项目名称' },
|
||||||
|
{ minLength: 2, message: '项目名称至少2个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:我的应用" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
field="description"
|
||||||
|
label="项目描述"
|
||||||
|
rules={[{ maxLength: 200, message: '描述不能超过200个字符' }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="请输入项目描述"
|
||||||
|
rows={3}
|
||||||
|
maxLength={200}
|
||||||
|
showWordLimit
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
field="repository"
|
||||||
|
label="Git 仓库地址"
|
||||||
|
rules={[{ required: true, message: '请输入仓库地址' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:https://github.com/user/repo.git" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<DeployModal
|
<DeployModal
|
||||||
visible={deployModalVisible}
|
visible={deployModalVisible}
|
||||||
onCancel={() => setDeployModalVisible(false)}
|
onCancel={() => setDeployModalVisible(false)}
|
||||||
|
|||||||
@@ -192,6 +192,35 @@ class DetailService {
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取项目详情(包含工作目录状态)
|
||||||
|
async getProjectDetail(id: number) {
|
||||||
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
|
url: `/api/projects/${id}`,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新项目
|
||||||
|
async updateProject(
|
||||||
|
id: number,
|
||||||
|
project: Partial<{ name: string; description: string; repository: string }>,
|
||||||
|
) {
|
||||||
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
|
url: `/api/projects/${id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
data: project,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除项目
|
||||||
|
async deleteProject(id: number) {
|
||||||
|
await net.request({
|
||||||
|
url: `/api/projects/${id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const detailService = new DetailService();
|
export const detailService = new DetailService();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { projectService } from '../service';
|
import { projectService } from '../service';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../../types';
|
||||||
|
|
||||||
interface CreateProjectModalProps {
|
interface CreateProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -97,6 +97,33 @@ function CreateProjectModal({
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="工作目录路径"
|
||||||
|
field="projectDir"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入工作目录路径' },
|
||||||
|
{
|
||||||
|
validator: (value, cb) => {
|
||||||
|
if (!value) {
|
||||||
|
return cb('工作目录路径不能为空');
|
||||||
|
}
|
||||||
|
if (!value.startsWith('/')) {
|
||||||
|
return cb('工作目录路径必须是绝对路径(以 / 开头)');
|
||||||
|
}
|
||||||
|
if (value.includes('..') || value.includes('~')) {
|
||||||
|
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||||
|
}
|
||||||
|
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
||||||
|
return cb('路径包含非法字符');
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Dropdown,
|
|
||||||
Menu,
|
|
||||||
Modal,
|
|
||||||
Space,
|
Space,
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
IconBranch,
|
IconBranch,
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconCloud,
|
IconCloud,
|
||||||
IconDelete,
|
|
||||||
IconEdit,
|
|
||||||
IconMore,
|
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import IconGitea from '@assets/images/gitea.svg?react';
|
import IconGitea from '@assets/images/gitea.svg?react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -27,27 +19,10 @@ const { Text, Paragraph } = Typography;
|
|||||||
|
|
||||||
interface ProjectCardProps {
|
interface ProjectCardProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
onEdit?: (project: Project) => void;
|
|
||||||
onDelete?: (project: Project) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
function ProjectCard({ project }: ProjectCardProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// 处理删除操作
|
|
||||||
const handleDelete = () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认删除项目',
|
|
||||||
content: `确定要删除项目 "${project.name}" 吗?此操作不可恢复。`,
|
|
||||||
okText: '删除',
|
|
||||||
cancelText: '取消',
|
|
||||||
okButtonProps: {
|
|
||||||
status: 'danger',
|
|
||||||
},
|
|
||||||
onOk: () => {
|
|
||||||
onDelete?.(project);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// 获取环境信息
|
// 获取环境信息
|
||||||
const environments = [
|
const environments = [
|
||||||
{ name: 'staging', color: 'orange', icon: '🚧' },
|
{ name: 'staging', color: 'orange', icon: '🚧' },
|
||||||
@@ -109,37 +84,9 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<Tag color="blue" size="small" className="font-medium">
|
||||||
<Tag color="blue" size="small" className="font-medium">
|
活跃
|
||||||
活跃
|
</Tag>
|
||||||
</Tag>
|
|
||||||
<Dropdown
|
|
||||||
droplist={
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key="edit" onClick={() => onEdit?.(project)}>
|
|
||||||
<IconEdit className="mr-2" />
|
|
||||||
编辑
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
key="delete"
|
|
||||||
onClick={() => handleDelete()}
|
|
||||||
className="text-red-500"
|
|
||||||
>
|
|
||||||
<IconDelete className="mr-2" />
|
|
||||||
删除
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
position="br"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<IconMore />}
|
|
||||||
className="text-gray-400 hover:text-blue-500 hover:bg-blue-50 transition-all duration-200 p-1 rounded-md"
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 项目描述 */}
|
{/* 项目描述 */}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useState } from 'react';
|
|||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../types';
|
||||||
import CreateProjectModal from './components/CreateProjectModal';
|
import CreateProjectModal from './components/CreateProjectModal';
|
||||||
import EditProjectModal from './components/EditProjectModal';
|
|
||||||
import ProjectCard from './components/ProjectCard';
|
import ProjectCard from './components/ProjectCard';
|
||||||
import { projectService } from './service';
|
import { projectService } from './service';
|
||||||
|
|
||||||
@@ -12,8 +11,6 @@ const { Text } = Typography;
|
|||||||
|
|
||||||
function ProjectPage() {
|
function ProjectPage() {
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
|
||||||
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
||||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
@@ -21,22 +18,6 @@ function ProjectPage() {
|
|||||||
setProjects(response.data);
|
setProjects(response.data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEditProject = (project: Project) => {
|
|
||||||
setEditingProject(project);
|
|
||||||
setEditModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditSuccess = (updatedProject: Project) => {
|
|
||||||
setProjects((prev) =>
|
|
||||||
prev.map((p) => (p.id === updatedProject.id ? updatedProject : p)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditCancel = () => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
setEditingProject(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateProject = () => {
|
const handleCreateProject = () => {
|
||||||
setCreateModalVisible(true);
|
setCreateModalVisible(true);
|
||||||
};
|
};
|
||||||
@@ -49,17 +30,6 @@ function ProjectPage() {
|
|||||||
setCreateModalVisible(false);
|
setCreateModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteProject = async (project: Project) => {
|
|
||||||
try {
|
|
||||||
await projectService.delete(project.id);
|
|
||||||
setProjects((prev) => prev.filter((p) => p.id !== project.id));
|
|
||||||
Message.success('项目删除成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除项目失败:', error);
|
|
||||||
Message.error('删除项目失败,请稍后重试');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
@@ -82,22 +52,11 @@ function ProjectPage() {
|
|||||||
<Grid.Row gutter={[16, 16]}>
|
<Grid.Row gutter={[16, 16]}>
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<Grid.Col key={project.id} span={8}>
|
<Grid.Col key={project.id} span={8}>
|
||||||
<ProjectCard
|
<ProjectCard project={project} />
|
||||||
project={project}
|
|
||||||
onEdit={handleEditProject}
|
|
||||||
onDelete={handleDeleteProject}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
))}
|
))}
|
||||||
</Grid.Row>
|
</Grid.Row>
|
||||||
|
|
||||||
<EditProjectModal
|
|
||||||
visible={editModalVisible}
|
|
||||||
project={editingProject}
|
|
||||||
onCancel={handleEditCancel}
|
|
||||||
onSuccess={handleEditSuccess}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreateProjectModal
|
<CreateProjectModal
|
||||||
visible={createModalVisible}
|
visible={createModalVisible}
|
||||||
onCancel={handleCreateCancel}
|
onCancel={handleCreateCancel}
|
||||||
|
|||||||
@@ -4,17 +4,45 @@ enum BuildStatus {
|
|||||||
Stopped = 'Stopped',
|
Stopped = 'Stopped',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 工作目录状态枚举
|
||||||
|
export enum WorkspaceDirStatus {
|
||||||
|
NOT_CREATED = 'not_created',
|
||||||
|
EMPTY = 'empty',
|
||||||
|
NO_GIT = 'no_git',
|
||||||
|
READY = 'ready',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git 仓库信息
|
||||||
|
export interface GitInfo {
|
||||||
|
branch?: string;
|
||||||
|
lastCommit?: string;
|
||||||
|
lastCommitMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工作目录状态信息
|
||||||
|
export interface WorkspaceStatus {
|
||||||
|
status: WorkspaceDirStatus;
|
||||||
|
exists: boolean;
|
||||||
|
isEmpty?: boolean;
|
||||||
|
hasGit?: boolean;
|
||||||
|
size?: number;
|
||||||
|
gitInfo?: GitInfo;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
|
projectDir: string; // 项目工作目录路径(必填)
|
||||||
valid: number;
|
valid: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
updatedBy: string;
|
updatedBy: string;
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
workspaceStatus?: WorkspaceStatus; // 工作目录状态信息
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流水线步骤类型定义
|
// 流水线步骤类型定义
|
||||||
|
|||||||
31
apps/web/src/utils/time.ts
Normal file
31
apps/web/src/utils/time.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间为 YYYY-MM-DD HH:mm:ss
|
||||||
|
* @param date 时间字符串或 Date 对象
|
||||||
|
* @returns 格式化后的时间字符串
|
||||||
|
*/
|
||||||
|
export function formatDateTime(date: string | Date | undefined | null): string {
|
||||||
|
if (!date) return '-';
|
||||||
|
return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间为 YYYY-MM-DD
|
||||||
|
* @param date 时间字符串或 Date 对象
|
||||||
|
* @returns 格式化后的日期字符串
|
||||||
|
*/
|
||||||
|
export function formatDate(date: string | Date | undefined | null): string {
|
||||||
|
if (!date) return '-';
|
||||||
|
return dayjs(date).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间为 HH:mm:ss
|
||||||
|
* @param date 时间字符串或 Date 对象
|
||||||
|
* @returns 格式化后的时间字符串
|
||||||
|
*/
|
||||||
|
export function formatTime(date: string | Date | undefined | null): string {
|
||||||
|
if (!date) return '-';
|
||||||
|
return dayjs(date).format('HH:mm:ss');
|
||||||
|
}
|
||||||
866
pnpm-lock.yaml
generated
866
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user