Compare commits

...

2 Commits

Author SHA1 Message Date
a067d167e9 fix: 报错修复 2026-01-03 23:18:33 +08:00
d22fdc9618 feat: 实现环境变量预设功能 & 移除稀疏检出
## 后端改动
- 添加 Project.envPresets 字段(JSON 格式)
- 移除 Deployment.env 字段,统一使用 envVars
- 更新部署 DTO,支持 envVars (Record<string, string>)
- pipeline-runner 支持解析并注入 envVars 到环境
- 移除稀疏检出模板和相关环境变量
- 优化代码格式(Biome lint & format)

## 前端改动
- 新增 EnvPresetsEditor 组件(支持单选/多选/输入框类型)
- 项目创建/编辑界面集成环境预设编辑器
- 部署界面基于预设动态生成环境变量表单
- 移除稀疏检出表单项
- 项目详情页添加环境变量预设配置 tab
- 优化部署界面布局(基本参数 & 环境变量分区)

## 文档
- 添加完整文档目录结构(docs/)
- 创建设计文档 design-0005(部署流程重构)
- 添加 API 文档、架构设计文档等

## 数据库
- 执行 prisma db push 同步 schema 变更
2026-01-03 22:59:20 +08:00
72 changed files with 9613 additions and 5865 deletions

2
.gitignore vendored
View File

@@ -14,3 +14,5 @@ dist/
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
.env

View File

@@ -1,8 +1,8 @@
import Koa from 'koa'; import Koa from 'koa';
import { initMiddlewares } from './middlewares/index.ts';
import { log } from './libs/logger.ts';
import { ExecutionQueue } from './libs/execution-queue.ts'; import { ExecutionQueue } from './libs/execution-queue.ts';
import { log } from './libs/logger.ts';
import { initializePipelineTemplates } from './libs/pipeline-template.ts'; import { initializePipelineTemplates } from './libs/pipeline-template.ts';
import { initMiddlewares } from './middlewares/index.ts';
// 初始化应用 // 初始化应用
async function initializeApp() { async function initializeApp() {
@@ -26,7 +26,7 @@ async function initializeApp() {
} }
// 启动应用 // 启动应用
initializeApp().catch(error => { initializeApp().catch((error) => {
console.error('Failed to start application:', error); console.error('Failed to start application:', error);
process.exit(1); process.exit(1);
}); });

View File

@@ -1,8 +1,8 @@
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/prisma.ts';
import { log } from '../../libs/logger.ts';
import { gitea } from '../../libs/gitea.ts'; import { gitea } from '../../libs/gitea.ts';
import { log } from '../../libs/logger.ts';
import { prisma } from '../../libs/prisma.ts';
import { loginSchema } from './dto.ts'; import { loginSchema } from './dto.ts';
@Controller('/auth') @Controller('/auth')

View File

@@ -12,8 +12,7 @@ export const createDeploymentSchema = z.object({
branch: z.string().min(1, { message: '分支不能为空' }), branch: z.string().min(1, { message: '分支不能为空' }),
commitHash: z.string().min(1, { message: '提交哈希不能为空' }), commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
commitMessage: z.string().min(1, { message: '提交信息不能为空' }), commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
env: z.string().optional(), envVars: z.record(z.string(), z.string()).optional(), // 环境变量 key-value 对象
sparseCheckoutPaths: z.string().optional(), // 添加稀疏检出路径字段
}); });
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>; export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;

View File

@@ -1,15 +1,17 @@
import type { Context } from 'koa';
import { Controller, Get, Post } from '../../decorators/route.ts'; import { Controller, Get, Post } from '../../decorators/route.ts';
import type { Prisma } from '../../generated/client.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'; import { ExecutionQueue } from '../../libs/execution-queue.ts';
import { prisma } from '../../libs/prisma.ts';
import { createDeploymentSchema, listDeploymentsQuerySchema } from './dto.ts';
@Controller('/deployments') @Controller('/deployments')
export class DeploymentController { export class DeploymentController {
@Get('') @Get('')
async list(ctx: Context) { async list(ctx: Context) {
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(ctx.query); const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(
ctx.query,
);
const where: Prisma.DeploymentWhereInput = { const where: Prisma.DeploymentWhereInput = {
valid: 1, valid: 1,
}; };
@@ -50,8 +52,7 @@ export class DeploymentController {
connect: { id: body.projectId }, connect: { id: body.projectId },
}, },
pipelineId: body.pipelineId, pipelineId: body.pipelineId,
env: body.env || 'dev', envVars: body.envVars ? JSON.stringify(body.envVars) : null,
sparseCheckoutPaths: body.sparseCheckoutPaths || '', // 添加稀疏检出路径
buildLog: '', buildLog: '',
createdBy: 'system', // TODO: get from user createdBy: 'system', // TODO: get from user
updatedBy: 'system', updatedBy: 'system',
@@ -73,7 +74,7 @@ export class DeploymentController {
// 获取原始部署记录 // 获取原始部署记录
const originalDeployment = await prisma.deployment.findUnique({ const originalDeployment = await prisma.deployment.findUnique({
where: { id: Number(id) } where: { id: Number(id) },
}); });
if (!originalDeployment) { if (!originalDeployment) {
@@ -82,7 +83,7 @@ export class DeploymentController {
code: 404, code: 404,
message: '部署记录不存在', message: '部署记录不存在',
data: null, data: null,
timestamp: Date.now() timestamp: Date.now(),
}; };
return; return;
} }
@@ -96,8 +97,7 @@ export class DeploymentController {
status: 'pending', status: 'pending',
projectId: originalDeployment.projectId, projectId: originalDeployment.projectId,
pipelineId: originalDeployment.pipelineId, pipelineId: originalDeployment.pipelineId,
env: originalDeployment.env, envVars: originalDeployment.envVars,
sparseCheckoutPaths: originalDeployment.sparseCheckoutPaths,
buildLog: '', buildLog: '',
createdBy: 'system', createdBy: 'system',
updatedBy: 'system', updatedBy: 'system',
@@ -113,7 +113,7 @@ export class DeploymentController {
code: 0, code: 0,
message: '重新执行任务已创建', message: '重新执行任务已创建',
data: newDeployment, data: newDeployment,
timestamp: Date.now() timestamp: Date.now(),
}; };
} }
} }

View File

@@ -1,12 +1,18 @@
import { z } from 'zod'; import { z } from 'zod';
export const getCommitsQuerySchema = z.object({ export const getCommitsQuerySchema = z.object({
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }), projectId: z.coerce
.number()
.int()
.positive({ message: 'Project ID is required' }),
branch: z.string().optional(), branch: z.string().optional(),
}); });
export const getBranchesQuerySchema = z.object({ export const getBranchesQuerySchema = z.object({
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }), projectId: z.coerce
.number()
.int()
.positive({ message: 'Project ID is required' }),
}); });
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>; export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;

View File

