Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5c550f5c5 |
@@ -14,5 +14,3 @@ dist/
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
.env
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ this.routeScanner.registerControllers([
|
|||||||
## TC39 装饰器特性
|
## TC39 装饰器特性
|
||||||
|
|
||||||
### 1. 标准语法
|
### 1. 标准语法
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// TC39 标准装饰器使用 addInitializer
|
// TC39 标准装饰器使用 addInitializer
|
||||||
@Get('/users')
|
@Get('/users')
|
||||||
@@ -90,7 +89,6 @@ async getUsers(ctx: Context) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2. 类型安全
|
### 2. 类型安全
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 完整的 TypeScript 类型检查
|
// 完整的 TypeScript 类型检查
|
||||||
@Controller('/api')
|
@Controller('/api')
|
||||||
@@ -103,7 +101,6 @@ export class ApiController {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 3. 无外部依赖
|
### 3. 无外部依赖
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 不再需要 reflect-metadata
|
// 不再需要 reflect-metadata
|
||||||
// 使用内置的 WeakMap 存储元数据
|
// 使用内置的 WeakMap 存储元数据
|
||||||
@@ -139,7 +136,6 @@ export class ApiController {
|
|||||||
最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径
|
最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径
|
||||||
|
|
||||||
例如:
|
例如:
|
||||||
|
|
||||||
- 全局前缀:`/api`
|
- 全局前缀:`/api`
|
||||||
- 控制器前缀:`/user`
|
- 控制器前缀:`/user`
|
||||||
- 方法路径:`/list`
|
- 方法路径:`/list`
|
||||||
@@ -180,11 +176,56 @@ async getUser(ctx: Context) {
|
|||||||
|
|
||||||
## 现有路由
|
## 现有路由
|
||||||
|
|
||||||
### UserController
|
项目中已注册的路由:
|
||||||
|
|
||||||
|
### ApplicationController
|
||||||
|
- `GET /api/application/list` - 获取应用列表
|
||||||
|
- `GET /api/application/detail/:id` - 获取应用详情
|
||||||
|
|
||||||
|
### UserController
|
||||||
- `GET /api/user/list` - 获取用户列表
|
- `GET /api/user/list` - 获取用户列表
|
||||||
- `GET /api/user/detail/:id` - 获取用户详情
|
- `GET /api/user/detail/:id` - 获取用户详情
|
||||||
- `POST /api/user` - 创建用户
|
- `POST /api/user` - 创建用户
|
||||||
- `PUT /api/user/:id` - 更新用户
|
- `PUT /api/user/:id` - 更新用户
|
||||||
- `DELETE /api/user/:id` - 删除用户
|
- `DELETE /api/user/:id` - 删除用户
|
||||||
- `GET /api/user/search` - 搜索用户
|
- `GET /api/user/search` - 搜索用户
|
||||||
|
|
||||||
|
## 与旧版本装饰器的区别
|
||||||
|
|
||||||
|
| 特性 | 实验性装饰器 | TC39 标准装饰器 |
|
||||||
|
|------|-------------|----------------|
|
||||||
|
| 标准化 | ❌ TypeScript 特有 | ✅ ECMAScript 标准 |
|
||||||
|
| 依赖 | ❌ 需要 reflect-metadata | ✅ 零依赖 |
|
||||||
|
| 性能 | ❌ 运行时反射 | ✅ 编译时优化 |
|
||||||
|
| 类型安全 | ⚠️ 部分支持 | ✅ 完整支持 |
|
||||||
|
| 未来兼容 | ❌ 可能被废弃 | ✅ 持续演进 |
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
从实验性装饰器迁移到 TC39 标准装饰器:
|
||||||
|
|
||||||
|
1. **更新 tsconfig.json**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"experimentalDecorators": false,
|
||||||
|
"emitDecoratorMetadata": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **移除依赖**
|
||||||
|
```bash
|
||||||
|
pnpm remove reflect-metadata
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **代码无需修改**
|
||||||
|
- 装饰器语法保持不变
|
||||||
|
- 控制器代码无需修改
|
||||||
|
- 自动兼容新标准
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 需要 TypeScript 5.0+ 支持
|
||||||
|
2. 需要 Node.js 16+ 运行环境
|
||||||
|
3. 控制器类需要导出并在路由中间件中注册
|
||||||
|
4. 控制器方法应该返回数据而不是直接操作 `ctx.body`
|
||||||
|
5. TC39 装饰器使用 `addInitializer` 进行初始化,性能更优
|
||||||
|
|||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import { ExecutionQueue } from './libs/execution-queue.ts';
|
|
||||||
import { log } from './libs/logger.ts';
|
|
||||||
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
|
||||||
import { initMiddlewares } from './middlewares/index.ts';
|
import { initMiddlewares } from './middlewares/index.ts';
|
||||||
|
import { log } from './libs/logger.ts';
|
||||||
|
import { ExecutionQueue } from './libs/execution-queue.ts';
|
||||||
|
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
||||||
|
|
||||||
// 初始化应用
|
// 初始化应用
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
@@ -26,7 +26,7 @@ async function initializeApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动应用
|
// 启动应用
|
||||||
initializeApp().catch((error) => {
|
initializeApp().catch(error => {
|
||||||
log.error('APP', 'Failed to start application:', error);
|
console.error('Failed to start application:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { gitea } from '../../libs/gitea.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
import { loginSchema } from './dto.ts';
|
import { loginSchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/auth')
|
@Controller('/auth')
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const listDeploymentsQuerySchema = z.object({
|
export const listDeploymentsQuerySchema = z.object({
|
||||||
page: z.coerce.number().int().min(1).optional(),
|
page: z.coerce.number().int().min(1).optional().default(1),
|
||||||
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
pageSize: z.coerce.number().int().min(1).max(100).optional().default(10),
|
||||||
projectId: z.coerce.number().int().positive().optional(),
|
projectId: z.coerce.number().int().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -12,7 +12,8 @@ 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: '提交信息不能为空' }),
|
||||||
envVars: z.record(z.string(), z.string()).optional(), // 环境变量 key-value 对象
|
env: z.string().optional(),
|
||||||
|
sparseCheckoutPaths: z.string().optional(), // 添加稀疏检出路径字段
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
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 { ExecutionQueue } from '../../libs/execution-queue.ts';
|
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { createDeploymentSchema, listDeploymentsQuerySchema } from './dto.ts';
|
import type { Context } from 'koa';
|
||||||
|
import { listDeploymentsQuerySchema, createDeploymentSchema } from './dto.ts';
|
||||||
|
import { ExecutionQueue } from '../../libs/execution-queue.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(
|
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(ctx.query);
|
||||||
ctx.query,
|
|
||||||
);
|
|
||||||
const where: Prisma.DeploymentWhereInput = {
|
const where: Prisma.DeploymentWhereInput = {
|
||||||
valid: 1,
|
valid: 1,
|
||||||
};
|
};
|
||||||
@@ -20,30 +18,24 @@ export class DeploymentController {
|
|||||||
where.projectId = projectId;
|
where.projectId = projectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPagination = page !== undefined && pageSize !== undefined;
|
|
||||||
|
|
||||||
const result = await prisma.deployment.findMany({
|
const result = await prisma.deployment.findMany({
|
||||||
where,
|
where,
|
||||||
take: isPagination ? pageSize : undefined,
|
take: pageSize,
|
||||||
skip: isPagination ? (page! - 1) * pageSize! : 0,
|
skip: (page - 1) * pageSize,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const total = await prisma.deployment.count({ where });
|
const total = await prisma.deployment.count({ where });
|
||||||
|
|
||||||
if (isPagination) {
|
|
||||||
return {
|
return {
|
||||||
list: result,
|
data: result,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('')
|
@Post('')
|
||||||
async create(ctx: Context) {
|
async create(ctx: Context) {
|
||||||
const body = createDeploymentSchema.parse(ctx.request.body);
|
const body = createDeploymentSchema.parse(ctx.request.body);
|
||||||
@@ -58,7 +50,8 @@ export class DeploymentController {
|
|||||||
connect: { id: body.projectId },
|
connect: { id: body.projectId },
|
||||||
},
|
},
|
||||||
pipelineId: body.pipelineId,
|
pipelineId: body.pipelineId,
|
||||||
envVars: body.envVars ? JSON.stringify(body.envVars) : null,
|
env: body.env || 'dev',
|
||||||
|
sparseCheckoutPaths: body.sparseCheckoutPaths || '', // 添加稀疏检出路径
|
||||||
buildLog: '',
|
buildLog: '',
|
||||||
createdBy: 'system', // TODO: get from user
|
createdBy: 'system', // TODO: get from user
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
@@ -80,7 +73,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) {
|
||||||
@@ -89,7 +82,7 @@ export class DeploymentController {
|
|||||||
code: 404,
|
code: 404,
|
||||||
message: '部署记录不存在',
|
message: '部署记录不存在',
|
||||||
data: null,
|
data: null,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -103,7 +96,8 @@ export class DeploymentController {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
projectId: originalDeployment.projectId,
|
projectId: originalDeployment.projectId,
|
||||||
pipelineId: originalDeployment.pipelineId,
|
pipelineId: originalDeployment.pipelineId,
|
||||||
envVars: originalDeployment.envVars,
|
env: originalDeployment.env,
|
||||||
|
sparseCheckoutPaths: originalDeployment.sparseCheckoutPaths,
|
||||||
buildLog: '',
|
buildLog: '',
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
@@ -119,7 +113,7 @@ export class DeploymentController {
|
|||||||
code: 0,
|
code: 0,
|
||||||
message: '重新执行任务已创建',
|
message: '重新执行任务已创建',
|
||||||
data: newDeployment,
|
data: newDeployment,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const getCommitsQuerySchema = z.object({
|
export const getCommitsQuerySchema = z.object({
|
||||||
projectId: z.coerce
|
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
||||||
.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
|
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.positive({ message: 'Project ID is required' }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
||||||
|
|||||||
@@ -1,12 +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 { gitea } from '../../libs/gitea.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { getBranchesQuerySchema, getCommitsQuerySchema } from './dto.ts';
|
import { getCommitsQuerySchema, getBranchesQuerySchema } from './dto.ts';
|
||||||
|
|
||||||
const TAG = 'Git';
|
|
||||||
|
|
||||||
@Controller('/git')
|
@Controller('/git')
|
||||||
export class GitController {
|
export class GitController {
|
||||||
@@ -33,21 +30,17 @@ export class GitController {
|
|||||||
|
|
||||||
// Get access token from session
|
// Get access token from session
|
||||||
const accessToken = ctx.session?.gitea?.access_token;
|
const accessToken = ctx.session?.gitea?.access_token;
|
||||||
log.debug(TAG, 'Access token present: %s', !!accessToken);
|
console.log('Access token present:', !!accessToken);
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new BusinessError(
|
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
||||||
'Gitea access token not found. Please login again.',
|
|
||||||
1004,
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const commits = await gitea.getCommits(owner, repo, accessToken, branch);
|
const commits = await gitea.getCommits(owner, repo, accessToken, branch);
|
||||||
return commits;
|
return commits;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, 'Failed to fetch commits:', error);
|
console.error('Failed to fetch commits:', error);
|
||||||
throw new BusinessError('Failed to fetch commits from Gitea', 1005, 500);
|
throw new BusinessError('Failed to fetch commits from Gitea', 1005, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,18 +65,14 @@ export class GitController {
|
|||||||
const accessToken = ctx.session?.gitea?.access_token;
|
const accessToken = ctx.session?.gitea?.access_token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new BusinessError(
|
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
||||||
'Gitea access token not found. Please login again.',
|
|
||||||
1004,
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const branches = await gitea.getBranches(owner, repo, accessToken);
|
const branches = await gitea.getBranches(owner, repo, accessToken);
|
||||||
return branches;
|
return branches;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, 'Failed to fetch branches:', error);
|
console.error('Failed to fetch branches:', error);
|
||||||
throw new BusinessError('Failed to fetch branches from Gitea', 1006, 500);
|
throw new BusinessError('Failed to fetch branches from Gitea', 1006, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +85,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] };
|
||||||
}
|
}
|
||||||
@@ -107,15 +96,13 @@ 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()!;
|
||||||
if (repo && owner) {
|
|
||||||
return { owner, repo };
|
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] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
// 控制器统一导出
|
// 控制器统一导出
|
||||||
|
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 { GitController } from './git/index.ts';
|
|
||||||
export { PipelineController } from './pipeline/index.ts';
|
export { PipelineController } from './pipeline/index.ts';
|
||||||
export { ProjectController } from './project/index.ts';
|
export { StepController } from './step/index.ts'
|
||||||
export { StepController } from './step/index.ts';
|
export { GitController } from './git/index.ts';
|
||||||
export { UserController } from './user/index.ts';
|
|
||||||
|
|||||||
@@ -2,59 +2,36 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
// 定义验证架构
|
// 定义验证架构
|
||||||
export const createPipelineSchema = z.object({
|
export const createPipelineSchema = z.object({
|
||||||
name: z
|
name: z.string({
|
||||||
.string({
|
|
||||||
message: '流水线名称必须是字符串',
|
message: '流水线名称必须是字符串',
|
||||||
})
|
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }),
|
||||||
.min(1, { message: '流水线名称不能为空' })
|
|
||||||
.max(100, { message: '流水线名称不能超过100个字符' }),
|
|
||||||
|
|
||||||
description: z
|
description: z.string({
|
||||||
.string({
|
|
||||||
message: '流水线描述必须是字符串',
|
message: '流水线描述必须是字符串',
|
||||||
})
|
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
||||||
.max(500, { message: '流水线描述不能超过500个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
projectId: z
|
projectId: z.number({
|
||||||
.number({
|
|
||||||
message: '项目ID必须是数字',
|
message: '项目ID必须是数字',
|
||||||
})
|
}).int().positive({ message: '项目ID必须是正整数' }).optional(),
|
||||||
.int()
|
|
||||||
.positive({ message: '项目ID必须是正整数' })
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updatePipelineSchema = z.object({
|
export const updatePipelineSchema = z.object({
|
||||||
name: z
|
name: z.string({
|
||||||
.string({
|
|
||||||
message: '流水线名称必须是字符串',
|
message: '流水线名称必须是字符串',
|
||||||
})
|
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }).optional(),
|
||||||
.min(1, { message: '流水线名称不能为空' })
|
|
||||||
.max(100, { message: '流水线名称不能超过100个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
description: z
|
description: z.string({
|
||||||
.string({
|
|
||||||
message: '流水线描述必须是字符串',
|
message: '流水线描述必须是字符串',
|
||||||
})
|
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
||||||
.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
|
export const listPipelinesQuerySchema = z.object({
|
||||||
.object({
|
projectId: z.coerce.number().int().positive({ message: '项目ID必须是正整数' }).optional(),
|
||||||
projectId: z.coerce
|
}).optional();
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.positive({ message: '项目ID必须是正整数' })
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
// 类型
|
// 类型
|
||||||
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import {
|
|
||||||
createPipelineFromTemplate,
|
|
||||||
getAvailableTemplates,
|
|
||||||
} from '../../libs/pipeline-template.ts';
|
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { log } from '../../libs/logger.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,
|
||||||
listPipelinesQuerySchema,
|
|
||||||
pipelineIdSchema,
|
|
||||||
updatePipelineSchema,
|
updatePipelineSchema,
|
||||||
|
pipelineIdSchema,
|
||||||
|
listPipelinesQuerySchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/pipelines')
|
@Controller('/pipelines')
|
||||||
@@ -49,12 +46,12 @@ 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;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('pipeline', 'Failed to get templates:', error);
|
console.error('Failed to get templates:', error);
|
||||||
throw new BusinessError('获取模板失败', 3002, 500);
|
throw new BusinessError('获取模板失败', 3002, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +126,7 @@ export class PipelineController {
|
|||||||
templateId,
|
templateId,
|
||||||
projectId,
|
projectId,
|
||||||
name,
|
name,
|
||||||
description || '',
|
description || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
// 返回新创建的流水线
|
// 返回新创建的流水线
|
||||||
@@ -154,7 +151,7 @@ export class PipelineController {
|
|||||||
log.info('pipeline', 'Created pipeline from template: %s', pipeline.name);
|
log.info('pipeline', 'Created pipeline from template: %s', pipeline.name);
|
||||||
return pipeline;
|
return pipeline;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('pipeline', 'Failed to create pipeline from template:', error);
|
console.error('Failed to create pipeline from template:', error);
|
||||||
if (error instanceof BusinessError) {
|
if (error instanceof BusinessError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,72 +5,46 @@ import { projectDirSchema } from '../../libs/path-validator.js';
|
|||||||
* 创建项目验证架构
|
* 创建项目验证架构
|
||||||
*/
|
*/
|
||||||
export const createProjectSchema = z.object({
|
export const createProjectSchema = z.object({
|
||||||
name: z
|
name: z.string({
|
||||||
.string({
|
|
||||||
message: '项目名称必须是字符串',
|
message: '项目名称必须是字符串',
|
||||||
})
|
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }),
|
||||||
.min(2, { message: '项目名称至少2个字符' })
|
|
||||||
.max(50, { message: '项目名称不能超过50个字符' }),
|
|
||||||
|
|
||||||
description: z
|
description: z.string({
|
||||||
.string({
|
|
||||||
message: '项目描述必须是字符串',
|
message: '项目描述必须是字符串',
|
||||||
})
|
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
||||||
.max(200, { message: '项目描述不能超过200个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
repository: z
|
repository: z.string({
|
||||||
.string({
|
|
||||||
message: '仓库地址必须是字符串',
|
message: '仓库地址必须是字符串',
|
||||||
})
|
}).url({ message: '请输入有效的仓库地址' }).min(1, { 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
|
name: z.string({
|
||||||
.string({
|
|
||||||
message: '项目名称必须是字符串',
|
message: '项目名称必须是字符串',
|
||||||
})
|
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }).optional(),
|
||||||
.min(2, { message: '项目名称至少2个字符' })
|
|
||||||
.max(50, { message: '项目名称不能超过50个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
description: z
|
description: z.string({
|
||||||
.string({
|
|
||||||
message: '项目描述必须是字符串',
|
message: '项目描述必须是字符串',
|
||||||
})
|
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
||||||
.max(200, { message: '项目描述不能超过200个字符' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
repository: z
|
repository: z.string({
|
||||||
.string({
|
|
||||||
message: '仓库地址必须是字符串',
|
message: '仓库地址必须是字符串',
|
||||||
})
|
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }).optional(),
|
||||||
.url({ message: '请输入有效的仓库地址' })
|
|
||||||
.min(1, { message: '仓库地址不能为空' })
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
envPresets: z.string().optional(), // JSON 字符串格式
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目列表查询参数验证架构
|
* 项目列表查询参数验证架构
|
||||||
*/
|
*/
|
||||||
export const listProjectQuerySchema = z
|
export const listProjectQuerySchema = z.object({
|
||||||
.object({
|
page: z.coerce.number().int().min(1, { message: '页码必须大于0' }).optional().default(1),
|
||||||
page: z.coerce.number().int().min(1).optional(),
|
limit: z.coerce.number().int().min(1, { message: '每页数量必须大于0' }).max(100, { message: '每页数量不能超过100' }).optional().default(10),
|
||||||
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
})
|
}).optional();
|
||||||
.optional();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目ID验证架构
|
* 项目ID验证架构
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
|
||||||
import { GitManager } from '../../libs/git-manager.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
|
import { GitManager } from '../../libs/git-manager.ts';
|
||||||
import {
|
import {
|
||||||
createProjectSchema,
|
createProjectSchema,
|
||||||
|
updateProjectSchema,
|
||||||
listProjectQuerySchema,
|
listProjectQuerySchema,
|
||||||
projectIdSchema,
|
projectIdSchema,
|
||||||
updateProjectSchema,
|
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/projects')
|
@Controller('/projects')
|
||||||
@@ -29,32 +29,29 @@ export class ProjectController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPagination = query?.page !== undefined && query?.pageSize !== undefined;
|
|
||||||
|
|
||||||
const [total, projects] = await Promise.all([
|
const [total, projects] = await Promise.all([
|
||||||
prisma.project.count({ where: whereCondition }),
|
prisma.project.count({ where: whereCondition }),
|
||||||
prisma.project.findMany({
|
prisma.project.findMany({
|
||||||
where: whereCondition,
|
where: whereCondition,
|
||||||
skip: isPagination ? (query.page! - 1) * query.pageSize! : 0,
|
skip: query ? (query.page - 1) * query.limit : 0,
|
||||||
take: isPagination ? query.pageSize : undefined,
|
take: query?.limit,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (isPagination) {
|
|
||||||
return {
|
return {
|
||||||
list: projects,
|
data: projects,
|
||||||
page: query.page,
|
pagination: {
|
||||||
pageSize: query.pageSize,
|
page: query?.page || 1,
|
||||||
|
limit: query?.limit || 10,
|
||||||
total,
|
total,
|
||||||
|
totalPages: Math.ceil(total / (query?.limit || 10)),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return projects;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/projects/:id - 获取单个项目
|
// GET /api/projects/:id - 获取单个项目
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async show(ctx: Context) {
|
async show(ctx: Context) {
|
||||||
@@ -71,21 +68,27 @@ export class ProjectController {
|
|||||||
throw new BusinessError('项目不存在', 1002, 404);
|
throw new BusinessError('项目不存在', 1002, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取工作目录状态信息(不包含目录大小)
|
// 获取工作目录状态信息
|
||||||
let workspaceStatus = null;
|
let workspaceStatus = null;
|
||||||
if (project.projectDir) {
|
if (project.projectDir) {
|
||||||
try {
|
try {
|
||||||
const status = await GitManager.checkWorkspaceStatus(
|
const status = await GitManager.checkWorkspaceStatus(
|
||||||
project.projectDir,
|
project.projectDir,
|
||||||
);
|
);
|
||||||
|
let size = 0;
|
||||||
let gitInfo = null;
|
let gitInfo = null;
|
||||||
|
|
||||||
|
if (status.exists && !status.isEmpty) {
|
||||||
|
size = await GitManager.getDirectorySize(project.projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
if (status.hasGit) {
|
if (status.hasGit) {
|
||||||
gitInfo = await GitManager.getGitInfo(project.projectDir);
|
gitInfo = await GitManager.getGitInfo(project.projectDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaceStatus = {
|
workspaceStatus = {
|
||||||
...status,
|
...status,
|
||||||
|
size,
|
||||||
gitInfo,
|
gitInfo,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -132,7 +135,6 @@ 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,
|
||||||
@@ -180,9 +182,6 @@ 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 },
|
||||||
|
|||||||
@@ -84,13 +84,15 @@ export const listStepsQuerySchema = z
|
|||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1, { message: '页码必须大于0' })
|
.min(1, { message: '页码必须大于0' })
|
||||||
.optional(),
|
.optional()
|
||||||
pageSize: z.coerce
|
.default(1),
|
||||||
|
limit: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1, { message: '每页数量必须大于0' })
|
.min(1, { message: '每页数量必须大于0' })
|
||||||
.max(100, { message: '每页数量不能超过100' })
|
.max(100, { message: '每页数量不能超过100' })
|
||||||
.optional(),
|
.optional()
|
||||||
|
.default(10),
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
import {
|
import {
|
||||||
createStepSchema,
|
createStepSchema,
|
||||||
listStepsQuerySchema,
|
|
||||||
stepIdSchema,
|
|
||||||
updateStepSchema,
|
updateStepSchema,
|
||||||
|
stepIdSchema,
|
||||||
|
listStepsQuerySchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/steps')
|
@Controller('/steps')
|
||||||
@@ -26,32 +26,29 @@ export class StepController {
|
|||||||
whereCondition.pipelineId = query.pipelineId;
|
whereCondition.pipelineId = query.pipelineId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPagination = query?.page !== undefined && query?.pageSize !== undefined;
|
|
||||||
|
|
||||||
const [total, steps] = await Promise.all([
|
const [total, steps] = await Promise.all([
|
||||||
prisma.step.count({ where: whereCondition }),
|
prisma.step.count({ where: whereCondition }),
|
||||||
prisma.step.findMany({
|
prisma.step.findMany({
|
||||||
where: whereCondition,
|
where: whereCondition,
|
||||||
skip: isPagination ? (query.page! - 1) * query.pageSize! : 0,
|
skip: query ? (query.page - 1) * query.limit : 0,
|
||||||
take: isPagination ? query.pageSize : undefined,
|
take: query?.limit,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
order: 'asc',
|
order: 'asc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (isPagination) {
|
|
||||||
return {
|
return {
|
||||||
list: steps,
|
data: steps,
|
||||||
page: query.page,
|
pagination: {
|
||||||
pageSize: query.pageSize,
|
page: query?.page || 1,
|
||||||
|
limit: query?.limit || 10,
|
||||||
total,
|
total,
|
||||||
|
totalPages: Math.ceil(total / (query?.limit || 10)),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return steps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/steps/:id - 获取单个步骤
|
// GET /api/steps/:id - 获取单个步骤
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async show(ctx: Context) {
|
async show(ctx: Context) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import {
|
import {
|
||||||
createUserSchema,
|
|
||||||
searchUserQuerySchema,
|
|
||||||
updateUserSchema,
|
|
||||||
userIdSchema,
|
userIdSchema,
|
||||||
|
createUserSchema,
|
||||||
|
updateUserSchema,
|
||||||
|
searchUserQuerySchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,18 +13,14 @@ 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;
|
||||||
@@ -37,10 +33,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) {
|
||||||
@@ -59,7 +55,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;
|
||||||
@@ -74,7 +70,7 @@ export class UserController {
|
|||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
id,
|
id,
|
||||||
...body,
|
...body,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
@@ -92,7 +88,7 @@ export class UserController {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `用户 ${id} 已删除`,
|
message: `用户 ${id} 已删除`,
|
||||||
deletedAt: new Date().toISOString(),
|
deletedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,21 +99,25 @@ 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(
|
results = results.filter(user =>
|
||||||
(user) =>
|
|
||||||
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
user.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||||
user.email.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 results;
|
return {
|
||||||
|
keyword,
|
||||||
|
status,
|
||||||
|
total: results.length,
|
||||||
|
results
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,24 +25,17 @@ const metadataStore = new WeakMap<any, Map<string | symbol, any>>();
|
|||||||
/**
|
/**
|
||||||
* 设置元数据(降级方案)
|
* 设置元数据(降级方案)
|
||||||
*/
|
*/
|
||||||
function setMetadata<T = any>(
|
function setMetadata<T = any>(key: string | symbol, value: T, target: any): void {
|
||||||
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>(
|
function getMetadata<T = any>(key: string | symbol, target: any): T | undefined {
|
||||||
key: string | symbol,
|
|
||||||
target: any,
|
|
||||||
): T | undefined {
|
|
||||||
return metadataStore.get(target)?.get(key);
|
return metadataStore.get(target)?.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,28 +43,24 @@ function getMetadata<T = any>(
|
|||||||
* 创建HTTP方法装饰器的工厂函数(TC39标准)
|
* 创建HTTP方法装饰器的工厂函数(TC39标准)
|
||||||
*/
|
*/
|
||||||
function createMethodDecorator(method: HttpMethod) {
|
function createMethodDecorator(method: HttpMethod) {
|
||||||
return (path: string = '') =>
|
return function (path: string = '') {
|
||||||
<This, Args extends any[], Return>(
|
return function <This, Args extends any[], Return>(
|
||||||
target: (this: This, ...args: Args) => Return,
|
target: (this: This, ...args: Args) => Return,
|
||||||
context: ClassMethodDecoratorContext<
|
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
|
||||||
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[] =
|
const existingRoutes: RouteMetadata[] = getMetadata(ROUTE_METADATA_KEY, ctor) || [];
|
||||||
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);
|
||||||
@@ -82,6 +71,7 @@ function createMethodDecorator(method: HttpMethod) {
|
|||||||
|
|
||||||
return target;
|
return target;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,10 +109,10 @@ export const Patch = createMethodDecorator('PATCH');
|
|||||||
* @param prefix 路由前缀
|
* @param prefix 路由前缀
|
||||||
*/
|
*/
|
||||||
export function Controller(prefix: string = '') {
|
export function Controller(prefix: string = '') {
|
||||||
return <T extends abstract new (...args: any) => any>(
|
return function <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);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/* !!! 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
|
||||||
@@ -12,32 +13,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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/* !!! 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,19 +10,18 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from 'node:path';
|
import * as process from 'node:process'
|
||||||
import * as process from 'node:process';
|
import * as path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url'
|
||||||
|
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url));
|
import * as runtime from "@prisma/client/runtime/client"
|
||||||
|
import * as $Enums from "./enums.ts"
|
||||||
|
import * as $Class from "./internal/class.ts"
|
||||||
|
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||||
|
|
||||||
import * as runtime from '@prisma/client/runtime/client';
|
export * as $Enums from './enums.ts'
|
||||||
import * as $Enums from './enums.ts';
|
export * 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,38 +35,32 @@ 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<
|
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>
|
||||||
LogOpts extends Prisma.LogLevel = never,
|
export { Prisma }
|
||||||
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
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/* !!! 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,419 +9,394 @@
|
|||||||
* 🟢 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?:
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
| string
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
| null;
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
_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?:
|
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||||
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
| number
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
| null;
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
_max?: Prisma.NestedIntNullableFilter<$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?:
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
| 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?:
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
| Date
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
| string
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
| 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?:
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
| string
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
| null;
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
_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?:
|
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
||||||
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
| number
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
||||||
| null;
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
_max?: Prisma.NestedIntNullableFilter<$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?:
|
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
||||||
| 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?:
|
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
||||||
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
| Date
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
| string
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
| null;
|
}
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
|
||||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
|
||||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
|
||||||
/* !!! 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 {}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/* !!! 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,53 +11,44 @@
|
|||||||
* 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:
|
"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",
|
||||||
'// 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',
|
"runtimeDataModel": {
|
||||||
runtimeDataModel: {
|
"models": {},
|
||||||
models: {},
|
"enums": {},
|
||||||
enums: {},
|
"types": {}
|
||||||
types: {},
|
}
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
|
||||||
config.runtimeDataModel = JSON.parse(
|
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\":{}}")
|
||||||
'{"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(
|
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||||
wasmBase64: string,
|
const { Buffer } = await import('node:buffer')
|
||||||
): Promise<WebAssembly.Module> {
|
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||||
const { Buffer } = await import('node:buffer');
|
return new WebAssembly.Module(wasmArray)
|
||||||
const wasmArray = Buffer.from(wasmBase64, 'base64');
|
|
||||||
return new WebAssembly.Module(wasmArray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config.compilerWasm = {
|
config.compilerWasm = {
|
||||||
getRuntime: async () =>
|
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.sqlite.mjs"),
|
||||||
await import('@prisma/client/runtime/query_compiler_bg.sqlite.mjs'),
|
|
||||||
|
|
||||||
getQueryCompilerWasmModule: async () => {
|
getQueryCompilerWasmModule: async () => {
|
||||||
const { wasm } = await import(
|
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs")
|
||||||
'@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs'
|
return await decodeBase64AsWasm(wasm)
|
||||||
);
|
}
|
||||||
return await decodeBase64AsWasm(wasm);
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||||
'log' extends keyof ClientOptions
|
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||||
? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
|
|
||||||
? Prisma.GetEvents<ClientOptions['log']>
|
|
||||||
: never
|
|
||||||
: never;
|
|
||||||
|
|
||||||
export interface PrismaClientConstructor {
|
export interface PrismaClientConstructor {
|
||||||
/**
|
/**
|
||||||
@@ -76,16 +68,9 @@ 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 {
|
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||||
omit: infer U;
|
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||||
}
|
>(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>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,17 +90,11 @@ 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
|
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||||
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>(
|
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||||
eventType: V,
|
|
||||||
callback: (
|
|
||||||
event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent,
|
|
||||||
) => void,
|
|
||||||
): PrismaClient;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect with the database
|
* Connect with the database
|
||||||
@@ -127,7 +106,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
|
||||||
* ```
|
* ```
|
||||||
@@ -136,10 +115,7 @@ 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>(
|
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
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.
|
||||||
@@ -151,10 +127,7 @@ 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>(
|
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||||
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.
|
||||||
@@ -165,10 +138,7 @@ 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>(
|
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
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.
|
||||||
@@ -180,10 +150,8 @@ 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>(
|
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||||
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.
|
||||||
@@ -198,33 +166,13 @@ 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>[]>(
|
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||||
arg: [...P],
|
|
||||||
options?: { isolationLevel?: Prisma.TransactionIsolationLevel },
|
|
||||||
): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>;
|
|
||||||
|
|
||||||
$transaction<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>
|
||||||
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: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||||
'extends',
|
extArgs: ExtArgs
|
||||||
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.
|
||||||
@@ -278,5 +226,5 @@ export interface PrismaClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
/* !!! 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
|
||||||
@@ -14,65 +15,61 @@
|
|||||||
* 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 (
|
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||||
secret: never,
|
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||||
) => typeof runtime.DbNull,
|
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||||
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',
|
||||||
@@ -80,16 +77,15 @@ 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',
|
||||||
@@ -102,11 +98,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',
|
||||||
@@ -117,11 +113,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',
|
||||||
@@ -133,16 +129,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',
|
||||||
envVars: 'envVars',
|
env: 'env',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
commitHash: 'commitHash',
|
commitHash: 'commitHash',
|
||||||
commitMessage: 'commitMessage',
|
commitMessage: 'commitMessage',
|
||||||
@@ -156,22 +152,24 @@ 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];
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
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
|
||||||
@@ -10,6 +8,9 @@ export type * from './models/Pipeline.ts';
|
|||||||
*
|
*
|
||||||
* 🟢 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/Step.ts';
|
export type * from './models/User.ts'
|
||||||
export type * from './models/User.ts';
|
export type * from './models/Pipeline.ts'
|
||||||
|
export type * from './models/Step.ts'
|
||||||
|
export type * from './models/Deployment.ts'
|
||||||
|
export type * from './commonInputTypes.ts'
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+766
-1193
File diff suppressed because it is too large
Load Diff
+705
-1064
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
|||||||
import { PipelineRunner } from '../runners/index.ts';
|
import { PipelineRunner } from '../runners/index.ts';
|
||||||
import { prisma } from './prisma.ts';
|
import { prisma } from './prisma.ts';
|
||||||
import { log } from '../libs/logger.ts';
|
|
||||||
|
|
||||||
const TAG = 'Queue';
|
|
||||||
// 存储正在运行的部署任务
|
// 存储正在运行的部署任务
|
||||||
const runningDeployments = new Set<number>();
|
const runningDeployments = new Set<number>();
|
||||||
|
|
||||||
@@ -42,14 +40,14 @@ export class ExecutionQueue {
|
|||||||
* 初始化执行队列,包括恢复未完成的任务
|
* 初始化执行队列,包括恢复未完成的任务
|
||||||
*/
|
*/
|
||||||
public async initialize(): Promise<void> {
|
public async initialize(): Promise<void> {
|
||||||
log.info(TAG, 'Initializing execution queue...');
|
console.log('Initializing execution queue...');
|
||||||
// 恢复未完成的任务
|
// 恢复未完成的任务
|
||||||
await this.recoverPendingDeployments();
|
await this.recoverPendingDeployments();
|
||||||
|
|
||||||
// 启动定时轮询
|
// 启动定时轮询
|
||||||
this.startPolling();
|
this.startPolling();
|
||||||
|
|
||||||
log.info(TAG, 'Execution queue initialized');
|
console.log('Execution queue initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +55,7 @@ export class ExecutionQueue {
|
|||||||
*/
|
*/
|
||||||
private async recoverPendingDeployments(): Promise<void> {
|
private async recoverPendingDeployments(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
log.info(TAG, 'Recovering pending deployments from database...');
|
console.log('Recovering pending deployments from database...');
|
||||||
|
|
||||||
// 查询数据库中状态为pending的部署任务
|
// 查询数据库中状态为pending的部署任务
|
||||||
const pendingDeployments = await prisma.deployment.findMany({
|
const pendingDeployments = await prisma.deployment.findMany({
|
||||||
@@ -71,16 +69,16 @@ export class ExecutionQueue {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info(TAG, `Found ${pendingDeployments.length} pending deployments`);
|
console.log(`Found ${pendingDeployments.length} pending deployments`);
|
||||||
|
|
||||||
// 将这些任务添加到执行队列中
|
// 将这些任务添加到执行队列中
|
||||||
for (const deployment of pendingDeployments) {
|
for (const deployment of pendingDeployments) {
|
||||||
await this.addTask(deployment.id, deployment.pipelineId);
|
await this.addTask(deployment.id, deployment.pipelineId);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(TAG, 'Pending deployments recovery completed');
|
console.log('Pending deployments recovery completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, 'Failed to recover pending deployments:', error);
|
console.error('Failed to recover pending deployments:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,12 +87,12 @@ export class ExecutionQueue {
|
|||||||
*/
|
*/
|
||||||
private startPolling(): void {
|
private startPolling(): void {
|
||||||
if (this.isPolling) {
|
if (this.isPolling) {
|
||||||
log.info(TAG, 'Polling is already running');
|
console.log('Polling is already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isPolling = true;
|
this.isPolling = true;
|
||||||
log.info(TAG, `Starting polling with interval ${POLLING_INTERVAL}ms`);
|
console.log(`Starting polling with interval ${POLLING_INTERVAL}ms`);
|
||||||
|
|
||||||
// 立即执行一次检查
|
// 立即执行一次检查
|
||||||
this.checkPendingDeployments();
|
this.checkPendingDeployments();
|
||||||
@@ -113,7 +111,7 @@ export class ExecutionQueue {
|
|||||||
clearInterval(pollingTimer);
|
clearInterval(pollingTimer);
|
||||||
pollingTimer = null;
|
pollingTimer = null;
|
||||||
this.isPolling = false;
|
this.isPolling = false;
|
||||||
log.info(TAG, 'Polling stopped');
|
console.log('Polling stopped');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +120,7 @@ export class ExecutionQueue {
|
|||||||
*/
|
*/
|
||||||
private async checkPendingDeployments(): Promise<void> {
|
private async checkPendingDeployments(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
log.info(TAG, 'Checking for pending deployments in database...');
|
console.log('Checking for pending deployments in database...');
|
||||||
|
|
||||||
// 查询数据库中状态为pending的部署任务
|
// 查询数据库中状态为pending的部署任务
|
||||||
const pendingDeployments = await prisma.deployment.findMany({
|
const pendingDeployments = await prisma.deployment.findMany({
|
||||||
@@ -136,8 +134,7 @@ export class ExecutionQueue {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info(
|
console.log(
|
||||||
TAG,
|
|
||||||
`Found ${pendingDeployments.length} pending deployments in polling`,
|
`Found ${pendingDeployments.length} pending deployments in polling`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -145,15 +142,14 @@ export class ExecutionQueue {
|
|||||||
for (const deployment of pendingDeployments) {
|
for (const deployment of pendingDeployments) {
|
||||||
// 检查是否已经在运行队列中
|
// 检查是否已经在运行队列中
|
||||||
if (!runningDeployments.has(deployment.id)) {
|
if (!runningDeployments.has(deployment.id)) {
|
||||||
log.info(
|
console.log(
|
||||||
TAG,
|
|
||||||
`Adding deployment ${deployment.id} to queue from polling`,
|
`Adding deployment ${deployment.id} to queue from polling`,
|
||||||
);
|
);
|
||||||
await this.addTask(deployment.id, deployment.pipelineId);
|
await this.addTask(deployment.id, deployment.pipelineId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, 'Failed to check pending deployments:', error);
|
console.error('Failed to check pending deployments:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +164,7 @@ export class ExecutionQueue {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 检查是否已经在运行队列中
|
// 检查是否已经在运行队列中
|
||||||
if (runningDeployments.has(deploymentId)) {
|
if (runningDeployments.has(deploymentId)) {
|
||||||
log.info(TAG, `Deployment ${deploymentId} is already queued or running`);
|
console.log(`Deployment ${deploymentId} is already queued or running`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +194,7 @@ export class ExecutionQueue {
|
|||||||
// 执行流水线
|
// 执行流水线
|
||||||
await this.executePipeline(task.deploymentId, task.pipelineId);
|
await this.executePipeline(task.deploymentId, task.pipelineId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, '执行流水线失败:', error);
|
console.error('执行流水线失败:', error);
|
||||||
// 这里可以添加更多的错误处理逻辑
|
// 这里可以添加更多的错误处理逻辑
|
||||||
} finally {
|
} finally {
|
||||||
// 从运行队列中移除
|
// 从运行队列中移除
|
||||||
@@ -249,7 +245,7 @@ export class ExecutionQueue {
|
|||||||
);
|
);
|
||||||
await runner.run(pipelineId);
|
await runner.run(pipelineId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, '执行流水线失败:', error);
|
console.error('执行流水线失败:', error);
|
||||||
// 错误处理可以在这里添加,比如更新部署状态为失败
|
// 错误处理可以在这里添加,比如更新部署状态为失败
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+16
-27
@@ -1,7 +1,3 @@
|
|||||||
import { log } from './logger.ts';
|
|
||||||
|
|
||||||
const TAG = 'Gitea';
|
|
||||||
|
|
||||||
interface TokenResponse {
|
interface TokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
@@ -42,13 +38,15 @@ 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;
|
||||||
log.debug(TAG, 'Gitea token request started');
|
console.log('this.config', this.config);
|
||||||
const response = await fetch(`${giteaUrl}/login/oauth/access_token`, {
|
const response = await fetch(
|
||||||
|
`${giteaUrl}/login/oauth/access_token`,
|
||||||
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -58,15 +56,10 @@ class Gitea {
|
|||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
}),
|
}),
|
||||||
});
|
},
|
||||||
if (!response.ok) {
|
|
||||||
const payload = await response.json().catch(() => null as unknown);
|
|
||||||
log.error(
|
|
||||||
TAG,
|
|
||||||
'Gitea token request failed: status=%d payload=%o',
|
|
||||||
response.status,
|
|
||||||
payload,
|
|
||||||
);
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log(await response.json());
|
||||||
throw new Error(`Fetch failed: ${response.status}`);
|
throw new Error(`Fetch failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
return (await response.json()) as TokenResponse;
|
return (await response.json()) as TokenResponse;
|
||||||
@@ -115,23 +108,19 @@ class Gitea {
|
|||||||
* @param accessToken 访问令牌
|
* @param accessToken 访问令牌
|
||||||
* @param sha 分支名称或提交SHA
|
* @param sha 分支名称或提交SHA
|
||||||
*/
|
*/
|
||||||
async getCommits(
|
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) {
|
||||||
owner: string,
|
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
||||||
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(url.toString(), {
|
const response = await fetch(
|
||||||
|
url.toString(),
|
||||||
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.getHeaders(accessToken),
|
headers: this.getHeaders(accessToken),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Fetch failed: ${response.status}`);
|
throw new Error(`Fetch failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -144,7 +133,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { prisma } from './prisma.ts';
|
import { prisma } from './prisma.ts';
|
||||||
import { log } from './logger.ts';
|
|
||||||
|
|
||||||
const TAG = 'PipelineTemplate';
|
|
||||||
|
|
||||||
// 默认流水线模板
|
// 默认流水线模板
|
||||||
export interface PipelineTemplate {
|
export interface PipelineTemplate {
|
||||||
@@ -20,6 +17,11 @@ 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,
|
||||||
@@ -34,43 +36,73 @@ 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:
|
script: '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
||||||
'# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
}
|
||||||
},
|
]
|
||||||
],
|
}
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化系统默认流水线模板
|
* 初始化系统默认流水线模板
|
||||||
*/
|
*/
|
||||||
export async function initializePipelineTemplates(): Promise<void> {
|
export async function initializePipelineTemplates(): Promise<void> {
|
||||||
log.info(TAG, 'Initializing pipeline templates...');
|
console.log('Initializing pipeline templates...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查是否已经存在模板流水线
|
// 检查是否已经存在模板流水线
|
||||||
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
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果没有现有的模板,则创建默认模板
|
// 如果没有现有的模板,则创建默认模板
|
||||||
if (existingTemplates.length === 0) {
|
if (existingTemplates.length === 0) {
|
||||||
log.info(TAG, 'Creating default pipeline templates...');
|
console.log('Creating default pipeline templates...');
|
||||||
|
|
||||||
for (const template of DEFAULT_PIPELINE_TEMPLATES) {
|
for (const template of DEFAULT_PIPELINE_TEMPLATES) {
|
||||||
// 创建模板流水线(使用负数ID表示模板)
|
// 创建模板流水线(使用负数ID表示模板)
|
||||||
@@ -81,8 +113,8 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
projectId: null, // 模板不属于任何特定项目
|
projectId: null // 模板不属于任何特定项目
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建模板步骤
|
// 创建模板步骤
|
||||||
@@ -95,23 +127,20 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
pipelineId: pipeline.id,
|
pipelineId: pipeline.id,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(TAG, `Created template: ${template.name}`);
|
console.log(`Created template: ${template.name}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
console.log('Pipeline templates already exist, skipping initialization');
|
||||||
TAG,
|
|
||||||
'Pipeline templates already exist, skipping initialization',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(TAG, 'Pipeline templates initialization completed');
|
console.log('Pipeline templates initialization completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, 'Failed to initialize pipeline templates:', error);
|
console.error('Failed to initialize pipeline templates:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,30 +148,28 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* 获取所有可用的流水线模板
|
* 获取所有可用的流水线模板
|
||||||
*/
|
*/
|
||||||
export async function getAvailableTemplates(): Promise<
|
export async function getAvailableTemplates(): Promise<Array<{id: number, name: string, description: string}>> {
|
||||||
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) {
|
||||||
log.error(TAG, 'Failed to get pipeline templates:', error);
|
console.error('Failed to get pipeline templates:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +185,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 {
|
||||||
// 获取模板流水线及其步骤
|
// 获取模板流水线及其步骤
|
||||||
@@ -166,18 +193,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) {
|
||||||
@@ -192,8 +219,8 @@ export async function createPipelineFromTemplate(
|
|||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 复制模板步骤到新流水线
|
// 复制模板步骤到新流水线
|
||||||
@@ -206,18 +233,15 @@ export async function createPipelineFromTemplate(
|
|||||||
pipelineId: newPipeline.id,
|
pipelineId: newPipeline.id,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
console.log(`Created pipeline from template ${templateId}: ${newPipeline.name}`);
|
||||||
TAG,
|
|
||||||
`Created pipeline from template ${templateId}: ${newPipeline.name}`,
|
|
||||||
);
|
|
||||||
return newPipeline.id;
|
return newPipeline.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(TAG, 'Failed to create pipeline from template:', error);
|
console.error('Failed to create pipeline from template:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import KoaRouter from '@koa/router';
|
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import {
|
import KoaRouter from '@koa/router';
|
||||||
getControllerPrefix,
|
import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts';
|
||||||
getRouteMetadata,
|
|
||||||
type RouteMetadata,
|
|
||||||
} from '../decorators/route.ts';
|
|
||||||
import { createSuccessResponse } from '../middlewares/exception.ts';
|
import { createSuccessResponse } from '../middlewares/exception.ts';
|
||||||
import { log } from './logger.ts';
|
|
||||||
|
|
||||||
const TAG = 'RouteScanner';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制器类型
|
* 控制器类型
|
||||||
@@ -40,7 +33,7 @@ export class RouteScanner {
|
|||||||
* 注册多个控制器类
|
* 注册多个控制器类
|
||||||
*/
|
*/
|
||||||
registerControllers(controllers: ControllerClass[]): void {
|
registerControllers(controllers: ControllerClass[]): void {
|
||||||
controllers.forEach((controller) => this.registerController(controller));
|
controllers.forEach(controller => this.registerController(controller));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,12 +50,9 @@ 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(
|
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey);
|
||||||
controllerInstance,
|
|
||||||
route.propertyKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 根据HTTP方法注册路由
|
// 根据HTTP方法注册路由
|
||||||
switch (route.method) {
|
switch (route.method) {
|
||||||
@@ -82,7 +72,7 @@ export class RouteScanner {
|
|||||||
this.router.patch(fullPath, handler);
|
this.router.patch(fullPath, handler);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
log.info(TAG, `未支持的HTTP方法: ${route.method}`);
|
console.warn(`未支持的HTTP方法: ${route.method}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -97,10 +87,10 @@ export class RouteScanner {
|
|||||||
|
|
||||||
let fullPath = '';
|
let fullPath = '';
|
||||||
if (cleanControllerPrefix) {
|
if (cleanControllerPrefix) {
|
||||||
fullPath += `/${cleanControllerPrefix}`;
|
fullPath += '/' + cleanControllerPrefix;
|
||||||
}
|
}
|
||||||
if (cleanRoutePath) {
|
if (cleanRoutePath) {
|
||||||
fullPath += `/${cleanRoutePath}`;
|
fullPath += '/' + cleanRoutePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果路径为空,返回根路径
|
// 如果路径为空,返回根路径
|
||||||
@@ -115,11 +105,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);
|
||||||
};
|
};
|
||||||
@@ -143,29 +133,19 @@ export class RouteScanner {
|
|||||||
/**
|
/**
|
||||||
* 获取已注册的路由信息(用于调试)
|
* 获取已注册的路由信息(用于调试)
|
||||||
*/
|
*/
|
||||||
getRegisteredRoutes(): Array<{
|
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> {
|
||||||
method: string;
|
const routes: Array<{ method: string; path: string; controller: string; action: 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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type Koa from 'koa';
|
|
||||||
import bodyParser from 'koa-bodyparser';
|
import bodyParser from 'koa-bodyparser';
|
||||||
|
import type Koa from 'koa';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { log } from '../libs/logger.ts';
|
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
import { log } from '../libs/logger.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一响应体结构
|
* 统一响应体结构
|
||||||
@@ -58,26 +58,15 @@ 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(
|
log.info('Exception', 'Zod validation failed: %s at %s', errorMessage, fieldPath);
|
||||||
'Exception',
|
this.sendResponse(ctx, 1003, errorMessage, {
|
||||||
'Zod validation failed: %s at %s',
|
|
||||||
errorMessage,
|
|
||||||
fieldPath,
|
|
||||||
);
|
|
||||||
this.sendResponse(
|
|
||||||
ctx,
|
|
||||||
1003,
|
|
||||||
errorMessage,
|
|
||||||
{
|
|
||||||
field: fieldPath,
|
field: fieldPath,
|
||||||
validationErrors: error.issues.map((issue) => ({
|
validationErrors: error.issues.map(issue => ({
|
||||||
field: issue.path.join('.'),
|
field: issue.path.join('.'),
|
||||||
message: issue.message,
|
message: issue.message,
|
||||||
code: issue.code,
|
code: issue.code,
|
||||||
})),
|
}))
|
||||||
},
|
}, 400);
|
||||||
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);
|
||||||
|
|||||||
@@ -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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化中间件
|
* 初始化中间件
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type Koa from 'koa';
|
import Koa, { type Context } 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';
|
||||||
|
|
||||||
@@ -9,7 +8,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`)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
UserController,
|
GitController
|
||||||
} 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
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 输出注册的路由信息
|
// 输出注册的路由信息
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type Koa from 'koa';
|
|
||||||
import session from 'koa-session';
|
import session from 'koa-session';
|
||||||
|
import type Koa from 'koa';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
export class Session implements Middleware {
|
export class Session implements Middleware {
|
||||||
apply(app: Koa): void {
|
apply(app: Koa): void {
|
||||||
app.keys = ['mini-ci'];
|
app.keys = ['foka-ci'];
|
||||||
app.use(
|
app.use(
|
||||||
session(
|
session(
|
||||||
{
|
{
|
||||||
key: 'mini-ci.sid',
|
key: 'foka.sid',
|
||||||
maxAge: 86400000,
|
maxAge: 86400000,
|
||||||
autoCommit: true /** (boolean) automatically commit headers (default true) */,
|
autoCommit: true /** (boolean) automatically commit headers (default true) */,
|
||||||
overwrite: true /** (boolean) can overwrite or not (default true) */,
|
overwrite: true /** (boolean) can overwrite or not (default true) */,
|
||||||
|
|||||||
Binary file not shown.
@@ -16,7 +16,6 @@ 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[]
|
||||||
@@ -76,7 +75,7 @@ model Step {
|
|||||||
model Deployment {
|
model Deployment {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
branch String
|
branch String
|
||||||
envVars String? // 环境变量(JSON格式),统一存储所有配置
|
env String?
|
||||||
status String // pending, running, success, failed, cancelled
|
status String // pending, running, success, failed, cancelled
|
||||||
commitHash String?
|
commitHash String?
|
||||||
commitMessage String?
|
commitMessage String?
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -66,14 +66,11 @@ export class PipelineRunner {
|
|||||||
|
|
||||||
// 依次执行每个步骤
|
// 依次执行每个步骤
|
||||||
for (const [index, step] of pipeline.steps.entries()) {
|
for (const [index, step] of pipeline.steps.entries()) {
|
||||||
const progress = `[${index + 1}/${pipeline.steps.length}]`;
|
|
||||||
// 准备环境变量
|
// 准备环境变量
|
||||||
const envVars = this.prepareEnvironmentVariables(pipeline, deployment);
|
const envVars = this.prepareEnvironmentVariables(pipeline, deployment);
|
||||||
|
|
||||||
// 记录开始执行步骤的日志
|
// 记录开始执行步骤的日志
|
||||||
const startLog = this.addTimestamp(
|
const startLog = `[${new Date().toISOString()}] 开始执行步骤 ${index + 1}/${pipeline.steps.length}: ${step.name}\n`;
|
||||||
`${progress} 开始执行: ${step.name}`,
|
|
||||||
);
|
|
||||||
logs += startLog;
|
logs += startLog;
|
||||||
|
|
||||||
// 实时更新日志
|
// 实时更新日志
|
||||||
@@ -84,10 +81,10 @@ export class PipelineRunner {
|
|||||||
|
|
||||||
// 执行步骤
|
// 执行步骤
|
||||||
const stepLog = await this.executeStep(step, envVars);
|
const stepLog = await this.executeStep(step, envVars);
|
||||||
logs += stepLog;
|
logs += stepLog + '\n';
|
||||||
|
|
||||||
// 记录步骤执行完成的日志
|
// 记录步骤执行完成的日志
|
||||||
const endLog = this.addTimestamp(`${progress} 执行完成: ${step.name}`);
|
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
|
||||||
logs += endLog;
|
logs += endLog;
|
||||||
|
|
||||||
// 实时更新日志
|
// 实时更新日志
|
||||||
@@ -96,16 +93,9 @@ export class PipelineRunner {
|
|||||||
data: { buildLog: logs },
|
data: { buildLog: logs },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await prisma.deployment.update({
|
|
||||||
where: { id: this.deploymentId },
|
|
||||||
data: {
|
|
||||||
buildLog: logs,
|
|
||||||
status: 'success',
|
|
||||||
finishedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = this.addTimestamp(`${(error as Error).message}`);
|
hasError = true;
|
||||||
|
const errorMsg = `[${new Date().toISOString()}] Error: ${(error as Error).message}\n`;
|
||||||
logs += errorMsg;
|
logs += errorMsg;
|
||||||
|
|
||||||
log.error(
|
log.error(
|
||||||
@@ -126,6 +116,18 @@ export class PipelineRunner {
|
|||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新最终状态
|
||||||
|
if (!hasError) {
|
||||||
|
await prisma.deployment.update({
|
||||||
|
where: { id: this.deploymentId },
|
||||||
|
data: {
|
||||||
|
buildLog: logs,
|
||||||
|
status: 'success',
|
||||||
|
finishedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,20 +141,21 @@ export class PipelineRunner {
|
|||||||
branch: string,
|
branch: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
let logs = '';
|
let logs = '';
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logs += this.addTimestamp(`检查工作目录状态: ${this.projectDir}`);
|
logs += `[${timestamp}] 检查工作目录状态: ${this.projectDir}\n`;
|
||||||
|
|
||||||
// 检查工作目录状态
|
// 检查工作目录状态
|
||||||
const status = await GitManager.checkWorkspaceStatus(this.projectDir);
|
const status = await GitManager.checkWorkspaceStatus(this.projectDir);
|
||||||
logs += this.addTimestamp(`工作目录状态: ${status.status}`);
|
logs += `[${new Date().toISOString()}] 工作目录状态: ${status.status}\n`;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
status.status === WorkspaceDirStatus.NOT_CREATED ||
|
status.status === WorkspaceDirStatus.NOT_CREATED ||
|
||||||
status.status === WorkspaceDirStatus.EMPTY
|
status.status === WorkspaceDirStatus.EMPTY
|
||||||
) {
|
) {
|
||||||
// 目录不存在或为空,需要克隆
|
// 目录不存在或为空,需要克隆
|
||||||
logs += this.addTimestamp('工作目录不存在或为空,开始克隆仓库');
|
logs += `[${new Date().toISOString()}] 工作目录不存在或为空,开始克隆仓库\n`;
|
||||||
|
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
await GitManager.ensureDirectory(this.projectDir);
|
await GitManager.ensureDirectory(this.projectDir);
|
||||||
@@ -165,7 +168,7 @@ export class PipelineRunner {
|
|||||||
// TODO: 添加 token 支持
|
// TODO: 添加 token 支持
|
||||||
);
|
);
|
||||||
|
|
||||||
logs += this.addTimestamp('仓库克隆成功');
|
logs += `[${new Date().toISOString()}] 仓库克隆成功\n`;
|
||||||
} else if (status.status === WorkspaceDirStatus.NO_GIT) {
|
} else if (status.status === WorkspaceDirStatus.NO_GIT) {
|
||||||
// 目录存在但不是 Git 仓库
|
// 目录存在但不是 Git 仓库
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -173,16 +176,14 @@ export class PipelineRunner {
|
|||||||
);
|
);
|
||||||
} else if (status.status === WorkspaceDirStatus.READY) {
|
} else if (status.status === WorkspaceDirStatus.READY) {
|
||||||
// 已存在 Git 仓库,更新代码
|
// 已存在 Git 仓库,更新代码
|
||||||
logs += this.addTimestamp('工作目录已存在 Git 仓库,开始更新代码');
|
logs += `[${new Date().toISOString()}] 工作目录已存在 Git 仓库,开始更新代码\n`;
|
||||||
await GitManager.updateRepository(this.projectDir, branch);
|
await GitManager.updateRepository(this.projectDir, branch);
|
||||||
logs += this.addTimestamp('代码更新成功');
|
logs += `[${new Date().toISOString()}] 代码更新成功\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return logs;
|
return logs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorLog = this.addTimestamp(
|
const errorLog = `[${new Date().toISOString()}] 准备工作目录失败: ${(error as Error).message}\n`;
|
||||||
`准备工作目录失败: ${(error as Error).message}`,
|
|
||||||
);
|
|
||||||
logs += errorLog;
|
logs += errorLog;
|
||||||
log.error(
|
log.error(
|
||||||
this.TAG,
|
this.TAG,
|
||||||
@@ -214,18 +215,12 @@ export class PipelineRunner {
|
|||||||
envVars.BRANCH_NAME = deployment.branch || '';
|
envVars.BRANCH_NAME = deployment.branch || '';
|
||||||
envVars.COMMIT_HASH = deployment.commitHash || '';
|
envVars.COMMIT_HASH = deployment.commitHash || '';
|
||||||
|
|
||||||
// 注入用户配置的环境变量
|
// 稀疏检出路径(如果有配置的话)
|
||||||
if (deployment.envVars) {
|
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || '';
|
||||||
try {
|
|
||||||
const userEnvVars = JSON.parse(deployment.envVars);
|
|
||||||
Object.assign(envVars, userEnvVars);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(this.TAG, '解析环境变量失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工作空间路径(使用配置的项目目录)
|
// 工作空间路径(使用配置的项目目录)
|
||||||
envVars.WORKSPACE = this.projectDir;
|
envVars.WORKSPACE = this.projectDir;
|
||||||
|
envVars.PROJECT_DIR = this.projectDir;
|
||||||
|
|
||||||
return envVars;
|
return envVars;
|
||||||
}
|
}
|
||||||
@@ -236,9 +231,30 @@ export class PipelineRunner {
|
|||||||
* @param isError 是否为错误日志
|
* @param isError 是否为错误日志
|
||||||
* @returns 带时间戳的日志消息
|
* @returns 带时间戳的日志消息
|
||||||
*/
|
*/
|
||||||
private addTimestamp(message: string): string {
|
private addTimestamp(message: string, isError = false): string {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
return `[${timestamp}] [ERROR] ${message}\n`;
|
if (isError) {
|
||||||
|
return `[${timestamp}] [ERROR] ${message}`;
|
||||||
|
}
|
||||||
|
return `[${timestamp}] ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为多行日志添加时间戳前缀
|
||||||
|
* @param content 多行日志内容
|
||||||
|
* @param isError 是否为错误日志
|
||||||
|
* @returns 带时间戳的多行日志消息
|
||||||
|
*/
|
||||||
|
private addTimestampToLines(content: string, isError = false): string {
|
||||||
|
if (!content) return '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
content
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim() !== '')
|
||||||
|
.map((line) => this.addTimestamp(line, isError))
|
||||||
|
.join('\n') + '\n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,21 +268,35 @@ export class PipelineRunner {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
let logs = '';
|
let logs = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 添加步骤开始执行的时间戳
|
||||||
|
logs += this.addTimestamp(`执行脚本: ${step.script}`) + '\n';
|
||||||
|
|
||||||
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
// 使用zx执行脚本,设置项目目录为工作目录和环境变量
|
||||||
const script = step.script;
|
const script = step.script;
|
||||||
|
|
||||||
// bash -c 执行脚本,确保环境变量能被正确解析
|
// 通过bash -c执行脚本,确保环境变量能被正确解析
|
||||||
const result = await $({
|
const result = await $({
|
||||||
cwd: this.projectDir,
|
cwd: this.projectDir,
|
||||||
env: { ...process.env, ...envVars },
|
env: { ...process.env, ...envVars },
|
||||||
})`bash -c ${script}`;
|
})`bash -c ${script}`;
|
||||||
|
|
||||||
if (result.stdout) {
|
if (result.stdout) {
|
||||||
logs += this.addTimestamp(`\n${result.stdout}`);
|
// 为stdout中的每一行添加时间戳
|
||||||
|
logs += this.addTimestampToLines(result.stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.stderr) {
|
if (result.stderr) {
|
||||||
logs += this.addTimestamp(`\n${result.stderr}`);
|
// 为stderr中的每一行添加时间戳和错误标记
|
||||||
|
logs += this.addTimestampToLines(result.stderr, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
logs += this.addTimestamp(`步骤执行完成`) + '\n';
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `Error executing step "${step.name}": ${(error as Error).message}`;
|
||||||
|
logs += this.addTimestamp(errorMsg, true) + '\n';
|
||||||
|
log.error(this.TAG, errorMsg);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return logs;
|
return logs;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { pluginSvgr } from '@rsbuild/plugin-svgr';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [pluginReact(), pluginLess(), pluginSvgr()],
|
plugins: [pluginReact(), pluginLess(), pluginSvgr()],
|
||||||
html: {
|
html: {
|
||||||
title: 'Mini CI',
|
title: 'Foka CI',
|
||||||
},
|
},
|
||||||
source: {
|
source: {
|
||||||
define: {
|
define: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
export function useAsyncEffect(
|
export function useAsyncEffect(
|
||||||
effect: () => Promise<any | (() => void)>,
|
effect: () => Promise<void | (() => 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?.());
|
cleanupPromise.then(cleanup => cleanup && cleanup());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [callback]);
|
}, [callback]);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
import Env from '@pages/env';
|
||||||
import Home from '@pages/home';
|
import Home from '@pages/home';
|
||||||
import Login from '@pages/login';
|
import Login from '@pages/login';
|
||||||
import ProjectDetail from '@pages/project/detail';
|
import ProjectDetail from '@pages/project/detail';
|
||||||
@@ -13,7 +13,7 @@ const App = () => {
|
|||||||
<Route index element={<Navigate to="project" replace />} />
|
<Route index element={<Navigate to="project" replace />} />
|
||||||
<Route path="project" element={<ProjectList />} />
|
<Route path="project" element={<ProjectList />} />
|
||||||
<Route path="project/:id" element={<ProjectDetail />} />
|
<Route path="project/:id" element={<ProjectDetail />} />
|
||||||
|
<Route path="env" element={<Env />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
function Env() {
|
||||||
|
return <div>env page</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Env;
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
IconExport,
|
IconExport,
|
||||||
IconMenuFold,
|
IconMenuFold,
|
||||||
IconMenuUnfold,
|
IconMenuUnfold,
|
||||||
|
IconRobot,
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import Logo from '@assets/images/logo.svg?react';
|
import Logo from '@assets/images/logo.svg?react';
|
||||||
import { loginService } from '@pages/login/service';
|
import { loginService } from '@pages/login/service';
|
||||||
@@ -31,7 +31,7 @@ function Home() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-row items-center justify-center h-[56px]">
|
<div className="flex flex-row items-center justify-center h-[56px]">
|
||||||
<Logo />
|
<Logo />
|
||||||
{!collapsed && <h2 className="ml-4 text-xl font-medium">Mini CI</h2>}
|
{!collapsed && <h2 className="ml-4 text-xl font-medium">Foka CI</h2>}
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<Menu
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -45,7 +45,12 @@ function Home() {
|
|||||||
<span>项目管理</span>
|
<span>项目管理</span>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="1">
|
||||||
|
<Link to="/env">
|
||||||
|
<IconRobot fontSize={16} />
|
||||||
|
环境管理
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Layout.Sider>
|
</Layout.Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Message, Notification } from '@arco-design/web-react';
|
import { Message, Notification } from '@arco-design/web-react';
|
||||||
import { net } from '../../utils';
|
import { net } from '@shared';
|
||||||
import type { NavigateFunction } from 'react-router';
|
import type { NavigateFunction } from 'react-router';
|
||||||
import { useGlobalStore } from '../../stores/global';
|
import { useGlobalStore } from '../../stores/global';
|
||||||
import type { AuthURL, User } from './types';
|
import type { AuthLoginResponse, AuthURLResponse } from './types';
|
||||||
|
|
||||||
class LoginService {
|
class LoginService {
|
||||||
async getAuthUrl() {
|
async getAuthUrl() {
|
||||||
const { code, data } = await net.request<AuthURL>({
|
const { code, data } = await net.request<AuthURLResponse>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/url',
|
url: '/api/auth/url',
|
||||||
params: {
|
params: {
|
||||||
@@ -19,7 +19,7 @@ class LoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(authCode: string, navigate: NavigateFunction) {
|
async login(authCode: string, navigate: NavigateFunction) {
|
||||||
const { data, code } = await net.request<User>({
|
const { data, code } = await net.request<AuthLoginResponse>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/auth/login',
|
url: '/api/auth/login',
|
||||||
data: {
|
data: {
|
||||||
@@ -37,7 +37,7 @@ class LoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
const { code } = await net.request<null>({
|
const { code } = await net.request<AuthURLResponse>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/logout',
|
url: '/api/auth/logout',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { APIResponse } from '@shared';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -6,6 +8,8 @@ export interface User {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthURL {
|
export type AuthURLResponse = APIResponse<{
|
||||||
url: string;
|
url: string;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
|
export type AuthLoginResponse = APIResponse<User>;
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { Form, Input, Message, Modal, Select } from '@arco-design/web-react';
|
import {
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
import { formatDateTime } from '../../../../utils/time';
|
import { formatDateTime } from '../../../../utils/time';
|
||||||
import type { Branch, Commit, Pipeline, Project } from '../../types';
|
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import type { Branch, Commit, Pipeline } from '../../types';
|
||||||
import { detailService } from '../service';
|
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({
|
||||||
@@ -27,29 +26,12 @@ 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) => {
|
||||||
@@ -109,27 +91,16 @@ 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(
|
const selectedPipeline = pipelines.find((p) => p.id === values.pipelineId);
|
||||||
(p) => p.id === values.pipelineId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedCommit || !selectedPipeline) {
|
if (!selectedCommit || !selectedPipeline) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集所有环境变量(从预设项中提取)
|
// 格式化环境变量
|
||||||
const envVars: Record<string, string> = {};
|
const env = values.envVars
|
||||||
for (const preset of envPresets) {
|
?.map((item: { key: string; value: string }) => `${item.key}=${item.value}`)
|
||||||
const value = values[preset.key];
|
.join('\n');
|
||||||
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,
|
||||||
@@ -137,7 +108,8 @@ function DeployModal({
|
|||||||
branch: values.branch,
|
branch: values.branch,
|
||||||
commitHash: selectedCommit.sha,
|
commitHash: selectedCommit.sha,
|
||||||
commitMessage: selectedCommit.commit.message,
|
commitMessage: selectedCommit.commit.message,
|
||||||
envVars, // 提交所有环境变量
|
env: env,
|
||||||
|
sparseCheckoutPaths: values.sparseCheckoutPaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
Message.success('部署任务已创建');
|
Message.success('部署任务已创建');
|
||||||
@@ -156,15 +128,8 @@ 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">
|
||||||
{/* 基本参数 */}
|
|
||||||
<div className="mb-4 pb-4 border-b border-gray-200">
|
|
||||||
<div className="text-sm font-semibold text-gray-700 mb-3">
|
|
||||||
基本参数
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="选择流水线"
|
label="选择流水线"
|
||||||
field="pipelineId"
|
field="pipelineId"
|
||||||
@@ -232,86 +197,57 @@ function DeployModal({
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 环境变量预设 */}
|
<Form.Item
|
||||||
{envPresets.length > 0 && (
|
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>
|
<div>
|
||||||
<div className="text-sm font-semibold text-gray-700 mb-3">
|
{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>
|
</div>
|
||||||
{envPresets.map((preset) => {
|
|
||||||
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>
|
<Button
|
||||||
</Form.Item>
|
type="dashed"
|
||||||
);
|
long
|
||||||
}
|
onClick={() => add()}
|
||||||
|
icon={<IconPlus />}
|
||||||
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"
|
</Button>
|
||||||
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Form.List>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,17 @@ 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}
|
||||||
@@ -57,6 +68,9 @@ 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>
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Message,
|
Message,
|
||||||
Modal,
|
Modal,
|
||||||
Pagination,
|
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
IconCode,
|
IconCode,
|
||||||
IconCommand,
|
|
||||||
IconCopy,
|
IconCopy,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
@@ -51,12 +49,9 @@ 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 } from '../types';
|
import type { Deployment, Pipeline, Project, Step, WorkspaceDirStatus, WorkspaceStatus } from '../types';
|
||||||
import DeployModal from './components/DeployModal';
|
import DeployModal from './components/DeployModal';
|
||||||
import DeployRecordItem from './components/DeployRecordItem';
|
import DeployRecordItem from './components/DeployRecordItem';
|
||||||
import EnvPresetsEditor, {
|
|
||||||
type EnvPreset,
|
|
||||||
} from './components/EnvPresetsEditor';
|
|
||||||
import PipelineStepItem from './components/PipelineStepItem';
|
import PipelineStepItem from './components/PipelineStepItem';
|
||||||
import { detailService } from './service';
|
import { detailService } from './service';
|
||||||
|
|
||||||
@@ -89,32 +84,20 @@ function ProjectDetailPage() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
||||||
const [editingPipeline, setEditingPipeline] =
|
const [editingPipeline, setEditingPipeline] = useState<PipelineWithEnabled | null>(null);
|
||||||
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[]>([]);
|
||||||
const [deployModalVisible, setDeployModalVisible] = useState(false);
|
const [deployModalVisible, setDeployModalVisible] = useState(false);
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 流水线模板相关状态
|
// 流水线模板相关状态
|
||||||
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(
|
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
||||||
null,
|
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
||||||
);
|
|
||||||
const [templates, setTemplates] = useState<
|
|
||||||
Array<{ id: number; name: string; description: string }>
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
// 项目设置相关状态
|
// 项目设置相关状态
|
||||||
const [isEditingProject, setIsEditingProject] = useState(false);
|
const [projectEditModalVisible, setProjectEditModalVisible] = 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();
|
||||||
|
|
||||||
@@ -159,15 +142,10 @@ function ProjectDetailPage() {
|
|||||||
|
|
||||||
// 获取部署记录
|
// 获取部署记录
|
||||||
try {
|
try {
|
||||||
const res = await detailService.getDeployments(
|
const records = await detailService.getDeployments(Number(id));
|
||||||
Number(id),
|
setDeployRecords(records);
|
||||||
1,
|
if (records.length > 0) {
|
||||||
pagination.pageSize,
|
setSelectedRecordId(records[0].id);
|
||||||
);
|
|
||||||
setDeployRecords(res.list);
|
|
||||||
setPagination((prev) => ({ ...prev, total: res.total, current: 1 }));
|
|
||||||
if (res.list.length > 0) {
|
|
||||||
setSelectedRecordId(res.list[0].id);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取部署记录失败:', error);
|
console.error('获取部署记录失败:', error);
|
||||||
@@ -186,40 +164,26 @@ function ProjectDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 定期轮询部署记录以更新状态和日志
|
// 定期轮询部署记录以更新状态和日志
|
||||||
useEffect(() => {
|
useAsyncEffect(async () => {
|
||||||
if (!id) return;
|
const interval = setInterval(async () => {
|
||||||
|
if (id) {
|
||||||
const poll = async () => {
|
|
||||||
try {
|
try {
|
||||||
const res = await detailService.getDeployments(
|
const records = await detailService.getDeployments(Number(id));
|
||||||
Number(id),
|
setDeployRecords(records);
|
||||||
pagination.current,
|
|
||||||
pagination.pageSize,
|
|
||||||
);
|
|
||||||
setDeployRecords(res.list);
|
|
||||||
setPagination((prev) => ({ ...prev, total: res.total }));
|
|
||||||
|
|
||||||
// 如果当前选中的记录正在运行,则更新选中记录
|
// 如果当前选中的记录正在运行,则更新选中记录
|
||||||
const selectedRecord = res.list.find(
|
const selectedRecord = records.find((r: Deployment) => r.id === selectedRecordId);
|
||||||
(r: Deployment) => r.id === selectedRecordId,
|
if (selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending')) {
|
||||||
);
|
|
||||||
if (
|
|
||||||
selectedRecord &&
|
|
||||||
(selectedRecord.status === 'running' ||
|
|
||||||
selectedRecord.status === 'pending')
|
|
||||||
) {
|
|
||||||
// 保持当前选中状态,但更新数据
|
// 保持当前选中状态,但更新数据
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('轮询部署记录失败:', error);
|
console.error('轮询部署记录失败:', error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}, 3000); // 每3秒轮询一次
|
||||||
poll(); // 立即执行一次
|
|
||||||
const interval = setInterval(poll, 3000); // 每3秒轮询一次
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [id, selectedRecordId, pagination.current, pagination.pageSize]);
|
}, [id, selectedRecordId]);
|
||||||
|
|
||||||
// 触发部署
|
// 触发部署
|
||||||
const handleDeploy = () => {
|
const handleDeploy = () => {
|
||||||
@@ -390,14 +354,13 @@ function ProjectDetailPage() {
|
|||||||
selectedTemplateId,
|
selectedTemplateId,
|
||||||
Number(id),
|
Number(id),
|
||||||
values.name,
|
values.name,
|
||||||
values.description || '',
|
values.description || ''
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新本地状态 - 需要转换步骤数据结构
|
// 更新本地状态 - 需要转换步骤数据结构
|
||||||
const transformedSteps =
|
const transformedSteps = newPipeline.steps?.map(step => ({
|
||||||
newPipeline.steps?.map((step) => ({
|
|
||||||
...step,
|
...step,
|
||||||
enabled: step.valid === 1,
|
enabled: step.valid === 1
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const pipelineWithDefaults = {
|
const pipelineWithDefaults = {
|
||||||
@@ -620,13 +583,8 @@ function ProjectDetailPage() {
|
|||||||
|
|
||||||
// 刷新部署记录
|
// 刷新部署记录
|
||||||
if (id) {
|
if (id) {
|
||||||
const res = await detailService.getDeployments(
|
const records = await detailService.getDeployments(Number(id));
|
||||||
Number(id),
|
setDeployRecords(records);
|
||||||
pagination.current,
|
|
||||||
pagination.pageSize,
|
|
||||||
);
|
|
||||||
setDeployRecords(res.list);
|
|
||||||
setPagination((prev) => ({ ...prev, total: res.total }));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重新执行部署失败:', error);
|
console.error('重新执行部署失败:', error);
|
||||||
@@ -634,21 +592,6 @@ 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) {
|
||||||
@@ -657,21 +600,16 @@ function ProjectDetailPage() {
|
|||||||
description: detail.description,
|
description: detail.description,
|
||||||
repository: detail.repository,
|
repository: detail.repository,
|
||||||
});
|
});
|
||||||
setIsEditingProject(true);
|
setProjectEditModalVisible(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelEditProject = () => {
|
const handleProjectEditSuccess = async () => {
|
||||||
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('项目更新成功');
|
||||||
setIsEditingProject(false);
|
setProjectEditModalVisible(false);
|
||||||
|
|
||||||
// 刷新项目详情
|
// 刷新项目详情
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -684,27 +622,6 @@ 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: '删除项目',
|
||||||
@@ -749,13 +666,26 @@ function ProjectDetailPage() {
|
|||||||
item={item}
|
item={item}
|
||||||
isSelected={selectedRecordId === item.id}
|
isSelected={selectedRecordId === item.id}
|
||||||
onSelect={setSelectedRecordId}
|
onSelect={setSelectedRecordId}
|
||||||
|
onRetry={handleRetryDeployment} // 传递重新执行函数
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 获取选中的流水线
|
||||||
|
const selectedPipeline = pipelines.find(
|
||||||
|
(pipeline) => pipeline.id === selectedPipelineId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
// 获取工作目录状态标签
|
// 获取工作目录状态标签
|
||||||
const getWorkspaceStatusTag = (
|
const getWorkspaceStatusTag = (status: string): { text: string; color: string } => {
|
||||||
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' },
|
||||||
@@ -773,15 +703,7 @@ function ProjectDetailPage() {
|
|||||||
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card className="mb-6" title={<Space><IconFolder />工作目录状态</Space>}>
|
||||||
className="mb-6"
|
|
||||||
title={
|
|
||||||
<Space>
|
|
||||||
<IconFolder />
|
|
||||||
工作目录状态
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Descriptions
|
<Descriptions
|
||||||
column={2}
|
column={2}
|
||||||
data={[
|
data={[
|
||||||
@@ -793,6 +715,10 @@ function ProjectDetailPage() {
|
|||||||
label: '状态',
|
label: '状态',
|
||||||
value: <Tag color={statusInfo.color}>{statusInfo.text}</Tag>,
|
value: <Tag color={statusInfo.color}>{statusInfo.text}</Tag>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '目录大小',
|
||||||
|
value: workspaceStatus.size ? formatSize(workspaceStatus.size) : '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '当前分支',
|
label: '当前分支',
|
||||||
value: workspaceStatus.gitInfo?.branch || '-',
|
value: workspaceStatus.gitInfo?.branch || '-',
|
||||||
@@ -800,25 +726,17 @@ function ProjectDetailPage() {
|
|||||||
{
|
{
|
||||||
label: '最后提交',
|
label: '最后提交',
|
||||||
value: workspaceStatus.gitInfo?.lastCommit ? (
|
value: workspaceStatus.gitInfo?.lastCommit ? (
|
||||||
<Space size="small">
|
<Space direction="vertical" size="mini">
|
||||||
<Typography.Text code>
|
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text>
|
||||||
{workspaceStatus.gitInfo.lastCommit}
|
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text>
|
||||||
</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="error">
|
<Typography.Text type="danger">{workspaceStatus.error}</Typography.Text>
|
||||||
{workspaceStatus.error}
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -839,26 +757,17 @@ function ProjectDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-md flex-1 overflow-hidden">
|
<div className="bg-white p-6 rounded-lg shadow-md flex-1 flex flex-col overflow-hidden">
|
||||||
<Tabs
|
<Tabs
|
||||||
type="line"
|
type="line"
|
||||||
size="large"
|
size="large"
|
||||||
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
||||||
>
|
>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane title={<Space><IconHistory />部署记录</Space>} key="deployRecords">
|
||||||
title={
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
<Space>
|
|
||||||
<IconHistory />
|
|
||||||
部署记录
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
className="h-full"
|
|
||||||
key="deployRecords"
|
|
||||||
>
|
|
||||||
<div className="flex flex-row gap-6 h-full">
|
|
||||||
{/* 左侧部署记录列表 */}
|
{/* 左侧部署记录列表 */}
|
||||||
<div className="w-150 h-full flex flex-col">
|
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between py-3">
|
<div className="flex items-center justify-between shrink-0">
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
共 {deployRecords.length} 条部署记录
|
共 {deployRecords.length} 条部署记录
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@@ -866,7 +775,7 @@ function ProjectDetailPage() {
|
|||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
{deployRecords.length > 0 ? (
|
{deployRecords.length > 0 ? (
|
||||||
<List
|
<List
|
||||||
className="bg-white rounded-lg border"
|
className="bg-white rounded-lg border"
|
||||||
@@ -880,22 +789,10 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 flex flex-row justify-end">
|
|
||||||
<Pagination
|
|
||||||
total={pagination.total}
|
|
||||||
current={pagination.current}
|
|
||||||
pageSize={pagination.pageSize}
|
|
||||||
showTotal
|
|
||||||
size="default"
|
|
||||||
onChange={(page) =>
|
|
||||||
setPagination((prev) => ({ ...prev, current: page }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧构建日志 */}
|
{/* 右侧构建日志 */}
|
||||||
<div className="flex-1 bg-white rounded-lg border h-full overflow-hidden flex flex-col">
|
<div className="col-span-3 bg-white rounded-lg border h-full overflow-hidden flex flex-col">
|
||||||
<div className="p-4 border-b bg-gray-50 shrink-0">
|
<div className="p-4 border-b bg-gray-50 shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -904,7 +801,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.branch} · {selectedRecord.env} ·{' '}
|
||||||
{formatDateTime(selectedRecord.createdAt)}
|
{formatDateTime(selectedRecord.createdAt)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
@@ -916,9 +813,7 @@ function ProjectDetailPage() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() => handleRetryDeployment(selectedRecord.id)}
|
||||||
handleRetryDeployment(selectedRecord.id)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
重新执行
|
重新执行
|
||||||
</Button>
|
</Button>
|
||||||
@@ -943,15 +838,7 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane title={<Space><IconCode />流水线</Space>} key="pipeline">
|
||||||
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">
|
||||||
@@ -1064,7 +951,9 @@ 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>{pipeline.steps?.length || 0} 个步骤</span>
|
<span>
|
||||||
|
{pipeline.steps?.length || 0} 个步骤
|
||||||
|
</span>
|
||||||
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1116,11 +1005,7 @@ function ProjectDetailPage() {
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={
|
items={selectedPipeline.steps?.map(step => step.id) || []}
|
||||||
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">
|
||||||
@@ -1161,19 +1046,9 @@ function ProjectDetailPage() {
|
|||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|
||||||
{/* 项目设置标签页 */}
|
{/* 项目设置标签页 */}
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane key="settings" title={<Space><IconSettings />项目设置</Space>}>
|
||||||
key="settings"
|
|
||||||
title={
|
|
||||||
<Space>
|
|
||||||
<IconSettings />
|
|
||||||
项目设置
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<Card title="项目信息" className="mb-4">
|
<Card title="项目信息" className="mb-4">
|
||||||
{!isEditingProject ? (
|
|
||||||
<>
|
|
||||||
<Descriptions
|
<Descriptions
|
||||||
column={1}
|
column={1}
|
||||||
data={[
|
data={[
|
||||||
@@ -1207,94 +1082,12 @@ function ProjectDetailPage() {
|
|||||||
删除项目
|
删除项目
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
@@ -1346,9 +1139,7 @@ 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">
|
<div className="text-xs text-gray-500">{template.description}</div>
|
||||||
{template.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
@@ -1364,7 +1155,10 @@ function ProjectDetailPage() {
|
|||||||
>
|
>
|
||||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item field="description" label="流水线描述">
|
<Form.Item
|
||||||
|
field="description"
|
||||||
|
label="流水线描述"
|
||||||
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="描述这个流水线的用途和特点..."
|
placeholder="描述这个流水线的用途和特点..."
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -1382,7 +1176,10 @@ function ProjectDetailPage() {
|
|||||||
>
|
>
|
||||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item field="description" label="流水线描述">
|
<Form.Item
|
||||||
|
field="description"
|
||||||
|
label="流水线描述"
|
||||||
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="描述这个流水线的用途和特点..."
|
placeholder="描述这个流水线的用途和特点..."
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -1420,7 +1217,55 @@ function ProjectDetailPage() {
|
|||||||
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<div className="bg-blue-50 p-3 rounded text-sm">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
<strong>可用环境变量:</strong>
|
||||||
|
<br />• $PROJECT_NAME - 项目名称
|
||||||
|
<br />• $BUILD_NUMBER - 构建编号
|
||||||
|
<br />• $REGISTRY - 镜像仓库地址
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</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>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -1431,24 +1276,16 @@ function ProjectDetailPage() {
|
|||||||
setDeployModalVisible(false);
|
setDeployModalVisible(false);
|
||||||
// 刷新部署记录
|
// 刷新部署记录
|
||||||
if (id) {
|
if (id) {
|
||||||
detailService
|
detailService.getDeployments(Number(id)).then((records) => {
|
||||||
.getDeployments(Number(id), 1, pagination.pageSize)
|
setDeployRecords(records);
|
||||||
.then((res) => {
|
if (records.length > 0) {
|
||||||
setDeployRecords(res.list);
|
setSelectedRecordId(records[0].id);
|
||||||
setPagination((prev) => ({
|
|
||||||
...prev,
|
|
||||||
total: res.total,
|
|
||||||
current: 1,
|
|
||||||
}));
|
|
||||||
if (res.list.length > 0) {
|
|
||||||
setSelectedRecordId(res.list[0].id);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
pipelines={pipelines}
|
pipelines={pipelines}
|
||||||
projectId={Number(id)}
|
projectId={Number(id)}
|
||||||
project={detail}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import { net } from '../../../utils';
|
import { type APIResponse, net } from '@shared';
|
||||||
import type {
|
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types';
|
||||||
Branch,
|
|
||||||
Commit,
|
|
||||||
CreateDeploymentRequest,
|
|
||||||
Deployment,
|
|
||||||
Pipeline,
|
|
||||||
Project,
|
|
||||||
Step,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
class DetailService {
|
class DetailService {
|
||||||
async getProject(id: string) {
|
async getProject(id: string) {
|
||||||
const { data } = await net.request<Project>({
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -19,32 +11,26 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目的所有流水线
|
// 获取项目的所有流水线
|
||||||
async getPipelines(projectId: number) {
|
async getPipelines(projectId: number) {
|
||||||
const { data } = await net.request<Pipeline[] | { list: Pipeline[] }>({
|
const { data } = await net.request<APIResponse<Pipeline[]>>({
|
||||||
url: `/api/pipelines?projectId=${projectId}`,
|
url: `/api/pipelines?projectId=${projectId}`,
|
||||||
});
|
});
|
||||||
return Array.isArray(data) ? data : data.list;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取可用的流水线模板
|
// 获取可用的流水线模板
|
||||||
async getPipelineTemplates() {
|
async getPipelineTemplates() {
|
||||||
const { data } = await net.request<
|
const { data } = await net.request<APIResponse<{id: number, name: string, description: string}[]>>({
|
||||||
| { id: number; name: string; description: string }[]
|
|
||||||
| { list: { id: number; name: string; description: string }[] }
|
|
||||||
>({
|
|
||||||
url: '/api/pipelines/templates',
|
url: '/api/pipelines/templates',
|
||||||
});
|
});
|
||||||
return Array.isArray(data) ? data : data.list;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployments(
|
// 获取项目的部署记录
|
||||||
projectId: number,
|
async getDeployments(projectId: number) {
|
||||||
page: number = 1,
|
const { data } = await net.request<any>({
|
||||||
pageSize: number = 10,
|
url: `/api/deployments?projectId=${projectId}`,
|
||||||
) {
|
|
||||||
const { data } = await net.request<DeploymentListResponse>({
|
|
||||||
url: `/api/deployments?projectId=${projectId}&page=${page}&pageSize=${pageSize}`,
|
|
||||||
});
|
});
|
||||||
return data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建流水线
|
// 创建流水线
|
||||||
@@ -60,7 +46,7 @@ class DetailService {
|
|||||||
| 'steps'
|
| 'steps'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<Pipeline>({
|
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||||
url: '/api/pipelines',
|
url: '/api/pipelines',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: pipeline,
|
data: pipeline,
|
||||||
@@ -73,16 +59,16 @@ class DetailService {
|
|||||||
templateId: number,
|
templateId: number,
|
||||||
projectId: number,
|
projectId: number,
|
||||||
name: string,
|
name: string,
|
||||||
description?: string,
|
description?: string
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<Pipeline>({
|
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||||
url: '/api/pipelines/from-template',
|
url: '/api/pipelines/from-template',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
templateId,
|
templateId,
|
||||||
projectId,
|
projectId,
|
||||||
name,
|
name,
|
||||||
description,
|
description
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -104,7 +90,7 @@ class DetailService {
|
|||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<Pipeline>({
|
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||||
url: `/api/pipelines/${id}`,
|
url: `/api/pipelines/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: pipeline,
|
data: pipeline,
|
||||||
@@ -114,7 +100,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 删除流水线
|
// 删除流水线
|
||||||
async deletePipeline(id: number) {
|
async deletePipeline(id: number) {
|
||||||
const { data } = await net.request<null>({
|
const { data } = await net.request<APIResponse<null>>({
|
||||||
url: `/api/pipelines/${id}`,
|
url: `/api/pipelines/${id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -123,10 +109,10 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取流水线的所有步骤
|
// 获取流水线的所有步骤
|
||||||
async getSteps(pipelineId: number) {
|
async getSteps(pipelineId: number) {
|
||||||
const { data } = await net.request<Step[] | { list: Step[] }>({
|
const { data } = await net.request<APIResponse<Step[]>>({
|
||||||
url: `/api/steps?pipelineId=${pipelineId}`,
|
url: `/api/steps?pipelineId=${pipelineId}`,
|
||||||
});
|
});
|
||||||
return Array.isArray(data) ? data : data.list;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建步骤
|
// 创建步骤
|
||||||
@@ -136,7 +122,7 @@ class DetailService {
|
|||||||
'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid'
|
'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<Step>({
|
const { data } = await net.request<APIResponse<Step>>({
|
||||||
url: '/api/steps',
|
url: '/api/steps',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: step,
|
data: step,
|
||||||
@@ -154,7 +140,7 @@ class DetailService {
|
|||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<Step>({
|
const { data } = await net.request<APIResponse<Step>>({
|
||||||
url: `/api/steps/${id}`,
|
url: `/api/steps/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: step,
|
data: step,
|
||||||
@@ -165,7 +151,7 @@ class DetailService {
|
|||||||
// 删除步骤
|
// 删除步骤
|
||||||
async deleteStep(id: number) {
|
async deleteStep(id: number) {
|
||||||
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
||||||
const { data } = await net.request<null>({
|
const { data } = await net.request<APIResponse<null>>({
|
||||||
url: `/api/steps/${id}`,
|
url: `/api/steps/${id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -174,23 +160,23 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目的提交记录
|
// 获取项目的提交记录
|
||||||
async getCommits(projectId: number, branch?: string) {
|
async getCommits(projectId: number, branch?: string) {
|
||||||
const { data } = await net.request<Commit[] | { list: Commit[] }>({
|
const { data } = await net.request<APIResponse<Commit[]>>({
|
||||||
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
||||||
});
|
});
|
||||||
return Array.isArray(data) ? data : data.list;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取项目的分支列表
|
// 获取项目的分支列表
|
||||||
async getBranches(projectId: number) {
|
async getBranches(projectId: number) {
|
||||||
const { data } = await net.request<Branch[] | { list: Branch[] }>({
|
const { data } = await net.request<APIResponse<Branch[]>>({
|
||||||
url: `/api/git/branches?projectId=${projectId}`,
|
url: `/api/git/branches?projectId=${projectId}`,
|
||||||
});
|
});
|
||||||
return Array.isArray(data) ? data : data.list;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建部署
|
// 创建部署
|
||||||
async createDeployment(deployment: CreateDeploymentRequest) {
|
async createDeployment(deployment: CreateDeploymentRequest) {
|
||||||
const { data } = await net.request<Deployment>({
|
const { data } = await net.request<APIResponse<Deployment>>({
|
||||||
url: '/api/deployments',
|
url: '/api/deployments',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: deployment,
|
data: deployment,
|
||||||
@@ -200,7 +186,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 重新执行部署
|
// 重新执行部署
|
||||||
async retryDeployment(deploymentId: number) {
|
async retryDeployment(deploymentId: number) {
|
||||||
const { data } = await net.request<Deployment>({
|
const { data } = await net.request<APIResponse<Deployment>>({
|
||||||
url: `/api/deployments/${deploymentId}/retry`,
|
url: `/api/deployments/${deploymentId}/retry`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
@@ -209,15 +195,18 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目详情(包含工作目录状态)
|
// 获取项目详情(包含工作目录状态)
|
||||||
async getProjectDetail(id: number) {
|
async getProjectDetail(id: number) {
|
||||||
const { data } = await net.request<Project>({
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新项目
|
// 更新项目
|
||||||
async updateProject(id: number, project: Partial<Project>) {
|
async updateProject(
|
||||||
const { data } = await net.request<Project>({
|
id: number,
|
||||||
|
project: Partial<{ name: string; description: string; repository: string }>,
|
||||||
|
) {
|
||||||
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: project,
|
data: project,
|
||||||
@@ -235,10 +224,3 @@ class DetailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const detailService = new DetailService();
|
export const detailService = new DetailService();
|
||||||
|
|
||||||
export interface DeploymentListResponse {
|
|
||||||
list: Deployment[];
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Project } from '../../types';
|
|
||||||
import { projectService } from '../service';
|
import { projectService } from '../service';
|
||||||
|
import type { Project } from '../../types';
|
||||||
|
|
||||||
interface CreateProjectModalProps {
|
interface CreateProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -114,9 +114,7 @@ function CreateProjectModal({
|
|||||||
if (value.includes('..') || value.includes('~')) {
|
if (value.includes('..') || value.includes('~')) {
|
||||||
return cb('不能包含路径遍历字符(.. 或 ~)');
|
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||||
}
|
}
|
||||||
// 检查非法字符(控制字符 0x00-0x1F)
|
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
||||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: 需要检测路径中的控制字符
|
|
||||||
if (/[<>:"|?*\u0000-\u001f]/.test(value)) {
|
|
||||||
return cb('路径包含非法字符');
|
return cb('路径包含非法字符');
|
||||||
}
|
}
|
||||||
cb();
|
cb();
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import {
|
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||||
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;
|
||||||
@@ -32,20 +22,10 @@ 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]);
|
||||||
@@ -57,18 +37,7 @@ 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);
|
||||||
@@ -142,14 +111,6 @@ 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Space,
|
Space,
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
@@ -59,7 +58,7 @@ function ProjectCard({ project }: ProjectCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="!rounded-xl border border-gray-200 h-[280px] cursor-pointer"
|
className="foka-card !rounded-xl border border-gray-200 h-[280px] cursor-pointer"
|
||||||
hoverable
|
hoverable
|
||||||
bodyStyle={{ padding: '20px' }}
|
bodyStyle={{ padding: '20px' }}
|
||||||
onClick={onProjectClick}
|
onClick={onProjectClick}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Grid, Typography } from '@arco-design/web-react';
|
import { Button, Grid, Message, Typography } from '@arco-design/web-react';
|
||||||
import { IconPlus } from '@arco-design/web-react/icon';
|
import { IconPlus } from '@arco-design/web-react/icon';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
@@ -15,7 +15,7 @@ function ProjectPage() {
|
|||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
const response = await projectService.list();
|
const response = await projectService.list();
|
||||||
setProjects(response.list);
|
setProjects(response.data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateProject = () => {
|
const handleCreateProject = () => {
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { net } from '../../../utils';
|
import { type APIResponse, net } from '@shared';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../types';
|
||||||
|
|
||||||
class ProjectService {
|
class ProjectService {
|
||||||
async list(params?: ProjectQueryParams) {
|
async list(params?: ProjectQueryParams) {
|
||||||
const { data } = await net.request<Project[] | ProjectListResponse>({
|
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/projects',
|
url: '/api/projects',
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
return Array.isArray(data)
|
return data;
|
||||||
? { list: data, page: 1, pageSize: data.length, total: data.length }
|
|
||||||
: data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async show(id: string) {
|
async show(id: string) {
|
||||||
const { data } = await net.request<Project>({
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
@@ -26,7 +24,7 @@ class ProjectService {
|
|||||||
description?: string;
|
description?: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
}) {
|
}) {
|
||||||
const { data } = await net.request<Project>({
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/projects',
|
url: '/api/projects',
|
||||||
data: project,
|
data: project,
|
||||||
@@ -38,7 +36,7 @@ class ProjectService {
|
|||||||
id: string,
|
id: string,
|
||||||
project: Partial<{ name: string; description: string; repository: string }>,
|
project: Partial<{ name: string; description: string; repository: string }>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<Project>({
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
data: project,
|
data: project,
|
||||||
@@ -58,14 +56,17 @@ class ProjectService {
|
|||||||
export const projectService = new ProjectService();
|
export const projectService = new ProjectService();
|
||||||
|
|
||||||
interface ProjectListResponse {
|
interface ProjectListResponse {
|
||||||
list: Project[];
|
data: Project[];
|
||||||
|
pagination: {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectQueryParams {
|
interface ProjectQueryParams {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
limit?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ 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;
|
||||||
@@ -78,11 +77,12 @@ export interface Pipeline {
|
|||||||
export interface Deployment {
|
export interface Deployment {
|
||||||
id: number;
|
id: number;
|
||||||
branch: string;
|
branch: string;
|
||||||
envVars?: string; // JSON 字符串
|
env?: string;
|
||||||
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,5 +127,6 @@ export interface CreateDeploymentRequest {
|
|||||||
branch: string;
|
branch: string;
|
||||||
commitHash: string;
|
commitHash: string;
|
||||||
commitMessage: string;
|
commitMessage: string;
|
||||||
envVars?: Record<string, string>; // 环境变量 key-value 对象
|
env?: string;
|
||||||
|
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ class Net {
|
|||||||
(error) => {
|
(error) => {
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
// 对于DELETE请求返回204状态码的情况,视为成功
|
// 对于DELETE请求返回204状态码的情况,视为成功
|
||||||
if (
|
if (error.response && error.response.status === 204 && error.config.method === 'delete') {
|
||||||
error.response &&
|
|
||||||
error.response.status === 204 &&
|
|
||||||
error.config.method === 'delete'
|
|
||||||
) {
|
|
||||||
// 创建一个模拟的成功响应
|
// 创建一个模拟的成功响应
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
...error.response,
|
...error.response,
|
||||||
@@ -42,9 +38,9 @@ class Net {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async request<T>(config: AxiosRequestConfig): Promise<APIResponse<T>> {
|
async request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const response = await this.instance.request<APIResponse<T>>(config);
|
const response = await this.instance.request<T>(config);
|
||||||
if (!response || !response.data) {
|
if (!response || !response.data) {
|
||||||
throw new Error('Invalid response');
|
throw new Error('Invalid response');
|
||||||
}
|
}
|
||||||
@@ -56,13 +52,6 @@ class Net {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIPagination<T> {
|
|
||||||
list: T[];
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APIResponse<T> {
|
export interface APIResponse<T> {
|
||||||
code: number;
|
code: number;
|
||||||
data: T;
|
data: T;
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { net } from '@utils';
|
import { type APIResponse, net } from '@shared';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { GlobalStore } from './types';
|
|
||||||
import type { User } from '@pages/login/types';
|
interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
avatar_url: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalStore {
|
||||||
|
user: User | null;
|
||||||
|
setUser: (user: User) => void;
|
||||||
|
refreshUser: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export const useGlobalStore = create<GlobalStore>((set) => ({
|
export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||||
user: null,
|
user: null,
|
||||||
setUser: (user: User) => set({ user }),
|
setUser: (user: User) => set({ user }),
|
||||||
async refreshUser() {
|
async refreshUser() {
|
||||||
const { data } = await net.request<User>({
|
const { data } = await net.request<APIResponse<User>>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/info',
|
url: '/api/auth/info',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
avatar_url: string;
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GlobalStore {
|
|
||||||
user: User | null;
|
|
||||||
setUser: (user: User) => void;
|
|
||||||
refreshUser: () => Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -24,8 +24,7 @@
|
|||||||
"@pages/*": ["./src/pages/*"],
|
"@pages/*": ["./src/pages/*"],
|
||||||
"@styles/*": ["./src/styles/*"],
|
"@styles/*": ["./src/styles/*"],
|
||||||
"@assets/*": ["./src/assets/*"],
|
"@assets/*": ["./src/assets/*"],
|
||||||
"@utils/*": ["./src/utils/*"],
|
"@shared": ["./src/shared"]
|
||||||
"@utils": ["./src/utils"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# MiniCI 文档中心
|
|
||||||
|
|
||||||
欢迎查阅 MiniCI 开发者文档。本项目是一个轻量级的自研 CI 系统。
|
|
||||||
|
|
||||||
## 目录索引
|
|
||||||
|
|
||||||
- [系统架构](./architecture.md) - 核心设计与模块划分
|
|
||||||
- [决策记录 (ADR)](./decisions/0001-tech-stack.md) - 技术选型背后的逻辑
|
|
||||||
- [约束与禁区](./constraints.md) - 开发中不可触碰的红线
|
|
||||||
- [编码规范](./conventions.md) - 风格与最佳实践
|
|
||||||
- [踩坑指南](./pitfalls.md) - 已解决的问题与易错点
|
|
||||||
- [当前进度](./status.md) - 项目现状与待办
|
|
||||||
- [AI 助手说明](./ai.md) - 专门为 coding agent 准备的作业指南
|
|
||||||
|
|
||||||
## 快速上手
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
项目访问:前端 `localhost:3000` (Rsbuild),后端 `localhost:3001` (Koa)。
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
# AI 助手作业指南 (ai.md)
|
|
||||||
|
|
||||||
你好,Agent!在处理 MiniCI 项目时,请遵循以下原则:
|
|
||||||
|
|
||||||
## 1. 增加新 API 的步骤
|
|
||||||
|
|
||||||
1. 在 `controllers/` 对应模块下创建/修改 `dto.ts` 定义输入。
|
|
||||||
2. 在 `index.ts` 中编写类,使用 `@Controller` 和 `@Post/Get` 等装饰器。
|
|
||||||
3. 如果涉及数据库,修改 `schema.prisma` 并运行 `npx prisma db push`。
|
|
||||||
|
|
||||||
## 2. 核心逻辑位置
|
|
||||||
|
|
||||||
- 如果要修改 **流水线如何运行**,请看 `apps/server/runners/pipeline-runner.ts`。
|
|
||||||
- 如果要修改 **任务调度**,请看 `apps/server/libs/execution-queue.ts`。
|
|
||||||
- 如果要修改 **路由扫描**,请看 `apps/server/libs/route-scanner.ts`。
|
|
||||||
|
|
||||||
## 3. 交互规范
|
|
||||||
|
|
||||||
- 前端 `import` 代码优先使用路径别名,例如:`import {net} from '@utils'`。
|
|
||||||
- 始终保持代码简洁,优先使用现有的 `libs` 工具类。
|
|
||||||
- 修改代码后,务必确认 `pnpm dev` 是否能正常编译通过。
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# 系统架构
|
|
||||||
|
|
||||||
## 1. 概览
|
|
||||||
|
|
||||||
MiniCI 采用典型的 **Monorepo** 架构,前端 React 配合后端 Koa,数据持久化使用 SQLite。
|
|
||||||
|
|
||||||
## 2. 核心模块
|
|
||||||
|
|
||||||
### 2.1 后端 (apps/server)
|
|
||||||
|
|
||||||
- **Route System**: 基于 TC39 装饰器的自动扫描路由。
|
|
||||||
- **Execution Queue**: 单例模式的执行队列,控制并发并支持任务持久化恢复。
|
|
||||||
- **Pipeline Runner**: 核心执行逻辑,利用 `zx` 在独立的工作目录下运行 Shell 脚本。
|
|
||||||
- **Data Access**: Prisma ORM 提供类型安全的数据访问。
|
|
||||||
|
|
||||||
### 2.2 前端 (apps/web)
|
|
||||||
|
|
||||||
- **Build Tool**: Rsbuild (基于 Rspack),提供极速的开发体验。
|
|
||||||
- **UI Framework**: React 19 + Arco Design。
|
|
||||||
- **State Management**: Zustand 实现轻量级全局状态。
|
|
||||||
|
|
||||||
## 3. 部署流 (Pipeline Flow)
|
|
||||||
|
|
||||||
1. 用户触发部署 -> 创建 `Deployment` 记录 (Status: pending)。
|
|
||||||
2. `ExecutionQueue` 捕获新任务 -> 实例化 `PipelineRunner`。
|
|
||||||
3. `GitManager` 准备工作目录 (`git clone` 或 `git pull`)。
|
|
||||||
4. `PipelineRunner` 逐个执行 `Step` 脚本。
|
|
||||||
5. 执行过程中实时更新 `Deployment.buildLog`。
|
|
||||||
6. 完成后更新状态为 `success` 或 `failed`。
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# 约束与禁区
|
|
||||||
|
|
||||||
## 1. 路由规范
|
|
||||||
|
|
||||||
- **禁止** 在 `app.ts` 中手动编写 `router.get/post`。
|
|
||||||
- **必须** 使用 `@Controller` 和 `@Get/Post` 装饰器,并放在 `controllers/` 目录下。
|
|
||||||
|
|
||||||
## 2. 数据库安全
|
|
||||||
|
|
||||||
- **禁止** 绕过 Prisma 直接操作数据库文件。
|
|
||||||
- **禁止** 在生产环境中手动修改 `dev.db`。
|
|
||||||
|
|
||||||
## 3. 环境变量
|
|
||||||
|
|
||||||
- **禁区**: 严禁将敏感 Token 或密钥直接硬编码在代码或 `schema.prisma` 中。
|
|
||||||
- 请使用 `.env` 文件配合 `dotenv` 加载。
|
|
||||||
|
|
||||||
## 4. 依赖管理
|
|
||||||
|
|
||||||
- **禁止** 混合使用 npm/yarn,必须统一使用 `pnpm`。
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# 编码规范
|
|
||||||
|
|
||||||
## 1. 命名习惯
|
|
||||||
|
|
||||||
- **前端组件**: PascalCase (如 `ProjectCard.tsx`)。
|
|
||||||
- **后端文件**: kebab-case (如 `route-scanner.ts`)。
|
|
||||||
- **DTO**: 文件名为 `dto.ts`,类名为 `XxxDTO`。
|
|
||||||
|
|
||||||
## 2. 代码组织
|
|
||||||
|
|
||||||
web 项目代码组织如下:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
├── web/
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ ├── components/ # 页面组件
|
|
||||||
│ │ ├── index.tsx # 页面入口组件
|
|
||||||
│ │ ├── service.ts # 当前页面使用到的 api 请求方法和纯函数
|
|
||||||
│ │ ├── types.ts # 当前页面使用到的类型定义
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── hooks/ # 通用 hooks (不止一个组件引用)
|
|
||||||
│ └── stores/ # 全局状态
|
|
||||||
│ └── utils/ # 通用工具类
|
|
||||||
│ └── styles/ # 全局样式
|
|
||||||
│ └── assets/ # 静态资源
|
|
||||||
│ └── components/ # 通用组件
|
|
||||||
│ └── types/ # 全局类型定义
|
|
||||||
│ └── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 代码规范
|
|
||||||
|
|
||||||
- 注释符合 jsdoc 规范
|
|
||||||
- 代码简洁,避免冗余,移除无用的代码引用、变量、函数和css样式
|
|
||||||
- 禁止使用 any 类型
|
|
||||||
|
|
||||||
## 4. 前端发送net请求示例
|
|
||||||
|
|
||||||
- 分页
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {net} from '@utils';
|
|
||||||
import type {APIPagination} from '@utils/net';
|
|
||||||
|
|
||||||
const data = await net.request<APIPagination<Deployment>>({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/deployments',
|
|
||||||
// 注意:查询参数使用 params 传递,不要手动拼接到 url 上
|
|
||||||
params: {
|
|
||||||
projectId: 1,
|
|
||||||
page: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
- 其他
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {net} from '@utils';
|
|
||||||
|
|
||||||
const data = await net.request<void>({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/deployment',
|
|
||||||
data: {
|
|
||||||
name: 'xxx',
|
|
||||||
description: 'xxx',
|
|
||||||
repository: 'https://a.com',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (data.code === 0) {
|
|
||||||
console.log("创建成功")
|
|
||||||
} else {
|
|
||||||
console.log("创建失败")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 异步处理
|
|
||||||
|
|
||||||
- 统一使用 `async/await`。
|
|
||||||
- 后端错误通过抛出异常由 `exception.ts` 中间件统一捕获。
|
|
||||||
|
|
||||||
## 6. 格式化
|
|
||||||
|
|
||||||
- 使用 Biome 进行 Lint 和 Format。
|
|
||||||
- 提交代码前建议运行 `pnpm --filter web format`。
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# ADR 0001: 技术选型
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
需要构建一个轻量级、易扩展且易于本地部署的 CI 系统。
|
|
||||||
|
|
||||||
## 决策
|
|
||||||
|
|
||||||
- **语言**: 全栈 TypeScript,确保模型定义在前后端的一致性。
|
|
||||||
- **后端框架**: Koa。相比 Express 更加轻量,利用 async/await 处理异步中间件更优雅。
|
|
||||||
- **数据库**: SQLite。CI 系统通常是单机或小规模使用,SQLite 无需独立服务,运维成本极低。
|
|
||||||
- **执行工具**: `zx`。相比原生的 `child_process`,`zx` 处理 Shell 交互更加直观和安全。
|
|
||||||
|
|
||||||
## 后果
|
|
||||||
|
|
||||||
- 优势:开发效率极高,部署简单。
|
|
||||||
- 挑战:SQLite 在极高并发写入(如数百个任务同时输出日志)时可能存在性能瓶颈。
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# ADR 0002: 状态管理
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
需要在前端管理用户信息、全局配置以及各页面的复杂 UI 状态。
|
|
||||||
|
|
||||||
## 决策
|
|
||||||
|
|
||||||
- **全局状态**: 使用 Zustand。
|
|
||||||
- **理由**:
|
|
||||||
- 相比 Redux 模板代码极少。
|
|
||||||
- 相比 Context API 性能更好且不引起全量重绘。
|
|
||||||
- 符合 React 19 的 Concurrent 模式。
|
|
||||||
- **持久化**: 对关键状态(如 Token)使用 Zustand 的 persist 中间件。
|
|
||||||
|
|
||||||
## 后果
|
|
||||||
|
|
||||||
状态管理逻辑高度内聚在 `apps/web/src/stores` 中。
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# ADR 0003: 流水线执行策略
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
如何确保流水线执行的隔离性与可靠性。
|
|
||||||
|
|
||||||
## 决策
|
|
||||||
|
|
||||||
- **工作目录**: 每个项目在服务器上拥有独立的 `projectDir`。
|
|
||||||
- **执行器**: 采用线性执行。目前不支持多步骤并行,以确保日志顺序的确定性。
|
|
||||||
- **队列**: 使用内存队列 + 数据库扫描实现。系统重启后能通过数据库中的 `pending` 状态恢复任务。
|
|
||||||
|
|
||||||
## 后果
|
|
||||||
|
|
||||||
目前的隔离级别为目录级。未来可能需要引入 Docker 容器化执行以增强安全性。
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# 踩坑指南 (Pitfalls)
|
|
||||||
|
|
||||||
## 1. Prisma 客户端生成
|
|
||||||
|
|
||||||
- **现象**: 修改 `schema.prisma` 后代码报错找不到类型。
|
|
||||||
- **解决**: 需要在 `apps/server` 下运行 `pnpm prisma generate`。本项目将生成的代码放在了 `generated/` 目录而非 node_modules,请注意引用路径。
|
|
||||||
|
|
||||||
## 2. zx 环境变量继承
|
|
||||||
|
|
||||||
- **现象**: 在流水线脚本中找不到 `node` 或 `git` 命令。
|
|
||||||
- **解决**: `PipelineRunner` 在调用 `zx` 时必须手动扩展 `env: { ...process.env, ...userEnv }`,否则会丢失系统 PATH。
|
|
||||||
|
|
||||||
## 3. Koa BodyParser 顺序
|
|
||||||
|
|
||||||
- **现象**: 获取不到 `ctx.request.body`。
|
|
||||||
- **解决**: `koa-bodyparser` 中间件必须在 `router` 中间件之前注册。
|
|
||||||
|
|
||||||
## 4. SQLite 并发写入
|
|
||||||
|
|
||||||
- **现象**: 部署日志极快输出时偶发 `SQLITE_BUSY`。
|
|
||||||
- **解决**: 适当增加 `better-sqlite3` 的 busy timeout。
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# 标准化响应结构
|
|
||||||
|
|
||||||
1. 需要标准化接口响应的结构,修改后端接口中不符合规范的代码。
|
|
||||||
2. 前端接口类型定义需要与后端接口响应结构保持一致。
|
|
||||||
|
|
||||||
## 响应示例
|
|
||||||
|
|
||||||
- 列表分页响应
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 0, // 响应状态码,0 表示成功,其他值表示失败
|
|
||||||
"message": "success", // 响应消息
|
|
||||||
"data": { // 响应数据
|
|
||||||
"list": [], // 列表数据
|
|
||||||
"page": 1, // 当前页码
|
|
||||||
"pageSize": 10, // 每页显示数量
|
|
||||||
"total": 10 // 总数量
|
|
||||||
},
|
|
||||||
"timestamp": "12346579" // 响应时间戳
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 其他响应
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "success",
|
|
||||||
"data": {},
|
|
||||||
"timestamp": "12346579"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 当前进度 (Status)
|
|
||||||
|
|
||||||
## 已完成 ✅
|
|
||||||
|
|
||||||
- 基础 Monorepo 框架搭设
|
|
||||||
- TC39 装饰器路由系统
|
|
||||||
- 项目管理 (CRUD)
|
|
||||||
- 基础流水线执行流程 (Git Clone -> zx Run -> Log Update)
|
|
||||||
- 前端项目列表与详情页预览
|
|
||||||
- 优化: 移除菜单环境管理及页面(目前无用)
|
|
||||||
- 优化:移除创建 Step 弹窗可用环境变量区域
|
|
||||||
- 优化: 部署记录的分页查询每页 10 条
|
|
||||||
|
|
||||||
## 进行中 🚧
|
|
||||||
|
|
||||||
- [标准化接口响应结构](./requirements/0001-Fix-Response-structure.md)
|
|
||||||
|
|
||||||
## 待办 📅
|
|
||||||
|
|
||||||
- [ ] Gitea Webhook 自动触发
|
|
||||||
- [ ] 用户权限管理 (RBAC)
|
|
||||||
- [ ] 修复: 表单必填项,*号和 label 不在一行
|
|
||||||
- [ ] 修复:项目详情页,未选中 tab【部署记录】还会拉取日志信息
|
|
||||||
+2
-6
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "MiniCI",
|
"name": "ark-ci",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,11 +8,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.6"
|
"@biomejs/biome": "2.0.6"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": ["ci", "ark", "ark-ci"],
|
||||||
"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"
|
||||||
|
|||||||
Reference in New Issue
Block a user