Compare commits
7 Commits
ef4fce6d42
...
001-projec
| Author | SHA1 | Date | |
|---|---|---|---|
| b5c550f5c5 | |||
| 9897bd04c2 | |||
| 73240d94b1 | |||
| 378070179f | |||
| 02b7c3edb2 | |||
| f8697b87e1 | |||
| fd0cf782c4 |
7
apps/server/README.md
Normal file
7
apps/server/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
## 表
|
||||
- user
|
||||
- project
|
||||
- pipeline
|
||||
- deployment
|
||||
- runner
|
||||
@@ -1,13 +1,32 @@
|
||||
import Koa from 'koa';
|
||||
import { initMiddlewares } from './middlewares/index.ts';
|
||||
import { log } from './libs/logger.ts';
|
||||
import { ExecutionQueue } from './libs/execution-queue.ts';
|
||||
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
||||
|
||||
const app = new Koa();
|
||||
// 初始化应用
|
||||
async function initializeApp() {
|
||||
// 初始化流水线模板
|
||||
await initializePipelineTemplates();
|
||||
|
||||
initMiddlewares(app);
|
||||
// 初始化执行队列
|
||||
const executionQueue = ExecutionQueue.getInstance();
|
||||
await executionQueue.initialize();
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const app = new Koa();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
initMiddlewares(app);
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
log.info('APP', 'Server started at port %d', PORT);
|
||||
log.info('QUEUE', 'Execution queue initialized');
|
||||
});
|
||||
}
|
||||
|
||||
// 启动应用
|
||||
initializeApp().catch(error => {
|
||||
console.error('Failed to start application:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
7
apps/server/controllers/auth/dto.ts
Normal file
7
apps/server/controllers/auth/dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
code: z.string().min(1, { message: 'Code不能为空' }),
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Context } from 'koa';
|
||||
import { Controller, Get, Post } from '../../decorators/route.ts';
|
||||
import prisma from '../../libs/db.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { log } from '../../libs/logger.ts';
|
||||
import { gitea } from '../../libs/gitea.ts';
|
||||
import { loginSchema } from './dto.ts';
|
||||
|
||||
@Controller('/auth')
|
||||
export class AuthController {
|
||||
@@ -20,7 +21,7 @@ export class AuthController {
|
||||
if (ctx.session.user) {
|
||||
return ctx.session.user;
|
||||
}
|
||||
const { code } = ctx.request.body as LoginRequestBody;
|
||||
const { code } = loginSchema.parse(ctx.request.body);
|
||||
const { access_token, refresh_token, expires_in } =
|
||||
await gitea.getToken(code);
|
||||
const giteaAuth = {
|
||||
@@ -81,7 +82,3 @@ export class AuthController {
|
||||
return ctx.session?.user;
|
||||
}
|
||||
}
|
||||
|
||||
interface LoginRequestBody {
|
||||
code: string;
|
||||
}
|
||||
|
||||
20
apps/server/controllers/deployment/dto.ts
Normal file
20
apps/server/controllers/deployment/dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const listDeploymentsQuerySchema = z.object({
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
pageSize: z.coerce.number().int().min(1).max(100).optional().default(10),
|
||||
projectId: z.coerce.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
export const createDeploymentSchema = z.object({
|
||||
projectId: z.number().int().positive({ message: '项目ID必须是正整数' }),
|
||||
pipelineId: z.number().int().positive({ message: '流水线ID必须是正整数' }),
|
||||
branch: z.string().min(1, { message: '分支不能为空' }),
|
||||
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
||||
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
||||
env: z.string().optional(),
|
||||
sparseCheckoutPaths: z.string().optional(), // 添加稀疏检出路径字段
|
||||
});
|
||||
|
||||
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
||||
export type CreateDeploymentInput = z.infer<typeof createDeploymentSchema>;
|
||||
119
apps/server/controllers/deployment/index.ts
Normal file
119
apps/server/controllers/deployment/index.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Controller, Get, Post } from '../../decorators/route.ts';
|
||||
import type { Prisma } from '../../generated/client.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import type { Context } from 'koa';
|
||||
import { listDeploymentsQuerySchema, createDeploymentSchema } from './dto.ts';
|
||||
import { ExecutionQueue } from '../../libs/execution-queue.ts';
|
||||
|
||||
@Controller('/deployments')
|
||||
export class DeploymentController {
|
||||
@Get('')
|
||||
async list(ctx: Context) {
|
||||
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(ctx.query);
|
||||
const where: Prisma.DeploymentWhereInput = {
|
||||
valid: 1,
|
||||
};
|
||||
|
||||
if (projectId) {
|
||||
where.projectId = projectId;
|
||||
}
|
||||
|
||||
const result = await prisma.deployment.findMany({
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
const total = await prisma.deployment.count({ where });
|
||||
|
||||
return {
|
||||
data: result,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('')
|
||||
async create(ctx: Context) {
|
||||
const body = createDeploymentSchema.parse(ctx.request.body);
|
||||
|
||||
const result = await prisma.deployment.create({
|
||||
data: {
|
||||
branch: body.branch,
|
||||
commitHash: body.commitHash,
|
||||
commitMessage: body.commitMessage,
|
||||
status: 'pending',
|
||||
Project: {
|
||||
connect: { id: body.projectId },
|
||||
},
|
||||
pipelineId: body.pipelineId,
|
||||
env: body.env || 'dev',
|
||||
sparseCheckoutPaths: body.sparseCheckoutPaths || '', // 添加稀疏检出路径
|
||||
buildLog: '',
|
||||
createdBy: 'system', // TODO: get from user
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 将新创建的部署任务添加到执行队列
|
||||
const executionQueue = ExecutionQueue.getInstance();
|
||||
await executionQueue.addTask(result.id, result.pipelineId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 添加重新执行部署的接口
|
||||
@Post('/:id/retry')
|
||||
async retry(ctx: Context) {
|
||||
const { id } = ctx.params;
|
||||
|
||||
// 获取原始部署记录
|
||||
const originalDeployment = await prisma.deployment.findUnique({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
if (!originalDeployment) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
code: 404,
|
||||
message: '部署记录不存在',
|
||||
data: null,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个新的部署记录,复制原始记录的信息
|
||||
const newDeployment = await prisma.deployment.create({
|
||||
data: {
|
||||
branch: originalDeployment.branch,
|
||||
commitHash: originalDeployment.commitHash,
|
||||
commitMessage: originalDeployment.commitMessage,
|
||||
status: 'pending',
|
||||
projectId: originalDeployment.projectId,
|
||||
pipelineId: originalDeployment.pipelineId,
|
||||
env: originalDeployment.env,
|
||||
sparseCheckoutPaths: originalDeployment.sparseCheckoutPaths,
|
||||
buildLog: '',
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 将新创建的部署任务添加到执行队列
|
||||
const executionQueue = ExecutionQueue.getInstance();
|
||||
await executionQueue.addTask(newDeployment.id, newDeployment.pipelineId);
|
||||
|
||||
ctx.body = {
|
||||
code: 0,
|
||||
message: '重新执行任务已创建',
|
||||
data: newDeployment,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
13
apps/server/controllers/git/dto.ts
Normal file
13
apps/server/controllers/git/dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const getCommitsQuerySchema = z.object({
|
||||
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
||||
branch: z.string().optional(),
|
||||
});
|
||||
|
||||
export const getBranchesQuerySchema = z.object({
|
||||
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
||||
});
|
||||
|
||||
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
||||
export type GetBranchesQuery = z.infer<typeof getBranchesQuerySchema>;
|
||||
113
apps/server/controllers/git/index.ts
Normal file
113
apps/server/controllers/git/index.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { Context } from 'koa';
|
||||
import { Controller, Get } from '../../decorators/route.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { gitea } from '../../libs/gitea.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { getCommitsQuerySchema, getBranchesQuerySchema } from './dto.ts';
|
||||
|
||||
@Controller('/git')
|
||||
export class GitController {
|
||||
@Get('/commits')
|
||||
async getCommits(ctx: Context) {
|
||||
const { projectId, branch } = getCommitsQuerySchema.parse(ctx.query);
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: {
|
||||
id: projectId,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BusinessError('Project not found', 1002, 404);
|
||||
}
|
||||
|
||||
// Parse repository URL to get owner and repo
|
||||
// Supports:
|
||||
// https://gitea.com/owner/repo.git
|
||||
// http://gitea.com/owner/repo
|
||||
const { owner, repo } = this.parseRepoUrl(project.repository);
|
||||
|
||||
// Get access token from session
|
||||
const accessToken = ctx.session?.gitea?.access_token;
|
||||
console.log('Access token present:', !!accessToken);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
||||
}
|
||||
|
||||
try {
|
||||
const commits = await gitea.getCommits(owner, repo, accessToken, branch);
|
||||
return commits;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch commits:', error);
|
||||
throw new BusinessError('Failed to fetch commits from Gitea', 1005, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/branches')
|
||||
async getBranches(ctx: Context) {
|
||||
const { projectId } = getBranchesQuerySchema.parse(ctx.query);
|
||||
|
||||
const project = await prisma.project.findFirst({
|
||||
where: {
|
||||
id: projectId,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new BusinessError('Project not found', 1002, 404);
|
||||
}
|
||||
|
||||
const { owner, repo } = this.parseRepoUrl(project.repository);
|
||||
|
||||
const accessToken = ctx.session?.gitea?.access_token;
|
||||
|
||||
if (!accessToken) {
|
||||
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
||||
}
|
||||
|
||||
try {
|
||||
const branches = await gitea.getBranches(owner, repo, accessToken);
|
||||
return branches;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch branches:', error);
|
||||
throw new BusinessError('Failed to fetch branches from Gitea', 1006, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private parseRepoUrl(url: string) {
|
||||
let cleanUrl = url.trim();
|
||||
if (cleanUrl.endsWith('/')) {
|
||||
cleanUrl = cleanUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
// Handle SCP-like syntax: git@host:owner/repo.git
|
||||
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
|
||||
const scpMatch = cleanUrl.match(/:([^\/]+)\/([^\/]+?)(\.git)?$/);
|
||||
if (scpMatch) {
|
||||
return { owner: scpMatch[1], repo: scpMatch[2] };
|
||||
}
|
||||
}
|
||||
|
||||
// Handle HTTP/HTTPS/SSH URLs
|
||||
try {
|
||||
const urlObj = new URL(cleanUrl);
|
||||
const parts = urlObj.pathname.split('/').filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
const repo = parts.pop()!.replace(/\.git$/, '');
|
||||
const owner = parts.pop()!;
|
||||
return { owner, repo };
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to simple regex
|
||||
const match = cleanUrl.match(/([^\/]+)\/([^\/]+?)(\.git)?$/);
|
||||
if (match) {
|
||||
return { owner: match[1], repo: match[2] };
|
||||
}
|
||||
}
|
||||
|
||||
throw new BusinessError('Invalid repository URL format', 1003, 400);
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,7 @@
|
||||
export { ProjectController } from './project/index.ts';
|
||||
export { UserController } from './user/index.ts';
|
||||
export { AuthController } from './auth/index.ts';
|
||||
export { DeploymentController } from './deployment/index.ts';
|
||||
export { PipelineController } from './pipeline/index.ts';
|
||||
export { StepController } from './step/index.ts'
|
||||
export { GitController } from './git/index.ts';
|
||||
|
||||
40
apps/server/controllers/pipeline/dto.ts
Normal file
40
apps/server/controllers/pipeline/dto.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// 定义验证架构
|
||||
export const createPipelineSchema = z.object({
|
||||
name: z.string({
|
||||
message: '流水线名称必须是字符串',
|
||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }),
|
||||
|
||||
description: z.string({
|
||||
message: '流水线描述必须是字符串',
|
||||
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
||||
|
||||
projectId: z.number({
|
||||
message: '项目ID必须是数字',
|
||||
}).int().positive({ message: '项目ID必须是正整数' }).optional(),
|
||||
});
|
||||
|
||||
export const updatePipelineSchema = z.object({
|
||||
name: z.string({
|
||||
message: '流水线名称必须是字符串',
|
||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }).optional(),
|
||||
|
||||
description: z.string({
|
||||
message: '流水线描述必须是字符串',
|
||||
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
||||
});
|
||||
|
||||
export const pipelineIdSchema = z.object({
|
||||
id: z.coerce.number().int().positive({ message: '流水线 ID 必须是正整数' }),
|
||||
});
|
||||
|
||||
export const listPipelinesQuerySchema = z.object({
|
||||
projectId: z.coerce.number().int().positive({ message: '项目ID必须是正整数' }).optional(),
|
||||
}).optional();
|
||||
|
||||
// 类型
|
||||
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
||||
export type UpdatePipelineInput = z.infer<typeof updatePipelineSchema>;
|
||||
export type PipelineIdParams = z.infer<typeof pipelineIdSchema>;
|
||||
export type ListPipelinesQuery = z.infer<typeof listPipelinesQuerySchema>;
|
||||
242
apps/server/controllers/pipeline/index.ts
Normal file
242
apps/server/controllers/pipeline/index.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import type { Context } from 'koa';
|
||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { log } from '../../libs/logger.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { getAvailableTemplates, createPipelineFromTemplate } from '../../libs/pipeline-template.ts';
|
||||
import {
|
||||
createPipelineSchema,
|
||||
updatePipelineSchema,
|
||||
pipelineIdSchema,
|
||||
listPipelinesQuerySchema,
|
||||
} from './dto.ts';
|
||||
|
||||
@Controller('/pipelines')
|
||||
export class PipelineController {
|
||||
// GET /api/pipelines - 获取流水线列表
|
||||
@Get('')
|
||||
async list(ctx: Context) {
|
||||
const query = listPipelinesQuerySchema.parse(ctx.query);
|
||||
|
||||
const whereCondition: any = {
|
||||
valid: 1,
|
||||
};
|
||||
|
||||
// 如果提供了项目ID参数
|
||||
if (query?.projectId) {
|
||||
whereCondition.projectId = query.projectId;
|
||||
}
|
||||
|
||||
const pipelines = await prisma.pipeline.findMany({
|
||||
where: whereCondition,
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return pipelines;
|
||||
}
|
||||
|
||||
// GET /api/pipelines/templates - 获取可用的流水线模板
|
||||
@Get('/templates')
|
||||
async getTemplates(ctx: Context) {
|
||||
try {
|
||||
const templates = await getAvailableTemplates();
|
||||
return templates;
|
||||
} catch (error) {
|
||||
console.error('Failed to get templates:', error);
|
||||
throw new BusinessError('获取模板失败', 3002, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/pipelines/:id - 获取单个流水线
|
||||
@Get('/:id')
|
||||
async get(ctx: Context) {
|
||||
const { id } = pipelineIdSchema.parse(ctx.params);
|
||||
|
||||
const pipeline = await prisma.pipeline.findFirst({
|
||||
where: {
|
||||
id,
|
||||
valid: 1,
|
||||
},
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!pipeline) {
|
||||
throw new BusinessError('流水线不存在', 3001, 404);
|
||||
}
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
// POST /api/pipelines - 创建流水线
|
||||
@Post('')
|
||||
async create(ctx: Context) {
|
||||
const validatedData = createPipelineSchema.parse(ctx.request.body);
|
||||
|
||||
const pipeline = await prisma.pipeline.create({
|
||||
data: {
|
||||
name: validatedData.name,
|
||||
description: validatedData.description || '',
|
||||
projectId: validatedData.projectId,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
log.info('pipeline', 'Created new pipeline: %s', pipeline.name);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
// POST /api/pipelines/from-template - 基于模板创建流水线
|
||||
@Post('/from-template')
|
||||
async createFromTemplate(ctx: Context) {
|
||||
try {
|
||||
const { templateId, projectId, name, description } = ctx.request.body as {
|
||||
templateId: number;
|
||||
projectId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// 验证必要参数
|
||||
if (!templateId || !projectId || !name) {
|
||||
throw new BusinessError('缺少必要参数', 3003, 400);
|
||||
}
|
||||
|
||||
// 基于模板创建流水线
|
||||
const newPipelineId = await createPipelineFromTemplate(
|
||||
templateId,
|
||||
projectId,
|
||||
name,
|
||||
description || ''
|
||||
);
|
||||
|
||||
// 返回新创建的流水线
|
||||
const pipeline = await prisma.pipeline.findUnique({
|
||||
where: { id: newPipelineId },
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!pipeline) {
|
||||
throw new BusinessError('创建流水线失败', 3004, 500);
|
||||
}
|
||||
|
||||
log.info('pipeline', 'Created pipeline from template: %s', pipeline.name);
|
||||
return pipeline;
|
||||
} catch (error) {
|
||||
console.error('Failed to create pipeline from template:', error);
|
||||
if (error instanceof BusinessError) {
|
||||
throw error;
|
||||
}
|
||||
throw new BusinessError('基于模板创建流水线失败', 3005, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/pipelines/:id - 更新流水线
|
||||
@Put('/:id')
|
||||
async update(ctx: Context) {
|
||||
const { id } = pipelineIdSchema.parse(ctx.params);
|
||||
const validatedData = updatePipelineSchema.parse(ctx.request.body);
|
||||
|
||||
// 检查流水线是否存在
|
||||
const existingPipeline = await prisma.pipeline.findFirst({
|
||||
where: {
|
||||
id,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingPipeline) {
|
||||
throw new BusinessError('流水线不存在', 3001, 404);
|
||||
}
|
||||
|
||||
// 只更新提供的字段
|
||||
const updateData: any = {
|
||||
updatedBy: 'system',
|
||||
};
|
||||
|
||||
if (validatedData.name !== undefined) {
|
||||
updateData.name = validatedData.name;
|
||||
}
|
||||
if (validatedData.description !== undefined) {
|
||||
updateData.description = validatedData.description;
|
||||
}
|
||||
|
||||
const pipeline = await prisma.pipeline.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
log.info('pipeline', 'Updated pipeline: %s', pipeline.name);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
// DELETE /api/pipelines/:id - 删除流水线(软删除)
|
||||
@Delete('/:id')
|
||||
async destroy(ctx: Context) {
|
||||
const { id } = pipelineIdSchema.parse(ctx.params);
|
||||
|
||||
// 检查流水线是否存在
|
||||
const existingPipeline = await prisma.pipeline.findFirst({
|
||||
where: {
|
||||
id,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingPipeline) {
|
||||
throw new BusinessError('流水线不存在', 3001, 404);
|
||||
}
|
||||
|
||||
// 软删除:将 valid 设置为 0
|
||||
await prisma.pipeline.update({
|
||||
where: { id },
|
||||
data: {
|
||||
valid: 0,
|
||||
updatedBy: 'system',
|
||||
},
|
||||
});
|
||||
|
||||
// 同时软删除关联的步骤
|
||||
await prisma.step.updateMany({
|
||||
where: { pipelineId: id },
|
||||
data: {
|
||||
valid: 0,
|
||||
updatedBy: 'system',
|
||||
},
|
||||
});
|
||||
|
||||
log.info('pipeline', 'Deleted pipeline: %s', existingPipeline.name);
|
||||
|
||||
// RESTful 删除成功返回 204 No Content
|
||||
ctx.status = 204;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { projectDirSchema } from '../../libs/path-validator.js';
|
||||
|
||||
/**
|
||||
* 创建项目验证架构
|
||||
@@ -15,6 +16,8 @@ export const createProjectSchema = z.object({
|
||||
repository: z.string({
|
||||
message: '仓库地址必须是字符串',
|
||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }),
|
||||
|
||||
projectDir: projectDirSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { Context } from 'koa';
|
||||
import prisma from '../../libs/db.ts';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { log } from '../../libs/logger.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import { GitManager } from '../../libs/git-manager.ts';
|
||||
import {
|
||||
createProjectSchema,
|
||||
updateProjectSchema,
|
||||
listProjectQuerySchema,
|
||||
projectIdSchema,
|
||||
} from './schema.ts';
|
||||
} from './dto.ts';
|
||||
|
||||
@Controller('/projects')
|
||||
export class ProjectController {
|
||||
@@ -37,7 +38,7 @@ export class ProjectController {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -47,7 +48,7 @@ export class ProjectController {
|
||||
limit: query?.limit || 10,
|
||||
total,
|
||||
totalPages: Math.ceil(total / (query?.limit || 10)),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +68,48 @@ export class ProjectController {
|
||||
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 - 创建项目
|
||||
@@ -75,18 +117,36 @@ export class ProjectController {
|
||||
async create(ctx: Context) {
|
||||
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({
|
||||
data: {
|
||||
name: validatedData.name,
|
||||
description: validatedData.description || '',
|
||||
repository: validatedData.repository,
|
||||
projectDir: validatedData.projectDir,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
103
apps/server/controllers/step/dto.ts
Normal file
103
apps/server/controllers/step/dto.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// 定义验证架构
|
||||
export const createStepSchema = z.object({
|
||||
name: z
|
||||
.string({
|
||||
message: '步骤名称必须是字符串',
|
||||
})
|
||||
.min(1, { message: '步骤名称不能为空' })
|
||||
.max(100, { message: '步骤名称不能超过100个字符' }),
|
||||
|
||||
description: z
|
||||
.string({
|
||||
message: '步骤描述必须是字符串',
|
||||
})
|
||||
.max(500, { message: '步骤描述不能超过500个字符' })
|
||||
.optional(),
|
||||
|
||||
order: z
|
||||
.number({
|
||||
message: '步骤顺序必须是数字',
|
||||
})
|
||||
.int()
|
||||
.min(0, { message: '步骤顺序必须是非负整数' }),
|
||||
|
||||
script: z
|
||||
.string({
|
||||
message: '脚本命令必须是字符串',
|
||||
})
|
||||
.min(1, { message: '脚本命令不能为空' }),
|
||||
|
||||
pipelineId: z
|
||||
.number({
|
||||
message: '流水线ID必须是数字',
|
||||
})
|
||||
.int()
|
||||
.positive({ message: '流水线ID必须是正整数' }),
|
||||
});
|
||||
|
||||
export const updateStepSchema = z.object({
|
||||
name: z
|
||||
.string({
|
||||
message: '步骤名称必须是字符串',
|
||||
})
|
||||
.min(1, { message: '步骤名称不能为空' })
|
||||
.max(100, { message: '步骤名称不能超过100个字符' })
|
||||
.optional(),
|
||||
|
||||
description: z
|
||||
.string({
|
||||
message: '步骤描述必须是字符串',
|
||||
})
|
||||
.max(500, { message: '步骤描述不能超过500个字符' })
|
||||
.optional(),
|
||||
|
||||
order: z
|
||||
.number({
|
||||
message: '步骤顺序必须是数字',
|
||||
})
|
||||
.int()
|
||||
.min(0, { message: '步骤顺序必须是非负整数' })
|
||||
.optional(),
|
||||
|
||||
script: z
|
||||
.string({
|
||||
message: '脚本命令必须是字符串',
|
||||
})
|
||||
.min(1, { message: '脚本命令不能为空' })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const stepIdSchema = z.object({
|
||||
id: z.coerce.number().int().positive({ message: '步骤 ID 必须是正整数' }),
|
||||
});
|
||||
|
||||
export const listStepsQuerySchema = z
|
||||
.object({
|
||||
pipelineId: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive({ message: '流水线ID必须是正整数' })
|
||||
.optional(),
|
||||
page: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, { message: '页码必须大于0' })
|
||||
.optional()
|
||||
.default(1),
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, { message: '每页数量必须大于0' })
|
||||
.max(100, { message: '每页数量不能超过100' })
|
||||
.optional()
|
||||
.default(10),
|
||||
})
|
||||
.optional();
|
||||
|
||||
// TypeScript 类型
|
||||
export type CreateStepInput = z.infer<typeof createStepSchema>;
|
||||
export type UpdateStepInput = z.infer<typeof updateStepSchema>;
|
||||
export type StepIdParams = z.infer<typeof stepIdSchema>;
|
||||
export type ListStepsQuery = z.infer<typeof listStepsQuerySchema>;
|
||||
181
apps/server/controllers/step/index.ts
Normal file
181
apps/server/controllers/step/index.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import type { Context } from 'koa';
|
||||
import { prisma } from '../../libs/prisma.ts';
|
||||
import { log } from '../../libs/logger.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import {
|
||||
createStepSchema,
|
||||
updateStepSchema,
|
||||
stepIdSchema,
|
||||
listStepsQuerySchema,
|
||||
} from './dto.ts';
|
||||
|
||||
@Controller('/steps')
|
||||
export class StepController {
|
||||
// GET /api/steps - 获取步骤列表
|
||||
@Get('')
|
||||
async list(ctx: Context) {
|
||||
const query = listStepsQuerySchema.parse(ctx.query);
|
||||
|
||||
const whereCondition: any = {
|
||||
valid: 1,
|
||||
};
|
||||
|
||||
// 如果提供了流水线ID参数
|
||||
if (query?.pipelineId) {
|
||||
whereCondition.pipelineId = query.pipelineId;
|
||||
}
|
||||
|
||||
const [total, steps] = await Promise.all([
|
||||
prisma.step.count({ where: whereCondition }),
|
||||
prisma.step.findMany({
|
||||
where: whereCondition,
|
||||
skip: query ? (query.page - 1) * query.limit : 0,
|
||||
take: query?.limit,
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: steps,
|
||||
pagination: {
|
||||
page: query?.page || 1,
|
||||
limit: query?.limit || 10,
|
||||
total,
|
||||
totalPages: Math.ceil(total / (query?.limit || 10)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// GET /api/steps/:id - 获取单个步骤
|
||||
@Get(':id')
|
||||
async show(ctx: Context) {
|
||||
const { id } = stepIdSchema.parse(ctx.params);
|
||||
|
||||
const step = await prisma.step.findFirst({
|
||||
where: {
|
||||
id,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!step) {
|
||||
throw new BusinessError('步骤不存在', 2001, 404);
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
// POST /api/steps - 创建步骤
|
||||
@Post('')
|
||||
async create(ctx: Context) {
|
||||
const validatedData = createStepSchema.parse(ctx.request.body);
|
||||
|
||||
// 检查关联的流水线是否存在
|
||||
const pipeline = await prisma.pipeline.findFirst({
|
||||
where: {
|
||||
id: validatedData.pipelineId,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!pipeline) {
|
||||
throw new BusinessError('关联的流水线不存在', 2002, 404);
|
||||
}
|
||||
|
||||
const step = await prisma.step.create({
|
||||
data: {
|
||||
name: validatedData.name,
|
||||
order: validatedData.order,
|
||||
script: validatedData.script,
|
||||
pipelineId: validatedData.pipelineId,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
log.info('step', 'Created new step: %s', step.name);
|
||||
return step;
|
||||
}
|
||||
|
||||
// PUT /api/steps/:id - 更新步骤
|
||||
@Put(':id')
|
||||
async update(ctx: Context) {
|
||||
const { id } = stepIdSchema.parse(ctx.params);
|
||||
const validatedData = updateStepSchema.parse(ctx.request.body);
|
||||
|
||||
// 检查步骤是否存在
|
||||
const existingStep = await prisma.step.findFirst({
|
||||
where: {
|
||||
id,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingStep) {
|
||||
throw new BusinessError('步骤不存在', 2001, 404);
|
||||
}
|
||||
|
||||
// 只更新提供的字段
|
||||
const updateData: any = {
|
||||
updatedBy: 'system',
|
||||
};
|
||||
|
||||
if (validatedData.name !== undefined) {
|
||||
updateData.name = validatedData.name;
|
||||
}
|
||||
if (validatedData.description !== undefined) {
|
||||
updateData.description = validatedData.description;
|
||||
}
|
||||
if (validatedData.order !== undefined) {
|
||||
updateData.order = validatedData.order;
|
||||
}
|
||||
if (validatedData.script !== undefined) {
|
||||
updateData.script = validatedData.script;
|
||||
}
|
||||
|
||||
const step = await prisma.step.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
log.info('step', 'Updated step: %s', step.name);
|
||||
return step;
|
||||
}
|
||||
|
||||
// DELETE /api/steps/:id - 删除步骤(软删除)
|
||||
@Delete(':id')
|
||||
async destroy(ctx: Context) {
|
||||
const { id } = stepIdSchema.parse(ctx.params);
|
||||
|
||||
// 检查步骤是否存在
|
||||
const existingStep = await prisma.step.findFirst({
|
||||
where: {
|
||||
id,
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingStep) {
|
||||
throw new BusinessError('步骤不存在', 2001, 404);
|
||||
}
|
||||
|
||||
// 软删除:将 valid 设置为 0
|
||||
await prisma.step.update({
|
||||
where: { id },
|
||||
data: {
|
||||
valid: 0,
|
||||
updatedBy: 'system',
|
||||
},
|
||||
});
|
||||
|
||||
log.info('step', 'Deleted step: %s', existingStep.name);
|
||||
|
||||
// RESTful 删除成功返回 204 No Content
|
||||
ctx.status = 204;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
26
apps/server/controllers/user/dto.ts
Normal file
26
apps/server/controllers/user/dto.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const userIdSchema = z.object({
|
||||
id: z.coerce.number().int().positive({ message: '用户ID必须是正整数' }),
|
||||
});
|
||||
|
||||
export const createUserSchema = z.object({
|
||||
name: z.string().min(1, { message: '用户名不能为空' }),
|
||||
email: z.string().email({ message: '邮箱格式不正确' }),
|
||||
status: z.enum(['active', 'inactive']).optional().default('active'),
|
||||
});
|
||||
|
||||
export const updateUserSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
email: z.string().email().optional(),
|
||||
status: z.enum(['active', 'inactive']).optional(),
|
||||
});
|
||||
|
||||
export const searchUserQuerySchema = z.object({
|
||||
keyword: z.string().optional(),
|
||||
status: z.enum(['active', 'inactive']).optional(),
|
||||
});
|
||||
|
||||
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
||||
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
||||
export type SearchUserQuery = z.infer<typeof searchUserQuerySchema>;
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { Context } from 'koa';
|
||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||
import { BusinessError } from '../../middlewares/exception.ts';
|
||||
import {
|
||||
userIdSchema,
|
||||
createUserSchema,
|
||||
updateUserSchema,
|
||||
searchUserQuerySchema,
|
||||
} from './dto.ts';
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
@@ -22,18 +28,18 @@ export class UserController {
|
||||
|
||||
@Get('/detail/:id')
|
||||
async detail(ctx: Context) {
|
||||
const { id } = ctx.params;
|
||||
const { id } = userIdSchema.parse(ctx.params);
|
||||
|
||||
// 模拟根据ID查找用户
|
||||
const user = {
|
||||
id: Number(id),
|
||||
id,
|
||||
name: 'User ' + id,
|
||||
email: `user${id}@example.com`,
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (Number(id) > 100) {
|
||||
if (id > 100) {
|
||||
throw new BusinessError('用户不存在', 2001, 404);
|
||||
}
|
||||
|
||||
@@ -42,14 +48,14 @@ export class UserController {
|
||||
|
||||
@Post('')
|
||||
async create(ctx: Context) {
|
||||
const body = (ctx.request as any).body;
|
||||
const body = createUserSchema.parse(ctx.request.body);
|
||||
|
||||
// 模拟创建用户
|
||||
const newUser = {
|
||||
id: Date.now(),
|
||||
...body,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'active'
|
||||
status: body.status
|
||||
};
|
||||
|
||||
return newUser;
|
||||
@@ -57,12 +63,12 @@ export class UserController {
|
||||
|
||||
@Put('/:id')
|
||||
async update(ctx: Context) {
|
||||
const { id } = ctx.params;
|
||||
const body = (ctx.request as any).body;
|
||||
const { id } = userIdSchema.parse(ctx.params);
|
||||
const body = updateUserSchema.parse(ctx.request.body);
|
||||
|
||||
// 模拟更新用户
|
||||
const updatedUser = {
|
||||
id: Number(id),
|
||||
id,
|
||||
...body,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
@@ -72,9 +78,9 @@ export class UserController {
|
||||
|
||||
@Delete('/:id')
|
||||
async delete(ctx: Context) {
|
||||
const { id } = ctx.params;
|
||||
const { id } = userIdSchema.parse(ctx.params);
|
||||
|
||||
if (Number(id) === 1) {
|
||||
if (id === 1) {
|
||||
throw new BusinessError('管理员账户不能删除', 2002, 403);
|
||||
}
|
||||
|
||||
@@ -88,7 +94,7 @@ export class UserController {
|
||||
|
||||
@Get('/search')
|
||||
async search(ctx: Context) {
|
||||
const { keyword, status } = ctx.query;
|
||||
const { keyword, status } = searchUserQuerySchema.parse(ctx.query);
|
||||
|
||||
// 模拟搜索逻辑
|
||||
let results = [
|
||||
@@ -98,8 +104,8 @@ export class UserController {
|
||||
|
||||
if (keyword) {
|
||||
results = results.filter(user =>
|
||||
user.name.toLowerCase().includes(String(keyword).toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(String(keyword).toLowerCase())
|
||||
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
44
apps/server/generated/browser.ts
Normal file
44
apps/server/generated/browser.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* Use it to get access to models, enums, and input types.
|
||||
*
|
||||
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||
* See `client.ts` for the standard, server-side entry point.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from './enums.ts';
|
||||
/**
|
||||
* Model Project
|
||||
*
|
||||
*/
|
||||
export type Project = Prisma.ProjectModel
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model Pipeline
|
||||
*
|
||||
*/
|
||||
export type Pipeline = Prisma.PipelineModel
|
||||
/**
|
||||
* Model Step
|
||||
*
|
||||
*/
|
||||
export type Step = Prisma.StepModel
|
||||
/**
|
||||
* Model Deployment
|
||||
*
|
||||
*/
|
||||
export type Deployment = Prisma.DeploymentModel
|
||||
66
apps/server/generated/client.ts
Normal file
66
apps/server/generated/client.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as process from 'node:process'
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import * as $Class from "./internal/class.ts"
|
||||
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from "./enums.ts"
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||
*/
|
||||
export const PrismaClient = $Class.getPrismaClientClass()
|
||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export { Prisma }
|
||||
|
||||
/**
|
||||
* Model Project
|
||||
*
|
||||
*/
|
||||
export type Project = Prisma.ProjectModel
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model Pipeline
|
||||
*
|
||||
*/
|
||||
export type Pipeline = Prisma.PipelineModel
|
||||
/**
|
||||
* Model Step
|
||||
*
|
||||
*/
|
||||
export type Step = Prisma.StepModel
|
||||
/**
|
||||
* Model Deployment
|
||||
*
|
||||
*/
|
||||
export type Deployment = Prisma.DeploymentModel
|
||||
402
apps/server/generated/commonInputTypes.ts
Normal file
402
apps/server/generated/commonInputTypes.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import type * as Prisma from "./internal/prismaNamespace.ts"
|
||||
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type StringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type DateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type BoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
}
|
||||
|
||||
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedStringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||
}
|
||||
|
||||
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
||||
in?: Date[] | string[] | null
|
||||
notIn?: Date[] | string[] | null
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
|
||||
15
apps/server/generated/enums.ts
Normal file
15
apps/server/generated/enums.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// This file is empty because there are no enums in the schema.
|
||||
export {}
|
||||
230
apps/server/generated/internal/class.ts
Normal file
230
apps/server/generated/internal/class.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "./prismaNamespace.ts"
|
||||
|
||||
|
||||
const config: runtime.GetPrismaClientConfig = {
|
||||
"previewFeatures": [],
|
||||
"clientVersion": "7.0.0",
|
||||
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513",
|
||||
"activeProvider": "sqlite",
|
||||
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n 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": {
|
||||
"models": {},
|
||||
"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> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||
return new WebAssembly.Module(wasmArray)
|
||||
}
|
||||
|
||||
config.compilerWasm = {
|
||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.sqlite.mjs"),
|
||||
|
||||
getQueryCompilerWasmModule: async () => {
|
||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs")
|
||||
return await decodeBase64AsWasm(wasm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||
|
||||
export interface PrismaClientConstructor {
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||
*/
|
||||
|
||||
new <
|
||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||
*/
|
||||
|
||||
export interface PrismaClient<
|
||||
in LogOpts extends Prisma.LogLevel = never,
|
||||
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
> {
|
||||
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||
|
||||
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||
|
||||
/**
|
||||
* Connect with the database
|
||||
*/
|
||||
$connect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Disconnect from the database
|
||||
*/
|
||||
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Executes a prepared raw query and returns the number of affected rows.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||
*/
|
||||
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Executes a raw query and returns the number of affected rows.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||
*/
|
||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Performs a prepared raw query and returns the `SELECT` data.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||
*/
|
||||
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
/**
|
||||
* Performs a raw query and returns the `SELECT` data.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||
*/
|
||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
|
||||
/**
|
||||
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||
* @example
|
||||
* ```
|
||||
* const [george, bob, alice] = await prisma.$transaction([
|
||||
* prisma.user.create({ data: { name: 'George' } }),
|
||||
* prisma.user.create({ data: { name: 'Bob' } }),
|
||||
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||
*/
|
||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||
|
||||
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
||||
|
||||
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||
extArgs: ExtArgs
|
||||
}>>
|
||||
|
||||
/**
|
||||
* `prisma.project`: Exposes CRUD operations for the **Project** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Projects
|
||||
* const projects = await prisma.project.findMany()
|
||||
* ```
|
||||
*/
|
||||
get project(): Prisma.ProjectDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.user`: Exposes CRUD operations for the **User** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*/
|
||||
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.pipeline`: Exposes CRUD operations for the **Pipeline** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Pipelines
|
||||
* const pipelines = await prisma.pipeline.findMany()
|
||||
* ```
|
||||
*/
|
||||
get pipeline(): Prisma.PipelineDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.step`: Exposes CRUD operations for the **Step** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Steps
|
||||
* const steps = await prisma.step.findMany()
|
||||
* ```
|
||||
*/
|
||||
get step(): Prisma.StepDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.deployment`: Exposes CRUD operations for the **Deployment** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Deployments
|
||||
* const deployments = await prisma.deployment.findMany()
|
||||
* ```
|
||||
*/
|
||||
get deployment(): Prisma.DeploymentDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
}
|
||||
|
||||
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
|
||||
}
|
||||
1106
apps/server/generated/internal/prismaNamespace.ts
Normal file
1106
apps/server/generated/internal/prismaNamespace.ts
Normal file
File diff suppressed because it is too large
Load Diff
175
apps/server/generated/internal/prismaNamespaceBrowser.ts
Normal file
175
apps/server/generated/internal/prismaNamespaceBrowser.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||
*
|
||||
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||
|
||||
export type * from '../models.ts'
|
||||
export type * from './prismaNamespace.ts'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.DbNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.JsonNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
Project: 'Project',
|
||||
User: 'User',
|
||||
Pipeline: 'Pipeline',
|
||||
Step: 'Step',
|
||||
Deployment: 'Deployment'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
/*
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = {
|
||||
Serializable: 'Serializable'
|
||||
} as const
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
|
||||
export const ProjectScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
repository: 'repository',
|
||||
projectDir: 'projectDir',
|
||||
valid: 'valid',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy'
|
||||
} as const
|
||||
|
||||
export type ProjectScalarFieldEnum = (typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum]
|
||||
|
||||
|
||||
export const UserScalarFieldEnum = {
|
||||
id: 'id',
|
||||
username: 'username',
|
||||
login: 'login',
|
||||
email: 'email',
|
||||
avatar_url: 'avatar_url',
|
||||
active: 'active',
|
||||
valid: 'valid',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy'
|
||||
} as const
|
||||
|
||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||
|
||||
|
||||
export const PipelineScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
valid: 'valid',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy',
|
||||
projectId: 'projectId'
|
||||
} as const
|
||||
|
||||
export type PipelineScalarFieldEnum = (typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum]
|
||||
|
||||
|
||||
export const StepScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
order: 'order',
|
||||
script: 'script',
|
||||
valid: 'valid',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy',
|
||||
pipelineId: 'pipelineId'
|
||||
} as const
|
||||
|
||||
export type StepScalarFieldEnum = (typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum]
|
||||
|
||||
|
||||
export const DeploymentScalarFieldEnum = {
|
||||
id: 'id',
|
||||
branch: 'branch',
|
||||
env: 'env',
|
||||
status: 'status',
|
||||
commitHash: 'commitHash',
|
||||
commitMessage: 'commitMessage',
|
||||
buildLog: 'buildLog',
|
||||
sparseCheckoutPaths: 'sparseCheckoutPaths',
|
||||
startedAt: 'startedAt',
|
||||
finishedAt: 'finishedAt',
|
||||
valid: 'valid',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt',
|
||||
createdBy: 'createdBy',
|
||||
updatedBy: 'updatedBy',
|
||||
projectId: 'projectId',
|
||||
pipelineId: 'pipelineId'
|
||||
} as const
|
||||
|
||||
export type DeploymentScalarFieldEnum = (typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum]
|
||||
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
16
apps/server/generated/models.ts
Normal file
16
apps/server/generated/models.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This is a barrel export file for all models and their related types.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/Project.ts'
|
||||
export type * from './models/User.ts'
|
||||
export type * from './models/Pipeline.ts'
|
||||
export type * from './models/Step.ts'
|
||||
export type * from './models/Deployment.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
1837
apps/server/generated/models/Deployment.ts
Normal file
1837
apps/server/generated/models/Deployment.ts
Normal file
File diff suppressed because it is too large
Load Diff
1706
apps/server/generated/models/Pipeline.ts
Normal file
1706
apps/server/generated/models/Pipeline.ts
Normal file
File diff suppressed because it is too large
Load Diff
1681
apps/server/generated/models/Project.ts
Normal file
1681
apps/server/generated/models/Project.ts
Normal file
File diff suppressed because it is too large
Load Diff
1569
apps/server/generated/models/Step.ts
Normal file
1569
apps/server/generated/models/Step.ts
Normal file
File diff suppressed because it is too large
Load Diff
1361
apps/server/generated/models/User.ts
Normal file
1361
apps/server/generated/models/User.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
import { PrismaClient } from '../generated/prisma/index.js'
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient();
|
||||
};
|
||||
|
||||
export default prismaClientSingleton();
|
||||
266
apps/server/libs/execution-queue.ts
Normal file
266
apps/server/libs/execution-queue.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { PipelineRunner } from '../runners/index.ts';
|
||||
import { prisma } from './prisma.ts';
|
||||
|
||||
// 存储正在运行的部署任务
|
||||
const runningDeployments = new Set<number>();
|
||||
|
||||
// 存储待执行的任务队列
|
||||
const pendingQueue: Array<{
|
||||
deploymentId: number;
|
||||
pipelineId: number;
|
||||
}> = [];
|
||||
|
||||
// 定时器ID
|
||||
let pollingTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
// 轮询间隔(毫秒)
|
||||
const POLLING_INTERVAL = 30000; // 30秒
|
||||
|
||||
/**
|
||||
* 执行队列管理器
|
||||
*/
|
||||
export class ExecutionQueue {
|
||||
private static instance: ExecutionQueue;
|
||||
private isProcessing = false;
|
||||
private isPolling = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 获取执行队列的单例实例
|
||||
*/
|
||||
public static getInstance(): ExecutionQueue {
|
||||
if (!ExecutionQueue.instance) {
|
||||
ExecutionQueue.instance = new ExecutionQueue();
|
||||
}
|
||||
return ExecutionQueue.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化执行队列,包括恢复未完成的任务
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
console.log('Initializing execution queue...');
|
||||
// 恢复未完成的任务
|
||||
await this.recoverPendingDeployments();
|
||||
|
||||
// 启动定时轮询
|
||||
this.startPolling();
|
||||
|
||||
console.log('Execution queue initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库中恢复未完成的部署任务
|
||||
*/
|
||||
private async recoverPendingDeployments(): Promise<void> {
|
||||
try {
|
||||
console.log('Recovering pending deployments from database...');
|
||||
|
||||
// 查询数据库中状态为pending的部署任务
|
||||
const pendingDeployments = await prisma.deployment.findMany({
|
||||
where: {
|
||||
status: 'pending',
|
||||
valid: 1,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
pipelineId: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Found ${pendingDeployments.length} pending deployments`);
|
||||
|
||||
// 将这些任务添加到执行队列中
|
||||
for (const deployment of pendingDeployments) {
|
||||
await this.addTask(deployment.id, deployment.pipelineId);
|
||||
}
|
||||
|
||||
console.log('Pending deployments recovery completed');
|
||||
} catch (error) {
|
||||
console.error('Failed to recover pending deployments:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时轮询机制
|
||||
*/
|
||||
private startPolling(): void {
|
||||
if (this.isPolling) {
|
||||
console.log('Polling is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPolling = true;
|
||||
console.log(`Starting polling with interval ${POLLING_INTERVAL}ms`);
|
||||
|
||||
// 立即执行一次检查
|
||||
this.checkPendingDeployments();
|
||||
|
||||
// 设置定时器定期检查
|
||||
pollingTimer = setInterval(() => {
|
||||
this.checkPendingDeployments();
|
||||
}, POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时轮询机制
|
||||
*/
|
||||
public stopPolling(): void {
|
||||
if (pollingTimer) {
|
||||
clearInterval(pollingTimer);
|
||||
pollingTimer = null;
|
||||
this.isPolling = false;
|
||||
console.log('Polling stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库中的待处理部署任务
|
||||
*/
|
||||
private async checkPendingDeployments(): Promise<void> {
|
||||
try {
|
||||
console.log('Checking for pending deployments in database...');
|
||||
|
||||
// 查询数据库中状态为pending的部署任务
|
||||
const pendingDeployments = await prisma.deployment.findMany({
|
||||
where: {
|
||||
status: 'pending',
|
||||
valid: 1,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
pipelineId: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Found ${pendingDeployments.length} pending deployments in polling`,
|
||||
);
|
||||
|
||||
// 检查这些任务是否已经在队列中,如果没有则添加
|
||||
for (const deployment of pendingDeployments) {
|
||||
// 检查是否已经在运行队列中
|
||||
if (!runningDeployments.has(deployment.id)) {
|
||||
console.log(
|
||||
`Adding deployment ${deployment.id} to queue from polling`,
|
||||
);
|
||||
await this.addTask(deployment.id, deployment.pipelineId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check pending deployments:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将部署任务添加到执行队列
|
||||
* @param deploymentId 部署ID
|
||||
* @param pipelineId 流水线ID
|
||||
*/
|
||||
public async addTask(
|
||||
deploymentId: number,
|
||||
pipelineId: number,
|
||||
): Promise<void> {
|
||||
// 检查是否已经在运行队列中
|
||||
if (runningDeployments.has(deploymentId)) {
|
||||
console.log(`Deployment ${deploymentId} is already queued or running`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到运行队列
|
||||
runningDeployments.add(deploymentId);
|
||||
|
||||
// 添加到待执行队列
|
||||
pendingQueue.push({ deploymentId, pipelineId });
|
||||
|
||||
// 开始处理队列(如果尚未开始)
|
||||
if (!this.isProcessing) {
|
||||
this.processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理执行队列中的任务
|
||||
*/
|
||||
private async processQueue(): Promise<void> {
|
||||
this.isProcessing = true;
|
||||
|
||||
while (pendingQueue.length > 0) {
|
||||
const task = pendingQueue.shift();
|
||||
|
||||
if (task) {
|
||||
try {
|
||||
// 执行流水线
|
||||
await this.executePipeline(task.deploymentId, task.pipelineId);
|
||||
} catch (error) {
|
||||
console.error('执行流水线失败:', error);
|
||||
// 这里可以添加更多的错误处理逻辑
|
||||
} finally {
|
||||
// 从运行队列中移除
|
||||
runningDeployments.delete(task.deploymentId);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加一个小延迟以避免过度占用资源
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流水线
|
||||
* @param deploymentId 部署ID
|
||||
* @param pipelineId 流水线ID
|
||||
*/
|
||||
private async executePipeline(
|
||||
deploymentId: number,
|
||||
pipelineId: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 获取部署信息以获取项目和 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);
|
||||
} catch (error) {
|
||||
console.error('执行流水线失败:', error);
|
||||
// 错误处理可以在这里添加,比如更新部署状态为失败
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列状态
|
||||
*/
|
||||
public getQueueStatus(): {
|
||||
pendingCount: number;
|
||||
runningCount: number;
|
||||
} {
|
||||
return {
|
||||
pendingCount: pendingQueue.length,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,54 @@ class Gitea {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取仓库分支列表
|
||||
* @param owner 仓库拥有者
|
||||
* @param repo 仓库名称
|
||||
* @param accessToken 访问令牌
|
||||
*/
|
||||
async getBranches(owner: string, repo: string, accessToken: string) {
|
||||
const response = await fetch(
|
||||
`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/branches`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取仓库提交记录
|
||||
* @param owner 仓库拥有者
|
||||
* @param repo 仓库名称
|
||||
* @param accessToken 访问令牌
|
||||
* @param sha 分支名称或提交SHA
|
||||
*/
|
||||
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) {
|
||||
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
||||
if (sha) {
|
||||
url.searchParams.append('sha', sha);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
url.toString(),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
private getHeaders(accessToken?: string) {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
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);
|
||||
}
|
||||
247
apps/server/libs/pipeline-template.ts
Normal file
247
apps/server/libs/pipeline-template.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { prisma } from './prisma.ts';
|
||||
|
||||
// 默认流水线模板
|
||||
export interface PipelineTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
steps: Array<{
|
||||
name: string;
|
||||
order: number;
|
||||
script: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 系统默认的流水线模板
|
||||
export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
||||
{
|
||||
name: 'Git Clone Pipeline',
|
||||
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
||||
steps: [
|
||||
{
|
||||
name: 'Clone Repository',
|
||||
order: 0,
|
||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
||||
},
|
||||
{
|
||||
name: 'Install Dependencies',
|
||||
order: 1,
|
||||
script: '# 安装项目依赖\nnpm install',
|
||||
},
|
||||
{
|
||||
name: 'Run Tests',
|
||||
order: 2,
|
||||
script: '# 运行测试\nnpm test',
|
||||
},
|
||||
{
|
||||
name: 'Build Project',
|
||||
order: 3,
|
||||
script: '# 构建项目\nnpm run build',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Sparse Checkout Pipeline',
|
||||
description: '稀疏检出流水线,适用于monorepo项目,只获取指定目录的代码',
|
||||
steps: [
|
||||
{
|
||||
name: 'Sparse Checkout Repository',
|
||||
order: 0,
|
||||
script: '# 进行稀疏检出指定目录的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit config core.sparseCheckout true\necho "$SPARSE_CHECKOUT_PATHS" > .git/info/sparse-checkout\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
||||
},
|
||||
{
|
||||
name: 'Install Dependencies',
|
||||
order: 1,
|
||||
script: '# 安装项目依赖\nnpm install',
|
||||
},
|
||||
{
|
||||
name: 'Run Tests',
|
||||
order: 2,
|
||||
script: '# 运行测试\nnpm test',
|
||||
},
|
||||
{
|
||||
name: 'Build Project',
|
||||
order: 3,
|
||||
script: '# 构建项目\nnpm run build',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Simple Deploy Pipeline',
|
||||
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
||||
steps: [
|
||||
{
|
||||
name: 'Clone Repository',
|
||||
order: 0,
|
||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD',
|
||||
},
|
||||
{
|
||||
name: 'Build and Deploy',
|
||||
order: 1,
|
||||
script: '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 初始化系统默认流水线模板
|
||||
*/
|
||||
export async function initializePipelineTemplates(): Promise<void> {
|
||||
console.log('Initializing pipeline templates...');
|
||||
|
||||
try {
|
||||
// 检查是否已经存在模板流水线
|
||||
const existingTemplates = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
name: {
|
||||
in: DEFAULT_PIPELINE_TEMPLATES.map(template => template.name)
|
||||
},
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有现有的模板,则创建默认模板
|
||||
if (existingTemplates.length === 0) {
|
||||
console.log('Creating default pipeline templates...');
|
||||
|
||||
for (const template of DEFAULT_PIPELINE_TEMPLATES) {
|
||||
// 创建模板流水线(使用负数ID表示模板)
|
||||
const pipeline = await prisma.pipeline.create({
|
||||
data: {
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
projectId: null // 模板不属于任何特定项目
|
||||
}
|
||||
});
|
||||
|
||||
// 创建模板步骤
|
||||
for (const step of template.steps) {
|
||||
await prisma.step.create({
|
||||
data: {
|
||||
name: step.name,
|
||||
order: step.order,
|
||||
script: step.script,
|
||||
pipelineId: pipeline.id,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Created template: ${template.name}`);
|
||||
}
|
||||
} else {
|
||||
console.log('Pipeline templates already exist, skipping initialization');
|
||||
}
|
||||
|
||||
console.log('Pipeline templates initialization completed');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize pipeline templates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的流水线模板
|
||||
*/
|
||||
export async function getAvailableTemplates(): Promise<Array<{id: number, name: string, description: string}>> {
|
||||
try {
|
||||
const templates = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
projectId: null, // 模板流水线没有关联的项目
|
||||
valid: 1
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true
|
||||
}
|
||||
});
|
||||
|
||||
// 处理可能为null的description字段
|
||||
return templates.map(template => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description || ''
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to get pipeline templates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于模板创建新的流水线
|
||||
* @param templateId 模板ID
|
||||
* @param projectId 项目ID
|
||||
* @param pipelineName 新流水线名称
|
||||
* @param pipelineDescription 新流水线描述
|
||||
*/
|
||||
export async function createPipelineFromTemplate(
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
pipelineName: string,
|
||||
pipelineDescription: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
// 获取模板流水线及其步骤
|
||||
const templatePipeline = await prisma.pipeline.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
projectId: null, // 确保是模板流水线
|
||||
valid: 1
|
||||
},
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!templatePipeline) {
|
||||
throw new Error(`Template with id ${templateId} not found`);
|
||||
}
|
||||
|
||||
// 创建新的流水线
|
||||
const newPipeline = await prisma.pipeline.create({
|
||||
data: {
|
||||
name: pipelineName,
|
||||
description: pipelineDescription,
|
||||
projectId: projectId,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
|
||||
// 复制模板步骤到新流水线
|
||||
for (const templateStep of templatePipeline.steps) {
|
||||
await prisma.step.create({
|
||||
data: {
|
||||
name: templateStep.name,
|
||||
order: templateStep.order,
|
||||
script: templateStep.script,
|
||||
pipelineId: newPipeline.id,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Created pipeline from template ${templateId}: ${newPipeline.name}`);
|
||||
return newPipeline.id;
|
||||
} catch (error) {
|
||||
console.error('Failed to create pipeline from template:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
8
apps/server/libs/prisma.ts
Normal file
8
apps/server/libs/prisma.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'dotenv/config';
|
||||
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
|
||||
import { PrismaClient } from '../generated/client.ts';
|
||||
|
||||
const connectionString = `${process.env.DATABASE_URL}`;
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({ url: connectionString });
|
||||
export const prisma = new PrismaClient({ adapter });
|
||||
@@ -5,7 +5,7 @@ import { Session } from './session.ts';
|
||||
import { CORS } from './cors.ts';
|
||||
import { HttpLogger } from './logger.ts';
|
||||
import type Koa from 'koa';
|
||||
import { Authorization } from './Authorization.ts';
|
||||
import { Authorization } from './authorization.ts';
|
||||
|
||||
/**
|
||||
* 初始化中间件
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
ProjectController,
|
||||
UserController,
|
||||
AuthController,
|
||||
DeploymentController,
|
||||
PipelineController,
|
||||
StepController,
|
||||
GitController
|
||||
} from '../controllers/index.ts';
|
||||
import { log } from '../libs/logger.ts';
|
||||
|
||||
@@ -38,6 +42,10 @@ export class Router implements Middleware {
|
||||
ProjectController,
|
||||
UserController,
|
||||
AuthController,
|
||||
DeploymentController,
|
||||
PipelineController,
|
||||
StepController,
|
||||
GitController
|
||||
]);
|
||||
|
||||
// 输出注册的路由信息
|
||||
|
||||
@@ -12,13 +12,17 @@
|
||||
"dependencies": {
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@koa/router": "^14.0.0",
|
||||
"@prisma/client": "^6.15.0",
|
||||
"@prisma/adapter-better-sqlite3": "^7.0.0",
|
||||
"@prisma/client": "^7.0.0",
|
||||
"better-sqlite3": "^12.4.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"koa": "^3.0.1",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"koa-session": "^7.0.2",
|
||||
"pino": "^9.9.1",
|
||||
"pino-pretty": "^13.1.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.5",
|
||||
"zx": "^8.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node-ts": "^23.6.1",
|
||||
@@ -28,7 +32,7 @@
|
||||
"@types/koa__cors": "^5.0.0",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/node": "^24.3.0",
|
||||
"prisma": "^6.15.0",
|
||||
"prisma": "^7.0.0",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
19
apps/server/prisma.config.ts
Normal file
19
apps/server/prisma.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'dotenv/config';
|
||||
import { defineConfig, env } from 'prisma/config';
|
||||
|
||||
export default defineConfig({
|
||||
// the main entry for your schema
|
||||
schema: 'prisma/schema.prisma',
|
||||
// where migrations should be generated
|
||||
// what script to run for "prisma db seed"
|
||||
migrations: {
|
||||
path: 'prisma/migrations',
|
||||
seed: 'tsx prisma/seed.ts',
|
||||
},
|
||||
// The database URL
|
||||
datasource: {
|
||||
// Type Safe env() helper
|
||||
// Does not replace the need for dotenv
|
||||
url: env('DATABASE_URL'),
|
||||
},
|
||||
});
|
||||
Binary file not shown.
@@ -2,13 +2,12 @@
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../generated/prisma"
|
||||
provider = "prisma-client"
|
||||
output = "../generated"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Project {
|
||||
@@ -16,17 +15,11 @@ model Project {
|
||||
name String
|
||||
description String?
|
||||
repository String
|
||||
valid Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdBy String
|
||||
updatedBy String
|
||||
}
|
||||
projectDir String @unique // 项目工作目录路径(必填)
|
||||
// Relations
|
||||
deployments Deployment[]
|
||||
pipelines Pipeline[]
|
||||
|
||||
model Environment {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
description String?
|
||||
valid Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -47,3 +40,56 @@ model User {
|
||||
createdBy String @default("system")
|
||||
updatedBy String @default("system")
|
||||
}
|
||||
|
||||
model Pipeline {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
description String?
|
||||
valid Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdBy String
|
||||
updatedBy String
|
||||
|
||||
// Relations
|
||||
projectId Int?
|
||||
Project Project? @relation(fields: [projectId], references: [id])
|
||||
steps Step[]
|
||||
}
|
||||
|
||||
model Step {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
order Int
|
||||
script String // 执行的脚本命令
|
||||
valid Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdBy String
|
||||
updatedBy String
|
||||
|
||||
pipelineId Int
|
||||
pipeline Pipeline @relation(fields: [pipelineId], references: [id])
|
||||
}
|
||||
|
||||
model Deployment {
|
||||
id Int @id @default(autoincrement())
|
||||
branch String
|
||||
env String?
|
||||
status String // pending, running, success, failed, cancelled
|
||||
commitHash String?
|
||||
commitMessage String?
|
||||
buildLog String?
|
||||
sparseCheckoutPaths String? // 稀疏检出路径,用于monorepo项目
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
valid Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdBy String
|
||||
updatedBy String
|
||||
|
||||
projectId Int
|
||||
Project Project? @relation(fields: [projectId], references: [id])
|
||||
pipelineId Int
|
||||
}
|
||||
|
||||
3
apps/server/runners/index.ts
Normal file
3
apps/server/runners/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PipelineRunner } from './pipeline-runner';
|
||||
|
||||
export { PipelineRunner };
|
||||
28
apps/server/runners/mq-interface.ts
Normal file
28
apps/server/runners/mq-interface.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// MQ集成接口设计 (暂不实现)
|
||||
// 该接口用于将来通过消息队列触发流水线执行
|
||||
|
||||
export interface MQPipelineMessage {
|
||||
deploymentId: number;
|
||||
pipelineId: number;
|
||||
// 其他可能需要的参数
|
||||
triggerUser?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface MQRunnerInterface {
|
||||
/**
|
||||
* 发送流水线执行消息到MQ
|
||||
* @param message 流水线执行消息
|
||||
*/
|
||||
sendPipelineExecutionMessage(message: MQPipelineMessage): Promise<void>;
|
||||
|
||||
/**
|
||||
* 监听MQ消息并执行流水线
|
||||
*/
|
||||
listenForPipelineMessages(): void;
|
||||
|
||||
/**
|
||||
* 停止监听MQ消息
|
||||
*/
|
||||
stopListening(): void;
|
||||
}
|
||||
304
apps/server/runners/pipeline-runner.ts
Normal file
304
apps/server/runners/pipeline-runner.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { $ } from 'zx';
|
||||
import { prisma } from '../libs/prisma.ts';
|
||||
import type { Step } from '../generated/client.ts';
|
||||
import { GitManager, WorkspaceDirStatus } from '../libs/git-manager.ts';
|
||||
import { log } from '../libs/logger.ts';
|
||||
|
||||
export class PipelineRunner {
|
||||
private readonly TAG = 'PipelineRunner';
|
||||
private deploymentId: number;
|
||||
private projectDir: string;
|
||||
|
||||
constructor(deploymentId: number, projectDir: string) {
|
||||
this.deploymentId = deploymentId;
|
||||
|
||||
if (!projectDir) {
|
||||
throw new Error('项目工作目录未配置,无法执行流水线');
|
||||
}
|
||||
|
||||
this.projectDir = projectDir;
|
||||
log.info(
|
||||
this.TAG,
|
||||
'PipelineRunner initialized with projectDir: %s',
|
||||
this.projectDir,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流水线
|
||||
* @param pipelineId 流水线ID
|
||||
*/
|
||||
async run(pipelineId: number): Promise<void> {
|
||||
// 获取流水线及其步骤
|
||||
const pipeline = await prisma.pipeline.findUnique({
|
||||
where: { id: pipelineId },
|
||||
include: {
|
||||
steps: { where: { valid: 1 }, orderBy: { order: 'asc' } },
|
||||
Project: true, // 同时获取关联的项目信息
|
||||
},
|
||||
});
|
||||
|
||||
if (!pipeline) {
|
||||
throw new Error(`Pipeline with id ${pipelineId} not found`);
|
||||
}
|
||||
|
||||
// 获取部署信息
|
||||
const deployment = await prisma.deployment.findUnique({
|
||||
where: { id: this.deploymentId },
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(`Deployment with id ${this.deploymentId} not found`);
|
||||
}
|
||||
|
||||
let logs = '';
|
||||
let hasError = false;
|
||||
|
||||
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()) {
|
||||
// 准备环境变量
|
||||
const envVars = this.prepareEnvironmentVariables(pipeline, deployment);
|
||||
|
||||
// 记录开始执行步骤的日志
|
||||
const startLog = `[${new Date().toISOString()}] 开始执行步骤 ${index + 1}/${pipeline.steps.length}: ${step.name}\n`;
|
||||
logs += startLog;
|
||||
|
||||
// 实时更新日志
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: { buildLog: logs },
|
||||
});
|
||||
|
||||
// 执行步骤
|
||||
const stepLog = await this.executeStep(step, envVars);
|
||||
logs += stepLog + '\n';
|
||||
|
||||
// 记录步骤执行完成的日志
|
||||
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
|
||||
logs += endLog;
|
||||
|
||||
// 实时更新日志
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: { buildLog: logs },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
hasError = true;
|
||||
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({
|
||||
where: { id: this.deploymentId },
|
||||
data: {
|
||||
buildLog: logs,
|
||||
status: 'failed',
|
||||
finishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 更新最终状态
|
||||
if (!hasError) {
|
||||
await prisma.deployment.update({
|
||||
where: { id: this.deploymentId },
|
||||
data: {
|
||||
buildLog: logs,
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备环境变量
|
||||
* @param pipeline 流水线信息
|
||||
* @param deployment 部署信息
|
||||
*/
|
||||
private prepareEnvironmentVariables(
|
||||
pipeline: any,
|
||||
deployment: any,
|
||||
): Record<string, string> {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
// 项目相关信息
|
||||
if (pipeline.Project) {
|
||||
envVars.REPOSITORY_URL = pipeline.Project.repository || '';
|
||||
envVars.PROJECT_NAME = pipeline.Project.name || '';
|
||||
}
|
||||
|
||||
// 部署相关信息
|
||||
envVars.BRANCH_NAME = deployment.branch || '';
|
||||
envVars.COMMIT_HASH = deployment.commitHash || '';
|
||||
|
||||
// 稀疏检出路径(如果有配置的话)
|
||||
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || '';
|
||||
|
||||
// 工作空间路径(使用配置的项目目录)
|
||||
envVars.WORKSPACE = this.projectDir;
|
||||
envVars.PROJECT_DIR = this.projectDir;
|
||||
|
||||
return envVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为日志添加时间戳前缀
|
||||
* @param message 日志消息
|
||||
* @param isError 是否为错误日志
|
||||
* @returns 带时间戳的日志消息
|
||||
*/
|
||||
private addTimestamp(message: string, isError = false): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
if (isError) {
|
||||
return `[${timestamp}] [ERROR] ${message}`;
|
||||
}
|
||||
return `[${timestamp}] ${message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为多行日志添加时间戳前缀
|
||||
* @param content 多行日志内容
|
||||
* @param isError 是否为错误日志
|
||||
* @returns 带时间戳的多行日志消息
|
||||
*/
|
||||
private addTimestampToLines(content: string, isError = false): string {
|
||||
if (!content) return '';
|
||||
|
||||
return (
|
||||
content
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() !== '')
|
||||
.map((line) => this.addTimestamp(line, isError))
|
||||
.join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个步骤
|
||||
* @param step 步骤对象
|
||||
* @param envVars 环境变量
|
||||
*/
|
||||
private async executeStep(
|
||||
step: Step,
|
||||
envVars: Record<string, string>,
|
||||
): Promise<string> {
|
||||
let logs = '';
|
||||
|
||||
try {
|
||||
// 添加步骤开始执行的时间戳
|
||||
logs += this.addTimestamp(`执行脚本: ${step.script}`) + '\n';
|
||||
|
||||
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
||||
const script = step.script;
|
||||
|
||||
// 通过bash -c执行脚本,确保环境变量能被正确解析
|
||||
const result = await $({
|
||||
cwd: this.projectDir,
|
||||
env: { ...process.env, ...envVars },
|
||||
})`bash -c ${script}`;
|
||||
|
||||
if (result.stdout) {
|
||||
// 为stdout中的每一行添加时间戳
|
||||
logs += this.addTimestampToLines(result.stdout);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
// 为stderr中的每一行添加时间戳和错误标记
|
||||
logs += this.addTimestampToLines(result.stderr, true);
|
||||
}
|
||||
|
||||
logs += this.addTimestamp(`步骤执行完成`) + '\n';
|
||||
} catch (error) {
|
||||
const errorMsg = `Error executing step "${step.name}": ${(error as Error).message}`;
|
||||
logs += this.addTimestamp(errorMsg, true) + '\n';
|
||||
log.error(this.TAG, errorMsg);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,10 @@
|
||||
"@tsconfig/node-ts/tsconfig.json"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": false
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"target": "ES2023",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,22 @@
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-react": "^2.66.4",
|
||||
"@arco-design/web-react": "^2.66.8",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"axios": "^1.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.8.0",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arco-plugins/unplugin-react": "2.0.0-beta.5",
|
||||
"@rsbuild/core": "^1.4.13",
|
||||
"@rsbuild/plugin-less": "^1.4.0",
|
||||
"@rsbuild/plugin-react": "^1.3.4",
|
||||
"@rsbuild/core": "^1.6.7",
|
||||
"@rsbuild/plugin-less": "^1.5.0",
|
||||
"@rsbuild/plugin-react": "^1.4.2",
|
||||
"@rsbuild/plugin-svgr": "^1.2.2",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/react": "^18.3.24",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ArcoDesignPlugin } from '@arco-plugins/unplugin-react';
|
||||
import { defineConfig } from '@rsbuild/core';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { pluginLess } from '@rsbuild/plugin-less';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { pluginSvgr } from '@rsbuild/plugin-svgr';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import type React from 'react';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
export function useAsyncEffect(
|
||||
effect: () => Promise<void>,
|
||||
effect: () => Promise<void | (() => void)>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const callback = useCallback(effect, [...deps]);
|
||||
|
||||
useEffect(() => {
|
||||
effect();
|
||||
}, [...deps]);
|
||||
const cleanupPromise = callback();
|
||||
return () => {
|
||||
if (cleanupPromise instanceof Promise) {
|
||||
cleanupPromise.then(cleanup => cleanup && cleanup());
|
||||
}
|
||||
};
|
||||
}, [callback]);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from '@pages/App';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { useGlobalStore } from './stores/global';
|
||||
import '@arco-design/web-react/es/_util/react-19-adapter'
|
||||
|
||||
const rootEl = document.getElementById('root');
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Route, Routes, Navigate } from 'react-router';
|
||||
import Env from '@pages/env';
|
||||
import Home from '@pages/home';
|
||||
import Login from '@pages/login';
|
||||
import ProjectList from '@pages/project/list';
|
||||
import ProjectDetail from '@pages/project/detail';
|
||||
import Env from '@pages/env';
|
||||
import ProjectList from '@pages/project/list';
|
||||
import { Navigate, Route, Routes } from 'react-router';
|
||||
|
||||
import '@styles/index.css';
|
||||
const App = () => {
|
||||
|
||||
9
apps/web/src/pages/env/index.tsx
vendored
9
apps/web/src/pages/env/index.tsx
vendored
@@ -1,12 +1,5 @@
|
||||
import { useState } from "react";
|
||||
|
||||
function Env() {
|
||||
const [env, setEnv] = useState([]);
|
||||
return (
|
||||
<div>
|
||||
env page
|
||||
</div>
|
||||
)
|
||||
return <div>env page</div>;
|
||||
}
|
||||
|
||||
export default Env;
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
IconMenuUnfold,
|
||||
IconRobot,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import { useState } from 'react';
|
||||
import Logo from '@assets/images/logo.svg?react';
|
||||
import { loginService } from '@pages/login/service';
|
||||
import { useState } from 'react';
|
||||
import { Link, Outlet } from 'react-router';
|
||||
import { useGlobalStore } from '../../stores/global';
|
||||
import { loginService } from '@pages/login/service';
|
||||
|
||||
function Home() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Button } from '@arco-design/web-react';
|
||||
import Gitea from '@assets/images/gitea.svg?react';
|
||||
import { loginService } from './service';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { loginService } from './service';
|
||||
|
||||
function Login() {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const authCode = searchParams.get('code');
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { net } from '@shared';
|
||||
import type { AuthURLResponse, AuthLoginResponse } from './types';
|
||||
import type { NavigateFunction } from 'react-router';
|
||||
import { Message, Notification } from '@arco-design/web-react';
|
||||
import { net } from '@shared';
|
||||
import type { NavigateFunction } from 'react-router';
|
||||
import { useGlobalStore } from '../../stores/global';
|
||||
import type { AuthLoginResponse, AuthURLResponse } from './types';
|
||||
|
||||
class LoginService {
|
||||
async getAuthUrl() {
|
||||
|
||||
256
apps/web/src/pages/project/detail/components/DeployModal.tsx
Normal file
256
apps/web/src/pages/project/detail/components/DeployModal.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Message,
|
||||
Modal,
|
||||
Select,
|
||||
} from '@arco-design/web-react';
|
||||
import { formatDateTime } from '../../../../utils/time';
|
||||
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Branch, Commit, Pipeline } from '../../types';
|
||||
import { detailService } from '../service';
|
||||
|
||||
interface DeployModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: () => void;
|
||||
pipelines: Pipeline[];
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
function DeployModal({
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
pipelines,
|
||||
projectId,
|
||||
}: DeployModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [commits, setCommits] = useState<Commit[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [branchLoading, setBranchLoading] = useState(false);
|
||||
|
||||
const fetchCommits = useCallback(
|
||||
async (branch: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await detailService.getCommits(projectId, branch);
|
||||
setCommits(data);
|
||||
if (data.length > 0) {
|
||||
form.setFieldValue('commitHash', data[0].sha);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提交记录失败:', error);
|
||||
Message.error('获取提交记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[projectId, form],
|
||||
);
|
||||
|
||||
const fetchBranches = useCallback(async () => {
|
||||
try {
|
||||
setBranchLoading(true);
|
||||
const data = await detailService.getBranches(projectId);
|
||||
setBranches(data);
|
||||
// 默认选中 master 或 main
|
||||
const defaultBranch = data.find(
|
||||
(b) => b.name === 'master' || b.name === 'main',
|
||||
);
|
||||
if (defaultBranch) {
|
||||
form.setFieldValue('branch', defaultBranch.name);
|
||||
fetchCommits(defaultBranch.name);
|
||||
} else if (data.length > 0) {
|
||||
form.setFieldValue('branch', data[0].name);
|
||||
fetchCommits(data[0].name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分支列表失败:', error);
|
||||
Message.error('获取分支列表失败');
|
||||
} finally {
|
||||
setBranchLoading(false);
|
||||
}
|
||||
}, [projectId, form, fetchCommits]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && projectId) {
|
||||
fetchBranches();
|
||||
}
|
||||
}, [visible, projectId, fetchBranches]);
|
||||
|
||||
const handleBranchChange = (value: string) => {
|
||||
fetchCommits(value);
|
||||
form.setFieldValue('commitHash', undefined);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validate();
|
||||
const selectedCommit = commits.find((c) => c.sha === values.commitHash);
|
||||
const selectedPipeline = pipelines.find((p) => p.id === values.pipelineId);
|
||||
|
||||
if (!selectedCommit || !selectedPipeline) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化环境变量
|
||||
const env = values.envVars
|
||||
?.map((item: { key: string; value: string }) => `${item.key}=${item.value}`)
|
||||
.join('\n');
|
||||
|
||||
await detailService.createDeployment({
|
||||
projectId,
|
||||
pipelineId: values.pipelineId,
|
||||
branch: values.branch,
|
||||
commitHash: selectedCommit.sha,
|
||||
commitMessage: selectedCommit.commit.message,
|
||||
env: env,
|
||||
sparseCheckoutPaths: values.sparseCheckoutPaths,
|
||||
});
|
||||
|
||||
Message.success('部署任务已创建');
|
||||
onOk();
|
||||
} catch (error) {
|
||||
console.error('创建部署失败:', error);
|
||||
Message.error('创建部署失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="开始部署"
|
||||
visible={visible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
autoFocus={false}
|
||||
focusLock={true}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="选择流水线"
|
||||
field="pipelineId"
|
||||
rules={[{ required: true, message: '请选择流水线' }]}
|
||||
>
|
||||
<Select placeholder="请选择流水线">
|
||||
{pipelines.map((pipeline) => (
|
||||
<Select.Option key={pipeline.id} value={pipeline.id}>
|
||||
{pipeline.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择分支"
|
||||
field="branch"
|
||||
rules={[{ required: true, message: '请选择分支' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择分支"
|
||||
loading={branchLoading}
|
||||
onChange={handleBranchChange}
|
||||
>
|
||||
{branches.map((branch) => (
|
||||
<Select.Option key={branch.name} value={branch.name}>
|
||||
{branch.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择提交"
|
||||
field="commitHash"
|
||||
rules={[{ required: true, message: '请选择提交记录' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择提交记录"
|
||||
loading={loading}
|
||||
renderFormat={(option) => {
|
||||
const commit = commits.find((c) => c.sha === option?.value);
|
||||
return commit ? commit.sha.substring(0, 7) : '';
|
||||
}}
|
||||
>
|
||||
{commits.map((commit) => (
|
||||
<Select.Option key={commit.sha} value={commit.sha}>
|
||||
<div className="flex flex-col py-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono font-medium">
|
||||
{commit.sha.substring(0, 7)}
|
||||
</span>
|
||||
<span className="text-gray-500 text-xs">
|
||||
{formatDateTime(commit.commit.author.date)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-600 text-sm truncate">
|
||||
{commit.commit.message}
|
||||
</div>
|
||||
<div className="text-gray-400 text-xs">
|
||||
{commit.commit.author.name}
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="稀疏检出路径(用于monorepo项目,每行一个路径)"
|
||||
field="sparseCheckoutPaths"
|
||||
tooltip="在monorepo项目中,指定需要检出的目录路径,每行一个路径。留空则检出整个仓库。"
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder={`例如:\n/packages/frontend\n/packages/backend`}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className="mb-2 font-medium text-gray-700">环境变量</div>
|
||||
<Form.List field="envVars">
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.key} className="flex items-center gap-2 mb-2">
|
||||
<Form.Item
|
||||
field={`${item.field}.key`}
|
||||
noStyle
|
||||
rules={[{ required: true, message: '请输入变量名' }]}
|
||||
>
|
||||
<Input placeholder="变量名" />
|
||||
</Form.Item>
|
||||
<span className="text-gray-400">=</span>
|
||||
<Form.Item
|
||||
field={`${item.field}.value`}
|
||||
noStyle
|
||||
rules={[{ required: true, message: '请输入变量值' }]}
|
||||
>
|
||||
<Input placeholder="变量值" />
|
||||
</Form.Item>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
status="danger"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
long
|
||||
onClick={() => add()}
|
||||
icon={<IconPlus />}
|
||||
>
|
||||
添加环境变量
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeployModal;
|
||||
@@ -1,17 +1,9 @@
|
||||
import { List, Tag, Space } from '@arco-design/web-react';
|
||||
|
||||
// 部署记录类型定义
|
||||
interface DeployRecord {
|
||||
id: number;
|
||||
branch: string;
|
||||
env: string;
|
||||
commit: string;
|
||||
status: 'success' | 'running' | 'failed' | 'pending';
|
||||
createdAt: string;
|
||||
}
|
||||
import { List, Space, Tag } from '@arco-design/web-react';
|
||||
import { formatDateTime } from '../../../../utils/time';
|
||||
import type { Deployment } from '../../types';
|
||||
|
||||
interface DeployRecordItemProps {
|
||||
item: DeployRecord;
|
||||
item: Deployment;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: number) => void;
|
||||
}
|
||||
@@ -22,11 +14,8 @@ function DeployRecordItem({
|
||||
onSelect,
|
||||
}: DeployRecordItemProps) {
|
||||
// 状态标签渲染函数
|
||||
const getStatusTag = (status: DeployRecord['status']) => {
|
||||
const statusMap: Record<
|
||||
DeployRecord['status'],
|
||||
{ color: string; text: string }
|
||||
> = {
|
||||
const getStatusTag = (status: Deployment['status']) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
success: { color: 'green', text: '成功' },
|
||||
running: { color: 'blue', text: '运行中' },
|
||||
failed: { color: 'red', text: '失败' },
|
||||
@@ -46,6 +35,7 @@ function DeployRecordItem({
|
||||
const config = envMap[env] || { color: 'gray', text: env };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
@@ -67,7 +57,7 @@ function DeployRecordItem({
|
||||
#{item.id}
|
||||
</span>
|
||||
<span className="text-gray-600 text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
||||
{item.commit}
|
||||
{item.commitHash?.substring(0, 7)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@@ -76,12 +66,10 @@ function DeployRecordItem({
|
||||
<Space size="medium" wrap>
|
||||
<span className="text-sm text-gray-500">
|
||||
分支:{' '}
|
||||
<span className="font-medium text-gray-700">
|
||||
{item.branch}
|
||||
</span>
|
||||
<span className="font-medium text-gray-700">{item.branch}</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
环境: {getEnvTag(item.env)}
|
||||
环境: {getEnvTag(item.env || 'unknown')}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
状态: {getStatusTag(item.status)}
|
||||
@@ -89,7 +77,7 @@ function DeployRecordItem({
|
||||
<span className="text-sm text-gray-500">
|
||||
执行时间:{' '}
|
||||
<span className="font-medium text-gray-700">
|
||||
{item.createdAt}
|
||||
{formatDateTime(item.createdAt)}
|
||||
</span>
|
||||
</span>
|
||||
</Space>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Typography, Tag, Switch, Button } from '@arco-design/web-react';
|
||||
import { IconDragArrow, IconEdit, IconDelete } from '@arco-design/web-react/icon';
|
||||
import { Button, Switch, Tag, Typography } from '@arco-design/web-react';
|
||||
import {
|
||||
IconDelete,
|
||||
IconDragArrow,
|
||||
IconEdit,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import type { Step } from '../../types';
|
||||
|
||||
// 流水线步骤类型定义
|
||||
interface PipelineStep {
|
||||
id: string;
|
||||
name: string;
|
||||
script: string;
|
||||
interface StepWithEnabled extends Step {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface PipelineStepItemProps {
|
||||
step: PipelineStep;
|
||||
step: StepWithEnabled;
|
||||
index: number;
|
||||
pipelineId: string;
|
||||
onToggle: (pipelineId: string, stepId: string, enabled: boolean) => void;
|
||||
onEdit: (pipelineId: string, step: PipelineStep) => void;
|
||||
onDelete: (pipelineId: string, stepId: string) => void;
|
||||
pipelineId: number;
|
||||
onToggle: (pipelineId: number, stepId: number, enabled: boolean) => void;
|
||||
onEdit: (pipelineId: number, step: StepWithEnabled) => void;
|
||||
onDelete: (pipelineId: number, stepId: number) => void;
|
||||
}
|
||||
|
||||
function PipelineStepItem({
|
||||
@@ -79,6 +80,9 @@ function PipelineStepItem({
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
{step.description && (
|
||||
<div className="text-gray-600 text-sm mb-2">{step.description}</div>
|
||||
)}
|
||||
<div className="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
|
||||
<pre className="whitespace-pre-wrap break-words">{step.script}</pre>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,226 @@
|
||||
import { net, type APIResponse } from '@shared';
|
||||
import type { Project } from '../types';
|
||||
import { type APIResponse, net } from '@shared';
|
||||
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types';
|
||||
|
||||
class DetailService {
|
||||
async getProject(id: string) {
|
||||
const { code, data } = await net.request<APIResponse<Project>>({
|
||||
const { data } = await net.request<APIResponse<Project>>({
|
||||
url: `/api/projects/${id}`,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取项目的所有流水线
|
||||
async getPipelines(projectId: number) {
|
||||
const { data } = await net.request<APIResponse<Pipeline[]>>({
|
||||
url: `/api/pipelines?projectId=${projectId}`,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取可用的流水线模板
|
||||
async getPipelineTemplates() {
|
||||
const { data } = await net.request<APIResponse<{id: number, name: string, description: string}[]>>({
|
||||
url: '/api/pipelines/templates',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取项目的部署记录
|
||||
async getDeployments(projectId: number) {
|
||||
const { data } = await net.request<any>({
|
||||
url: `/api/deployments?projectId=${projectId}`,
|
||||
});
|
||||
return data.data;
|
||||
}
|
||||
|
||||
// 创建流水线
|
||||
async createPipeline(
|
||||
pipeline: Omit<
|
||||
Pipeline,
|
||||
| 'id'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'createdBy'
|
||||
| 'updatedBy'
|
||||
| 'valid'
|
||||
| 'steps'
|
||||
>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
url: '/api/pipelines',
|
||||
method: 'POST',
|
||||
data: pipeline,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 基于模板创建流水线
|
||||
async createPipelineFromTemplate(
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
name: string,
|
||||
description?: string
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
url: '/api/pipelines/from-template',
|
||||
method: 'POST',
|
||||
data: {
|
||||
templateId,
|
||||
projectId,
|
||||
name,
|
||||
description
|
||||
},
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 更新流水线
|
||||
async updatePipeline(
|
||||
id: number,
|
||||
pipeline: Partial<
|
||||
Omit<
|
||||
Pipeline,
|
||||
| 'id'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'createdBy'
|
||||
| 'updatedBy'
|
||||
| 'valid'
|
||||
| 'steps'
|
||||
>
|
||||
>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
url: `/api/pipelines/${id}`,
|
||||
method: 'PUT',
|
||||
data: pipeline,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 删除流水线
|
||||
async deletePipeline(id: number) {
|
||||
const { data } = await net.request<APIResponse<null>>({
|
||||
url: `/api/pipelines/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取流水线的所有步骤
|
||||
async getSteps(pipelineId: number) {
|
||||
const { data } = await net.request<APIResponse<Step[]>>({
|
||||
url: `/api/steps?pipelineId=${pipelineId}`,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 创建步骤
|
||||
async createStep(
|
||||
step: Omit<
|
||||
Step,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid'
|
||||
>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Step>>({
|
||||
url: '/api/steps',
|
||||
method: 'POST',
|
||||
data: step,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 更新步骤
|
||||
async updateStep(
|
||||
id: number,
|
||||
step: Partial<
|
||||
Omit<
|
||||
Step,
|
||||
'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid'
|
||||
>
|
||||
>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Step>>({
|
||||
url: `/api/steps/${id}`,
|
||||
method: 'PUT',
|
||||
data: step,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 删除步骤
|
||||
async deleteStep(id: number) {
|
||||
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
||||
const { data } = await net.request<APIResponse<null>>({
|
||||
url: `/api/steps/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取项目的提交记录
|
||||
async getCommits(projectId: number, branch?: string) {
|
||||
const { data } = await net.request<APIResponse<Commit[]>>({
|
||||
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取项目的分支列表
|
||||
async getBranches(projectId: number) {
|
||||
const { data } = await net.request<APIResponse<Branch[]>>({
|
||||
url: `/api/git/branches?projectId=${projectId}`,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 创建部署
|
||||
async createDeployment(deployment: CreateDeploymentRequest) {
|
||||
const { data } = await net.request<APIResponse<Deployment>>({
|
||||
url: '/api/deployments',
|
||||
method: 'POST',
|
||||
data: deployment,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 重新执行部署
|
||||
async retryDeployment(deploymentId: number) {
|
||||
const { data } = await net.request<APIResponse<Deployment>>({
|
||||
url: `/api/deployments/${deploymentId}/retry`,
|
||||
method: 'POST',
|
||||
});
|
||||
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();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Modal, Form, Input, Button, Message } from '@arco-design/web-react';
|
||||
import React, { useState } from 'react';
|
||||
import type { Project } from '../types';
|
||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||
import { useState } from 'react';
|
||||
import { projectService } from '../service';
|
||||
import type { Project } from '../../types';
|
||||
|
||||
interface CreateProjectModalProps {
|
||||
visible: boolean;
|
||||
@@ -9,7 +9,11 @@ interface CreateProjectModalProps {
|
||||
onSuccess: (newProject: Project) => void;
|
||||
}
|
||||
|
||||
function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModalProps) {
|
||||
function CreateProjectModal({
|
||||
visible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}: CreateProjectModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -46,17 +50,18 @@ function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModal
|
||||
<Button key="cancel" onClick={handleCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" loading={loading} onClick={handleSubmit}>
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
创建
|
||||
</Button>,
|
||||
]}
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form form={form} layout="vertical" autoComplete="off">
|
||||
<Form.Item
|
||||
label="项目名称"
|
||||
field="name"
|
||||
@@ -71,9 +76,7 @@ function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModal
|
||||
<Form.Item
|
||||
label="项目描述"
|
||||
field="description"
|
||||
rules={[
|
||||
{ maxLength: 200, message: '项目描述不能超过200个字符' },
|
||||
]}
|
||||
rules={[{ maxLength: 200, message: '项目描述不能超过200个字符' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入项目描述(可选)"
|
||||
@@ -94,6 +97,33 @@ function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModal
|
||||
>
|
||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||
</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>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Modal, Form, Input, Button, Message } from '@arco-design/web-react';
|
||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||
import React, { useState } from 'react';
|
||||
import type { Project } from '../types';
|
||||
import { projectService } from '../service';
|
||||
import type { Project } from '../types';
|
||||
|
||||
interface EditProjectModalProps {
|
||||
visible: boolean;
|
||||
@@ -10,7 +10,12 @@ interface EditProjectModalProps {
|
||||
onSuccess: (updatedProject: Project) => void;
|
||||
}
|
||||
|
||||
function EditProjectModal({ visible, project, onCancel, onSuccess }: EditProjectModalProps) {
|
||||
function EditProjectModal({
|
||||
visible,
|
||||
project,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}: EditProjectModalProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -59,17 +64,18 @@ function EditProjectModal({ visible, project, onCancel, onSuccess }: EditProject
|
||||
<Button key="cancel" onClick={handleCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" loading={loading} onClick={handleSubmit}>
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form form={form} layout="vertical" autoComplete="off">
|
||||
<Form.Item
|
||||
label="项目名称"
|
||||
field="name"
|
||||
@@ -84,9 +90,7 @@ function EditProjectModal({ visible, project, onCancel, onSuccess }: EditProject
|
||||
<Form.Item
|
||||
label="项目描述"
|
||||
field="description"
|
||||
rules={[
|
||||
{ maxLength: 200, message: '项目描述不能超过200个字符' },
|
||||
]}
|
||||
rules={[{ maxLength: 200, message: '项目描述不能超过200个字符' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入项目描述"
|
||||
|
||||
@@ -1,53 +1,28 @@
|
||||
import {
|
||||
Card,
|
||||
Tag,
|
||||
Avatar,
|
||||
Card,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Modal,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconBranch,
|
||||
IconCalendar,
|
||||
IconCloud,
|
||||
IconEdit,
|
||||
IconMore,
|
||||
IconDelete,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import type { Project } from '../../types';
|
||||
import IconGitea from '@assets/images/gitea.svg?react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import type { Project } from '../../types';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: Project;
|
||||
onEdit?: (project: Project) => void;
|
||||
onDelete?: (project: Project) => void;
|
||||
}
|
||||
|
||||
function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
||||
function ProjectCard({ project }: ProjectCardProps) {
|
||||
const navigate = useNavigate();
|
||||
// 处理删除操作
|
||||
const handleDelete = () => {
|
||||
Modal.confirm({
|
||||
title: '确认删除项目',
|
||||
content: `确定要删除项目 "${project.name}" 吗?此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
cancelText: '取消',
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
onOk: () => {
|
||||
onDelete?.(project);
|
||||
},
|
||||
});
|
||||
};
|
||||
// 获取环境信息
|
||||
const environments = [
|
||||
{ name: 'staging', color: 'orange', icon: '🚧' },
|
||||
@@ -109,37 +84,9 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Tag color="blue" size="small" className="font-medium">
|
||||
活跃
|
||||
</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>
|
||||
|
||||
{/* 项目描述 */}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Grid, Typography, Button, Message } from '@arco-design/web-react';
|
||||
import { Button, Grid, Message, Typography } from '@arco-design/web-react';
|
||||
import { IconPlus } from '@arco-design/web-react/icon';
|
||||
import { useState } from 'react';
|
||||
import type { Project } from '../types';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
import { projectService } from './service';
|
||||
import ProjectCard from './components/ProjectCard';
|
||||
import EditProjectModal from './components/EditProjectModal';
|
||||
import type { Project } from '../types';
|
||||
import CreateProjectModal from './components/CreateProjectModal';
|
||||
import ProjectCard from './components/ProjectCard';
|
||||
import { projectService } from './service';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function ProjectPage() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
@@ -21,45 +18,18 @@ function ProjectPage() {
|
||||
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 = () => {
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCreateSuccess = (newProject: Project) => {
|
||||
setProjects(prev => [newProject, ...prev]);
|
||||
setProjects((prev) => [newProject, ...prev]);
|
||||
};
|
||||
|
||||
const handleCreateCancel = () => {
|
||||
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 (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
@@ -82,22 +52,11 @@ function ProjectPage() {
|
||||
<Grid.Row gutter={[16, 16]}>
|
||||
{projects.map((project) => (
|
||||
<Grid.Col key={project.id} span={8}>
|
||||
<ProjectCard
|
||||
project={project}
|
||||
onEdit={handleEditProject}
|
||||
onDelete={handleDeleteProject}
|
||||
/>
|
||||
<ProjectCard project={project} />
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid.Row>
|
||||
|
||||
<EditProjectModal
|
||||
visible={editModalVisible}
|
||||
project={editingProject}
|
||||
onCancel={handleEditCancel}
|
||||
onSuccess={handleEditSuccess}
|
||||
/>
|
||||
|
||||
<CreateProjectModal
|
||||
visible={createModalVisible}
|
||||
onCancel={handleCreateCancel}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { net, type APIResponse } from '@shared';
|
||||
import { type APIResponse, net } from '@shared';
|
||||
import type { Project } from '../types';
|
||||
|
||||
|
||||
class ProjectService {
|
||||
async list(params?: ProjectQueryParams) {
|
||||
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
enum BuildStatus {
|
||||
Idle = "Pending",
|
||||
Running = "Running",
|
||||
Stopped = "Stopped",
|
||||
Idle = 'Pending',
|
||||
Running = 'Running',
|
||||
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 {
|
||||
@@ -9,10 +35,98 @@ export interface Project {
|
||||
name: string;
|
||||
description: string;
|
||||
repository: string;
|
||||
projectDir: string; // 项目工作目录路径(必填)
|
||||
valid: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
status: BuildStatus;
|
||||
workspaceStatus?: WorkspaceStatus; // 工作目录状态信息
|
||||
}
|
||||
|
||||
// 流水线步骤类型定义
|
||||
export interface Step {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
order: number;
|
||||
script: string; // 执行的脚本命令
|
||||
valid: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
pipelineId: number;
|
||||
}
|
||||
|
||||
// 流水线类型定义
|
||||
export interface Pipeline {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
valid: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
projectId?: number;
|
||||
steps?: Step[];
|
||||
}
|
||||
|
||||
export interface Deployment {
|
||||
id: number;
|
||||
branch: string;
|
||||
env?: string;
|
||||
status: string;
|
||||
commitHash?: string;
|
||||
commitMessage?: string;
|
||||
buildLog?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
valid: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
export interface Commit {
|
||||
sha: string;
|
||||
commit: {
|
||||
message: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
date: string;
|
||||
};
|
||||
};
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
export interface Branch {
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
message: string;
|
||||
url: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
date: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// 创建部署请求的类型定义
|
||||
export interface CreateDeploymentRequest {
|
||||
projectId: number;
|
||||
pipelineId: number;
|
||||
branch: string;
|
||||
commitHash: string;
|
||||
commitMessage: string;
|
||||
env?: string;
|
||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, { Axios, type AxiosRequestConfig } from 'axios';
|
||||
import axios, { type Axios, type AxiosRequestConfig } from 'axios';
|
||||
|
||||
class Net {
|
||||
private readonly instance: Axios;
|
||||
@@ -18,7 +18,17 @@ class Net {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
console.log('error', error)
|
||||
console.log('error', error);
|
||||
// 对于DELETE请求返回204状态码的情况,视为成功
|
||||
if (error.response && error.response.status === 204 && error.config.method === 'delete') {
|
||||
// 创建一个模拟的成功响应
|
||||
return Promise.resolve({
|
||||
...error.response,
|
||||
data: error.response.data || null,
|
||||
status: 200, // 将204转换为200,避免被当作错误处理
|
||||
});
|
||||
}
|
||||
|
||||
if (error.status === 401 && error.config.url !== '/api/auth/info') {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
@@ -29,8 +39,16 @@ class Net {
|
||||
}
|
||||
|
||||
async request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||
const { data } = await this.instance.request<T>(config);
|
||||
return data;
|
||||
try {
|
||||
const response = await this.instance.request<T>(config);
|
||||
if (!response || !response.data) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { net, type APIResponse } from '@shared';
|
||||
import { type APIResponse, net } from '@shared';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface User {
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tailwindcss";
|
||||
|
||||
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');
|
||||
}
|
||||
2075
pnpm-lock.yaml
generated
2075
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user