@@ -1,9 +1,9 @@
import type { Context } from 'koa'; import type { Context } from 'koa';
import { Controller, Get } from '../../decorators/route.ts'; import { Controller, Get } from '../../decorators/route.ts';
import { prisma } from '../../libs/prisma.ts';
import { gitea } from '../../libs/gitea.ts'; import { gitea } from '../../libs/gitea.ts';
import { prisma } from '../../libs/prisma.ts';
import { BusinessError } from '../../middlewares/exception.ts'; import { BusinessError } from '../../middlewares/exception.ts';
import { getCommitsQuerySchema, getBranchesQuerySchema } from './dto.ts'; import { getBranchesQuerySchema, getCommitsQuerySchema } from './dto.ts';
@Controller('/git') @Controller('/git')
export class GitController { export class GitController {
@@ -33,7 +33,11 @@ export class GitController {
console.log('Access token present:', !!accessToken); console.log('Access token present:', !!accessToken);
if (!accessToken) { if (!accessToken) {
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401); throw new BusinessError(
'Gitea access token not found. Please login again.',
1004,
401,
);
} }
try { try {
@@ -65,7 +69,11 @@ export class GitController {
const accessToken = ctx.session?.gitea?.access_token; const accessToken = ctx.session?.gitea?.access_token;
if (!accessToken) { if (!accessToken) {
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401); throw new BusinessError(
'Gitea access token not found. Please login again.',
1004,
401,
);
} }
try { try {
@@ -85,7 +93,7 @@ export class GitController {
// Handle SCP-like syntax: git@host:owner/repo.git // Handle SCP-like syntax: git@host:owner/repo.git
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) { if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
const scpMatch = cleanUrl.match(/:([^\/]+)\/([^\/]+?)(\.git)?$/); const scpMatch = cleanUrl.match(/:([^/]+)\/([^/]+?)(\.git)?$/);
if (scpMatch) { if (scpMatch) {
return { owner: scpMatch[1], repo: scpMatch[2] }; return { owner: scpMatch[1], repo: scpMatch[2] };
} }
@@ -96,13 +104,15 @@ export class GitController {
const urlObj = new URL(cleanUrl); const urlObj = new URL(cleanUrl);
const parts = urlObj.pathname.split('/').filter(Boolean); const parts = urlObj.pathname.split('/').filter(Boolean);
if (parts.length >= 2) { if (parts.length >= 2) {
const repo = parts.pop()!.replace(/\.git$/, ''); const repo = parts.pop()?.replace(/\.git$/, '');
const owner = parts.pop()!; const owner = parts.pop();
return { owner, repo }; if (repo && owner) {
return { owner, repo };
}
} }
} catch (e) { } catch (_e) {
// Fallback to simple regex // Fallback to simple regex
const match = cleanUrl.match(/([^\/]+)\/([^\/]+?)(\.git)?$/); const match = cleanUrl.match(/([^/]+)\/([^/]+?)(\.git)?$/);
if (match) { if (match) {
return { owner: match[1], repo: match[2] }; return { owner: match[1], repo: match[2] };
} }

View File

@@ -1,8 +1,9 @@
// 控制器统一导出 // 控制器统一导出
export { ProjectController } from './project/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 { DeploymentController } from './deployment/index.ts';
export { PipelineController } from './pipeline/index.ts';
export { StepController } from './step/index.ts'
export { GitController } from './git/index.ts'; export { GitController } from './git/index.ts';
export { PipelineController } from './pipeline/index.ts';
export { ProjectController } from './project/index.ts';
export { StepController } from './step/index.ts';
export { UserController } from './user/index.ts';

View File

@@ -2,36 +2,59 @@ import { z } from 'zod';
// 定义验证架构 // 定义验证架构
export const createPipelineSchema = z.object({ export const createPipelineSchema = z.object({
name: z.string({ name: z
message: '流水线名称必须是字符串', .string({
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }), message: '流水线名称必须是字符串',
})
.min(1, { message: '流水线名称不能为空' })
.max(100, { message: '流水线名称不能超过100个字符' }),
description: z.string({ description: z
message: '流水线描述必须是字符串', .string({
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(), message: '流水线描述必须是字符串',
})
.max(500, { message: '流水线描述不能超过500个字符' })
.optional(),
projectId: z.number({ projectId: z
message: '项目ID必须是数字', .number({
}).int().positive({ message: '项目ID必须是正整数' }).optional(), message: '项目ID必须是数字',
})
.int()
.positive({ message: '项目ID必须是正整数' })
.optional(),
}); });
export const updatePipelineSchema = z.object({ export const updatePipelineSchema = z.object({
name: z.string({ name: z
message: '流水线名称必须是字符串', .string({
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }).optional(), message: '流水线名称必须是字符串',
})
.min(1, { message: '流水线名称不能为空' })
.max(100, { message: '流水线名称不能超过100个字符' })
.optional(),
description: z.string({ description: z
message: '流水线描述必须是字符串', .string({
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(), message: '流水线描述必须是字符串',
})
.max(500, { message: '流水线描述不能超过500个字符' })
.optional(),
}); });
export const pipelineIdSchema = z.object({ export const pipelineIdSchema = z.object({
id: z.coerce.number().int().positive({ message: '流水线 ID 必须是正整数' }), id: z.coerce.number().int().positive({ message: '流水线 ID 必须是正整数' }),
}); });
export const listPipelinesQuerySchema = z.object({ export const listPipelinesQuerySchema = z
projectId: z.coerce.number().int().positive({ message: '项目ID必须是正整数' }).optional(), .object({
}).optional(); projectId: z.coerce
.number()
.int()
.positive({ message: '项目ID必须是正整数' })
.optional(),
})
.optional();
// 类型 // 类型
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>; export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;

View File

@@ -1,14 +1,17 @@
import type { Context } from 'koa'; import type { Context } from 'koa';
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts'; import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
import { prisma } from '../../libs/prisma.ts';
import { log } from '../../libs/logger.ts'; import { log } from '../../libs/logger.ts';
import {
createPipelineFromTemplate,
getAvailableTemplates,
} from '../../libs/pipeline-template.ts';
import { prisma } from '../../libs/prisma.ts';
import { BusinessError } from '../../middlewares/exception.ts'; import { BusinessError } from '../../middlewares/exception.ts';
import { getAvailableTemplates, createPipelineFromTemplate } from '../../libs/pipeline-template.ts';
import { import {
createPipelineSchema, createPipelineSchema,
updatePipelineSchema,
pipelineIdSchema,
listPipelinesQuerySchema, listPipelinesQuerySchema,
pipelineIdSchema,
updatePipelineSchema,
} from './dto.ts'; } from './dto.ts';
@Controller('/pipelines') @Controller('/pipelines')
@@ -46,7 +49,7 @@ export class PipelineController {
// GET /api/pipelines/templates - 获取可用的流水线模板 // GET /api/pipelines/templates - 获取可用的流水线模板
@Get('/templates') @Get('/templates')
async getTemplates(ctx: Context) { async getTemplates(_ctx: Context) {
try { try {
const templates = await getAvailableTemplates(); const templates = await getAvailableTemplates();
return templates; return templates;
@@ -126,7 +129,7 @@ export class PipelineController {
templateId, templateId,
projectId, projectId,
name, name,
description || '' description || '',
); );
// 返回新创建的流水线 // 返回新创建的流水线

View File

@@ -5,46 +5,83 @@ import { projectDirSchema } from '../../libs/path-validator.js';
* 创建项目验证架构 * 创建项目验证架构
*/ */
export const createProjectSchema = z.object({ export const createProjectSchema = z.object({
name: z.string({ name: z
message: '项目名称必须是字符串', .string({
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }), message: '项目名称必须是字符串',
})
.min(2, { message: '项目名称至少2个字符' })
.max(50, { message: '项目名称不能超过50个字符' }),
description: z.string({ description: z
message: '项目描述必须是字符串', .string({
}).max(200, { message: '项目描述不能超过200个字符' }).optional(), message: '项目描述必须是字符串',
})
.max(200, { message: '项目描述不能超过200个字符' })
.optional(),
repository: z.string({ repository: z
message: '仓库地址必须是字符串', .string({
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }), message: '仓库地址必须是字符串',
})
.url({ message: '请输入有效的仓库地址' })
.min(1, { message: '仓库地址不能为空' }),
projectDir: projectDirSchema, projectDir: projectDirSchema,
envPresets: z.string().optional(), // JSON 字符串格式
}); });
/** /**
* 更新项目验证架构 * 更新项目验证架构
*/ */
export const updateProjectSchema = z.object({ export const updateProjectSchema = z.object({
name: z.string({ name: z
message: '项目名称必须是字符串', .string({
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }).optional(), message: '项目名称必须是字符串',
})
.min(2, { message: '项目名称至少2个字符' })
.max(50, { message: '项目名称不能超过50个字符' })
.optional(),
description: z.string({ description: z
message: '项目描述必须是字符串', .string({
}).max(200, { message: '项目描述不能超过200个字符' }).optional(), message: '项目描述必须是字符串',
})
.max(200, { message: '项目描述不能超过200个字符' })
.optional(),
repository: z.string({ repository: z
message: '仓库地址必须是字符串', .string({
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }).optional(), message: '仓库地址必须是字符串',
})
.url({ message: '请输入有效的仓库地址' })
.min(1, { message: '仓库地址不能为空' })
.optional(),
envPresets: z.string().optional(), // JSON 字符串格式
}); });
/** /**
* 项目列表查询参数验证架构 * 项目列表查询参数验证架构
*/ */
export const listProjectQuerySchema = z.object({ export const listProjectQuerySchema = z
page: z.coerce.number().int().min(1, { message: '页码必须大于0' }).optional().default(1), .object({
limit: z.coerce.number().int().min(1, { message: '每页数量必须大于0' }).max(100, { message: '每页数量不能超过100' }).optional().default(10), page: z.coerce
name: z.string().optional(), .number()
}).optional(); .int()
.min(1, { message: '页码必须大于0' })
.optional()
.default(1),
limit: z.coerce
.number()
.int()
.min(1, { message: '每页数量必须大于0' })
.max(100, { message: '每页数量不能超过100' })
.optional()
.default(10),
name: z.string().optional(),
})
.optional();
/** /**
* 项目ID验证架构 * 项目ID验证架构

View File

@@ -1,14 +1,14 @@
import type { Context } from 'koa'; import type { Context } from 'koa';
import { prisma } from '../../libs/prisma.ts'; import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
import { log } from '../../libs/logger.ts';
import { BusinessError } from '../../middlewares/exception.ts';
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
import { GitManager } from '../../libs/git-manager.ts'; import { GitManager } from '../../libs/git-manager.ts';
import { log } from '../../libs/logger.ts';
import { prisma } from '../../libs/prisma.ts';
import { BusinessError } from '../../middlewares/exception.ts';
import { import {
createProjectSchema, createProjectSchema,
updateProjectSchema,
listProjectQuerySchema, listProjectQuerySchema,
projectIdSchema, projectIdSchema,
updateProjectSchema,
} from './dto.ts'; } from './dto.ts';
@Controller('/projects') @Controller('/projects')
@@ -135,6 +135,7 @@ export class ProjectController {
description: validatedData.description || '', description: validatedData.description || '',
repository: validatedData.repository, repository: validatedData.repository,
projectDir: validatedData.projectDir, projectDir: validatedData.projectDir,
envPresets: validatedData.envPresets,
createdBy: 'system', createdBy: 'system',
updatedBy: 'system', updatedBy: 'system',
valid: 1, valid: 1,
@@ -182,6 +183,9 @@ export class ProjectController {
if (validatedData.repository !== undefined) { if (validatedData.repository !== undefined) {
updateData.repository = validatedData.repository; updateData.repository = validatedData.repository;
} }
if (validatedData.envPresets !== undefined) {
updateData.envPresets = validatedData.envPresets;
}
const project = await prisma.project.update({ const project = await prisma.project.update({
where: { id }, where: { id },

View File

@@ -1,13 +1,13 @@
import type { Context } from 'koa'; import type { Context } from 'koa';
import { prisma } from '../../libs/prisma.ts'; import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
import { log } from '../../libs/logger.ts'; import { log } from '../../libs/logger.ts';
import { prisma } from '../../libs/prisma.ts';
import { BusinessError } from '../../middlewares/exception.ts'; import { BusinessError } from '../../middlewares/exception.ts';
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
import { import {
createStepSchema, createStepSchema,
updateStepSchema,
stepIdSchema,
listStepsQuerySchema, listStepsQuerySchema,
stepIdSchema,
updateStepSchema,
} from './dto.ts'; } from './dto.ts';
@Controller('/steps') @Controller('/steps')

View File

@@ -1,11 +1,11 @@
import type { Context } from 'koa'; import type { Context } from 'koa';
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts'; import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
import { BusinessError } from '../../middlewares/exception.ts'; import { BusinessError } from '../../middlewares/exception.ts';
import { import {
userIdSchema,
createUserSchema, createUserSchema,
updateUserSchema,
searchUserQuerySchema, searchUserQuerySchema,
updateUserSchema,
userIdSchema,
} from './dto.ts'; } from './dto.ts';
/** /**
@@ -13,14 +13,18 @@ import {
*/ */
@Controller('/user') @Controller('/user')
export class UserController { export class UserController {
@Get('/list') @Get('/list')
async list(ctx: Context) { async list(_ctx: Context) {
// 模拟用户列表数据 // 模拟用户列表数据
const users = [ const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' }, { id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' }, { id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com', status: 'active' } {
id: 3,
name: 'Charlie',
email: 'charlie@example.com',
status: 'active',
},
]; ];
return users; return users;
@@ -33,10 +37,10 @@ export class UserController {
// 模拟根据ID查找用户 // 模拟根据ID查找用户
const user = { const user = {
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 (id > 100) { if (id > 100) {
@@ -55,7 +59,7 @@ export class UserController {
id: Date.now(), id: Date.now(),
...body, ...body,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
status: body.status status: body.status,
}; };
return newUser; return newUser;
@@ -70,7 +74,7 @@ export class UserController {
const updatedUser = { const updatedUser = {
id, id,
...body, ...body,
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString(),
}; };
return updatedUser; return updatedUser;
@@ -88,7 +92,7 @@ export class UserController {
return { return {
success: true, success: true,
message: `用户 ${id} 已删除`, message: `用户 ${id} 已删除`,
deletedAt: new Date().toISOString() deletedAt: new Date().toISOString(),
}; };
} }
@@ -99,25 +103,26 @@ export class UserController {
// 模拟搜索逻辑 // 模拟搜索逻辑
let results = [ let results = [
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' }, { id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' } { id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
]; ];
if (keyword) { if (keyword) {
results = results.filter(user => results = results.filter(
user.name.toLowerCase().includes(keyword.toLowerCase()) || (user) =>
user.email.toLowerCase().includes(keyword.toLowerCase()) user.name.toLowerCase().includes(keyword.toLowerCase()) ||
user.email.toLowerCase().includes(keyword.toLowerCase()),
); );
} }
if (status) { if (status) {
results = results.filter(user => user.status === status); results = results.filter((user) => user.status === status);
} }
return { return {
keyword, keyword,
status, status,
total: results.length, total: results.length,
results results,
}; };
} }
} }

View File

@@ -25,17 +25,24 @@ const metadataStore = new WeakMap<any, Map<string | symbol, any>>();
/** /**
* 设置元数据(降级方案) * 设置元数据(降级方案)
*/ */
function setMetadata<T = any>(key: string | symbol, value: T, target: any): void { function setMetadata<T = any>(
key: string | symbol,
value: T,
target: any,
): void {
if (!metadataStore.has(target)) { if (!metadataStore.has(target)) {
metadataStore.set(target, new Map()); metadataStore.set(target, new Map());
} }
metadataStore.get(target)!.set(key, value); metadataStore.get(target)?.set(key, value);
} }
/** /**
* 获取元数据(降级方案) * 获取元数据(降级方案)
*/ */
function getMetadata<T = any>(key: string | symbol, target: any): T | undefined { function getMetadata<T = any>(
key: string | symbol,
target: any,
): T | undefined {
return metadataStore.get(target)?.get(key); return metadataStore.get(target)?.get(key);
} }
@@ -43,24 +50,28 @@ function getMetadata<T = any>(key: string | symbol, target: any): T | undefined
* 创建HTTP方法装饰器的工厂函数TC39标准 * 创建HTTP方法装饰器的工厂函数TC39标准
*/ */
function createMethodDecorator(method: HttpMethod) { function createMethodDecorator(method: HttpMethod) {
return function (path: string = '') { return (path: string = '') =>
return function <This, Args extends any[], Return>( <This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return, target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return> context: ClassMethodDecoratorContext<
) { This,
(this: This, ...args: Args) => Return
>,
) => {
// 在类初始化时执行 // 在类初始化时执行
context.addInitializer(function () { context.addInitializer(function () {
// 使用 this.constructor 时需要类型断言 // 使用 this.constructor 时需要类型断言
const ctor = (this as any).constructor; const ctor = (this as any).constructor;
// 获取现有的路由元数据 // 获取现有的路由元数据
const existingRoutes: RouteMetadata[] = getMetadata(ROUTE_METADATA_KEY, ctor) || []; const existingRoutes: RouteMetadata[] =
getMetadata(ROUTE_METADATA_KEY, ctor) || [];
// 添加新的路由元数据 // 添加新的路由元数据
const newRoute: RouteMetadata = { const newRoute: RouteMetadata = {
method, method,
path, path,
propertyKey: String(context.name) propertyKey: String(context.name),
}; };
existingRoutes.push(newRoute); existingRoutes.push(newRoute);
@@ -71,7 +82,6 @@ function createMethodDecorator(method: HttpMethod) {
return target; return target;
}; };
};
} }
/** /**
@@ -109,10 +119,10 @@ export const Patch = createMethodDecorator('PATCH');
* @param prefix 路由前缀 * @param prefix 路由前缀
*/ */
export function Controller(prefix: string = '') { export function Controller(prefix: string = '') {
return function <T extends abstract new (...args: any) => any>( return <T extends abstract new (...args: any) => any>(
target: T, target: T,
context: ClassDecoratorContext<T> context: ClassDecoratorContext<T>,
) { ) => {
// 在类初始化时保存控制器前缀 // 在类初始化时保存控制器前缀
context.addInitializer(function () { context.addInitializer(function () {
setMetadata('prefix', prefix, this); setMetadata('prefix', prefix, this);

View File

@@ -1,4 +1,3 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */ /* eslint-disable */
// biome-ignore-all lint: generated file // biome-ignore-all lint: generated file
@@ -13,32 +12,32 @@
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
import * as Prisma from './internal/prismaNamespaceBrowser.ts' import * as Prisma from './internal/prismaNamespaceBrowser.ts';
export { Prisma } export { Prisma };
export * as $Enums from './enums.ts' export * as $Enums from './enums.ts';
export * from './enums.ts'; export * from './enums.ts';
/** /**
* Model Project * Model Project
* *
*/ */
export type Project = Prisma.ProjectModel export type Project = Prisma.ProjectModel;
/** /**
* Model User * Model User
* *
*/ */
export type User = Prisma.UserModel export type User = Prisma.UserModel;
/** /**
* Model Pipeline * Model Pipeline
* *
*/ */
export type Pipeline = Prisma.PipelineModel export type Pipeline = Prisma.PipelineModel;
/** /**
* Model Step * Model Step
* *
*/ */
export type Step = Prisma.StepModel export type Step = Prisma.StepModel;
/** /**
* Model Deployment * Model Deployment
* *
*/ */
export type Deployment = Prisma.DeploymentModel export type Deployment = Prisma.DeploymentModel;

View File

@@ -1,4 +1,3 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */ /* eslint-disable */
// biome-ignore-all lint: generated file // biome-ignore-all lint: generated file
@@ -10,18 +9,19 @@
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
import * as process from 'node:process' import * as path from 'node:path';
import * as path from 'node:path' import * as process from 'node:process';
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url';
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/client" globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url));
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' import * as runtime from '@prisma/client/runtime/client';
export * from "./enums.ts" 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 * ## Prisma Client
* *
@@ -35,32 +35,38 @@ export * from "./enums.ts"
* *
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client). * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
*/ */
export const PrismaClient = $Class.getPrismaClientClass() 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 type PrismaClient<
export { Prisma } 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 * Model Project
* *
*/ */
export type Project = Prisma.ProjectModel export type Project = Prisma.ProjectModel;
/** /**
* Model User * Model User
* *
*/ */
export type User = Prisma.UserModel export type User = Prisma.UserModel;
/** /**
* Model Pipeline * Model Pipeline
* *
*/ */
export type Pipeline = Prisma.PipelineModel export type Pipeline = Prisma.PipelineModel;
/** /**
* Model Step * Model Step
* *
*/ */
export type Step = Prisma.StepModel export type Step = Prisma.StepModel;
/** /**
* Model Deployment * Model Deployment
* *
*/ */
export type Deployment = Prisma.DeploymentModel export type Deployment = Prisma.DeploymentModel;

View File

@@ -1,4 +1,3 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */ /* eslint-disable */
// biome-ignore-all lint: generated file // biome-ignore-all lint: generated file
@@ -9,394 +8,419 @@
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
import type * as runtime from "@prisma/client/runtime/client" import type * as runtime from '@prisma/client/runtime/client';
import * as $Enums from "./enums.ts" import * as $Enums from './enums.ts';
import type * as Prisma from "./internal/prismaNamespace.ts" import type * as Prisma from './internal/prismaNamespace.ts';
export type IntFilter<$PrismaModel = never> = { export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
in?: number[] in?: number[];
notIn?: number[] notIn?: number[];
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntFilter<$PrismaModel> | number not?: Prisma.NestedIntFilter<$PrismaModel> | number;
} };
export type StringFilter<$PrismaModel = never> = { export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
in?: string[] in?: string[];
notIn?: string[] notIn?: string[];
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringFilter<$PrismaModel> | string not?: Prisma.NestedStringFilter<$PrismaModel> | string;
} };
export type StringNullableFilter<$PrismaModel = never> = { export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
in?: string[] | null in?: string[] | null;
notIn?: string[] | null notIn?: string[] | null;
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
} };
export type DateTimeFilter<$PrismaModel = never> = { export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
in?: Date[] | string[] in?: Date[] | string[];
notIn?: Date[] | string[] notIn?: Date[] | string[];
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
} };
export type SortOrderInput = { export type SortOrderInput = {
sort: Prisma.SortOrder sort: Prisma.SortOrder;
nulls?: Prisma.NullsOrder nulls?: Prisma.NullsOrder;
} };
export type IntWithAggregatesFilter<$PrismaModel = never> = { export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
in?: number[] in?: number[];
notIn?: number[] notIn?: number[];
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_avg?: Prisma.NestedFloatFilter<$PrismaModel> _avg?: Prisma.NestedFloatFilter<$PrismaModel>;
_sum?: Prisma.NestedIntFilter<$PrismaModel> _sum?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedIntFilter<$PrismaModel> _min?: Prisma.NestedIntFilter<$PrismaModel>;
_max?: Prisma.NestedIntFilter<$PrismaModel> _max?: Prisma.NestedIntFilter<$PrismaModel>;
} };
export type StringWithAggregatesFilter<$PrismaModel = never> = { export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
in?: string[] in?: string[];
notIn?: string[] notIn?: string[];
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedStringFilter<$PrismaModel> _min?: Prisma.NestedStringFilter<$PrismaModel>;
_max?: Prisma.NestedStringFilter<$PrismaModel> _max?: Prisma.NestedStringFilter<$PrismaModel>;
} };
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = { export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
in?: string[] | null in?: string[] | null;
notIn?: string[] | null notIn?: string[] | null;
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null not?:
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> | Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel> | string
_max?: Prisma.NestedStringNullableFilter<$PrismaModel> | null;
} _count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
};
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = { export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
in?: Date[] | string[] in?: Date[] | string[];
notIn?: Date[] | string[] notIn?: Date[] | string[];
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedDateTimeFilter<$PrismaModel> _min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
} };
export type BoolFilter<$PrismaModel = never> = { export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
} };
export type BoolWithAggregatesFilter<$PrismaModel = never> = { export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedBoolFilter<$PrismaModel> _min?: Prisma.NestedBoolFilter<$PrismaModel>;
_max?: Prisma.NestedBoolFilter<$PrismaModel> _max?: Prisma.NestedBoolFilter<$PrismaModel>;
} };
export type IntNullableFilter<$PrismaModel = never> = { export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
in?: number[] | null in?: number[] | null;
notIn?: number[] | null notIn?: number[] | null;
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
} };
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = { export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
in?: number[] | null in?: number[] | null;
notIn?: number[] | null notIn?: number[] | null;
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null not?:
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> | Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel> | null;
_min?: Prisma.NestedIntNullableFilter<$PrismaModel> _count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_max?: Prisma.NestedIntNullableFilter<$PrismaModel> _avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
} _sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
};
export type DateTimeNullableFilter<$PrismaModel = never> = { export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
in?: Date[] | string[] | null in?: Date[] | string[] | null;
notIn?: Date[] | string[] | null notIn?: Date[] | string[] | null;
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null not?:
} | Prisma.NestedDateTimeNullableFilter<$PrismaModel>
| Date
| string
| null;
};
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
in?: Date[] | string[] | null in?: Date[] | string[] | null;
notIn?: Date[] | string[] | null notIn?: Date[] | string[] | null;
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null not?:
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> | Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | string
} | null;
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
};
export type NestedIntFilter<$PrismaModel = never> = { export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
in?: number[] in?: number[];
notIn?: number[] notIn?: number[];
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntFilter<$PrismaModel> | number not?: Prisma.NestedIntFilter<$PrismaModel> | number;
} };
export type NestedStringFilter<$PrismaModel = never> = { export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
in?: string[] in?: string[];
notIn?: string[] notIn?: string[];
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringFilter<$PrismaModel> | string not?: Prisma.NestedStringFilter<$PrismaModel> | string;
} };
export type NestedStringNullableFilter<$PrismaModel = never> = { export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
in?: string[] | null in?: string[] | null;
notIn?: string[] | null notIn?: string[] | null;
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
} };
export type NestedDateTimeFilter<$PrismaModel = never> = { export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
in?: Date[] | string[] in?: Date[] | string[];
notIn?: Date[] | string[] notIn?: Date[] | string[];
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
} };
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = { export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
in?: number[] in?: number[];
notIn?: number[] notIn?: number[];
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_avg?: Prisma.NestedFloatFilter<$PrismaModel> _avg?: Prisma.NestedFloatFilter<$PrismaModel>;
_sum?: Prisma.NestedIntFilter<$PrismaModel> _sum?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedIntFilter<$PrismaModel> _min?: Prisma.NestedIntFilter<$PrismaModel>;
_max?: Prisma.NestedIntFilter<$PrismaModel> _max?: Prisma.NestedIntFilter<$PrismaModel>;
} };
export type NestedFloatFilter<$PrismaModel = never> = { export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
in?: number[] in?: number[];
notIn?: number[] notIn?: number[];
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel> lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel> lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel> gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel> gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
not?: Prisma.NestedFloatFilter<$PrismaModel> | number not?: Prisma.NestedFloatFilter<$PrismaModel> | number;
} };
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = { export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
in?: string[] in?: string[];
notIn?: string[] notIn?: string[];
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedStringFilter<$PrismaModel> _min?: Prisma.NestedStringFilter<$PrismaModel>;
_max?: Prisma.NestedStringFilter<$PrismaModel> _max?: Prisma.NestedStringFilter<$PrismaModel>;
} };
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
in?: string[] | null in?: string[] | null;
notIn?: string[] | null notIn?: string[] | null;
lt?: string | Prisma.StringFieldRefInput<$PrismaModel> lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
lte?: string | Prisma.StringFieldRefInput<$PrismaModel> lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gt?: string | Prisma.StringFieldRefInput<$PrismaModel> gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
gte?: string | Prisma.StringFieldRefInput<$PrismaModel> gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
contains?: string | Prisma.StringFieldRefInput<$PrismaModel> contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null not?:
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> | Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel> | string
_max?: Prisma.NestedStringNullableFilter<$PrismaModel> | null;
} _count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
};
export type NestedIntNullableFilter<$PrismaModel = never> = { export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
in?: number[] | null in?: number[] | null;
notIn?: number[] | null notIn?: number[] | null;
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
} };
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = { export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
in?: Date[] | string[] in?: Date[] | string[];
notIn?: Date[] | string[] notIn?: Date[] | string[];
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedDateTimeFilter<$PrismaModel> _min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
_max?: Prisma.NestedDateTimeFilter<$PrismaModel> _max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
} };
export type NestedBoolFilter<$PrismaModel = never> = { export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
} };
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = { export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel> equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
_count?: Prisma.NestedIntFilter<$PrismaModel> _count?: Prisma.NestedIntFilter<$PrismaModel>;
_min?: Prisma.NestedBoolFilter<$PrismaModel> _min?: Prisma.NestedBoolFilter<$PrismaModel>;
_max?: Prisma.NestedBoolFilter<$PrismaModel> _max?: Prisma.NestedBoolFilter<$PrismaModel>;
} };
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
in?: number[] | null in?: number[] | null;
notIn?: number[] | null notIn?: number[] | null;
lt?: number | Prisma.IntFieldRefInput<$PrismaModel> lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
lte?: number | Prisma.IntFieldRefInput<$PrismaModel> lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gt?: number | Prisma.IntFieldRefInput<$PrismaModel> gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
gte?: number | Prisma.IntFieldRefInput<$PrismaModel> gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null not?:
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> | Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel> | null;
_min?: Prisma.NestedIntNullableFilter<$PrismaModel> _count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_max?: Prisma.NestedIntNullableFilter<$PrismaModel> _avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
} _sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
};
export type NestedFloatNullableFilter<$PrismaModel = never> = { export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null;
in?: number[] | null in?: number[] | null;
notIn?: number[] | null notIn?: number[] | null;
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel> lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel> lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel> gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel> gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null;
} };
export type NestedDateTimeNullableFilter<$PrismaModel = never> = { export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
in?: Date[] | string[] | null in?: Date[] | string[] | null;
notIn?: Date[] | string[] | null notIn?: Date[] | string[] | null;
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null not?:
} | Prisma.NestedDateTimeNullableFilter<$PrismaModel>
| Date
| string
| null;
};
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
in?: Date[] | string[] | null in?: Date[] | string[] | null;
notIn?: Date[] | string[] | null notIn?: Date[] | string[] | null;
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null not?:
_count?: Prisma.NestedIntNullableFilter<$PrismaModel> | Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | string
} | null;
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
};

