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 Koa from 'koa';
|
||||||
import { initMiddlewares } from './middlewares/index.ts';
|
import { initMiddlewares } from './middlewares/index.ts';
|
||||||
import { log } from './libs/logger.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);
|
||||||
log.info('APP', 'Server started at port %d', PORT);
|
|
||||||
|
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 type { Context } from 'koa';
|
||||||
import { Controller, Get, Post } from '../../decorators/route.ts';
|
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 { log } from '../../libs/logger.ts';
|
||||||
import { gitea } from '../../libs/gitea.ts';
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
|
import { loginSchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/auth')
|
@Controller('/auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -20,7 +21,7 @@ export class AuthController {
|
|||||||
if (ctx.session.user) {
|
if (ctx.session.user) {
|
||||||
return 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 } =
|
const { access_token, refresh_token, expires_in } =
|
||||||
await gitea.getToken(code);
|
await gitea.getToken(code);
|
||||||
const giteaAuth = {
|
const giteaAuth = {
|
||||||
@@ -81,7 +82,3 @@ export class AuthController {
|
|||||||
return ctx.session?.user;
|
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 { ProjectController } from './project/index.ts';
|
||||||
export { UserController } from './user/index.ts';
|
export { UserController } from './user/index.ts';
|
||||||
export { AuthController } from './auth/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 { z } from 'zod';
|
||||||
|
import { projectDirSchema } from '../../libs/path-validator.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建项目验证架构
|
* 创建项目验证架构
|
||||||
@@ -15,6 +16,8 @@ export const createProjectSchema = z.object({
|
|||||||
repository: z.string({
|
repository: z.string({
|
||||||
message: '仓库地址必须是字符串',
|
message: '仓库地址必须是字符串',
|
||||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }),
|
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }),
|
||||||
|
|
||||||
|
projectDir: projectDirSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import prisma from '../../libs/db.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
|
import { GitManager } from '../../libs/git-manager.ts';
|
||||||
import {
|
import {
|
||||||
createProjectSchema,
|
createProjectSchema,
|
||||||
updateProjectSchema,
|
updateProjectSchema,
|
||||||
listProjectQuerySchema,
|
listProjectQuerySchema,
|
||||||
projectIdSchema,
|
projectIdSchema,
|
||||||
} from './schema.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/projects')
|
@Controller('/projects')
|
||||||
export class ProjectController {
|
export class ProjectController {
|
||||||
@@ -37,7 +38,7 @@ export class ProjectController {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -47,7 +48,7 @@ export class ProjectController {
|
|||||||
limit: query?.limit || 10,
|
limit: query?.limit || 10,
|
||||||
total,
|
total,
|
||||||
totalPages: Math.ceil(total / (query?.limit || 10)),
|
totalPages: Math.ceil(total / (query?.limit || 10)),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +68,48 @@ export class ProjectController {
|
|||||||
throw new BusinessError('项目不存在', 1002, 404);
|
throw new BusinessError('项目不存在', 1002, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return project;
|
// 获取工作目录状态信息
|
||||||
|
let workspaceStatus = null;
|
||||||
|
if (project.projectDir) {
|
||||||
|
try {
|
||||||
|
const status = await GitManager.checkWorkspaceStatus(
|
||||||
|
project.projectDir,
|
||||||
|
);
|
||||||
|
let size = 0;
|
||||||
|
let gitInfo = null;
|
||||||
|
|
||||||
|
if (status.exists && !status.isEmpty) {
|
||||||
|
size = await GitManager.getDirectorySize(project.projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.hasGit) {
|
||||||
|
gitInfo = await GitManager.getGitInfo(project.projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceStatus = {
|
||||||
|
...status,
|
||||||
|
size,
|
||||||
|
gitInfo,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'project',
|
||||||
|
'Failed to get workspace status for project %s: %s',
|
||||||
|
project.name,
|
||||||
|
(error as Error).message,
|
||||||
|
);
|
||||||
|
// 即使获取状态失败,也返回项目信息
|
||||||
|
workspaceStatus = {
|
||||||
|
status: 'error',
|
||||||
|
error: (error as Error).message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
workspaceStatus,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/projects - 创建项目
|
// POST /api/projects - 创建项目
|
||||||
@@ -75,18 +117,36 @@ export class ProjectController {
|
|||||||
async create(ctx: Context) {
|
async create(ctx: Context) {
|
||||||
const validatedData = createProjectSchema.parse(ctx.request.body);
|
const validatedData = createProjectSchema.parse(ctx.request.body);
|
||||||
|
|
||||||
|
// 检查工作目录是否已被其他项目使用
|
||||||
|
const existingProject = await prisma.project.findFirst({
|
||||||
|
where: {
|
||||||
|
projectDir: validatedData.projectDir,
|
||||||
|
valid: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingProject) {
|
||||||
|
throw new BusinessError('该工作目录已被其他项目使用', 1003, 400);
|
||||||
|
}
|
||||||
|
|
||||||
const project = await prisma.project.create({
|
const project = await prisma.project.create({
|
||||||
data: {
|
data: {
|
||||||
name: validatedData.name,
|
name: validatedData.name,
|
||||||
description: validatedData.description || '',
|
description: validatedData.description || '',
|
||||||
repository: validatedData.repository,
|
repository: validatedData.repository,
|
||||||
|
projectDir: validatedData.projectDir,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('project', 'Created new project: %s', project.name);
|
log.info(
|
||||||
|
'project',
|
||||||
|
'Created new project: %s with projectDir: %s',
|
||||||
|
project.name,
|
||||||
|
project.projectDir,
|
||||||
|
);
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 type { Context } from 'koa';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.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')
|
@Get('/detail/:id')
|
||||||
async detail(ctx: Context) {
|
async detail(ctx: Context) {
|
||||||
const { id } = ctx.params;
|
const { id } = userIdSchema.parse(ctx.params);
|
||||||
|
|
||||||
// 模拟根据ID查找用户
|
// 模拟根据ID查找用户
|
||||||
const user = {
|
const user = {
|
||||||
id: Number(id),
|
id,
|
||||||
name: 'User ' + id,
|
name: 'User ' + id,
|
||||||
email: `user${id}@example.com`,
|
email: `user${id}@example.com`,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Number(id) > 100) {
|
if (id > 100) {
|
||||||
throw new BusinessError('用户不存在', 2001, 404);
|
throw new BusinessError('用户不存在', 2001, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,14 +48,14 @@ export class UserController {
|
|||||||
|
|
||||||
@Post('')
|
@Post('')
|
||||||
async create(ctx: Context) {
|
async create(ctx: Context) {
|
||||||
const body = (ctx.request as any).body;
|
const body = createUserSchema.parse(ctx.request.body);
|
||||||
|
|
||||||
// 模拟创建用户
|
// 模拟创建用户
|
||||||
const newUser = {
|
const newUser = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
...body,
|
...body,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
status: 'active'
|
status: body.status
|
||||||
};
|
};
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
@@ -57,12 +63,12 @@ export class UserController {
|
|||||||
|
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
async update(ctx: Context) {
|
async update(ctx: Context) {
|
||||||
const { id } = ctx.params;
|
const { id } = userIdSchema.parse(ctx.params);
|
||||||
const body = (ctx.request as any).body;
|
const body = updateUserSchema.parse(ctx.request.body);
|
||||||
|
|
||||||
// 模拟更新用户
|
// 模拟更新用户
|
||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
id: Number(id),
|
id,
|
||||||
...body,
|
...body,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
@@ -72,9 +78,9 @@ export class UserController {
|
|||||||
|
|
||||||
@Delete('/:id')
|
@Delete('/:id')
|
||||||
async delete(ctx: Context) {
|
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);
|
throw new BusinessError('管理员账户不能删除', 2002, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +94,7 @@ export class UserController {
|
|||||||
|
|
||||||
@Get('/search')
|
@Get('/search')
|
||||||
async search(ctx: Context) {
|
async search(ctx: Context) {
|
||||||
const { keyword, status } = ctx.query;
|
const { keyword, status } = searchUserQuerySchema.parse(ctx.query);
|
||||||
|
|
||||||
// 模拟搜索逻辑
|
// 模拟搜索逻辑
|
||||||
let results = [
|
let results = [
|
||||||
@@ -98,8 +104,8 @@ export class UserController {
|
|||||||
|
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
results = results.filter(user =>
|
results = results.filter(user =>
|
||||||
user.name.toLowerCase().includes(String(keyword).toLowerCase()) ||
|
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||||
user.email.toLowerCase().includes(String(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;
|
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) {
|
private getHeaders(accessToken?: string) {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'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 { CORS } from './cors.ts';
|
||||||
import { HttpLogger } from './logger.ts';
|
import { HttpLogger } from './logger.ts';
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import { Authorization } from './Authorization.ts';
|
import { Authorization } from './authorization.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化中间件
|
* 初始化中间件
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
ProjectController,
|
ProjectController,
|
||||||
UserController,
|
UserController,
|
||||||
AuthController,
|
AuthController,
|
||||||
|
DeploymentController,
|
||||||
|
PipelineController,
|
||||||
|
StepController,
|
||||||
|
GitController
|
||||||
} from '../controllers/index.ts';
|
} from '../controllers/index.ts';
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
|
|
||||||
@@ -38,6 +42,10 @@ export class Router implements Middleware {
|
|||||||
ProjectController,
|
ProjectController,
|
||||||
UserController,
|
UserController,
|
||||||
AuthController,
|
AuthController,
|
||||||
|
DeploymentController,
|
||||||
|
PipelineController,
|
||||||
|
StepController,
|
||||||
|
GitController
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 输出注册的路由信息
|
// 输出注册的路由信息
|
||||||
|
|||||||
@@ -12,13 +12,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/cors": "^5.0.0",
|
"@koa/cors": "^5.0.0",
|
||||||
"@koa/router": "^14.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": "^3.0.1",
|
||||||
"koa-bodyparser": "^4.4.1",
|
"koa-bodyparser": "^4.4.1",
|
||||||
"koa-session": "^7.0.2",
|
"koa-session": "^7.0.2",
|
||||||
"pino": "^9.9.1",
|
"pino": "^9.9.1",
|
||||||
"pino-pretty": "^13.1.1",
|
"pino-pretty": "^13.1.1",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5",
|
||||||
|
"zx": "^8.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node-ts": "^23.6.1",
|
"@tsconfig/node-ts": "^23.6.1",
|
||||||
@@ -28,7 +32,7 @@
|
|||||||
"@types/koa__cors": "^5.0.0",
|
"@types/koa__cors": "^5.0.0",
|
||||||
"@types/koa__router": "^12.0.4",
|
"@types/koa__router": "^12.0.4",
|
||||||
"@types/node": "^24.3.0",
|
"@types/node": "^24.3.0",
|
||||||
"prisma": "^6.15.0",
|
"prisma": "^7.0.0",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.5",
|
||||||
"typescript": "^5.9.2"
|
"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,36 +2,29 @@
|
|||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client"
|
||||||
output = "../generated/prisma"
|
output = "../generated"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Project {
|
model Project {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
description String?
|
description String?
|
||||||
repository String
|
repository String
|
||||||
valid Int @default(1)
|
projectDir String @unique // 项目工作目录路径(必填)
|
||||||
createdAt DateTime @default(now())
|
// Relations
|
||||||
updatedAt DateTime @updatedAt
|
deployments Deployment[]
|
||||||
createdBy String
|
pipelines Pipeline[]
|
||||||
updatedBy String
|
|
||||||
}
|
|
||||||
|
|
||||||
model Environment {
|
valid Int @default(1)
|
||||||
id Int @id @default(autoincrement())
|
createdAt DateTime @default(now())
|
||||||
name String
|
updatedAt DateTime @updatedAt
|
||||||
description String?
|
createdBy String
|
||||||
valid Int @default(1)
|
updatedBy String
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
createdBy String
|
|
||||||
updatedBy String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
@@ -47,3 +40,56 @@ model User {
|
|||||||
createdBy String @default("system")
|
createdBy String @default("system")
|
||||||
updatedBy 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"
|
"@tsconfig/node-ts/tsconfig.json"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"module": "ESNext",
|
||||||
"useDefineForClassFields": false
|
"moduleResolution": "node",
|
||||||
|
"target": "ES2023",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,22 @@
|
|||||||
"preview": "rsbuild preview"
|
"preview": "rsbuild preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arco-design/web-react": "^2.66.4",
|
"@arco-design/web-react": "^2.66.8",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"react": "^18.2.0",
|
"dayjs": "^1.11.19",
|
||||||
"react-dom": "^18.2.0",
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.8.0",
|
"react-router": "^7.8.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@arco-plugins/unplugin-react": "2.0.0-beta.5",
|
"@arco-plugins/unplugin-react": "2.0.0-beta.5",
|
||||||
"@rsbuild/core": "^1.4.13",
|
"@rsbuild/core": "^1.6.7",
|
||||||
"@rsbuild/plugin-less": "^1.4.0",
|
"@rsbuild/plugin-less": "^1.5.0",
|
||||||
"@rsbuild/plugin-react": "^1.3.4",
|
"@rsbuild/plugin-react": "^1.4.2",
|
||||||
"@rsbuild/plugin-svgr": "^1.2.2",
|
"@rsbuild/plugin-svgr": "^1.2.2",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/react": "^18.3.24",
|
"@types/react": "^18.3.24",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ArcoDesignPlugin } from '@arco-plugins/unplugin-react';
|
import { ArcoDesignPlugin } from '@arco-plugins/unplugin-react';
|
||||||
import { defineConfig } from '@rsbuild/core';
|
import { defineConfig } from '@rsbuild/core';
|
||||||
import { pluginReact } from '@rsbuild/plugin-react';
|
|
||||||
import { pluginLess } from '@rsbuild/plugin-less';
|
import { pluginLess } from '@rsbuild/plugin-less';
|
||||||
|
import { pluginReact } from '@rsbuild/plugin-react';
|
||||||
import { pluginSvgr } from '@rsbuild/plugin-svgr';
|
import { pluginSvgr } from '@rsbuild/plugin-svgr';
|
||||||
|
|
||||||
export default defineConfig({
|
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(
|
export function useAsyncEffect(
|
||||||
effect: () => Promise<void>,
|
effect: () => Promise<void | (() => void)>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
) {
|
) {
|
||||||
|
const callback = useCallback(effect, [...deps]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
effect();
|
const cleanupPromise = callback();
|
||||||
}, [...deps]);
|
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 App from '@pages/App';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router';
|
import { BrowserRouter } from 'react-router';
|
||||||
import { useGlobalStore } from './stores/global';
|
import { useGlobalStore } from './stores/global';
|
||||||
|
import '@arco-design/web-react/es/_util/react-19-adapter'
|
||||||
|
|
||||||
const rootEl = document.getElementById('root');
|
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 Home from '@pages/home';
|
||||||
import Login from '@pages/login';
|
import Login from '@pages/login';
|
||||||
import ProjectList from '@pages/project/list';
|
|
||||||
import ProjectDetail from '@pages/project/detail';
|
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';
|
import '@styles/index.css';
|
||||||
const App = () => {
|
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() {
|
function Env() {
|
||||||
const [env, setEnv] = useState([]);
|
return <div>env page</div>;
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
env page
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Env;
|
export default Env;
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import {
|
|||||||
IconMenuUnfold,
|
IconMenuUnfold,
|
||||||
IconRobot,
|
IconRobot,
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import { useState } from 'react';
|
|
||||||
import Logo from '@assets/images/logo.svg?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 { Link, Outlet } from 'react-router';
|
||||||
import { useGlobalStore } from '../../stores/global';
|
import { useGlobalStore } from '../../stores/global';
|
||||||
import { loginService } from '@pages/login/service';
|
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Button } from '@arco-design/web-react';
|
import { Button } from '@arco-design/web-react';
|
||||||
import Gitea from '@assets/images/gitea.svg?react';
|
import Gitea from '@assets/images/gitea.svg?react';
|
||||||
import { loginService } from './service';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router';
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
|
import { loginService } from './service';
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const [ searchParams ] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const authCode = searchParams.get('code');
|
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 { Message, Notification } from '@arco-design/web-react';
|
||||||
|
import { net } from '@shared';
|
||||||
|
import type { NavigateFunction } from 'react-router';
|
||||||
import { useGlobalStore } from '../../stores/global';
|
import { useGlobalStore } from '../../stores/global';
|
||||||
|
import type { AuthLoginResponse, AuthURLResponse } from './types';
|
||||||
|
|
||||||
class LoginService {
|
class LoginService {
|
||||||
async getAuthUrl() {
|
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';
|
import { List, Space, Tag } from '@arco-design/web-react';
|
||||||
|
import { formatDateTime } from '../../../../utils/time';
|
||||||
// 部署记录类型定义
|
import type { Deployment } from '../../types';
|
||||||
interface DeployRecord {
|
|
||||||
id: number;
|
|
||||||
branch: string;
|
|
||||||
env: string;
|
|
||||||
commit: string;
|
|
||||||
status: 'success' | 'running' | 'failed' | 'pending';
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeployRecordItemProps {
|
interface DeployRecordItemProps {
|
||||||
item: DeployRecord;
|
item: Deployment;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: (id: number) => void;
|
onSelect: (id: number) => void;
|
||||||
}
|
}
|
||||||
@@ -22,11 +14,8 @@ function DeployRecordItem({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: DeployRecordItemProps) {
|
}: DeployRecordItemProps) {
|
||||||
// 状态标签渲染函数
|
// 状态标签渲染函数
|
||||||
const getStatusTag = (status: DeployRecord['status']) => {
|
const getStatusTag = (status: Deployment['status']) => {
|
||||||
const statusMap: Record<
|
const statusMap: Record<string, { color: string; text: string }> = {
|
||||||
DeployRecord['status'],
|
|
||||||
{ color: string; text: string }
|
|
||||||
> = {
|
|
||||||
success: { color: 'green', text: '成功' },
|
success: { color: 'green', text: '成功' },
|
||||||
running: { color: 'blue', text: '运行中' },
|
running: { color: 'blue', text: '运行中' },
|
||||||
failed: { color: 'red', text: '失败' },
|
failed: { color: 'red', text: '失败' },
|
||||||
@@ -46,6 +35,7 @@ function DeployRecordItem({
|
|||||||
const config = envMap[env] || { color: 'gray', text: env };
|
const config = envMap[env] || { color: 'gray', text: env };
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -67,7 +57,7 @@ function DeployRecordItem({
|
|||||||
#{item.id}
|
#{item.id}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-600 text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -76,12 +66,10 @@ function DeployRecordItem({
|
|||||||
<Space size="medium" wrap>
|
<Space size="medium" wrap>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
分支:{' '}
|
分支:{' '}
|
||||||
<span className="font-medium text-gray-700">
|
<span className="font-medium text-gray-700">{item.branch}</span>
|
||||||
{item.branch}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
环境: {getEnvTag(item.env)}
|
环境: {getEnvTag(item.env || 'unknown')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
状态: {getStatusTag(item.status)}
|
状态: {getStatusTag(item.status)}
|
||||||
@@ -89,7 +77,7 @@ function DeployRecordItem({
|
|||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
执行时间:{' '}
|
执行时间:{' '}
|
||||||
<span className="font-medium text-gray-700">
|
<span className="font-medium text-gray-700">
|
||||||
{item.createdAt}
|
{formatDateTime(item.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import { Typography, Tag, Switch, Button } from '@arco-design/web-react';
|
import { Button, Switch, Tag, Typography } from '@arco-design/web-react';
|
||||||
import { IconDragArrow, IconEdit, IconDelete } from '@arco-design/web-react/icon';
|
import {
|
||||||
|
IconDelete,
|
||||||
|
IconDragArrow,
|
||||||
|
IconEdit,
|
||||||
|
} from '@arco-design/web-react/icon';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import type { Step } from '../../types';
|
||||||
|
|
||||||
// 流水线步骤类型定义
|
interface StepWithEnabled extends Step {
|
||||||
interface PipelineStep {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
script: string;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PipelineStepItemProps {
|
interface PipelineStepItemProps {
|
||||||
step: PipelineStep;
|
step: StepWithEnabled;
|
||||||
index: number;
|
index: number;
|
||||||
pipelineId: string;
|
pipelineId: number;
|
||||||
onToggle: (pipelineId: string, stepId: string, enabled: boolean) => void;
|
onToggle: (pipelineId: number, stepId: number, enabled: boolean) => void;
|
||||||
onEdit: (pipelineId: string, step: PipelineStep) => void;
|
onEdit: (pipelineId: number, step: StepWithEnabled) => void;
|
||||||
onDelete: (pipelineId: string, stepId: string) => void;
|
onDelete: (pipelineId: number, stepId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PipelineStepItem({
|
function PipelineStepItem({
|
||||||
@@ -79,6 +80,9 @@ function PipelineStepItem({
|
|||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<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>
|
<pre className="whitespace-pre-wrap break-words">{step.script}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,226 @@
|
|||||||
import { net, type APIResponse } from '@shared';
|
import { type APIResponse, net } from '@shared';
|
||||||
import type { Project } from '../types';
|
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types';
|
||||||
|
|
||||||
class DetailService {
|
class DetailService {
|
||||||
async getProject(id: string) {
|
async getProject(id: string) {
|
||||||
const { code, data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
return data;
|
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();
|
export const detailService = new DetailService();
|
||||||
|
|||||||
@@ -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 { useState } from 'react';
|
||||||
import type { Project } from '../types';
|
|
||||||
import { projectService } from '../service';
|
import { projectService } from '../service';
|
||||||
|
import type { Project } from '../../types';
|
||||||
|
|
||||||
interface CreateProjectModalProps {
|
interface CreateProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -9,7 +9,11 @@ interface CreateProjectModalProps {
|
|||||||
onSuccess: (newProject: Project) => void;
|
onSuccess: (newProject: Project) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModalProps) {
|
function CreateProjectModal({
|
||||||
|
visible,
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
}: CreateProjectModalProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -46,17 +50,18 @@ function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModal
|
|||||||
<Button key="cancel" onClick={handleCancel}>
|
<Button key="cancel" onClick={handleCancel}>
|
||||||
取消
|
取消
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button key="submit" type="primary" loading={loading} onClick={handleSubmit}>
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
创建
|
创建
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
style={{ width: 500 }}
|
style={{ width: 500 }}
|
||||||
>
|
>
|
||||||
<Form
|
<Form form={form} layout="vertical" autoComplete="off">
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
autoComplete="off"
|
|
||||||
>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="项目名称"
|
label="项目名称"
|
||||||
field="name"
|
field="name"
|
||||||
@@ -71,9 +76,7 @@ function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModal
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label="项目描述"
|
label="项目描述"
|
||||||
field="description"
|
field="description"
|
||||||
rules={[
|
rules={[{ maxLength: 200, message: '项目描述不能超过200个字符' }]}
|
||||||
{ maxLength: 200, message: '项目描述不能超过200个字符' },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="请输入项目描述(可选)"
|
placeholder="请输入项目描述(可选)"
|
||||||
@@ -94,6 +97,33 @@ function CreateProjectModal({ visible, onCancel, onSuccess }: CreateProjectModal
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="工作目录路径"
|
||||||
|
field="projectDir"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入工作目录路径' },
|
||||||
|
{
|
||||||
|
validator: (value, cb) => {
|
||||||
|
if (!value) {
|
||||||
|
return cb('工作目录路径不能为空');
|
||||||
|
}
|
||||||
|
if (!value.startsWith('/')) {
|
||||||
|
return cb('工作目录路径必须是绝对路径(以 / 开头)');
|
||||||
|
}
|
||||||
|
if (value.includes('..') || value.includes('~')) {
|
||||||
|
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||||
|
}
|
||||||
|
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
||||||
|
return cb('路径包含非法字符');
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,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 React, { useState } from 'react';
|
||||||
import type { Project } from '../types';
|
|
||||||
import { projectService } from '../service';
|
import { projectService } from '../service';
|
||||||
|
import type { Project } from '../types';
|
||||||
|
|
||||||
interface EditProjectModalProps {
|
interface EditProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -10,7 +10,12 @@ interface EditProjectModalProps {
|
|||||||
onSuccess: (updatedProject: Project) => void;
|
onSuccess: (updatedProject: Project) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditProjectModal({ visible, project, onCancel, onSuccess }: EditProjectModalProps) {
|
function EditProjectModal({
|
||||||
|
visible,
|
||||||
|
project,
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
}: EditProjectModalProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -59,17 +64,18 @@ function EditProjectModal({ visible, project, onCancel, onSuccess }: EditProject
|
|||||||
<Button key="cancel" onClick={handleCancel}>
|
<Button key="cancel" onClick={handleCancel}>
|
||||||
取消
|
取消
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button key="submit" type="primary" loading={loading} onClick={handleSubmit}>
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
保存
|
保存
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
style={{ width: 500 }}
|
style={{ width: 500 }}
|
||||||
>
|
>
|
||||||
<Form
|
<Form form={form} layout="vertical" autoComplete="off">
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
autoComplete="off"
|
|
||||||
>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="项目名称"
|
label="项目名称"
|
||||||
field="name"
|
field="name"
|
||||||
@@ -84,9 +90,7 @@ function EditProjectModal({ visible, project, onCancel, onSuccess }: EditProject
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label="项目描述"
|
label="项目描述"
|
||||||
field="description"
|
field="description"
|
||||||
rules={[
|
rules={[{ maxLength: 200, message: '项目描述不能超过200个字符' }]}
|
||||||
{ maxLength: 200, message: '项目描述不能超过200个字符' },
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="请输入项目描述"
|
placeholder="请输入项目描述"
|
||||||
|
|||||||
@@ -1,53 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
Card,
|
|
||||||
Tag,
|
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Card,
|
||||||
Space,
|
Space,
|
||||||
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
Dropdown,
|
|
||||||
Menu,
|
|
||||||
Modal,
|
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
IconBranch,
|
IconBranch,
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconCloud,
|
IconCloud,
|
||||||
IconEdit,
|
|
||||||
IconMore,
|
|
||||||
IconDelete,
|
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import type { Project } from '../../types';
|
|
||||||
import IconGitea from '@assets/images/gitea.svg?react';
|
import IconGitea from '@assets/images/gitea.svg?react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
import type { Project } from '../../types';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
interface ProjectCardProps {
|
interface ProjectCardProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
onEdit?: (project: Project) => void;
|
|
||||||
onDelete?: (project: Project) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
function ProjectCard({ project }: ProjectCardProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// 处理删除操作
|
|
||||||
const handleDelete = () => {
|
|
||||||
Modal.confirm({
|
|
||||||
title: '确认删除项目',
|
|
||||||
content: `确定要删除项目 "${project.name}" 吗?此操作不可恢复。`,
|
|
||||||
okText: '删除',
|
|
||||||
cancelText: '取消',
|
|
||||||
okButtonProps: {
|
|
||||||
status: 'danger',
|
|
||||||
},
|
|
||||||
onOk: () => {
|
|
||||||
onDelete?.(project);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// 获取环境信息
|
// 获取环境信息
|
||||||
const environments = [
|
const environments = [
|
||||||
{ name: 'staging', color: 'orange', icon: '🚧' },
|
{ name: 'staging', color: 'orange', icon: '🚧' },
|
||||||
@@ -109,37 +84,9 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<Tag color="blue" size="small" className="font-medium">
|
||||||
<Tag color="blue" size="small" className="font-medium">
|
活跃
|
||||||
活跃
|
</Tag>
|
||||||
</Tag>
|
|
||||||
<Dropdown
|
|
||||||
droplist={
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key="edit" onClick={() => onEdit?.(project)}>
|
|
||||||
<IconEdit className="mr-2" />
|
|
||||||
编辑
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
key="delete"
|
|
||||||
onClick={() => handleDelete()}
|
|
||||||
className="text-red-500"
|
|
||||||
>
|
|
||||||
<IconDelete className="mr-2" />
|
|
||||||
删除
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
position="br"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<IconMore />}
|
|
||||||
className="text-gray-400 hover:text-blue-500 hover:bg-blue-50 transition-all duration-200 p-1 rounded-md"
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 项目描述 */}
|
{/* 项目描述 */}
|
||||||
|
|||||||
@@ -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 { IconPlus } from '@arco-design/web-react/icon';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Project } from '../types';
|
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
import { projectService } from './service';
|
import type { Project } from '../types';
|
||||||
import ProjectCard from './components/ProjectCard';
|
|
||||||
import EditProjectModal from './components/EditProjectModal';
|
|
||||||
import CreateProjectModal from './components/CreateProjectModal';
|
import CreateProjectModal from './components/CreateProjectModal';
|
||||||
|
import ProjectCard from './components/ProjectCard';
|
||||||
|
import { projectService } from './service';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
function ProjectPage() {
|
function ProjectPage() {
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
|
||||||
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
||||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
@@ -21,45 +18,18 @@ function ProjectPage() {
|
|||||||
setProjects(response.data);
|
setProjects(response.data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEditProject = (project: Project) => {
|
|
||||||
setEditingProject(project);
|
|
||||||
setEditModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditSuccess = (updatedProject: Project) => {
|
|
||||||
setProjects(prev =>
|
|
||||||
prev.map(p => p.id === updatedProject.id ? updatedProject : p)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditCancel = () => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
setEditingProject(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateProject = () => {
|
const handleCreateProject = () => {
|
||||||
setCreateModalVisible(true);
|
setCreateModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateSuccess = (newProject: Project) => {
|
const handleCreateSuccess = (newProject: Project) => {
|
||||||
setProjects(prev => [newProject, ...prev]);
|
setProjects((prev) => [newProject, ...prev]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateCancel = () => {
|
const handleCreateCancel = () => {
|
||||||
setCreateModalVisible(false);
|
setCreateModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteProject = async (project: Project) => {
|
|
||||||
try {
|
|
||||||
await projectService.delete(project.id);
|
|
||||||
setProjects(prev => prev.filter(p => p.id !== project.id));
|
|
||||||
Message.success('项目删除成功');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除项目失败:', error);
|
|
||||||
Message.error('删除项目失败,请稍后重试');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
@@ -82,22 +52,11 @@ function ProjectPage() {
|
|||||||
<Grid.Row gutter={[16, 16]}>
|
<Grid.Row gutter={[16, 16]}>
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<Grid.Col key={project.id} span={8}>
|
<Grid.Col key={project.id} span={8}>
|
||||||
<ProjectCard
|
<ProjectCard project={project} />
|
||||||
project={project}
|
|
||||||
onEdit={handleEditProject}
|
|
||||||
onDelete={handleDeleteProject}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
))}
|
))}
|
||||||
</Grid.Row>
|
</Grid.Row>
|
||||||
|
|
||||||
<EditProjectModal
|
|
||||||
visible={editModalVisible}
|
|
||||||
project={editingProject}
|
|
||||||
onCancel={handleEditCancel}
|
|
||||||
onSuccess={handleEditSuccess}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreateProjectModal
|
<CreateProjectModal
|
||||||
visible={createModalVisible}
|
visible={createModalVisible}
|
||||||
onCancel={handleCreateCancel}
|
onCancel={handleCreateCancel}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { net, type APIResponse } from '@shared';
|
import { type APIResponse, net } from '@shared';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../types';
|
||||||
|
|
||||||
|
|
||||||
class ProjectService {
|
class ProjectService {
|
||||||
async list(params?: ProjectQueryParams) {
|
async list(params?: ProjectQueryParams) {
|
||||||
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
||||||
|
|||||||
@@ -1,7 +1,33 @@
|
|||||||
enum BuildStatus {
|
enum BuildStatus {
|
||||||
Idle = "Pending",
|
Idle = 'Pending',
|
||||||
Running = "Running",
|
Running = 'Running',
|
||||||
Stopped = "Stopped",
|
Stopped = 'Stopped',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工作目录状态枚举
|
||||||
|
export enum WorkspaceDirStatus {
|
||||||
|
NOT_CREATED = 'not_created',
|
||||||
|
EMPTY = 'empty',
|
||||||
|
NO_GIT = 'no_git',
|
||||||
|
READY = 'ready',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git 仓库信息
|
||||||
|
export interface GitInfo {
|
||||||
|
branch?: string;
|
||||||
|
lastCommit?: string;
|
||||||
|
lastCommitMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工作目录状态信息
|
||||||
|
export interface WorkspaceStatus {
|
||||||
|
status: WorkspaceDirStatus;
|
||||||
|
exists: boolean;
|
||||||
|
isEmpty?: boolean;
|
||||||
|
hasGit?: boolean;
|
||||||
|
size?: number;
|
||||||
|
gitInfo?: GitInfo;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
@@ -9,10 +35,98 @@ export interface Project {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
|
projectDir: string; // 项目工作目录路径(必填)
|
||||||
valid: number;
|
valid: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
updatedBy: string;
|
updatedBy: string;
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
workspaceStatus?: WorkspaceStatus; // 工作目录状态信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流水线步骤类型定义
|
||||||
|
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 {
|
class Net {
|
||||||
private readonly instance: Axios;
|
private readonly instance: Axios;
|
||||||
@@ -18,7 +18,17 @@ class Net {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(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') {
|
if (error.status === 401 && error.config.url !== '/api/auth/info') {
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
return;
|
return;
|
||||||
@@ -29,8 +39,16 @@ class Net {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async request<T>(config: AxiosRequestConfig): Promise<T> {
|
async request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||||
const { data } = await this.instance.request<T>(config);
|
try {
|
||||||
return data;
|
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';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
interface User {
|
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');
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@pages/*": ["./src/pages/*"],
|
"@pages/*": ["./src/pages/*"],
|
||||||
"@styles/*": ["./src/styles/*"],
|
"@styles/*": ["./src/styles/*"],
|
||||||
"@assets/*": ["./src/assets/*"],
|
"@assets/*": ["./src/assets/*"],
|
||||||
|
|||||||
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