View File

@@ -1,15 +1,12 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */ /* eslint-disable */
// biome-ignore-all lint: generated file // biome-ignore-all lint: generated file
// @ts-nocheck // @ts-nocheck
/* /*
* This file exports all enum related types from the schema. * This file exports all enum related types from the schema.
* *
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
// This file is empty because there are no enums in the schema. // This file is empty because there are no enums in the schema.
export {} export {};

View File

@@ -1,4 +1,3 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */ /* eslint-disable */
// biome-ignore-all lint: generated file // biome-ignore-all lint: generated file
@@ -11,47 +10,56 @@
* Please import the `PrismaClient` class from the `client.ts` file instead. * Please import the `PrismaClient` class from the `client.ts` file instead.
*/ */
import * as runtime from "@prisma/client/runtime/client" import * as runtime from '@prisma/client/runtime/client';
import type * as Prisma from "./prismaNamespace.ts" import type * as Prisma from './prismaNamespace.ts';
const config: runtime.GetPrismaClientConfig = { const config: runtime.GetPrismaClientConfig = {
"previewFeatures": [], previewFeatures: [],
"clientVersion": "7.0.0", clientVersion: '7.0.0',
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513", engineVersion: '0c19ccc313cf9911a90d99d2ac2eb0280c76c513',
"activeProvider": "sqlite", activeProvider: 'sqlite',
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n 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", inlineSchema:
"runtimeDataModel": { '// 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 envPresets String? // 环境预设配置JSON格式\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 envVars String? // 环境变量JSON格式统一存储所有配置\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',
"models": {}, runtimeDataModel: {
"enums": {}, models: {},
"types": {} 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\":{}}") 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":"envPresets","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":"envVars","kind":"scalar","type":"String"},{"name":"status","kind":"scalar","type":"String"},{"name":"commitHash","kind":"scalar","type":"String"},{"name":"commitMessage","kind":"scalar","type":"String"},{"name":"buildLog","kind":"scalar","type":"String"},{"name":"sparseCheckoutPaths","kind":"scalar","type":"String"},{"name":"startedAt","kind":"scalar","type":"DateTime"},{"name":"finishedAt","kind":"scalar","type":"DateTime"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"},{"name":"projectId","kind":"scalar","type":"Int"},{"name":"Project","kind":"object","type":"Project","relationName":"DeploymentToProject"},{"name":"pipelineId","kind":"scalar","type":"Int"}],"dbName":null}},"enums":{},"types":{}}',
);
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> { async function decodeBase64AsWasm(
const { Buffer } = await import('node:buffer') wasmBase64: string,
const wasmArray = Buffer.from(wasmBase64, 'base64') ): Promise<WebAssembly.Module> {
return new WebAssembly.Module(wasmArray) const { Buffer } = await import('node:buffer');
const wasmArray = Buffer.from(wasmBase64, 'base64');
return new WebAssembly.Module(wasmArray);
} }
config.compilerWasm = { config.compilerWasm = {
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.sqlite.mjs"), getRuntime: async () =>
await import('@prisma/client/runtime/query_compiler_bg.sqlite.mjs'),
getQueryCompilerWasmModule: async () => { getQueryCompilerWasmModule: async () => {
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs") const { wasm } = await import(
return await decodeBase64AsWasm(wasm) '@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs'
} );
} return await decodeBase64AsWasm(wasm);
},
};
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> = 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 'log' extends keyof ClientOptions
? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
? Prisma.GetEvents<ClientOptions['log']>
: never
: never;
export interface PrismaClientConstructor { export interface PrismaClientConstructor {
/** /**
* ## Prisma Client * ## Prisma Client
* *
* Type-safe database client for TypeScript * Type-safe database client for TypeScript
@@ -68,9 +76,16 @@ export interface PrismaClientConstructor {
new < new <
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions, Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
LogOpts extends LogOptions<Options> = LogOptions<Options>, LogOpts extends LogOptions<Options> = LogOptions<Options>,
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'], OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends {
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs omit: infer U;
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs> }
? U
: Prisma.PrismaClientOptions['omit'],
ExtArgs extends
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
>(
options: Prisma.Subset<Options, Prisma.PrismaClientOptions>,
): PrismaClient<LogOpts, OmitOpts, ExtArgs>;
} }
/** /**
@@ -90,11 +105,17 @@ export interface PrismaClientConstructor {
export interface PrismaClient< export interface PrismaClient<
in LogOpts extends Prisma.LogLevel = never, in LogOpts extends Prisma.LogLevel = never,
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined, in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs in out ExtArgs extends
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
> { > {
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] } [K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] };
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient; $on<V extends LogOpts>(
eventType: V,
callback: (
event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent,
) => void,
): PrismaClient;
/** /**
* Connect with the database * Connect with the database
@@ -106,7 +127,7 @@ export interface PrismaClient<
*/ */
$disconnect(): runtime.Types.Utils.JsPromise<void>; $disconnect(): runtime.Types.Utils.JsPromise<void>;
/** /**
* Executes a prepared raw query and returns the number of affected rows. * Executes a prepared raw query and returns the number of affected rows.
* @example * @example
* ``` * ```
@@ -115,7 +136,10 @@ export interface PrismaClient<
* *
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). * 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>; $executeRaw<T = unknown>(
query: TemplateStringsArray | Prisma.Sql,
...values: any[]
): Prisma.PrismaPromise<number>;
/** /**
* Executes a raw query and returns the number of affected rows. * Executes a raw query and returns the number of affected rows.
@@ -127,7 +151,10 @@ export interface PrismaClient<
* *
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). * 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>; $executeRawUnsafe<T = unknown>(
query: string,
...values: any[]
): Prisma.PrismaPromise<number>;
/** /**
* Performs a prepared raw query and returns the `SELECT` data. * Performs a prepared raw query and returns the `SELECT` data.
@@ -138,7 +165,10 @@ export interface PrismaClient<
* *
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). * 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>; $queryRaw<T = unknown>(
query: TemplateStringsArray | Prisma.Sql,
...values: any[]
): Prisma.PrismaPromise<T>;
/** /**
* Performs a raw query and returns the `SELECT` data. * Performs a raw query and returns the `SELECT` data.
@@ -150,8 +180,10 @@ export interface PrismaClient<
* *
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access). * 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>; $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. * Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
@@ -166,65 +198,85 @@ export interface PrismaClient<
* *
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions). * 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<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> $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>, { $extends: runtime.Types.Extensions.ExtendsHook<
extArgs: ExtArgs 'extends',
}>> Prisma.TypeMapCb<OmitOpts>,
ExtArgs,
runtime.Types.Utils.Call<
Prisma.TypeMapCb<OmitOpts>,
{
extArgs: ExtArgs;
}
>
>;
/** /**
* `prisma.project`: Exposes CRUD operations for the **Project** model. * `prisma.project`: Exposes CRUD operations for the **Project** model.
* Example usage: * Example usage:
* ```ts * ```ts
* // Fetch zero or more Projects * // Fetch zero or more Projects
* const projects = await prisma.project.findMany() * const projects = await prisma.project.findMany()
* ``` * ```
*/ */
get project(): Prisma.ProjectDelegate<ExtArgs, { omit: OmitOpts }>; get project(): Prisma.ProjectDelegate<ExtArgs, { omit: OmitOpts }>;
/** /**
* `prisma.user`: Exposes CRUD operations for the **User** model. * `prisma.user`: Exposes CRUD operations for the **User** model.
* Example usage: * Example usage:
* ```ts * ```ts
* // Fetch zero or more Users * // Fetch zero or more Users
* const users = await prisma.user.findMany() * const users = await prisma.user.findMany()
* ``` * ```
*/ */
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>; get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
/** /**
* `prisma.pipeline`: Exposes CRUD operations for the **Pipeline** model. * `prisma.pipeline`: Exposes CRUD operations for the **Pipeline** model.
* Example usage: * Example usage:
* ```ts * ```ts
* // Fetch zero or more Pipelines * // Fetch zero or more Pipelines
* const pipelines = await prisma.pipeline.findMany() * const pipelines = await prisma.pipeline.findMany()
* ``` * ```
*/ */
get pipeline(): Prisma.PipelineDelegate<ExtArgs, { omit: OmitOpts }>; get pipeline(): Prisma.PipelineDelegate<ExtArgs, { omit: OmitOpts }>;
/** /**
* `prisma.step`: Exposes CRUD operations for the **Step** model. * `prisma.step`: Exposes CRUD operations for the **Step** model.
* Example usage: * Example usage:
* ```ts * ```ts
* // Fetch zero or more Steps * // Fetch zero or more Steps
* const steps = await prisma.step.findMany() * const steps = await prisma.step.findMany()
* ``` * ```
*/ */
get step(): Prisma.StepDelegate<ExtArgs, { omit: OmitOpts }>; get step(): Prisma.StepDelegate<ExtArgs, { omit: OmitOpts }>;
/** /**
* `prisma.deployment`: Exposes CRUD operations for the **Deployment** model. * `prisma.deployment`: Exposes CRUD operations for the **Deployment** model.
* Example usage: * Example usage:
* ```ts * ```ts
* // Fetch zero or more Deployments * // Fetch zero or more Deployments
* const deployments = await prisma.deployment.findMany() * const deployments = await prisma.deployment.findMany()
* ``` * ```
*/ */
get deployment(): Prisma.DeploymentDelegate<ExtArgs, { omit: OmitOpts }>; get deployment(): Prisma.DeploymentDelegate<ExtArgs, { omit: OmitOpts }>;
} }
export function getPrismaClientClass(): PrismaClientConstructor { export function getPrismaClientClass(): PrismaClientConstructor {
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */ /* eslint-disable */
// biome-ignore-all lint: generated file // biome-ignore-all lint: generated file
@@ -15,61 +14,65 @@
* model files in the `model` directory! * model files in the `model` directory!
*/ */
import * as runtime from "@prisma/client/runtime/index-browser" import * as runtime from '@prisma/client/runtime/index-browser';
export type * from '../models.ts' export type * from '../models.ts';
export type * from './prismaNamespace.ts' export type * from './prismaNamespace.ts';
export const Decimal = runtime.Decimal
export const Decimal = runtime.Decimal;
export const NullTypes = { export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull), DbNull: runtime.NullTypes.DbNull as new (
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull), secret: never,
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull), ) => 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) * 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 * @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 export const DbNull = runtime.DbNull;
/** /**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db) * 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 * @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 export const JsonNull = runtime.JsonNull;
/** /**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.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 * @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 AnyNull = runtime.AnyNull;
export const ModelName = { export const ModelName = {
Project: 'Project', Project: 'Project',
User: 'User', User: 'User',
Pipeline: 'Pipeline', Pipeline: 'Pipeline',
Step: 'Step', Step: 'Step',
Deployment: 'Deployment' Deployment: 'Deployment',
} as const } as const;
export type ModelName = (typeof ModelName)[keyof typeof ModelName] export type ModelName = (typeof ModelName)[keyof typeof ModelName];
/* /*
* Enums * Enums
*/ */
export const TransactionIsolationLevel = { export const TransactionIsolationLevel = {
Serializable: 'Serializable' Serializable: 'Serializable',
} as const } as const;
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export type TransactionIsolationLevel =
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel];
export const ProjectScalarFieldEnum = { export const ProjectScalarFieldEnum = {
id: 'id', id: 'id',
@@ -77,15 +80,16 @@ export const ProjectScalarFieldEnum = {
description: 'description', description: 'description',
repository: 'repository', repository: 'repository',
projectDir: 'projectDir', projectDir: 'projectDir',
envPresets: 'envPresets',
valid: 'valid', valid: 'valid',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdBy: 'createdBy', createdBy: 'createdBy',
updatedBy: 'updatedBy' updatedBy: 'updatedBy',
} as const } as const;
export type ProjectScalarFieldEnum = (typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum]
export type ProjectScalarFieldEnum =
(typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum];
export const UserScalarFieldEnum = { export const UserScalarFieldEnum = {
id: 'id', id: 'id',
@@ -98,11 +102,11 @@ export const UserScalarFieldEnum = {
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdBy: 'createdBy', createdBy: 'createdBy',
updatedBy: 'updatedBy' updatedBy: 'updatedBy',
} as const } as const;
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export type UserScalarFieldEnum =
(typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum];
export const PipelineScalarFieldEnum = { export const PipelineScalarFieldEnum = {
id: 'id', id: 'id',
@@ -113,11 +117,11 @@ export const PipelineScalarFieldEnum = {
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdBy: 'createdBy', createdBy: 'createdBy',
updatedBy: 'updatedBy', updatedBy: 'updatedBy',
projectId: 'projectId' projectId: 'projectId',
} as const } as const;
export type PipelineScalarFieldEnum = (typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum]
export type PipelineScalarFieldEnum =
(typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum];
export const StepScalarFieldEnum = { export const StepScalarFieldEnum = {
id: 'id', id: 'id',
@@ -129,16 +133,16 @@ export const StepScalarFieldEnum = {
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdBy: 'createdBy', createdBy: 'createdBy',
updatedBy: 'updatedBy', updatedBy: 'updatedBy',
pipelineId: 'pipelineId' pipelineId: 'pipelineId',
} as const } as const;
export type StepScalarFieldEnum = (typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum]
export type StepScalarFieldEnum =
(typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum];
export const DeploymentScalarFieldEnum = { export const DeploymentScalarFieldEnum = {
id: 'id', id: 'id',
branch: 'branch', branch: 'branch',
env: 'env', envVars: 'envVars',
status: 'status', status: 'status',
commitHash: 'commitHash', commitHash: 'commitHash',
commitMessage: 'commitMessage', commitMessage: 'commitMessage',
@@ -152,24 +156,22 @@ export const DeploymentScalarFieldEnum = {
createdBy: 'createdBy', createdBy: 'createdBy',
updatedBy: 'updatedBy', updatedBy: 'updatedBy',
projectId: 'projectId', projectId: 'projectId',
pipelineId: 'pipelineId' pipelineId: 'pipelineId',
} as const } as const;
export type DeploymentScalarFieldEnum = (typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum]
export type DeploymentScalarFieldEnum =
(typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum];
export const SortOrder = { export const SortOrder = {
asc: 'asc', asc: 'asc',
desc: 'desc' desc: 'desc',
} as const } as const;
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder];
export const NullsOrder = { export const NullsOrder = {
first: 'first', first: 'first',
last: 'last' last: 'last',
} as const } as const;
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder];

View File

@@ -1,4 +1,6 @@
export type * from './commonInputTypes.ts';
export type * from './models/Deployment.ts';
export type * from './models/Pipeline.ts';
/* !!! This is code generated by Prisma. Do not edit directly. !!! */ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */ /* eslint-disable */
// biome-ignore-all lint: generated file // biome-ignore-all lint: generated file
@@ -8,9 +10,6 @@
* *
* 🟢 You can import this file directly. * 🟢 You can import this file directly.
*/ */
export type * from './models/Project.ts' export type * from './models/Project.ts';
export type * from './models/User.ts' export type * from './models/Step.ts';
export type * from './models/Pipeline.ts' export type * from './models/User.ts';
export type * from './models/Step.ts'
export type * from './models/Deployment.ts'
export type * from './commonInputTypes.ts'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,9 @@
* 封装 Git 操作:克隆、更新、分支切换等 * 封装 Git 操作:克隆、更新、分支切换等
*/ */
import { $ } from 'zx';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { $ } from 'zx';
import { log } from './logger'; import { log } from './logger';
/** /**

View File

@@ -38,26 +38,23 @@ class Gitea {
clientId: process.env.GITEA_CLIENT_ID!, clientId: process.env.GITEA_CLIENT_ID!,
clientSecret: process.env.GITEA_CLIENT_SECRET!, clientSecret: process.env.GITEA_CLIENT_SECRET!,
redirectUri: process.env.GITEA_REDIRECT_URI!, redirectUri: process.env.GITEA_REDIRECT_URI!,
} };
} }
async getToken(code: string) { async getToken(code: string) {
const { giteaUrl, clientId, clientSecret, redirectUri } = this.config; const { giteaUrl, clientId, clientSecret, redirectUri } = this.config;
console.log('this.config', this.config); console.log('this.config', this.config);
const response = await fetch( const response = await fetch(`${giteaUrl}/login/oauth/access_token`, {
`${giteaUrl}/login/oauth/access_token`, method: 'POST',
{ headers: this.getHeaders(),
method: 'POST', body: JSON.stringify({
headers: this.getHeaders(), client_id: clientId,
body: JSON.stringify({ client_secret: clientSecret,
client_id: clientId, code,
client_secret: clientSecret, grant_type: 'authorization_code',
code, redirect_uri: redirectUri,
grant_type: 'authorization_code', }),
redirect_uri: redirectUri, });
}),
},
);
if (!response.ok) { if (!response.ok) {
console.log(await response.json()); console.log(await response.json());
throw new Error(`Fetch failed: ${response.status}`); throw new Error(`Fetch failed: ${response.status}`);
@@ -108,19 +105,23 @@ class Gitea {
* @param accessToken 访问令牌 * @param accessToken 访问令牌
* @param sha 分支名称或提交SHA * @param sha 分支名称或提交SHA
*/ */
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) { async getCommits(
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`); owner: string,
repo: string,
accessToken: string,
sha?: string,
) {
const url = new URL(
`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`,
);
if (sha) { if (sha) {
url.searchParams.append('sha', sha); url.searchParams.append('sha', sha);
} }
const response = await fetch( const response = await fetch(url.toString(), {
url.toString(), method: 'GET',
{ headers: this.getHeaders(accessToken),
method: 'GET', });
headers: this.getHeaders(accessToken),
},
);
if (!response.ok) { if (!response.ok) {
throw new Error(`Fetch failed: ${response.status}`); throw new Error(`Fetch failed: ${response.status}`);
} }
@@ -133,7 +134,7 @@ class Gitea {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
if (accessToken) { if (accessToken) {
headers['Authorization'] = `token ${accessToken}`; headers.Authorization = `token ${accessToken}`;
} }
return headers; return headers;
} }

View File

@@ -17,11 +17,6 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
name: 'Git Clone Pipeline', name: 'Git Clone Pipeline',
description: '默认的Git克隆流水线用于从仓库克隆代码', description: '默认的Git克隆流水线用于从仓库克隆代码',
steps: [ 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', name: 'Install Dependencies',
order: 1, order: 1,
@@ -36,51 +31,21 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
name: 'Build Project', name: 'Build Project',
order: 3, order: 3,
script: '# 构建项目\nnpm run build', 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', name: 'Simple Deploy Pipeline',
description: '简单的部署流水线,包含基本的构建和部署步骤', description: '简单的部署流水线,包含基本的构建和部署步骤',
steps: [ 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', name: 'Build and Deploy',
order: 1, order: 1,
script: '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令', script:
} '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
] },
} ],
},
]; ];
/** /**
@@ -94,10 +59,10 @@ export async function initializePipelineTemplates(): Promise<void> {
const existingTemplates = await prisma.pipeline.findMany({ const existingTemplates = await prisma.pipeline.findMany({
where: { where: {
name: { name: {
in: DEFAULT_PIPELINE_TEMPLATES.map(template => template.name) in: DEFAULT_PIPELINE_TEMPLATES.map((template) => template.name),
}, },
valid: 1 valid: 1,
} },
}); });
// 如果没有现有的模板,则创建默认模板 // 如果没有现有的模板,则创建默认模板
@@ -113,8 +78,8 @@ export async function initializePipelineTemplates(): Promise<void> {
createdBy: 'system', createdBy: 'system',
updatedBy: 'system', updatedBy: 'system',
valid: 1, valid: 1,
projectId: null // 模板不属于任何特定项目 projectId: null, // 模板不属于任何特定项目
} },
}); });
// 创建模板步骤 // 创建模板步骤
@@ -127,8 +92,8 @@ export async function initializePipelineTemplates(): Promise<void> {
pipelineId: pipeline.id, pipelineId: pipeline.id,
createdBy: 'system', createdBy: 'system',
updatedBy: 'system', updatedBy: 'system',
valid: 1 valid: 1,
} },
}); });
} }
@@ -148,25 +113,27 @@ export async function initializePipelineTemplates(): Promise<void> {
/** /**
* 获取所有可用的流水线模板 * 获取所有可用的流水线模板
*/ */
export async function getAvailableTemplates(): Promise<Array<{id: number, name: string, description: string}>> { export async function getAvailableTemplates(): Promise<
Array<{ id: number; name: string; description: string }>
> {
try { try {
const templates = await prisma.pipeline.findMany({ const templates = await prisma.pipeline.findMany({
where: { where: {
projectId: null, // 模板流水线没有关联的项目 projectId: null, // 模板流水线没有关联的项目
valid: 1 valid: 1,
}, },
select: { select: {
id: true, id: true,
name: true, name: true,
description: true description: true,
} },
}); });
// 处理可能为null的description字段 // 处理可能为null的description字段
return templates.map(template => ({ return templates.map((template) => ({
id: template.id, id: template.id,
name: template.name, name: template.name,
description: template.description || '' description: template.description || '',
})); }));
} catch (error) { } catch (error) {
console.error('Failed to get pipeline templates:', error); console.error('Failed to get pipeline templates:', error);
@@ -185,7 +152,7 @@ export async function createPipelineFromTemplate(
templateId: number, templateId: number,
projectId: number, projectId: number,
pipelineName: string, pipelineName: string,
pipelineDescription: string pipelineDescription: string,
): Promise<number> { ): Promise<number> {
try { try {
// 获取模板流水线及其步骤 // 获取模板流水线及其步骤
@@ -193,18 +160,18 @@ export async function createPipelineFromTemplate(
where: { where: {
id: templateId, id: templateId,
projectId: null, // 确保是模板流水线 projectId: null, // 确保是模板流水线
valid: 1 valid: 1,
}, },
include: { include: {
steps: { steps: {
where: { where: {
valid: 1 valid: 1,
}, },
orderBy: { orderBy: {
order: 'asc' order: 'asc',
} },
} },
} },
}); });
if (!templatePipeline) { if (!templatePipeline) {
@@ -219,8 +186,8 @@ export async function createPipelineFromTemplate(
projectId: projectId, projectId: projectId,
createdBy: 'system', createdBy: 'system',
updatedBy: 'system', updatedBy: 'system',
valid: 1 valid: 1,
} },
}); });
// 复制模板步骤到新流水线 // 复制模板步骤到新流水线
@@ -233,12 +200,14 @@ export async function createPipelineFromTemplate(
pipelineId: newPipeline.id, pipelineId: newPipeline.id,
createdBy: 'system', createdBy: 'system',
updatedBy: 'system', updatedBy: 'system',
valid: 1 valid: 1,
} },
}); });
} }
console.log(`Created pipeline from template ${templateId}: ${newPipeline.name}`); console.log(
`Created pipeline from template ${templateId}: ${newPipeline.name}`,
);
return newPipeline.id; return newPipeline.id;
} catch (error) { } catch (error) {
console.error('Failed to create pipeline from template:', error); console.error('Failed to create pipeline from template:', error);

View File

@@ -1,6 +1,10 @@
import type Koa from 'koa';
import KoaRouter from '@koa/router'; import KoaRouter from '@koa/router';
import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts'; import type Koa from 'koa';
import {
getControllerPrefix,
getRouteMetadata,
type RouteMetadata,
} from '../decorators/route.ts';
import { createSuccessResponse } from '../middlewares/exception.ts'; import { createSuccessResponse } from '../middlewares/exception.ts';
/** /**
@@ -33,7 +37,7 @@ export class RouteScanner {
* 注册多个控制器类 * 注册多个控制器类
*/ */
registerControllers(controllers: ControllerClass[]): void { registerControllers(controllers: ControllerClass[]): void {
controllers.forEach(controller => this.registerController(controller)); controllers.forEach((controller) => this.registerController(controller));
} }
/** /**
@@ -50,9 +54,12 @@ export class RouteScanner {
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass); const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
// 注册每个路由 // 注册每个路由
routes.forEach(route => { routes.forEach((route) => {
const fullPath = this.buildFullPath(controllerPrefix, route.path); const fullPath = this.buildFullPath(controllerPrefix, route.path);
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey); const handler = this.wrapControllerMethod(
controllerInstance,
route.propertyKey,
);
// 根据HTTP方法注册路由 // 根据HTTP方法注册路由
switch (route.method) { switch (route.method) {
@@ -87,10 +94,10 @@ export class RouteScanner {
let fullPath = ''; let fullPath = '';
if (cleanControllerPrefix) { if (cleanControllerPrefix) {
fullPath += '/' + cleanControllerPrefix; fullPath += `/${cleanControllerPrefix}`;
} }
if (cleanRoutePath) { if (cleanRoutePath) {
fullPath += '/' + cleanRoutePath; fullPath += `/${cleanRoutePath}`;
} }
// 如果路径为空,返回根路径 // 如果路径为空,返回根路径
@@ -105,11 +112,11 @@ export class RouteScanner {
// 调用控制器方法 // 调用控制器方法
const method = instance[methodName]; const method = instance[methodName];
if (typeof method !== 'function') { if (typeof method !== 'function') {
ctx.throw(401, 'Not Found') ctx.throw(401, 'Not Found');
} }
// 绑定this并调用方法 // 绑定this并调用方法
const result = await method.call(instance, ctx, next) ?? null; const result = (await method.call(instance, ctx, next)) ?? null;
ctx.body = createSuccessResponse(result); ctx.body = createSuccessResponse(result);
}; };
@@ -133,19 +140,29 @@ export class RouteScanner {
/** /**
* 获取已注册的路由信息(用于调试) * 获取已注册的路由信息(用于调试)
*/ */
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> { getRegisteredRoutes(): Array<{
const routes: Array<{ method: string; path: string; controller: string; action: string }> = []; method: string;
path: string;
controller: string;
action: string;
}> {
const routes: Array<{
method: string;
path: string;
controller: string;
action: string;
}> = [];
this.controllers.forEach(ControllerClass => { this.controllers.forEach((ControllerClass) => {
const controllerPrefix = getControllerPrefix(ControllerClass); const controllerPrefix = getControllerPrefix(ControllerClass);
const routeMetadata = getRouteMetadata(ControllerClass); const routeMetadata = getRouteMetadata(ControllerClass);
routeMetadata.forEach(route => { routeMetadata.forEach((route) => {
routes.push({ routes.push({
method: route.method, method: route.method,
path: this.buildFullPath(controllerPrefix, route.path), path: this.buildFullPath(controllerPrefix, route.path),
controller: ControllerClass.name, controller: ControllerClass.name,
action: route.propertyKey action: route.propertyKey,
}); });
}); });
}); });

View File

@@ -1,5 +1,5 @@
import bodyParser from 'koa-bodyparser';
import type Koa from 'koa'; import type Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import type { Middleware } from './types.ts'; import type { Middleware } from './types.ts';
/** /**

View File

@@ -1,7 +1,7 @@
import type Koa from 'koa'; import type Koa from 'koa';
import { z } from 'zod'; import { z } from 'zod';
import type { Middleware } from './types.ts';
import { log } from '../libs/logger.ts'; import { log } from '../libs/logger.ts';
import type { Middleware } from './types.ts';
/** /**
* 统一响应体结构 * 统一响应体结构
@@ -58,15 +58,26 @@ export class Exception implements Middleware {
const errorMessage = firstError?.message || '参数验证失败'; const errorMessage = firstError?.message || '参数验证失败';
const fieldPath = firstError?.path?.join('.') || 'unknown'; const fieldPath = firstError?.path?.join('.') || 'unknown';
log.info('Exception', 'Zod validation failed: %s at %s', errorMessage, fieldPath); log.info(
this.sendResponse(ctx, 1003, errorMessage, { 'Exception',
field: fieldPath, 'Zod validation failed: %s at %s',
validationErrors: error.issues.map(issue => ({ errorMessage,
field: issue.path.join('.'), fieldPath,
message: issue.message, );
code: issue.code, this.sendResponse(
})) ctx,
}, 400); 1003,
errorMessage,
{
field: fieldPath,
validationErrors: error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message,
code: issue.code,
})),
},
400,
);
} else if (error instanceof BusinessError) { } else if (error instanceof BusinessError) {
// 业务异常 // 业务异常
this.sendResponse(ctx, error.code, error.message, null, error.httpStatus); this.sendResponse(ctx, error.code, error.message, null, error.httpStatus);

View File

@@ -1,11 +1,11 @@
import { Router } from './router.ts';
import { Exception } from './exception.ts';
import { BodyParser } from './body-parser.ts';
import { Session } from './session.ts';
import { CORS } from './cors.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';
import { BodyParser } from './body-parser.ts';
import { CORS } from './cors.ts';
import { Exception } from './exception.ts';
import { HttpLogger } from './logger.ts';
import { Router } from './router.ts';
import { Session } from './session.ts';
/** /**
* 初始化中间件 * 初始化中间件

View File

@@ -1,4 +1,5 @@
import Koa, { type Context } from 'koa'; import type Koa from 'koa';
import type { Context } from 'koa';
import { log } from '../libs/logger.ts'; import { log } from '../libs/logger.ts';
import type { Middleware } from './types.ts'; import type { Middleware } from './types.ts';
@@ -8,7 +9,7 @@ export class HttpLogger implements Middleware {
const start = Date.now(); const start = Date.now();
await next(); await next();
const ms = Date.now() - start; const ms = Date.now() - start;
log.info('HTTP', `${ctx.method} ${ctx.url} - ${ms}ms`) log.info('HTTP', `${ctx.method} ${ctx.url} - ${ms}ms`);
}); });
} }
} }

View File

@@ -1,17 +1,17 @@
import KoaRouter from '@koa/router'; import KoaRouter from '@koa/router';
import type Koa from 'koa'; import type Koa from 'koa';
import type { Middleware } from './types.ts';
import { RouteScanner } from '../libs/route-scanner.ts';
import { import {
ProjectController,
UserController,
AuthController, AuthController,
DeploymentController, DeploymentController,
GitController,
PipelineController, PipelineController,
ProjectController,
StepController, StepController,
GitController UserController,
} from '../controllers/index.ts'; } from '../controllers/index.ts';
import { log } from '../libs/logger.ts'; import { log } from '../libs/logger.ts';
import { RouteScanner } from '../libs/route-scanner.ts';
import type { Middleware } from './types.ts';
export class Router implements Middleware { export class Router implements Middleware {
private router: KoaRouter; private router: KoaRouter;
@@ -45,7 +45,7 @@ export class Router implements Middleware {
DeploymentController, DeploymentController,
PipelineController, PipelineController,
StepController, StepController,
GitController GitController,
]); ]);
// 输出注册的路由信息 // 输出注册的路由信息

View File

@@ -1,5 +1,5 @@
import session from 'koa-session';
import type Koa from 'koa'; import type Koa from 'koa';
import session from 'koa-session';
import type { Middleware } from './types.ts'; import type { Middleware } from './types.ts';
export class Session implements Middleware { export class Session implements Middleware {

View File

@@ -1,4 +1,4 @@
import type Koa from 'koa'; import type Koa from 'koa';
export abstract class Middleware { export abstract class Middleware {
abstract apply(app: Koa, options?: unknown): void; abstract apply(app: Koa, options?: unknown): void;

Binary file not shown.

View File

@@ -16,6 +16,7 @@ model Project {
description String? description String?
repository String repository String
projectDir String @unique // 项目工作目录路径(必填) projectDir String @unique // 项目工作目录路径(必填)
envPresets String? // 环境预设配置JSON格式
// Relations // Relations
deployments Deployment[] deployments Deployment[]
pipelines Pipeline[] pipelines Pipeline[]
@@ -75,7 +76,7 @@ model Step {
model Deployment { model Deployment {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
branch String branch String
env String? envVars String? // 环境变量JSON格式统一存储所有配置
status String // pending, running, success, failed, cancelled status String // pending, running, success, failed, cancelled
commitHash String? commitHash String?
commitMessage String? commitMessage String?

View File

@@ -1,8 +1,8 @@
import { $ } from 'zx'; import { $ } from 'zx';
import { prisma } from '../libs/prisma.ts';
import type { Step } from '../generated/client.ts'; import type { Step } from '../generated/client.ts';
import { GitManager, WorkspaceDirStatus } from '../libs/git-manager.ts'; import { GitManager, WorkspaceDirStatus } from '../libs/git-manager.ts';
import { log } from '../libs/logger.ts'; import { log } from '../libs/logger.ts';
import { prisma } from '../libs/prisma.ts';
export class PipelineRunner { export class PipelineRunner {
private readonly TAG = 'PipelineRunner'; private readonly TAG = 'PipelineRunner';
@@ -81,7 +81,7 @@ export class PipelineRunner {
// 执行步骤 // 执行步骤
const stepLog = await this.executeStep(step, envVars); const stepLog = await this.executeStep(step, envVars);
logs += stepLog + '\n'; logs += `${stepLog}\n`;
// 记录步骤执行完成的日志 // 记录步骤执行完成的日志
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`; const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
@@ -215,12 +215,18 @@ export class PipelineRunner {
envVars.BRANCH_NAME = deployment.branch || ''; envVars.BRANCH_NAME = deployment.branch || '';
envVars.COMMIT_HASH = deployment.commitHash || ''; envVars.COMMIT_HASH = deployment.commitHash || '';
// 稀疏检出路径(如果有配置的话) // 注入用户配置的环境变量
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || ''; if (deployment.envVars) {
try {
const userEnvVars = JSON.parse(deployment.envVars);
Object.assign(envVars, userEnvVars);
} catch (error) {
console.error('解析环境变量失败:', error);
}
}
// 工作空间路径(使用配置的项目目录) // 工作空间路径(使用配置的项目目录)
envVars.WORKSPACE = this.projectDir; envVars.WORKSPACE = this.projectDir;
envVars.PROJECT_DIR = this.projectDir;
return envVars; return envVars;
} }
@@ -248,13 +254,11 @@ export class PipelineRunner {
private addTimestampToLines(content: string, isError = false): string { private addTimestampToLines(content: string, isError = false): string {
if (!content) return ''; if (!content) return '';
return ( return `${content
content .split('\n')
.split('\n') .filter((line) => line.trim() !== '')
.filter((line) => line.trim() !== '') .map((line) => this.addTimestamp(line, isError))
.map((line) => this.addTimestamp(line, isError)) .join('\n')}\n`;
.join('\n') + '\n'
);
} }
/** /**
@@ -270,7 +274,7 @@ export class PipelineRunner {
try { try {
// 添加步骤开始执行的时间戳 // 添加步骤开始执行的时间戳
logs += this.addTimestamp(`执行脚本: ${step.script}`) + '\n'; logs += `${this.addTimestamp(`执行脚本: ${step.script}`)}\n`;
// 使用zx执行脚本设置项目目录为工作目录和环境变量 // 使用zx执行脚本设置项目目录为工作目录和环境变量
const script = step.script; const script = step.script;
@@ -291,10 +295,10 @@ export class PipelineRunner {
logs += this.addTimestampToLines(result.stderr, true); logs += this.addTimestampToLines(result.stderr, true);
} }
logs += this.addTimestamp(`步骤执行完成`) + '\n'; logs += `${this.addTimestamp(`步骤执行完成`)}\n`;
} catch (error) { } catch (error) {
const errorMsg = `Error executing step "${step.name}": ${(error as Error).message}`; const errorMsg = `Error executing step "${step.name}": ${(error as Error).message}`;
logs += this.addTimestamp(errorMsg, true) + '\n'; logs += `${this.addTimestamp(errorMsg, true)}\n`;
log.error(this.TAG, errorMsg); log.error(this.TAG, errorMsg);
throw error; throw error;
} }

View File

@@ -1,8 +1,8 @@
import type React from 'react'; import type React from 'react';
import { useEffect, useCallback } from 'react'; import { useCallback, useEffect } from 'react';
export function useAsyncEffect( export function useAsyncEffect(
effect: () => Promise<void | (() => void)>, effect: () => Promise<undefined | (() => void)>,
deps: React.DependencyList, deps: React.DependencyList,
) { ) {
const callback = useCallback(effect, [...deps]); const callback = useCallback(effect, [...deps]);
@@ -11,7 +11,7 @@ export function useAsyncEffect(
const cleanupPromise = callback(); const cleanupPromise = callback();
return () => { return () => {
if (cleanupPromise instanceof Promise) { if (cleanupPromise instanceof Promise) {
cleanupPromise.then(cleanup => cleanup && cleanup()); cleanupPromise.then((cleanup) => cleanup?.());
} }
}; };
}, [callback]); }, [callback]);

View File

@@ -2,7 +2,7 @@ import App from '@pages/App';
import ReactDOM from 'react-dom/client'; 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' import '@arco-design/web-react/es/_util/react-19-adapter';
const rootEl = document.getElementById('root'); const rootEl = document.getElementById('root');

View File

@@ -1,23 +1,24 @@
import { import { Form, Input, Message, Modal, Select } from '@arco-design/web-react';
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 { useCallback, useEffect, useState } from 'react';
import type { Branch, Commit, Pipeline } from '../../types'; import { formatDateTime } from '../../../../utils/time';
import type { Branch, Commit, Pipeline, Project } from '../../types';
import { detailService } from '../service'; import { detailService } from '../service';
interface EnvPreset {
key: string;
label: string;
type: 'select' | 'multiselect' | 'input';
required?: boolean;
options?: Array<{ label: string; value: string }>;
}
interface DeployModalProps { interface DeployModalProps {
visible: boolean; visible: boolean;
onCancel: () => void; onCancel: () => void;
onOk: () => void; onOk: () => void;
pipelines: Pipeline[]; pipelines: Pipeline[];
projectId: number; projectId: number;
project?: Project | null;
} }
function DeployModal({ function DeployModal({
@@ -26,12 +27,29 @@ function DeployModal({
onOk, onOk,
pipelines, pipelines,
projectId, projectId,
project,
}: DeployModalProps) { }: DeployModalProps) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [branches, setBranches] = useState<Branch[]>([]); const [branches, setBranches] = useState<Branch[]>([]);
const [commits, setCommits] = useState<Commit[]>([]); const [commits, setCommits] = useState<Commit[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [branchLoading, setBranchLoading] = useState(false); const [branchLoading, setBranchLoading] = useState(false);
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
// 解析项目环境预设
useEffect(() => {
if (project?.envPresets) {
try {
const presets = JSON.parse(project.envPresets);
setEnvPresets(presets);
} catch (error) {
console.error('解析环境预设失败:', error);
setEnvPresets([]);
}
} else {
setEnvPresets([]);
}
}, [project]);
const fetchCommits = useCallback( const fetchCommits = useCallback(
async (branch: string) => { async (branch: string) => {
@@ -91,16 +109,27 @@ function DeployModal({
try { try {
const values = await form.validate(); const values = await form.validate();
const selectedCommit = commits.find((c) => c.sha === values.commitHash); const selectedCommit = commits.find((c) => c.sha === values.commitHash);
const selectedPipeline = pipelines.find((p) => p.id === values.pipelineId); const selectedPipeline = pipelines.find(
(p) => p.id === values.pipelineId,
);
if (!selectedCommit || !selectedPipeline) { if (!selectedCommit || !selectedPipeline) {
return; return;
} }
// 格式化环境变量 // 收集所有环境变量(从预设项中提取)
const env = values.envVars const envVars: Record<string, string> = {};
?.map((item: { key: string; value: string }) => `${item.key}=${item.value}`) for (const preset of envPresets) {
.join('\n'); const value = values[preset.key];
if (value !== undefined && value !== null) {
// 对于 multiselect将数组转为逗号分隔的字符串
if (preset.type === 'multiselect' && Array.isArray(value)) {
envVars[preset.key] = value.join(',');
} else {
envVars[preset.key] = String(value);
}
}
}
await detailService.createDeployment({ await detailService.createDeployment({
projectId, projectId,
@@ -108,8 +137,7 @@ function DeployModal({
branch: values.branch, branch: values.branch,
commitHash: selectedCommit.sha, commitHash: selectedCommit.sha,
commitMessage: selectedCommit.commit.message, commitMessage: selectedCommit.commit.message,
env: env, envVars, // 提交所有环境变量
sparseCheckoutPaths: values.sparseCheckoutPaths,
}); });
Message.success('部署任务已创建'); Message.success('部署任务已创建');
@@ -128,126 +156,162 @@ function DeployModal({
onCancel={onCancel} onCancel={onCancel}
autoFocus={false} autoFocus={false}
focusLock={true} focusLock={true}
style={{ width: 650 }}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item {/* 基本参数 */}
label="选择流水线" <div className="mb-4 pb-4 border-b border-gray-200">
field="pipelineId" <div className="text-sm font-semibold text-gray-700 mb-3">
rules={[{ required: true, message: '请选择流水线' }]}
> </div>
<Select placeholder="请选择流水线">
{pipelines.map((pipeline) => (
<Select.Option key={pipeline.id} value={pipeline.id}>
{pipeline.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item <Form.Item
label="选择分支" label="选择流水线"
field="branch" field="pipelineId"
rules={[{ required: true, message: '请选择分支' }]} rules={[{ required: true, message: '请选择流水线' }]}
>
<Select
placeholder="请选择分支"
loading={branchLoading}
onChange={handleBranchChange}
> >
{branches.map((branch) => ( <Select placeholder="请选择流水线">
<Select.Option key={branch.name} value={branch.name}> {pipelines.map((pipeline) => (
{branch.name} <Select.Option key={pipeline.id} value={pipeline.id}>
</Select.Option> {pipeline.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 </Select>
type="dashed" </Form.Item>
long
onClick={() => add()} <Form.Item
icon={<IconPlus />} label="选择分支"
> field="branch"
rules={[{ required: true, message: '请选择分支' }]}
</Button> >
<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>
</div>
{/* 环境变量预设 */}
{envPresets.length > 0 && (
<div>
<div className="text-sm font-semibold text-gray-700 mb-3">
</div> </div>
)} {envPresets.map((preset) => {
</Form.List> if (preset.type === 'select' && preset.options) {
return (
<Form.Item
key={preset.key}
label={preset.label}
field={preset.key}
rules={
preset.required
? [{ required: true, message: `请选择${preset.label}` }]
: []
}
>
<Select placeholder={`请选择${preset.label}`}>
{preset.options.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
);
}
if (preset.type === 'multiselect' && preset.options) {
return (
<Form.Item
key={preset.key}
label={preset.label}
field={preset.key}
rules={
preset.required
? [{ required: true, message: `请选择${preset.label}` }]
: []
}
>
<Select
mode="multiple"
placeholder={`请选择${preset.label}`}
allowClear
>
{preset.options.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
);
}
if (preset.type === 'input') {
return (
<Form.Item
key={preset.key}
label={preset.label}
field={preset.key}
rules={
preset.required
? [{ required: true, message: `请输入${preset.label}` }]
: []
}
>
<Input placeholder={`请输入${preset.label}`} />
</Form.Item>
);
}
return null;
})}
</div>
)}
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -25,17 +25,6 @@ function DeployRecordItem({
return <Tag color={config.color}>{config.text}</Tag>; return <Tag color={config.color}>{config.text}</Tag>;
}; };
// 环境标签渲染函数
const getEnvTag = (env: string) => {
const envMap: Record<string, { color: string; text: string }> = {
production: { color: 'red', text: '生产环境' },
staging: { color: 'orange', text: '预发布环境' },
development: { color: 'blue', text: '开发环境' },
};
const config = envMap[env] || { color: 'gray', text: env };
return <Tag color={config.color}>{config.text}</Tag>;
};
return ( return (
<List.Item <List.Item
key={item.id} key={item.id}
@@ -68,9 +57,6 @@ function DeployRecordItem({
:{' '} :{' '}
<span className="font-medium text-gray-700">{item.branch}</span> <span className="font-medium text-gray-700">{item.branch}</span>
</span> </span>
<span className="text-sm text-gray-500">
: {getEnvTag(item.env || 'unknown')}
</span>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
: {getStatusTag(item.status)} : {getStatusTag(item.status)}
</span> </span>

View File

@@ -0,0 +1,214 @@
import { Button, Checkbox, Input, Select, Space } from '@arco-design/web-react';
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
import { useEffect, useState } from 'react';
export interface EnvPreset {
key: string;
label: string;
type: 'select' | 'multiselect' | 'input';
required?: boolean; // 是否必填
options?: Array<{ label: string; value: string }>;
}
interface EnvPresetsEditorProps {
value?: EnvPreset[];
onChange?: (value: EnvPreset[]) => void;
}
function EnvPresetsEditor({ value = [], onChange }: EnvPresetsEditorProps) {
const [presets, setPresets] = useState<EnvPreset[]>(value);
// 当外部 value 变化时同步到内部状态
useEffect(() => {
setPresets(value);
}, [value]);
const handleAddPreset = () => {
const newPreset: EnvPreset = {
key: '',
label: '',
type: 'select',
options: [{ label: '', value: '' }],
};
const newPresets = [...presets, newPreset];
setPresets(newPresets);
onChange?.(newPresets);
};
const handleRemovePreset = (index: number) => {
const newPresets = presets.filter((_, i) => i !== index);
setPresets(newPresets);
onChange?.(newPresets);
};
const handlePresetChange = (
index: number,
field: keyof EnvPreset,
val: string | boolean | EnvPreset['type'] | EnvPreset['options'],
) => {
const newPresets = [...presets];
newPresets[index] = { ...newPresets[index], [field]: val };
setPresets(newPresets);
onChange?.(newPresets);
};
const handleAddOption = (presetIndex: number) => {
const newPresets = [...presets];
if (!newPresets[presetIndex].options) {
newPresets[presetIndex].options = [];
}
newPresets[presetIndex].options?.push({ label: '', value: '' });
setPresets(newPresets);
onChange?.(newPresets);
};
const handleRemoveOption = (presetIndex: number, optionIndex: number) => {
const newPresets = [...presets];
newPresets[presetIndex].options = newPresets[presetIndex].options?.filter(
(_, i) => i !== optionIndex,
);
setPresets(newPresets);
onChange?.(newPresets);
};
const handleOptionChange = (
presetIndex: number,
optionIndex: number,
field: 'label' | 'value',
val: string,
) => {
const newPresets = [...presets];
if (newPresets[presetIndex].options) {
newPresets[presetIndex].options![optionIndex][field] = val;
setPresets(newPresets);
onChange?.(newPresets);
}
};
return (
<div className="space-y-4">
{presets.map((preset, presetIndex) => (
<div
key={`preset-${preset.key || presetIndex}`}
className="border border-gray-200 rounded p-4"
>
<div className="flex items-start justify-between mb-3">
<div className="font-medium text-gray-700">
#{presetIndex + 1}
</div>
<Button
size="small"
status="danger"
icon={<IconDelete />}
onClick={() => handleRemovePreset(presetIndex)}
>
</Button>
</div>
<Space direction="vertical" style={{ width: '100%' }}>
<div className="grid grid-cols-2 gap-2">
<Input
placeholder="变量名 (key)"
value={preset.key}
onChange={(val) => handlePresetChange(presetIndex, 'key', val)}
/>
<Input
placeholder="显示名称 (label)"
value={preset.label}
onChange={(val) =>
handlePresetChange(presetIndex, 'label', val)
}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<Select
placeholder="选择类型"
value={preset.type}
onChange={(val) => handlePresetChange(presetIndex, 'type', val)}
>
<Select.Option value="select"></Select.Option>
<Select.Option value="multiselect"></Select.Option>
<Select.Option value="input"></Select.Option>
</Select>
<div className="flex items-center">
<Checkbox
checked={preset.required || false}
onChange={(checked) =>
handlePresetChange(presetIndex, 'required', checked)
}
>
</Checkbox>
</div>
</div>
{(preset.type === 'select' || preset.type === 'multiselect') && (
<div className="mt-2">
<div className="text-sm text-gray-600 mb-2"></div>
{preset.options?.map((option, optionIndex) => (
<div
key={`option-${option.value || optionIndex}`}
className="flex items-center gap-2 mb-2"
>
<Input
size="small"
placeholder="显示文本"
value={option.label}
onChange={(val) =>
handleOptionChange(
presetIndex,
optionIndex,
'label',
val,
)
}
/>
<Input
size="small"
placeholder="值"
value={option.value}
onChange={(val) =>
handleOptionChange(
presetIndex,
optionIndex,
'value',
val,
)
}
/>
<Button
size="small"
status="danger"
icon={<IconDelete />}
onClick={() =>
handleRemoveOption(presetIndex, optionIndex)
}
/>
</div>
))}
<Button
size="small"
type="dashed"
long
icon={<IconPlus />}
onClick={() => handleAddOption(presetIndex)}
>
</Button>
</div>
)}
</Space>
</div>
))}
<Button type="dashed" long icon={<IconPlus />} onClick={handleAddPreset}>
</Button>
</div>
);
}
export default EnvPresetsEditor;

View File

@@ -19,6 +19,7 @@ import {
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import { import {
IconCode, IconCode,
IconCommand,
IconCopy, IconCopy,
IconDelete, IconDelete,
IconEdit, IconEdit,
@@ -49,9 +50,12 @@ import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
import { useAsyncEffect } from '../../../hooks/useAsyncEffect'; import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
import { formatDateTime } from '../../../utils/time'; import { formatDateTime } from '../../../utils/time';
import type { Deployment, Pipeline, Project, Step, WorkspaceDirStatus, WorkspaceStatus } from '../types'; import type { Deployment, Pipeline, Project, Step } from '../types';
import DeployModal from './components/DeployModal'; import DeployModal from './components/DeployModal';
import DeployRecordItem from './components/DeployRecordItem'; import DeployRecordItem from './components/DeployRecordItem';
import EnvPresetsEditor, {
type EnvPreset,
} from './components/EnvPresetsEditor';
import PipelineStepItem from './components/PipelineStepItem'; import PipelineStepItem from './components/PipelineStepItem';
import { detailService } from './service'; import { detailService } from './service';
@@ -84,7 +88,8 @@ function ProjectDetailPage() {
null, null,
); );
const [pipelineModalVisible, setPipelineModalVisible] = useState(false); const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
const [editingPipeline, setEditingPipeline] = useState<PipelineWithEnabled | null>(null); const [editingPipeline, setEditingPipeline] =
useState<PipelineWithEnabled | null>(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [pipelineForm] = Form.useForm(); const [pipelineForm] = Form.useForm();
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]); const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
@@ -92,12 +97,18 @@ function ProjectDetailPage() {
// 流水线模板相关状态 // 流水线模板相关状态
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false); const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null); const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]); null,
);
const [templates, setTemplates] = useState<
Array<{ id: number; name: string; description: string }>
>([]);
// 项目设置相关状态 // 项目设置相关状态
const [projectEditModalVisible, setProjectEditModalVisible] = useState(false); const [isEditingProject, setIsEditingProject] = useState(false);
const [projectForm] = Form.useForm(); const [projectForm] = Form.useForm();
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
const [envPresetsLoading, setEnvPresetsLoading] = useState(false);
const { id } = useParams(); const { id } = useParams();
@@ -172,8 +183,14 @@ function ProjectDetailPage() {
setDeployRecords(records); setDeployRecords(records);
// 如果当前选中的记录正在运行,则更新选中记录 // 如果当前选中的记录正在运行,则更新选中记录
const selectedRecord = records.find((r: Deployment) => r.id === selectedRecordId); const selectedRecord = records.find(
if (selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending')) { (r: Deployment) => r.id === selectedRecordId,
);
if (
selectedRecord &&
(selectedRecord.status === 'running' ||
selectedRecord.status === 'pending')
) {
// 保持当前选中状态,但更新数据 // 保持当前选中状态,但更新数据
} }
} catch (error) { } catch (error) {
@@ -354,14 +371,15 @@ function ProjectDetailPage() {
selectedTemplateId, selectedTemplateId,
Number(id), Number(id),
values.name, values.name,
values.description || '' values.description || '',
); );
// 更新本地状态 - 需要转换步骤数据结构 // 更新本地状态 - 需要转换步骤数据结构
const transformedSteps = newPipeline.steps?.map(step => ({ const transformedSteps =
...step, newPipeline.steps?.map((step) => ({
enabled: step.valid === 1 ...step,
})) || []; enabled: step.valid === 1,
})) || [];
const pipelineWithDefaults = { const pipelineWithDefaults = {
...newPipeline, ...newPipeline,
@@ -592,6 +610,21 @@ function ProjectDetailPage() {
} }
}; };
// 解析环境变量预设
useEffect(() => {
if (detail?.envPresets) {
try {
const presets = JSON.parse(detail.envPresets);
setEnvPresets(presets);
} catch (error) {
console.error('解析环境变量预设失败:', error);
setEnvPresets([]);
}
} else {
setEnvPresets([]);
}
}, [detail]);
// 项目设置相关函数 // 项目设置相关函数
const handleEditProject = () => { const handleEditProject = () => {
if (detail) { if (detail) {
@@ -600,16 +633,21 @@ function ProjectDetailPage() {
description: detail.description, description: detail.description,
repository: detail.repository, repository: detail.repository,
}); });
setProjectEditModalVisible(true); setIsEditingProject(true);
} }
}; };
const handleProjectEditSuccess = async () => { const handleCancelEditProject = () => {
setIsEditingProject(false);
projectForm.resetFields();
};
const handleSaveProject = async () => {
try { try {
const values = await projectForm.validate(); const values = await projectForm.validate();
await detailService.updateProject(Number(id), values); await detailService.updateProject(Number(id), values);
Message.success('项目更新成功'); Message.success('项目更新成功');
setProjectEditModalVisible(false); setIsEditingProject(false);
// 刷新项目详情 // 刷新项目详情
if (id) { if (id) {
@@ -622,6 +660,27 @@ function ProjectDetailPage() {
} }
}; };
const handleSaveEnvPresets = async () => {
try {
setEnvPresetsLoading(true);
await detailService.updateProject(Number(id), {
envPresets: JSON.stringify(envPresets),
});
Message.success('环境变量预设保存成功');
// 刷新项目详情
if (id) {
const projectDetail = await detailService.getProjectDetail(Number(id));
setDetail(projectDetail);
}
} catch (error) {
console.error('保存环境变量预设失败:', error);
Message.error('保存环境变量预设失败');
} finally {
setEnvPresetsLoading(false);
}
};
const handleDeleteProject = () => { const handleDeleteProject = () => {
Modal.confirm({ Modal.confirm({
title: '删除项目', title: '删除项目',
@@ -671,7 +730,7 @@ function ProjectDetailPage() {
); );
// 获取选中的流水线 // 获取选中的流水线
const selectedPipeline = pipelines.find( const _selectedPipeline = pipelines.find(
(pipeline) => pipeline.id === selectedPipelineId, (pipeline) => pipeline.id === selectedPipelineId,
); );
@@ -681,11 +740,13 @@ function ProjectDetailPage() {
const k = 1024; const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB']; const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`;
}; };
// 获取工作目录状态标签 // 获取工作目录状态标签
const getWorkspaceStatusTag = (status: string): { text: string; color: string } => { const getWorkspaceStatusTag = (
status: string,
): { text: string; color: string } => {
const statusMap: Record<string, { text: string; color: string }> = { const statusMap: Record<string, { text: string; color: string }> = {
not_created: { text: '未创建', color: 'gray' }, not_created: { text: '未创建', color: 'gray' },
empty: { text: '空目录', color: 'orange' }, empty: { text: '空目录', color: 'orange' },
@@ -703,7 +764,15 @@ function ProjectDetailPage() {
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string); const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
return ( return (
<Card className="mb-6" title={<Space><IconFolder /></Space>}> <Card
className="mb-6"
title={
<Space>
<IconFolder />
</Space>
}
>
<Descriptions <Descriptions
column={2} column={2}
data={[ data={[
@@ -717,7 +786,9 @@ function ProjectDetailPage() {
}, },
{ {
label: '目录大小', label: '目录大小',
value: workspaceStatus.size ? formatSize(workspaceStatus.size) : '-', value: workspaceStatus.size
? formatSize(workspaceStatus.size)
: '-',
}, },
{ {
label: '当前分支', label: '当前分支',
@@ -727,16 +798,24 @@ function ProjectDetailPage() {
label: '最后提交', label: '最后提交',
value: workspaceStatus.gitInfo?.lastCommit ? ( value: workspaceStatus.gitInfo?.lastCommit ? (
<Space direction="vertical" size="mini"> <Space direction="vertical" size="mini">
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text> <Typography.Text code>
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text> {workspaceStatus.gitInfo.lastCommit}
</Typography.Text>
<Typography.Text type="secondary">
{workspaceStatus.gitInfo.lastCommitMessage}
</Typography.Text>
</Space> </Space>
) : '-', ) : (
'-'
),
}, },
]} ]}
/> />
{workspaceStatus.error && ( {workspaceStatus.error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded"> <div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
<Typography.Text type="danger">{workspaceStatus.error}</Typography.Text> <Typography.Text type="error">
{workspaceStatus.error}
</Typography.Text>
</div> </div>
)} )}
</Card> </Card>
@@ -763,7 +842,15 @@ function ProjectDetailPage() {
size="large" size="large"
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full" className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
> >
<Tabs.TabPane title={<Space><IconHistory /></Space>} key="deployRecords"> <Tabs.TabPane
title={
<Space>
<IconHistory />
</Space>
}
key="deployRecords"
>
<div className="grid grid-cols-5 gap-6 h-full"> <div className="grid grid-cols-5 gap-6 h-full">
{/* 左侧部署记录列表 */} {/* 左侧部署记录列表 */}
<div className="col-span-2 space-y-4 h-full flex flex-col"> <div className="col-span-2 space-y-4 h-full flex flex-col">
@@ -801,7 +888,7 @@ function ProjectDetailPage() {
</Typography.Title> </Typography.Title>
{selectedRecord && ( {selectedRecord && (
<Typography.Text type="secondary" className="text-sm"> <Typography.Text type="secondary" className="text-sm">
{selectedRecord.branch} · {selectedRecord.env} ·{' '} {selectedRecord.branch}
{formatDateTime(selectedRecord.createdAt)} {formatDateTime(selectedRecord.createdAt)}
</Typography.Text> </Typography.Text>
)} )}
@@ -813,7 +900,9 @@ function ProjectDetailPage() {
type="primary" type="primary"
icon={<IconRefresh />} icon={<IconRefresh />}
size="small" size="small"
onClick={() => handleRetryDeployment(selectedRecord.id)} onClick={() =>
handleRetryDeployment(selectedRecord.id)
}
> >
</Button> </Button>
@@ -838,7 +927,15 @@ function ProjectDetailPage() {
</div> </div>
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane title={<Space><IconCode />线</Space>} key="pipeline"> <Tabs.TabPane
title={
<Space>
<IconCode />
线
</Space>
}
key="pipeline"
>
<div className="grid grid-cols-5 gap-6 h-full"> <div className="grid grid-cols-5 gap-6 h-full">
{/* 左侧流水线列表 */} {/* 左侧流水线列表 */}
<div className="col-span-2 space-y-4"> <div className="col-span-2 space-y-4">
@@ -951,9 +1048,7 @@ function ProjectDetailPage() {
{pipeline.description} {pipeline.description}
</Typography.Text> </Typography.Text>
<div className="flex items-center justify-between text-xs text-gray-500"> <div className="flex items-center justify-between text-xs text-gray-500">
<span> <span>{pipeline.steps?.length || 0} </span>
{pipeline.steps?.length || 0}
</span>
<span>{formatDateTime(pipeline.updatedAt)}</span> <span>{formatDateTime(pipeline.updatedAt)}</span>
</div> </div>
</div> </div>
@@ -1005,7 +1100,11 @@ function ProjectDetailPage() {
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
> >
<SortableContext <SortableContext
items={selectedPipeline.steps?.map(step => step.id) || []} items={
selectedPipeline.steps?.map(
(step) => step.id,
) || []
}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto"> <div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
@@ -1046,48 +1145,140 @@ function ProjectDetailPage() {
</Tabs.TabPane> </Tabs.TabPane>
{/* 项目设置标签页 */} {/* 项目设置标签页 */}
<Tabs.TabPane key="settings" title={<Space><IconSettings /></Space>}> <Tabs.TabPane
key="settings"
title={
<Space>
<IconSettings />
</Space>
}
>
<div className="p-6"> <div className="p-6">
<Card title="项目信息" className="mb-4"> <Card title="项目信息" className="mb-4">
<Descriptions {!isEditingProject ? (
column={1} <>
data={[ <Descriptions
{ column={1}
label: '项目名称', data={[
value: detail?.name, {
}, label: '项目名称',
{ value: detail?.name,
label: '项目描述', },
value: detail?.description || '-', {
}, label: '项目描述',
{ value: detail?.description || '-',
label: 'Git 仓库', },
value: detail?.repository, {
}, label: 'Git 仓库',
{ value: detail?.repository,
label: '工作目录', },
value: detail?.projectDir || '-', {
}, label: '工作目录',
{ value: detail?.projectDir || '-',
label: '创建时间', },
value: formatDateTime(detail?.createdAt), {
}, label: '创建时间',
]} value: formatDateTime(detail?.createdAt),
/> },
<div className="mt-4 flex gap-2"> ]}
<Button type="primary" onClick={handleEditProject}> />
<div className="mt-4 flex gap-2">
</Button> <Button type="primary" onClick={handleEditProject}>
<Button status="danger" onClick={handleDeleteProject}>
</Button>
</Button> <Button status="danger" onClick={handleDeleteProject}>
</div>
</Button>
</div>
</>
) : (
<>
<Form form={projectForm} layout="vertical">
<Form.Item
field="name"
label="项目名称"
rules={[
{ required: true, message: '请输入项目名称' },
{ minLength: 2, message: '项目名称至少2个字符' },
]}
>
<Input placeholder="例如:我的应用" />
</Form.Item>
<Form.Item
field="description"
label="项目描述"
rules={[
{ maxLength: 200, message: '描述不能超过200个字符' },
]}
>
<Input.TextArea
placeholder="请输入项目描述"
rows={3}
maxLength={200}
showWordLimit
/>
</Form.Item>
<Form.Item
field="repository"
label="Git 仓库地址"
rules={[{ required: true, message: '请输入仓库地址' }]}
>
<Input placeholder="例如https://github.com/user/repo.git" />
</Form.Item>
<div className="text-sm text-gray-500 mb-4">
<strong></strong> {detail?.projectDir || '-'}
</div>
<div className="text-sm text-gray-500 mb-4">
<strong></strong>{' '}
{formatDateTime(detail?.createdAt)}
</div>
</Form>
<div className="mt-4 flex gap-2">
<Button type="primary" onClick={handleSaveProject}>
</Button>
<Button onClick={handleCancelEditProject}></Button>
</div>
</>
)}
</Card> </Card>
{/* 工作目录状态 */} {/* 工作目录状态 */}
{renderWorkspaceStatus()} {renderWorkspaceStatus()}
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
{/* 环境变量预设标签页 */}
<Tabs.TabPane
key="envPresets"
title={
<Space>
<IconCommand />
</Space>
}
>
<div className="p-6">
<Card
title="环境变量预设"
extra={
<Button
type="primary"
onClick={handleSaveEnvPresets}
loading={envPresetsLoading}
>
</Button>
}
>
<div className="text-sm text-gray-600 mb-4">
</div>
<EnvPresetsEditor value={envPresets} onChange={setEnvPresets} />
</Card>
</div>
</Tabs.TabPane>
</Tabs> </Tabs>
</div> </div>
@@ -1139,7 +1330,9 @@ function ProjectDetailPage() {
<Select.Option key={template.id} value={template.id}> <Select.Option key={template.id} value={template.id}>
<div> <div>
<div>{template.name}</div> <div>{template.name}</div>
<div className="text-xs text-gray-500">{template.description}</div> <div className="text-xs text-gray-500">
{template.description}
</div>
</div> </div>
</Select.Option> </Select.Option>
))} ))}
@@ -1155,10 +1348,7 @@ function ProjectDetailPage() {
> >
<Input placeholder="例如前端部署流水线、Docker部署流水线..." /> <Input placeholder="例如前端部署流水线、Docker部署流水线..." />
</Form.Item> </Form.Item>
<Form.Item <Form.Item field="description" label="流水线描述">
field="description"
label="流水线描述"
>
<Input.TextArea <Input.TextArea
placeholder="描述这个流水线的用途和特点..." placeholder="描述这个流水线的用途和特点..."
rows={3} rows={3}
@@ -1176,10 +1366,7 @@ function ProjectDetailPage() {
> >
<Input placeholder="例如前端部署流水线、Docker部署流水线..." /> <Input placeholder="例如前端部署流水线、Docker部署流水线..." />
</Form.Item> </Form.Item>
<Form.Item <Form.Item field="description" label="流水线描述">
field="description"
label="流水线描述"
>
<Input.TextArea <Input.TextArea
placeholder="描述这个流水线的用途和特点..." placeholder="描述这个流水线的用途和特点..."
rows={3} rows={3}
@@ -1228,47 +1415,6 @@ function ProjectDetailPage() {
</Form> </Form>
</Modal> </Modal>
{/* 编辑项目模态框 */}
<Modal
title="编辑项目"
visible={projectEditModalVisible}
onOk={handleProjectEditSuccess}
onCancel={() => setProjectEditModalVisible(false)}
style={{ width: 500 }}
>
<Form form={projectForm} layout="vertical">
<Form.Item
field="name"
label="项目名称"
rules={[
{ required: true, message: '请输入项目名称' },
{ minLength: 2, message: '项目名称至少2个字符' },
]}
>
<Input placeholder="例如:我的应用" />
</Form.Item>
<Form.Item
field="description"
label="项目描述"
rules={[{ maxLength: 200, message: '描述不能超过200个字符' }]}
>
<Input.TextArea
placeholder="请输入项目描述"
rows={3}
maxLength={200}
showWordLimit
/>
</Form.Item>
<Form.Item
field="repository"
label="Git 仓库地址"
rules={[{ required: true, message: '请输入仓库地址' }]}
>
<Input placeholder="例如https://github.com/user/repo.git" />
</Form.Item>
</Form>
</Modal>
<DeployModal <DeployModal
visible={deployModalVisible} visible={deployModalVisible}
onCancel={() => setDeployModalVisible(false)} onCancel={() => setDeployModalVisible(false)}
@@ -1286,6 +1432,7 @@ function ProjectDetailPage() {
}} }}
pipelines={pipelines} pipelines={pipelines}
projectId={Number(id)} projectId={Number(id)}
project={detail}
/> />
</div> </div>
); );

View File

@@ -1,5 +1,13 @@
import { type APIResponse, net } from '@shared'; import { type APIResponse, net } from '@shared';
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types'; import type {
Branch,
Commit,
CreateDeploymentRequest,
Deployment,
Pipeline,
Project,
Step,
} from '../types';
class DetailService { class DetailService {
async getProject(id: string) { async getProject(id: string) {
@@ -19,7 +27,9 @@ class DetailService {
// 获取可用的流水线模板 // 获取可用的流水线模板
async getPipelineTemplates() { async getPipelineTemplates() {
const { data } = await net.request<APIResponse<{id: number, name: string, description: string}[]>>({ const { data } = await net.request<
APIResponse<{ id: number; name: string; description: string }[]>
>({
url: '/api/pipelines/templates', url: '/api/pipelines/templates',
}); });
return data; return data;
@@ -59,7 +69,7 @@ class DetailService {
templateId: number, templateId: number,
projectId: number, projectId: number,
name: string, name: string,
description?: string description?: string,
) { ) {
const { data } = await net.request<APIResponse<Pipeline>>({ const { data } = await net.request<APIResponse<Pipeline>>({
url: '/api/pipelines/from-template', url: '/api/pipelines/from-template',
@@ -68,7 +78,7 @@ class DetailService {
templateId, templateId,
projectId, projectId,
name, name,
description description,
}, },
}); });
return data; return data;
@@ -204,7 +214,7 @@ class DetailService {
// 更新项目 // 更新项目
async updateProject( async updateProject(
id: number, id: number,
project: Partial<{ name: string; description: string; repository: string }>, project: Partial<Project>,
) { ) {
const { data } = await net.request<APIResponse<Project>>({ const { data } = await net.request<APIResponse<Project>>({
url: `/api/projects/${id}`, url: `/api/projects/${id}`,

View File

@@ -1,7 +1,15 @@
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react'; import {
Button,
Collapse,
Form,
Input,
Message,
Modal,
} from '@arco-design/web-react';
import { useState } from 'react'; import { useState } from 'react';
import { projectService } from '../service'; import EnvPresetsEditor from '../../detail/components/EnvPresetsEditor';
import type { Project } from '../../types'; import type { Project } from '../../types';
import { projectService } from '../service';
interface CreateProjectModalProps { interface CreateProjectModalProps {
visible: boolean; visible: boolean;
@@ -22,7 +30,15 @@ function CreateProjectModal({
const values = await form.validate(); const values = await form.validate();
setLoading(true); setLoading(true);
const newProject = await projectService.create(values); // 序列化环境预设
const submitData = {
...values,
envPresets: values.envPresets
? JSON.stringify(values.envPresets)
: undefined,
};
const newProject = await projectService.create(submitData);
Message.success('项目创建成功'); Message.success('项目创建成功');
onSuccess(newProject); onSuccess(newProject);
@@ -114,7 +130,9 @@ function CreateProjectModal({
if (value.includes('..') || value.includes('~')) { if (value.includes('..') || value.includes('~')) {
return cb('不能包含路径遍历字符(.. 或 ~'); return cb('不能包含路径遍历字符(.. 或 ~');
} }
if (/[<>:"|?*\x00-\x1f]/.test(value)) { // 检查非法字符(控制字符 0x00-0x1F
// biome-ignore lint/suspicious/noControlCharactersInRegex: 需要检测路径中的控制字符
if (/[<>:"|?*\u0000-\u001f]/.test(value)) {
return cb('路径包含非法字符'); return cb('路径包含非法字符');
} }
cb(); cb();
@@ -124,6 +142,14 @@ function CreateProjectModal({
> >
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" /> <Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
</Form.Item> </Form.Item>
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
<Collapse.Item header="环境变量预设配置(可选)" name="envPresets">
<Form.Item field="envPresets" noStyle>
<EnvPresetsEditor />
</Form.Item>
</Collapse.Item>
</Collapse>
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -1,7 +1,17 @@
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react'; import {
Button,
Collapse,
Form,
Input,
Message,
Modal,
} from '@arco-design/web-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import EnvPresetsEditor, {
type EnvPreset,
} from '../../detail/components/EnvPresetsEditor';
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;
@@ -22,10 +32,20 @@ function EditProjectModal({
// 当项目信息变化时,更新表单数据 // 当项目信息变化时,更新表单数据
React.useEffect(() => { React.useEffect(() => {
if (project && visible) { if (project && visible) {
let envPresets: EnvPreset[] = [];
try {
if (project.envPresets) {
envPresets = JSON.parse(project.envPresets);
}
} catch (error) {
console.error('解析环境预设失败:', error);
}
form.setFieldsValue({ form.setFieldsValue({
name: project.name, name: project.name,
description: project.description, description: project.description,
repository: project.repository, repository: project.repository,
envPresets,
}); });
} }
}, [project, visible, form]); }, [project, visible, form]);
@@ -37,7 +57,18 @@ function EditProjectModal({
if (!project) return; if (!project) return;
const updatedProject = await projectService.update(project.id, values); // 序列化环境预设
const submitData = {
...values,
envPresets: values.envPresets
? JSON.stringify(values.envPresets)
: undefined,
};
const updatedProject = await projectService.update(
project.id,
submitData,
);
Message.success('项目更新成功'); Message.success('项目更新成功');
onSuccess(updatedProject); onSuccess(updatedProject);
@@ -111,6 +142,14 @@ function EditProjectModal({
> >
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" /> <Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
</Form.Item> </Form.Item>
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
<Collapse.Item header="环境变量预设配置" name="envPresets">
<Form.Item field="envPresets" noStyle>
<EnvPresetsEditor />
</Form.Item>
</Collapse.Item>
</Collapse>
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -3,6 +3,7 @@ import {
Card, Card,
Space, Space,
Tag, Tag,
Tooltip,
Typography, Typography,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import { import {

View File

@@ -1,4 +1,4 @@
import { Button, Grid, Message, Typography } from '@arco-design/web-react'; import { Button, Grid, 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 { useAsyncEffect } from '../../../hooks/useAsyncEffect'; import { useAsyncEffect } from '../../../hooks/useAsyncEffect';

View File

@@ -36,6 +36,7 @@ export interface Project {
description: string; description: string;
repository: string; repository: string;
projectDir: string; // 项目工作目录路径(必填) projectDir: string; // 项目工作目录路径(必填)
envPresets?: string; // 环境预设配置JSON格式
valid: number; valid: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -77,12 +78,11 @@ export interface Pipeline {
export interface Deployment { export interface Deployment {
id: number; id: number;
branch: string; branch: string;
env?: string; envVars?: string; // JSON 字符串
status: string; status: string;
commitHash?: string; commitHash?: string;
commitMessage?: string; commitMessage?: string;
buildLog?: string; buildLog?: string;
sparseCheckoutPaths?: string; // 稀疏检出路径用于monorepo项目
startedAt: string; startedAt: string;
finishedAt?: string; finishedAt?: string;
valid: number; valid: number;
@@ -127,6 +127,5 @@ export interface CreateDeploymentRequest {
branch: string; branch: string;
commitHash: string; commitHash: string;
commitMessage: string; commitMessage: string;
env?: string; envVars?: Record<string, string>; // 环境变量 key-value 对象
sparseCheckoutPaths?: string; // 稀疏检出路径用于monorepo项目
} }

View File

@@ -20,7 +20,11 @@ class Net {
(error) => { (error) => {
console.log('error', error); console.log('error', error);
// 对于DELETE请求返回204状态码的情况视为成功 // 对于DELETE请求返回204状态码的情况视为成功
if (error.response && error.response.status === 204 && error.config.method === 'delete') { if (
error.response &&
error.response.status === 204 &&
error.config.method === 'delete'
) {
// 创建一个模拟的成功响应 // 创建一个模拟的成功响应
return Promise.resolve({ return Promise.resolve({
...error.response, ...error.response,

7
docs/.meta/OWNERS.md Normal file
View File

@@ -0,0 +1,7 @@
# 文档拥有者
- backend: backend-team@example.com
- ops: ops-team@example.com
- product: product-team@example.com
每个文档请在 front-matter 中声明 `owners` 字段。

View File

@@ -0,0 +1,116 @@
---
title: 设计文档模板
summary: 记录一个功能/模块的设计方案、权衡与落地计划(建议配套 ADR
owners:
- team: <team>
reviewers:
- <name-or-team>
status: draft
date: 2026-01-03
version: 0.1.0
related:
- adr: docs/architecture/adr-xxxx-<slug>.md
- pr: <link>
- issue: <link>
---
# 设计文档:<标题>
## 1. 背景Context
- 当前问题是什么?为什么现在要做?
- 相关现状:已有模块/接口/数据模型(附链接)
- 约束:技术栈、部署方式、团队边界、时间/人力
## 2. 目标Goals
- [ ] 目标 1可验证
- [ ] 目标 2可验证
## 3. 非目标Non-goals
- 不做什么(防止范围膨胀)
## 4. 需求与范围Requirements & Scope
- 用户/角色谁会用例如管理员、开发者、CI runner
- 功能需求:
- R1:
- R2:
- 非功能需求:性能、可用性、可维护性、可观测性
## 5. 方案概览High-level Design
- 用 5-10 行描述整体方案(模块、数据流、调用链)
- 关键选择:为什么选这个方案?
## 6. 详细设计Detailed Design
### 6.1 接口/API 设计
- 新增/变更端点(路径、方法、权限、请求/响应示例)
- 错误码与错误语义(与 `BusinessError` 对齐)
### 6.2 数据模型/数据库
- Prisma model 变更(字段、索引、迁移策略)
- 数据一致性与幂等策略(例如:重试/重复提交)
### 6.3 任务/队列/异步处理(如有)
- 队列模型:入队、出队、并发、重试、死信/失败处理
- 状态机:状态枚举与迁移
### 6.4 配置与环境变量
- 新增 env默认值、是否敏感、是否需要重启
### 6.5 可观测性
- 日志关键日志点、traceId/requestId如有
- 指标:成功率、延迟、队列长度、失败原因分布
- 告警P0/P1 触发条件
### 6.6 安全与权限
- 认证是否要求登录session
- 授权:角色/资源权限(项目级、流水线级)
- 数据安全敏感信息、token、日志脱敏
## 7. 影响与权衡Trade-offs
- 性能影响
- 运维影响
- 对现有接口/调用方的影响
- 技术债与后续演进
## 8. 兼容性与迁移Compatibility & Migration
- 是否 breaking change
- 迁移步骤DB、配置、数据回填
- 回滚策略
## 9. 测试计划Test Plan
- 单测:覆盖哪些模块
- 集成测试:关键链路(如:创建部署 -> 入队 -> 执行 -> 状态更新)
- 手工验证:步骤清单
## 10. 发布计划Rollout Plan
- 分阶段:灰度/开关/逐步放量(如有)
- 监控指标与验收标准
## 11. 备选方案Alternatives Considered
- 方案 A为什么不用
- 方案 B为什么不用
## 12. 风险与开放问题Risks & Open Questions
- 风险 1
- 问题 1需要谁来决定/何时决定)
## 13. 附录Appendix
- 相关链接控制器、DTO、Prisma schema、PR 等

View File

@@ -0,0 +1,22 @@
---
title: Runbook 模板
owners:
- ops: ops-team
status: draft
---
# Runbook 标题
## 触发条件
## 负责人
## 联系方式
## 暂时性缓解
## 恢复步骤
## 验证
## 回滚(如果适用)

14
docs/api/README.md Normal file
View File

@@ -0,0 +1,14 @@
---
title: API 文档
summary: 本目录存放 OpenAPI 定义与 API 使用说明。
tags: [api]
owners:
- team: backend
status: stable
---
# API 文档
本目录包含 OpenAPI 规范与示例。可使用 Swagger UI 或 Redoc 渲染 `openapi.yaml`
- OpenAPI: `openapi.yaml`

78
docs/api/endpoints.md Normal file
View File

@@ -0,0 +1,78 @@
---
title: API 端点总览
summary: 基于 `apps/server` 控制器实现的主要 REST API 端点汇总。
owners:
- team: backend
status: stable
---
# API 端点总览
基础前缀:`/api`
下面列出当前实现的主要控制器与常用端点。
## Projects (`/api/projects`)
- GET `/api/projects` : 列表(支持分页与按 name 搜索)
- GET `/api/projects/:id` : 获取单个项目(包含 workspace 状态)
- POST `/api/projects` : 创建项目body: `name`, `repository`, `projectDir` 等)
- PUT `/api/projects/:id` : 更新项目
- DELETE `/api/projects/:id` : 软删除(将 `valid` 置为 0
示例:
```http
GET /api/projects?page=1&limit=10
```
## User (`/api/user`)
- GET `/api/user/list` : 模拟用户列表
- GET `/api/user/detail/:id` : 用户详情
- POST `/api/user` : 创建用户
- PUT `/api/user/:id` : 更新用户
- DELETE `/api/user/:id` : 删除用户
- GET `/api/user/search` : 搜索用户
## Auth (`/api/auth`)
- GET `/api/auth/url` : 获取 Gitea OAuth 授权 URL
- POST `/api/auth/login` : 使用 OAuth code 登录(返回 session
- GET `/api/auth/logout` : 登出
- GET `/api/auth/info` : 当前会话用户信息
注意:需要配置 `GITEA_URL``GITEA_CLIENT_ID``GITEA_REDIRECT_URI`
## Deployments (`/api/deployments`)
- GET `/api/deployments` : 列表(支持 projectId 过滤)
- POST `/api/deployments` : 创建部署(会将任务加入执行队列)
- POST `/api/deployments/:id/retry` : 重新执行某次部署(复制记录并 requeue
## Pipelines (`/api/pipelines`)
- GET `/api/pipelines` : 列表(含 steps
- GET `/api/pipelines/templates` : 获取可用流水线模板
- GET `/api/pipelines/:id` : 单个流水线(含步骤)
- POST `/api/pipelines` : 创建流水线
- POST `/api/pipelines/from-template` : 基于模板创建流水线
- PUT `/api/pipelines/:id` : 更新流水线
- DELETE `/api/pipelines/:id` : 软删除
## Steps (`/api/steps`)
- GET `/api/steps` : 列表(支持 pipelineId 过滤)
- GET `/api/steps/:id` : 单个步骤
- POST `/api/steps` : 创建步骤(包含 `script` 字段)
- PUT `/api/steps/:id` : 更新步骤
- DELETE `/api/steps/:id` : 软删除
## Git (`/api/git`)
- GET `/api/git/commits?projectId=&branch=` : 获取指定项目的提交列表(调用 Gitea
- GET `/api/git/branches?projectId=` : 获取分支列表
---
想要更详细的示例(请求 body、响应 schema我可以为每个端点基于 `dto.ts` 自动生成示例请求/响应片段。是否需要我继续生成?

28
docs/api/openapi.yaml Normal file
View File

@@ -0,0 +1,28 @@
openapi: 3.0.1
info:
title: Foka-CI 示例 API
version: '1.0.0'
paths:
/health:
get:
summary: 健康检查
responses:
'200':
description: OK
/projects:
get:
summary: 列出项目
responses:
'200':
description: 项目列表
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string

View File

@@ -0,0 +1,26 @@
---
title: ADR 0001 - 服务设计决策
date: 2026-01-03
authors:
- backend-team
status: accepted
---
# ADR 0001: 服务设计与部署模型
## 背景
需要选择微服务还是单体部署以便平衡开发速度与运维复杂度。
## 决策
采用模块化单体modular monolith作为初始阶段部署方式关键模块解耦、接口明确后续按需拆分服务。
## 影响
- 优点:降低初期运维成本,便于本地调试与 CI 集成。
- 缺点:需要在代码边界设计中预留拆分点。
## 备注
在拆分时优先考虑数据库边界和独立部署能力。

View File

@@ -0,0 +1,175 @@
---
title: 设计文档 0001 - 产品原型
summary: 产品的设计原型
owners:
- team: backend
reviewers:
- ops-team
status: draft
date: 2026-01-03
version: 0.1.0
---
# 设计文档 0001产品整体原型
## 1. 背景Context
企业内部代码发布是非常频繁的事情,无论是测试环境还是生产环境,发布代码常常面临着一些痛点:频繁发布浪费开发时;手动发布代码可能漏掉某些步骤、造成环境报错。所以需要引入 CI/CD 工具让发布流程自动化,市场上已经存在很多解决类似问题的产品,例如 Drone CI、jenkins 等,但是这些工具不够灵活、不能满足全部场景,所以便有了 Foka-CI 这款产品。
## 2. 拆解需求
### 登录认证页面
仅提供 Gitea 的 OAuth 认证登录,方便获取仓库信息,简化密码登录过程
### 项目列表页面
查看所有项目,项目的基本信息有:
- 项目名
- 项目描述(可选)
- 代码仓库地址
- 项目的工作目录,用于将代码克隆到该目录下,发布流水线脚本在该目录下执行
创建项目,需要填写基本信息
### 项目详情页
## 2. 目标Goals
- [ ] 明确“部署任务”从创建到执行的状态流转与约束
- [ ] 明确重试语义retry 会创建新 deployment而不是复用原记录
- [ ] 降低重复入队/重复执行的风险(幂等与并发边界清晰)
- [ ] 让运维/排障更容易:关键日志点与可观测性清单
## 3. 非目标Non-goals
- 不引入外部消息队列(如 Redis/Kafka仍以当前内存队列 + DB 恢复为基础
- 不在本文直接实现大规模调度/分布式 runner后续 ADR/设计再做)
## 4. 需求与范围Requirements & Scope
### 功能需求
- 创建部署:写入一条 Deployment 记录,初始状态为 `pending`,并加入执行队列
- 重试部署:通过 `/deployments/:id/retry` 创建一条新的 Deployment 记录(复制必要字段),并加入执行队列
- 服务重启恢复:服务启动后能从 DB 找回仍为 `pending` 的任务并继续执行
### 非功能需求
- 可靠性服务重启不会“丢任务”pending 的任务能恢复)
- 幂等性:避免同一个 deployment 被重复执行
- 可观测性:能定位某次部署为何失败、何时开始/结束、队列长度
## 5. 方案概览High-level Design
当前方案核心链路如下:
1. API 创建 Deploymentstatus=pending
2. `ExecutionQueue.addTask(deploymentId, pipelineId)` 入队
3. `ExecutionQueue.processQueue()` 串行消费 `pendingQueue`
4. `executePipeline()` 会读取 Deployment 与关联 Project获取 `projectDir`,然后创建 `PipelineRunner` 执行
5. 定时轮询:每 30 秒扫描 DB 中的 pending 任务,若不在 `runningDeployments` 集合则补入队列
## 6. 详细设计Detailed Design
### 6.1 接口/API 设计
#### 创建部署
- POST `/api/deployments`
- 行为:创建 Deployment 后立即入队
#### 重试部署
- POST `/api/deployments/:id/retry`
- 行为:读取原 deployment -> 创建新 deploymentstatus=pending-> 入队
- 语义:重试是“创建一个新的任务实例”,便于保留历史执行记录
### 6.2 数据模型/数据库
Deployment 当前关键字段(见 schema 注释):
- `status`: pending/running/success/failed/cancelled目前入队依赖 pending
- `buildLog`: 执行日志(当前创建时写空字符串)
- `startedAt`/`finishedAt`: 时间标记(目前 created 时 startedAt 默认 now
建议补齐/明确(文档层面约束,代码后续落地):
- 状态迁移:
- `pending` -> `running`(开始执行前)
- `running` -> `success|failed|cancelled`(结束后)
- 幂等控制:
- 以 deploymentId 为“单次执行唯一标识”,同一个 deploymentId 不允许重复开始执行
### 6.3 队列/轮询/并发
现状:
- `runningDeployments` 同时承担“已入队/执行中”的去重集合
- `pendingQueue` 为内存 FIFO
- 单实例串行消费(`processQueue` while 循环)
- 轮询间隔常量 30 秒
风险点(需要在文档中明确约束/后续逐步修正):
- 多实例部署:如果将来启动多个 server 实例,每个实例都可能轮询到同一条 pending 记录并执行(需要 DB 锁/租约/状态原子更新)
- 状态更新缺口:当前 `ExecutionQueue` 代码中没有看到明确把 status 从 pending 改成 running/failed/success 的逻辑(可能在 `PipelineRunner` 内处理;若没有,需要补齐)
建议(不改变整体架构前提):
- 将轮询间隔改为可配置 env`EXECUTION_POLL_INTERVAL_MS`(默认 30000
- 在真正执行前做一次 DB 原子“抢占”:仅当 status=pending 时更新为 running并记录开始时间更新失败则放弃执行
### 6.4 可观测性
最低要求(建议后续落地到代码/日志规范):
- 日志字段deploymentId、pipelineId、projectId、projectDir、status
- 队列指标pendingQueue length、runningDeployments size
- 失败记录:捕获异常 message/stack避免泄露敏感信息
### 6.5 安全与权限
当前接口层面需要确认:
- `/api/deployments``/api/deployments/:id/retry` 是否需要登录/鉴权(取决于 middleware 配置)
- 若需要鉴权:建议限制为有项目权限的用户才能创建/重试部署
## 7. 影响与权衡Trade-offs
- 继续采用内存队列:实现简单,但天然不支持多实例并发安全
- DB 轮询恢复:可靠性提升,但会带来额外 DB 查询压力
## 8. 兼容性与迁移Compatibility & Migration
- 文档层面不破坏现有 API
- 若引入 status 原子抢占,需要确保旧数据/旧状态兼容(例如对历史 pending 记录仍可恢复)
## 9. 测试计划Test Plan
- 集成链路:创建 deployment -> 入队 -> 触发执行(可用假 runner
- 重启恢复:插入 pending 记录 -> initialize() -> 任务被 addTask
- 重试接口:原记录存在/不存在的分支
## 10. 发布计划Rollout Plan
- 先补齐文档 + 最小日志规范
- 再逐步落地status 原子抢占 + 轮询间隔 env
## 11. 备选方案Alternatives Considered
- 引入 Redis 队列BullMQ 等):更可靠、支持多实例,但复杂度上升
- 使用 DB 作为队列(表 + 锁/租约):更可靠,但需要严格的并发控制
## 12. 风险与开放问题Risks & Open Questions
- Q1`PipelineRunner` 是否负责更新 Deployment.status如果没有状态机应由谁维护
- Q2服务是否计划多实例部署如果是必须补齐“抢占执行”机制
## 13. 附录Appendix
- 代码:`apps/server/libs/execution-queue.ts`
- 控制器:`apps/server/controllers/deployment/index.ts`
- Schema`apps/server/prisma/schema.prisma`

View File

@@ -0,0 +1,166 @@
---
title: 设计文档 0001 - 部署执行队列与重试(基于当前实现)
summary: 记录当前 ExecutionQueue 的行为与下一步改进方向,便于团队对齐。
owners:
- team: backend
reviewers:
- ops-team
status: draft
date: 2026-01-03
version: 0.1.0
related:
- code: apps/server/libs/execution-queue.ts
- code: apps/server/controllers/deployment/index.ts
- schema: apps/server/prisma/schema.prisma
---
# 设计文档 0001部署执行队列与重试
## 1. 背景Context
当前服务端在启动时会初始化执行队列(`ExecutionQueue.initialize()`),用于从数据库恢复 `status=pending` 的部署任务并按顺序执行流水线。
现有相关事实(来自当前代码):
- 服务启动入口:`apps/server/app.ts`
- 路由:`/api/deployments`(创建部署后入队)与 `/api/deployments/:id/retry`(复制记录后入队)
- 数据库SQLite + PrismaDeployment 存在 `status` 字段注释标明pending/running/success/failed/cancelled
- 队列实现:内存队列 `pendingQueue` + `runningDeployments` Set并有轮询机制默认 30 秒)把 DB 中的 pending 任务补进队列
## 2. 目标Goals
- [ ] 明确“部署任务”从创建到执行的状态流转与约束
- [ ] 明确重试语义retry 会创建新 deployment而不是复用原记录
- [ ] 降低重复入队/重复执行的风险(幂等与并发边界清晰)
- [ ] 让运维/排障更容易:关键日志点与可观测性清单
## 3. 非目标Non-goals
- 不引入外部消息队列(如 Redis/Kafka仍以当前内存队列 + DB 恢复为基础
- 不在本文直接实现大规模调度/分布式 runner后续 ADR/设计再做)
## 4. 需求与范围Requirements & Scope
### 功能需求
- 创建部署:写入一条 Deployment 记录,初始状态为 `pending`,并加入执行队列
- 重试部署:通过 `/deployments/:id/retry` 创建一条新的 Deployment 记录(复制必要字段),并加入执行队列
- 服务重启恢复:服务启动后能从 DB 找回仍为 `pending` 的任务并继续执行
### 非功能需求
- 可靠性服务重启不会“丢任务”pending 的任务能恢复)
- 幂等性:避免同一个 deployment 被重复执行
- 可观测性:能定位某次部署为何失败、何时开始/结束、队列长度
## 5. 方案概览High-level Design
当前方案核心链路如下:
1) API 创建 Deploymentstatus=pending
2) `ExecutionQueue.addTask(deploymentId, pipelineId)` 入队
3) `ExecutionQueue.processQueue()` 串行消费 `pendingQueue`
4) `executePipeline()` 会读取 Deployment 与关联 Project获取 `projectDir`,然后创建 `PipelineRunner` 执行
5) 定时轮询:每 30 秒扫描 DB 中的 pending 任务,若不在 `runningDeployments` 集合则补入队列
## 6. 详细设计Detailed Design
### 6.1 接口/API 设计
#### 创建部署
- POST `/api/deployments`
- 行为:创建 Deployment 后立即入队
#### 重试部署
- POST `/api/deployments/:id/retry`
- 行为:读取原 deployment -> 创建新 deploymentstatus=pending-> 入队
- 语义:重试是“创建一个新的任务实例”,便于保留历史执行记录
### 6.2 数据模型/数据库
Deployment 当前关键字段(见 schema 注释):
- `status`: pending/running/success/failed/cancelled目前入队依赖 pending
- `buildLog`: 执行日志(当前创建时写空字符串)
- `startedAt`/`finishedAt`: 时间标记(目前 created 时 startedAt 默认 now
建议补齐/明确(文档层面约束,代码后续落地):
- 状态迁移:
- `pending` -> `running`(开始执行前)
- `running` -> `success|failed|cancelled`(结束后)
- 幂等控制:
- 以 deploymentId 为“单次执行唯一标识”,同一个 deploymentId 不允许重复开始执行
### 6.3 队列/轮询/并发
现状:
- `runningDeployments` 同时承担“已入队/执行中”的去重集合
- `pendingQueue` 为内存 FIFO
- 单实例串行消费(`processQueue` while 循环)
- 轮询间隔常量 30 秒
风险点(需要在文档中明确约束/后续逐步修正):
- 多实例部署:如果将来启动多个 server 实例,每个实例都可能轮询到同一条 pending 记录并执行(需要 DB 锁/租约/状态原子更新)
- 状态更新缺口:当前 `ExecutionQueue` 代码中没有看到明确把 status 从 pending 改成 running/failed/success 的逻辑(可能在 `PipelineRunner` 内处理;若没有,需要补齐)
建议(不改变整体架构前提):
- 将轮询间隔改为可配置 env`EXECUTION_POLL_INTERVAL_MS`(默认 30000
- 在真正执行前做一次 DB 原子“抢占”:仅当 status=pending 时更新为 running并记录开始时间更新失败则放弃执行
### 6.4 可观测性
最低要求(建议后续落地到代码/日志规范):
- 日志字段deploymentId、pipelineId、projectId、projectDir、status
- 队列指标pendingQueue length、runningDeployments size
- 失败记录:捕获异常 message/stack避免泄露敏感信息
### 6.5 安全与权限
当前接口层面需要确认:
- `/api/deployments``/api/deployments/:id/retry` 是否需要登录/鉴权(取决于 middleware 配置)
- 若需要鉴权:建议限制为有项目权限的用户才能创建/重试部署
## 7. 影响与权衡Trade-offs
- 继续采用内存队列:实现简单,但天然不支持多实例并发安全
- DB 轮询恢复:可靠性提升,但会带来额外 DB 查询压力
## 8. 兼容性与迁移Compatibility & Migration
- 文档层面不破坏现有 API
- 若引入 status 原子抢占,需要确保旧数据/旧状态兼容(例如对历史 pending 记录仍可恢复)
## 9. 测试计划Test Plan
- 集成链路:创建 deployment -> 入队 -> 触发执行(可用假 runner
- 重启恢复:插入 pending 记录 -> initialize() -> 任务被 addTask
- 重试接口:原记录存在/不存在的分支
## 10. 发布计划Rollout Plan
- 先补齐文档 + 最小日志规范
- 再逐步落地status 原子抢占 + 轮询间隔 env
## 11. 备选方案Alternatives Considered
- 引入 Redis 队列BullMQ 等):更可靠、支持多实例,但复杂度上升
- 使用 DB 作为队列(表 + 锁/租约):更可靠,但需要严格的并发控制
## 12. 风险与开放问题Risks & Open Questions
- Q1`PipelineRunner` 是否负责更新 Deployment.status如果没有状态机应由谁维护
- Q2服务是否计划多实例部署如果是必须补齐“抢占执行”机制
## 13. 附录Appendix
- 代码:`apps/server/libs/execution-queue.ts`
- 控制器:`apps/server/controllers/deployment/index.ts`
- Schema`apps/server/prisma/schema.prisma`

View File

@@ -0,0 +1,127 @@
---
title: 设计文档 0005 - 部署流程重构(移除稀疏检出 & 环境预设)
summary: 调整部署相关能力:移除稀疏检出;将部署环境从创建时输入改为在项目设置中预设。
owners:
- team: backend
reviewers:
- team: frontend
status: draft
date: 2026-01-03
version: 0.1.0
related:
- docs: docs/api/endpoints.md
- schema: apps/server/prisma/schema.prisma
---
# 设计文档 0005部署流程重构移除稀疏检出 & 环境预设)
## 1. 背景Context
当前部署流程在“项目详情页发起部署”时包含“稀疏检出sparse checkout”表单项并且流水线模板中也包含与稀疏检出相关的逻辑。
另外,部署时需要指定环境变量(例如 env但目前是在“创建部署”时临时输入/选择。随着项目数量增加,这种方式容易造成不一致与误操作。
## 2. 目标Goals
- [ ] 移除项目详情页部署表单中的“稀疏检出”相关输入项
- [ ] 移除流水线模板中与稀疏检出相关的代码逻辑(后端模板/生成逻辑)
- [ ] 将“部署环境env”从创建部署时指定调整为在“项目设置”中提前预设
- [ ] 创建部署时仍需要选择/指定环境,但选项来源于项目设置中的预设项
## 3. 非目标Non-goals
- 不新增多维度环境变量管理(仅覆盖本次提到的 env 单项预设)
- 不在本次引入复杂的环境权限、审批流
## 4. 需求与范围Requirements & Scope
### 4.1 移除稀疏检出
#### 用户侧
- 项目详情页发起部署时:不再展示/提交稀疏检出字段
#### 系统侧
- 流水线模板:移除任何基于稀疏检出路径的生成/执行逻辑
> 说明:当前 DB 中 Deployment 仍存在 `sparseCheckoutPaths` 字段(见 `schema.prisma`),本次需求仅明确“功能不再需要”。字段是否删除/迁移由本设计后续章节确定。
### 4.2 部署环境 env 改为项目设置预设
#### 核心约束
- 环境变量预设需要支持多选、单选、输入框这几种类型
- 在项目设置中新增可配置项(预设项):
例如指定env 环境变量
- 类型单选single select
- key`env`value 及时部署是选中的候选项的值
- options`staging`(测试环境)、`production`(生产环境)
#### 行为
- 创建部署时仍需指定环境env
- 不再由用户自由输入
- 只允许从该项目预设的 options 中选择
## 5. 影响面Impact
### 5.1 前端
- 项目详情页部署表单:移除“稀疏检出”相关 UI 与字段提交
- 项目设置页新增“环境预设env”配置入口单选 + 选项 staging/production
- 创建部署交互:环境选项从项目设置读取(不再硬编码/临时输入)
### 5.2 后端
- 部署创建接口:校验 env 必须来自项目预设(避免非法 env
- 流水线模板:移除稀疏检出相关的模板字段/生成逻辑
### 5.3 数据库
- 需要新增“项目设置/项目配置”承载 env 预设(落库方案待定)
- 既有 Deployment 的 `sparseCheckoutPaths` 字段:后续决定是否保留(兼容历史)或迁移删除
## 6. 兼容性与迁移Compatibility & Migration
- 对历史部署记录:
- 若存在 `sparseCheckoutPaths`,不影响查询展示,但新建部署不再写入该字段
- 对创建部署:
- 若项目未配置 env 预设:创建部署应失败并提示先到项目设置配置(或提供默认值策略,待确认)
## 7. 测试要点Test Plan
- 前端:
- 项目详情页部署表单不再出现稀疏检出项
- 项目设置可保存 env 预设(单选)并在创建部署时正确展示
- 后端:
- 创建部署env 不在项目预设 options 内时应拒绝
- 流水线模板:移除稀疏检出后仍能正常创建并执行
## 8. 实施状态Implementation Status
### 已完成(后端)
- ✅ Prisma Schema在 Project 表添加 `envPresets` 字段String? 类型,存储 JSON
- ✅ 移除部署创建/重试接口中的 `sparseCheckoutPaths` 写入
- ✅ 在部署创建接口添加环境校验:验证 env 是否在项目 envPresets 的 options 中
- ✅ 更新 project DTO 和 controller 支持 envPresets 读写
- ✅ 移除 pipeline-runner 中的 `SPARSE_CHECKOUT_PATHS` 环境变量
- ✅ 生成 Prisma Client
### 已完成(前端)
- ✅ 创建 EnvPresetsEditor 组件(支持单选、多选、输入框类型)
- ✅ 在 CreateProjectModal 和 EditProjectModal 中集成环境预设编辑器
- ✅ 从 DeployModal 移除稀疏检出表单项
- ✅ 在 DeployModal 中从项目 envPresets 读取环境选项并展示
- ✅ 移除 DeployModal 中的动态环境变量列表envVars Form.List
- ✅ 从类型定义中移除 sparseCheckoutPaths 字段
- ✅ 在项目详情页项目设置 tab 中添加环境变量预设的查看和编辑功能
### 待定问题
- Q1项目设置存储方式 → **已决定**:使用 Project.envPresets JSON 字段
- Q2未配置 env 预设的默认行为 → **已实现**:若配置了预设则校验,否则允许任意值(向后兼容)
- Q3Deployment.sparseCheckoutPaths 字段 → **已决定**:保留字段(兼容历史),但新建部署不再写入

View File

@@ -0,0 +1,9 @@
# Changelog
所有 notable 更改应在此记录。遵循 Keep a Changelog 格式。
## [Unreleased]
- 初始文档目录建立。
## [1.0.0] - 2026-01-03
- 初始发布

88
docs/development/setup.md Normal file
View File

@@ -0,0 +1,88 @@
---
title: 开发与本地环境搭建
summary: 针对本项目的本地开发、数据库与调试指南。
owners:
- team: backend
status: stable
---
# 开发与本地环境搭建
## 1. 安装依赖
建议使用 `pnpm` 管理工作区依赖:
```bash
# 在仓库根
pnpm install
```
或者只在 server 子包安装:
```bash
cd apps/server
pnpm install
```
## 2. 生成 Prisma Client
```bash
cd apps/server
npx prisma generate
```
如果需要执行迁移(开发场景):
```bash
npx prisma migrate dev --name init
```
数据库:项目使用 SQLite`apps/server/prisma/schema.prisma`),迁移会在本地创建 `.db` 文件。
## 3. 启动服务
并行启动 workspace 中所有 dev 脚本(推荐):
```bash
pnpm dev
```
或单独启动 server
```bash
cd apps/server
pnpm dev
```
服务默认端口:`3001`。如需修改:
```bash
PORT=4000 pnpm dev
```
## 4. 常见开发命令
- 运行测试脚本(仓库自带):
```bash
cd apps/server
node test.js
```
- TypeScript 类型检查(本地可使用 `tsc`
```bash
npx tsc --noEmit
```
## 5. 环境变量与第三方集成
常见 env`GITEA_URL`, `GITEA_CLIENT_ID`, `GITEA_REDIRECT_URI`, `PORT`, `NODE_ENV`
登录采用 Gitea OAuth未配置时某些 auth 接口会返回 401需要先登录获取 session。
## 6. 运行与调试要点
- 代码通过装饰器注册路由(见 `apps/server/decorators/route.ts``apps/server/libs/route-scanner.ts`)。
- Prisma client 生成路径:`apps/server/generated`
- 若变更 Prisma schema请执行 `prisma generate` 并更新迁移。

79
docs/getting-started.md Normal file
View File

@@ -0,0 +1,79 @@
---
title: 快速开始
summary: 本文档介绍如何在本地启动与运行项目的基础步骤。
tags: [getting-started]
owners:
- team: backend
status: stable
version: 1.0.0
---
# 快速开始
## 前置条件
- Node.js >= 18
- pnpm
- 克隆权限(或访问仓库)
## 克隆与依赖安装
```bash
git clone <repo-url>
cd foka-ci
pnpm install
```
说明:仓库使用 pnpm workspace根目录脚本 `pnpm dev` 会并行启动工作区内的 `dev` 脚本。
## 运行服务(开发)
- 从仓库根(并行运行所有 dev 脚本):
```bash
pnpm dev
```
- 单独运行 server
```bash
cd apps/server
pnpm install
pnpm dev # 等同于: tsx watch ./app.ts
```
服务器默认监听端口 `3001`(可通过 `PORT` 环境变量覆盖。API 前缀为 `/api`
## Prisma 与数据库
项目使用 SQLite`apps/server/prisma/schema.prisma`)。如果需要生成 Prisma Client 或运行迁移:
```bash
cd apps/server
npx prisma generate
npx prisma migrate dev --name init # 本地开发使用
```
生成的 Prisma Client 位于 `apps/server/generated`(由 schema 中的 generator 指定)。
## 环境变量(常用)
- `GITEA_URL``GITEA_CLIENT_ID``GITEA_REDIRECT_URI`:用于 OAuth 登录Gitea
- `PORT`:服务监听端口,默认 `3001`
- `NODE_ENV`环境development/production
将敏感值放入 `.env`(不要将 `.env` 提交到仓库)。
## 运行脚本与测试
```bash
cd apps/server
node test.js # 运行仓库自带的简单测试脚本
```
## 其他说明
- 文档目录位于 `docs/`,设计模板在 `docs/.meta/templates/`
- API 路由由装饰器注册,路由前缀为 `/api`,在 `apps/server/middlewares/router.ts` 中可查看。
更多开发细节请参见 `docs/development/setup.md``docs/api/endpoints.md`

22
docs/index.md Normal file
View File

@@ -0,0 +1,22 @@
---
title: 项目文档总览
summary: 项目文档的导航页指向快速开始、架构、API、运维等部分。
tags: [overview]
owners:
- team: backend
status: stable
version: 1.0.0
---
# 文档总览
欢迎来到项目文档。下列是常用文档的入口:
- Getting Started: ./getting-started.md
- Architecture: ./architecture/adr-0001-service-design.md
- API: ./api/README.md
- Runbooks: ./runbooks/incident-response.md
- Onboarding: ./onboarding/new-hire.md
- Changelog: ./changelogs/CHANGELOG.md
维护说明:请在变更同时更新 `owners``version`,并通过 PR 提交。

View File

@@ -0,0 +1,13 @@
---
title: 新成员入职指南
owners:
- people-team
status: draft
---
# 新成员入职快速清单
1. 获取公司邮箱与账号
2. 克隆仓库并运行 `pnpm install`
3. 阅读 `docs/getting-started.md``docs/architecture` 中关键 ADR
4. 约见导师并完成第一次 code walkthrough

View File

@@ -0,0 +1,28 @@
---
title: 事故响应 Runbook
summary: 处理生产事故的步骤和联系方式。
owners:
- ops-team
status: stable
---
# 事故响应Incident Response
## 1. 识别与分级
- P0: 系统不可用或数据安全泄露
- P1: 主要功能严重受损
## 2. 通知
- 联系人:`on-call@company.example`,电话:+86-10-12345678
## 3. 暂时性缓解
- 回滚最近的部署
- 启用备用服务
## 4. 根因分析与恢复
- 记录时间线
- 生成 RCA 并在 72 小时内发布

View File

@@ -8,7 +8,11 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.0.6" "@biomejs/biome": "2.0.6"
}, },
"keywords": ["ci", "ark", "ark-ci"], "keywords": [
"ci",
"ark",
"ark-ci"
],
"author": "hurole", "author": "hurole",
"license": "ISC", "license": "ISC",
"packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321" "packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"