Compare commits
9 Commits
001-projec
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 45047f40aa | |||
| cd50716dc6 | |||
| 14aa3436cf | |||
| a2f31ec5a0 | |||
| 82da2a1a04 | |||
| db2b2af0d3 | |||
| a067d167e9 | |||
| d22fdc9618 | |||
| c40532c757 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ dist/
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
.env
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ this.routeScanner.registerControllers([
|
|||||||
## TC39 装饰器特性
|
## TC39 装饰器特性
|
||||||
|
|
||||||
### 1. 标准语法
|
### 1. 标准语法
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// TC39 标准装饰器使用 addInitializer
|
// TC39 标准装饰器使用 addInitializer
|
||||||
@Get('/users')
|
@Get('/users')
|
||||||
@@ -89,6 +90,7 @@ async getUsers(ctx: Context) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2. 类型安全
|
### 2. 类型安全
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 完整的 TypeScript 类型检查
|
// 完整的 TypeScript 类型检查
|
||||||
@Controller('/api')
|
@Controller('/api')
|
||||||
@@ -101,6 +103,7 @@ export class ApiController {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 3. 无外部依赖
|
### 3. 无外部依赖
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 不再需要 reflect-metadata
|
// 不再需要 reflect-metadata
|
||||||
// 使用内置的 WeakMap 存储元数据
|
// 使用内置的 WeakMap 存储元数据
|
||||||
@@ -136,6 +139,7 @@ export class ApiController {
|
|||||||
最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径
|
最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径
|
||||||
|
|
||||||
例如:
|
例如:
|
||||||
|
|
||||||
- 全局前缀:`/api`
|
- 全局前缀:`/api`
|
||||||
- 控制器前缀:`/user`
|
- 控制器前缀:`/user`
|
||||||
- 方法路径:`/list`
|
- 方法路径:`/list`
|
||||||
@@ -176,56 +180,11 @@ async getUser(ctx: Context) {
|
|||||||
|
|
||||||
## 现有路由
|
## 现有路由
|
||||||
|
|
||||||
项目中已注册的路由:
|
|
||||||
|
|
||||||
### ApplicationController
|
|
||||||
- `GET /api/application/list` - 获取应用列表
|
|
||||||
- `GET /api/application/detail/:id` - 获取应用详情
|
|
||||||
|
|
||||||
### UserController
|
### 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` 进行初始化,性能更优
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import { initMiddlewares } from './middlewares/index.ts';
|
|
||||||
import { log } from './libs/logger.ts';
|
|
||||||
import { ExecutionQueue } from './libs/execution-queue.ts';
|
import { ExecutionQueue } from './libs/execution-queue.ts';
|
||||||
|
import { log } from './libs/logger.ts';
|
||||||
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
import { initializePipelineTemplates } from './libs/pipeline-template.ts';
|
||||||
|
import { initMiddlewares } from './middlewares/index.ts';
|
||||||
|
|
||||||
// 初始化应用
|
// 初始化应用
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
@@ -26,7 +26,7 @@ async function initializeApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动应用
|
// 启动应用
|
||||||
initializeApp().catch(error => {
|
initializeApp().catch((error) => {
|
||||||
console.error('Failed to start application:', error);
|
log.error('APP', '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 { prisma } from '../../libs/prisma.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { gitea } from '../../libs/gitea.ts';
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { loginSchema } from './dto.ts';
|
import { loginSchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/auth')
|
@Controller('/auth')
|
||||||
|
|||||||
@@ -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().default(1),
|
page: z.coerce.number().int().min(1).optional(),
|
||||||
pageSize: z.coerce.number().int().min(1).max(100).optional().default(10),
|
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
||||||
projectId: z.coerce.number().int().positive().optional(),
|
projectId: z.coerce.number().int().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -12,8 +12,7 @@ export const createDeploymentSchema = z.object({
|
|||||||
branch: z.string().min(1, { message: '分支不能为空' }),
|
branch: z.string().min(1, { message: '分支不能为空' }),
|
||||||
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
commitHash: z.string().min(1, { message: '提交哈希不能为空' }),
|
||||||
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
commitMessage: z.string().min(1, { message: '提交信息不能为空' }),
|
||||||
env: z.string().optional(),
|
envVars: z.record(z.string(), z.string()).optional(), // 环境变量 key-value 对象
|
||||||
sparseCheckoutPaths: z.string().optional(), // 添加稀疏检出路径字段
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
export type ListDeploymentsQuery = z.infer<typeof listDeploymentsQuerySchema>;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get, Post } from '../../decorators/route.ts';
|
import { Controller, Get, Post } from '../../decorators/route.ts';
|
||||||
import type { Prisma } from '../../generated/client.ts';
|
import type { Prisma } from '../../generated/client.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
|
||||||
import type { Context } from 'koa';
|
|
||||||
import { listDeploymentsQuerySchema, createDeploymentSchema } from './dto.ts';
|
|
||||||
import { ExecutionQueue } from '../../libs/execution-queue.ts';
|
import { ExecutionQueue } from '../../libs/execution-queue.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { createDeploymentSchema, listDeploymentsQuerySchema } from './dto.ts';
|
||||||
|
|
||||||
@Controller('/deployments')
|
@Controller('/deployments')
|
||||||
export class DeploymentController {
|
export class DeploymentController {
|
||||||
@Get('')
|
@Get('')
|
||||||
async list(ctx: Context) {
|
async list(ctx: Context) {
|
||||||
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(ctx.query);
|
const { page, pageSize, projectId } = listDeploymentsQuerySchema.parse(
|
||||||
|
ctx.query,
|
||||||
|
);
|
||||||
const where: Prisma.DeploymentWhereInput = {
|
const where: Prisma.DeploymentWhereInput = {
|
||||||
valid: 1,
|
valid: 1,
|
||||||
};
|
};
|
||||||
@@ -18,24 +20,30 @@ 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: pageSize,
|
take: isPagination ? pageSize : undefined,
|
||||||
skip: (page - 1) * pageSize,
|
skip: isPagination ? (page! - 1) * pageSize! : 0,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const total = await prisma.deployment.count({ where });
|
const total = await prisma.deployment.count({ where });
|
||||||
|
|
||||||
|
if (isPagination) {
|
||||||
return {
|
return {
|
||||||
data: result,
|
list: 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);
|
||||||
@@ -50,8 +58,7 @@ export class DeploymentController {
|
|||||||
connect: { id: body.projectId },
|
connect: { id: body.projectId },
|
||||||
},
|
},
|
||||||
pipelineId: body.pipelineId,
|
pipelineId: body.pipelineId,
|
||||||
env: body.env || 'dev',
|
envVars: body.envVars ? JSON.stringify(body.envVars) : null,
|
||||||
sparseCheckoutPaths: body.sparseCheckoutPaths || '', // 添加稀疏检出路径
|
|
||||||
buildLog: '',
|
buildLog: '',
|
||||||
createdBy: 'system', // TODO: get from user
|
createdBy: 'system', // TODO: get from user
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
@@ -73,7 +80,7 @@ export class DeploymentController {
|
|||||||
|
|
||||||
// 获取原始部署记录
|
// 获取原始部署记录
|
||||||
const originalDeployment = await prisma.deployment.findUnique({
|
const originalDeployment = await prisma.deployment.findUnique({
|
||||||
where: { id: Number(id) }
|
where: { id: Number(id) },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!originalDeployment) {
|
if (!originalDeployment) {
|
||||||
@@ -82,7 +89,7 @@ export class DeploymentController {
|
|||||||
code: 404,
|
code: 404,
|
||||||
message: '部署记录不存在',
|
message: '部署记录不存在',
|
||||||
data: null,
|
data: null,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -96,8 +103,7 @@ export class DeploymentController {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
projectId: originalDeployment.projectId,
|
projectId: originalDeployment.projectId,
|
||||||
pipelineId: originalDeployment.pipelineId,
|
pipelineId: originalDeployment.pipelineId,
|
||||||
env: originalDeployment.env,
|
envVars: originalDeployment.envVars,
|
||||||
sparseCheckoutPaths: originalDeployment.sparseCheckoutPaths,
|
|
||||||
buildLog: '',
|
buildLog: '',
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
@@ -113,7 +119,7 @@ export class DeploymentController {
|
|||||||
code: 0,
|
code: 0,
|
||||||
message: '重新执行任务已创建',
|
message: '重新执行任务已创建',
|
||||||
data: newDeployment,
|
data: newDeployment,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const getCommitsQuerySchema = z.object({
|
export const getCommitsQuerySchema = z.object({
|
||||||
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
projectId: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive({ message: 'Project ID is required' }),
|
||||||
branch: z.string().optional(),
|
branch: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getBranchesQuerySchema = z.object({
|
export const getBranchesQuerySchema = z.object({
|
||||||
projectId: z.coerce.number().int().positive({ message: 'Project ID is required' }),
|
projectId: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive({ message: 'Project ID is required' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
export type GetCommitsQuery = z.infer<typeof getCommitsQuerySchema>;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get } from '../../decorators/route.ts';
|
import { Controller, Get } from '../../decorators/route.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
|
||||||
import { gitea } from '../../libs/gitea.ts';
|
import { gitea } from '../../libs/gitea.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { getCommitsQuerySchema, getBranchesQuerySchema } from './dto.ts';
|
import { getBranchesQuerySchema, getCommitsQuerySchema } from './dto.ts';
|
||||||
|
|
||||||
|
const TAG = 'Git';
|
||||||
|
|
||||||
@Controller('/git')
|
@Controller('/git')
|
||||||
export class GitController {
|
export class GitController {
|
||||||
@@ -30,17 +33,21 @@ 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;
|
||||||
console.log('Access token present:', !!accessToken);
|
log.debug(TAG, 'Access token present: %s', !!accessToken);
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
throw new BusinessError(
|
||||||
|
'Gitea access token not found. Please login again.',
|
||||||
|
1004,
|
||||||
|
401,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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) {
|
||||||
console.error('Failed to fetch commits:', error);
|
log.error(TAG, '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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,14 +72,18 @@ export class GitController {
|
|||||||
const accessToken = ctx.session?.gitea?.access_token;
|
const accessToken = ctx.session?.gitea?.access_token;
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new BusinessError('Gitea access token not found. Please login again.', 1004, 401);
|
throw new BusinessError(
|
||||||
|
'Gitea access token not found. Please login again.',
|
||||||
|
1004,
|
||||||
|
401,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const branches = await gitea.getBranches(owner, repo, accessToken);
|
const branches = await gitea.getBranches(owner, repo, accessToken);
|
||||||
return branches;
|
return branches;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch branches:', error);
|
log.error(TAG, '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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,7 @@ export class GitController {
|
|||||||
|
|
||||||
// Handle SCP-like syntax: git@host:owner/repo.git
|
// Handle SCP-like syntax: git@host:owner/repo.git
|
||||||
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
|
if (!cleanUrl.includes('://') && cleanUrl.includes(':')) {
|
||||||
const scpMatch = cleanUrl.match(/:([^\/]+)\/([^\/]+?)(\.git)?$/);
|
const scpMatch = cleanUrl.match(/:([^/]+)\/([^/]+?)(\.git)?$/);
|
||||||
if (scpMatch) {
|
if (scpMatch) {
|
||||||
return { owner: scpMatch[1], repo: scpMatch[2] };
|
return { owner: scpMatch[1], repo: scpMatch[2] };
|
||||||
}
|
}
|
||||||
@@ -96,13 +107,15 @@ export class GitController {
|
|||||||
const urlObj = new URL(cleanUrl);
|
const urlObj = new URL(cleanUrl);
|
||||||
const parts = urlObj.pathname.split('/').filter(Boolean);
|
const parts = urlObj.pathname.split('/').filter(Boolean);
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
const repo = parts.pop()!.replace(/\.git$/, '');
|
const repo = parts.pop()?.replace(/\.git$/, '');
|
||||||
const owner = parts.pop()!;
|
const owner = parts.pop();
|
||||||
|
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,8 +1,9 @@
|
|||||||
// 控制器统一导出
|
// 控制器统一导出
|
||||||
export { ProjectController } from './project/index.ts';
|
|
||||||
export { UserController } from './user/index.ts';
|
|
||||||
export { AuthController } from './auth/index.ts';
|
export { AuthController } from './auth/index.ts';
|
||||||
export { DeploymentController } from './deployment/index.ts';
|
export { DeploymentController } from './deployment/index.ts';
|
||||||
export { PipelineController } from './pipeline/index.ts';
|
|
||||||
export { StepController } from './step/index.ts'
|
|
||||||
export { GitController } from './git/index.ts';
|
export { GitController } from './git/index.ts';
|
||||||
|
export { PipelineController } from './pipeline/index.ts';
|
||||||
|
export { ProjectController } from './project/index.ts';
|
||||||
|
export { StepController } from './step/index.ts';
|
||||||
|
export { UserController } from './user/index.ts';
|
||||||
|
|||||||
@@ -2,36 +2,59 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
// 定义验证架构
|
// 定义验证架构
|
||||||
export const createPipelineSchema = z.object({
|
export const createPipelineSchema = z.object({
|
||||||
name: z.string({
|
name: z
|
||||||
|
.string({
|
||||||
message: '流水线名称必须是字符串',
|
message: '流水线名称必须是字符串',
|
||||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }),
|
})
|
||||||
|
.min(1, { message: '流水线名称不能为空' })
|
||||||
|
.max(100, { message: '流水线名称不能超过100个字符' }),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
|
.string({
|
||||||
message: '流水线描述必须是字符串',
|
message: '流水线描述必须是字符串',
|
||||||
}).max(500, { message: '流水线描述不能超过500个字符' }).optional(),
|
})
|
||||||
|
.max(500, { message: '流水线描述不能超过500个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
projectId: z.number({
|
projectId: z
|
||||||
|
.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.string({
|
name: z
|
||||||
|
.string({
|
||||||
message: '流水线名称必须是字符串',
|
message: '流水线名称必须是字符串',
|
||||||
}).min(1, { message: '流水线名称不能为空' }).max(100, { message: '流水线名称不能超过100个字符' }).optional(),
|
})
|
||||||
|
.min(1, { message: '流水线名称不能为空' })
|
||||||
|
.max(100, { message: '流水线名称不能超过100个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
|
.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.object({
|
export const listPipelinesQuerySchema = z
|
||||||
projectId: z.coerce.number().int().positive({ message: '项目ID必须是正整数' }).optional(),
|
.object({
|
||||||
}).optional();
|
projectId: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive({ message: '项目ID必须是正整数' })
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
// 类型
|
// 类型
|
||||||
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
export type CreatePipelineInput = z.infer<typeof createPipelineSchema>;
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import type { Context } from 'koa';
|
import type { Context } from 'koa';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { prisma } from '../../libs/prisma.ts';
|
|
||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import {
|
||||||
|
createPipelineFromTemplate,
|
||||||
|
getAvailableTemplates,
|
||||||
|
} from '../../libs/pipeline-template.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { getAvailableTemplates, createPipelineFromTemplate } from '../../libs/pipeline-template.ts';
|
|
||||||
import {
|
import {
|
||||||
createPipelineSchema,
|
createPipelineSchema,
|
||||||
updatePipelineSchema,
|
|
||||||
pipelineIdSchema,
|
|
||||||
listPipelinesQuerySchema,
|
listPipelinesQuerySchema,
|
||||||
|
pipelineIdSchema,
|
||||||
|
updatePipelineSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/pipelines')
|
@Controller('/pipelines')
|
||||||
@@ -46,12 +49,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) {
|
||||||
console.error('Failed to get templates:', error);
|
log.error('pipeline', 'Failed to get templates:', error);
|
||||||
throw new BusinessError('获取模板失败', 3002, 500);
|
throw new BusinessError('获取模板失败', 3002, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +129,7 @@ export class PipelineController {
|
|||||||
templateId,
|
templateId,
|
||||||
projectId,
|
projectId,
|
||||||
name,
|
name,
|
||||||
description || ''
|
description || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 返回新创建的流水线
|
// 返回新创建的流水线
|
||||||
@@ -151,7 +154,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) {
|
||||||
console.error('Failed to create pipeline from template:', error);
|
log.error('pipeline', 'Failed to create pipeline from template:', error);
|
||||||
if (error instanceof BusinessError) {
|
if (error instanceof BusinessError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,46 +5,72 @@ import { projectDirSchema } from '../../libs/path-validator.js';
|
|||||||
* 创建项目验证架构
|
* 创建项目验证架构
|
||||||
*/
|
*/
|
||||||
export const createProjectSchema = z.object({
|
export const createProjectSchema = z.object({
|
||||||
name: z.string({
|
name: z
|
||||||
|
.string({
|
||||||
message: '项目名称必须是字符串',
|
message: '项目名称必须是字符串',
|
||||||
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }),
|
})
|
||||||
|
.min(2, { message: '项目名称至少2个字符' })
|
||||||
|
.max(50, { message: '项目名称不能超过50个字符' }),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
|
.string({
|
||||||
message: '项目描述必须是字符串',
|
message: '项目描述必须是字符串',
|
||||||
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
})
|
||||||
|
.max(200, { message: '项目描述不能超过200个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
repository: z.string({
|
repository: z
|
||||||
|
.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.string({
|
name: z
|
||||||
|
.string({
|
||||||
message: '项目名称必须是字符串',
|
message: '项目名称必须是字符串',
|
||||||
}).min(2, { message: '项目名称至少2个字符' }).max(50, { message: '项目名称不能超过50个字符' }).optional(),
|
})
|
||||||
|
.min(2, { message: '项目名称至少2个字符' })
|
||||||
|
.max(50, { message: '项目名称不能超过50个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
description: z.string({
|
description: z
|
||||||
|
.string({
|
||||||
message: '项目描述必须是字符串',
|
message: '项目描述必须是字符串',
|
||||||
}).max(200, { message: '项目描述不能超过200个字符' }).optional(),
|
})
|
||||||
|
.max(200, { message: '项目描述不能超过200个字符' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
repository: z.string({
|
repository: z
|
||||||
|
.string({
|
||||||
message: '仓库地址必须是字符串',
|
message: '仓库地址必须是字符串',
|
||||||
}).url({ message: '请输入有效的仓库地址' }).min(1, { message: '仓库地址不能为空' }).optional(),
|
})
|
||||||
|
.url({ message: '请输入有效的仓库地址' })
|
||||||
|
.min(1, { message: '仓库地址不能为空' })
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
envPresets: z.string().optional(), // JSON 字符串格式
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目列表查询参数验证架构
|
* 项目列表查询参数验证架构
|
||||||
*/
|
*/
|
||||||
export const listProjectQuerySchema = z.object({
|
export const listProjectQuerySchema = z
|
||||||
page: z.coerce.number().int().min(1, { message: '页码必须大于0' }).optional().default(1),
|
.object({
|
||||||
limit: z.coerce.number().int().min(1, { message: '每页数量必须大于0' }).max(100, { message: '每页数量不能超过100' }).optional().default(10),
|
page: z.coerce.number().int().min(1).optional(),
|
||||||
|
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 { prisma } from '../../libs/prisma.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
|
||||||
import { GitManager } from '../../libs/git-manager.ts';
|
import { GitManager } from '../../libs/git-manager.ts';
|
||||||
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import {
|
import {
|
||||||
createProjectSchema,
|
createProjectSchema,
|
||||||
updateProjectSchema,
|
|
||||||
listProjectQuerySchema,
|
listProjectQuerySchema,
|
||||||
projectIdSchema,
|
projectIdSchema,
|
||||||
|
updateProjectSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/projects')
|
@Controller('/projects')
|
||||||
@@ -29,29 +29,32 @@ 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: query ? (query.page - 1) * query.limit : 0,
|
skip: isPagination ? (query.page! - 1) * query.pageSize! : 0,
|
||||||
take: query?.limit,
|
take: isPagination ? query.pageSize : undefined,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (isPagination) {
|
||||||
return {
|
return {
|
||||||
data: projects,
|
list: projects,
|
||||||
pagination: {
|
page: query.page,
|
||||||
page: query?.page || 1,
|
pageSize: query.pageSize,
|
||||||
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) {
|
||||||
@@ -68,27 +71,21 @@ 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) {
|
||||||
@@ -135,6 +132,7 @@ export class ProjectController {
|
|||||||
description: validatedData.description || '',
|
description: validatedData.description || '',
|
||||||
repository: validatedData.repository,
|
repository: validatedData.repository,
|
||||||
projectDir: validatedData.projectDir,
|
projectDir: validatedData.projectDir,
|
||||||
|
envPresets: validatedData.envPresets,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
@@ -182,6 +180,9 @@ export class ProjectController {
|
|||||||
if (validatedData.repository !== undefined) {
|
if (validatedData.repository !== undefined) {
|
||||||
updateData.repository = validatedData.repository;
|
updateData.repository = validatedData.repository;
|
||||||
}
|
}
|
||||||
|
if (validatedData.envPresets !== undefined) {
|
||||||
|
updateData.envPresets = validatedData.envPresets;
|
||||||
|
}
|
||||||
|
|
||||||
const project = await prisma.project.update({
|
const project = await prisma.project.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -84,15 +84,13 @@ export const listStepsQuerySchema = z
|
|||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1, { message: '页码必须大于0' })
|
.min(1, { message: '页码必须大于0' })
|
||||||
.optional()
|
.optional(),
|
||||||
.default(1),
|
pageSize: z.coerce
|
||||||
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 { prisma } from '../../libs/prisma.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { log } from '../../libs/logger.ts';
|
import { log } from '../../libs/logger.ts';
|
||||||
|
import { prisma } from '../../libs/prisma.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import { Controller, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
|
||||||
import {
|
import {
|
||||||
createStepSchema,
|
createStepSchema,
|
||||||
updateStepSchema,
|
|
||||||
stepIdSchema,
|
|
||||||
listStepsQuerySchema,
|
listStepsQuerySchema,
|
||||||
|
stepIdSchema,
|
||||||
|
updateStepSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
@Controller('/steps')
|
@Controller('/steps')
|
||||||
@@ -26,29 +26,32 @@ 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: query ? (query.page - 1) * query.limit : 0,
|
skip: isPagination ? (query.page! - 1) * query.pageSize! : 0,
|
||||||
take: query?.limit,
|
take: isPagination ? query.pageSize : undefined,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
order: 'asc',
|
order: 'asc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (isPagination) {
|
||||||
return {
|
return {
|
||||||
data: steps,
|
list: steps,
|
||||||
pagination: {
|
page: query.page,
|
||||||
page: query?.page || 1,
|
pageSize: query.pageSize,
|
||||||
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, Get, Post, Put, Delete } from '../../decorators/route.ts';
|
import { Controller, Delete, Get, Post, Put } from '../../decorators/route.ts';
|
||||||
import { BusinessError } from '../../middlewares/exception.ts';
|
import { BusinessError } from '../../middlewares/exception.ts';
|
||||||
import {
|
import {
|
||||||
userIdSchema,
|
|
||||||
createUserSchema,
|
createUserSchema,
|
||||||
updateUserSchema,
|
|
||||||
searchUserQuerySchema,
|
searchUserQuerySchema,
|
||||||
|
updateUserSchema,
|
||||||
|
userIdSchema,
|
||||||
} from './dto.ts';
|
} from './dto.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,14 +13,18 @@ import {
|
|||||||
*/
|
*/
|
||||||
@Controller('/user')
|
@Controller('/user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
|
|
||||||
@Get('/list')
|
@Get('/list')
|
||||||
async list(ctx: Context) {
|
async list(_ctx: Context) {
|
||||||
// 模拟用户列表数据
|
// 模拟用户列表数据
|
||||||
const users = [
|
const users = [
|
||||||
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
||||||
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
|
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
|
||||||
{ id: 3, name: 'Charlie', email: 'charlie@example.com', status: 'active' }
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Charlie',
|
||||||
|
email: 'charlie@example.com',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
@@ -33,10 +37,10 @@ export class UserController {
|
|||||||
// 模拟根据ID查找用户
|
// 模拟根据ID查找用户
|
||||||
const user = {
|
const user = {
|
||||||
id,
|
id,
|
||||||
name: 'User ' + id,
|
name: `User ${id}`,
|
||||||
email: `user${id}@example.com`,
|
email: `user${id}@example.com`,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id > 100) {
|
if (id > 100) {
|
||||||
@@ -55,7 +59,7 @@ export class UserController {
|
|||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
...body,
|
...body,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
status: body.status
|
status: body.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
@@ -70,7 +74,7 @@ export class UserController {
|
|||||||
const updatedUser = {
|
const updatedUser = {
|
||||||
id,
|
id,
|
||||||
...body,
|
...body,
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return updatedUser;
|
return updatedUser;
|
||||||
@@ -88,7 +92,7 @@ export class UserController {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `用户 ${id} 已删除`,
|
message: `用户 ${id} 已删除`,
|
||||||
deletedAt: new Date().toISOString()
|
deletedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,25 +103,21 @@ export class UserController {
|
|||||||
// 模拟搜索逻辑
|
// 模拟搜索逻辑
|
||||||
let results = [
|
let results = [
|
||||||
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' },
|
||||||
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' }
|
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'inactive' },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
results = results.filter(user =>
|
results = results.filter(
|
||||||
|
(user) =>
|
||||||
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 {
|
return results;
|
||||||
keyword,
|
|
||||||
status,
|
|
||||||
total: results.length,
|
|
||||||
results
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,24 @@ const metadataStore = new WeakMap<any, Map<string | symbol, any>>();
|
|||||||
/**
|
/**
|
||||||
* 设置元数据(降级方案)
|
* 设置元数据(降级方案)
|
||||||
*/
|
*/
|
||||||
function setMetadata<T = any>(key: string | symbol, value: T, target: any): void {
|
function setMetadata<T = any>(
|
||||||
|
key: string | symbol,
|
||||||
|
value: T,
|
||||||
|
target: any,
|
||||||
|
): void {
|
||||||
if (!metadataStore.has(target)) {
|
if (!metadataStore.has(target)) {
|
||||||
metadataStore.set(target, new Map());
|
metadataStore.set(target, new Map());
|
||||||
}
|
}
|
||||||
metadataStore.get(target)!.set(key, value);
|
metadataStore.get(target)?.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取元数据(降级方案)
|
* 获取元数据(降级方案)
|
||||||
*/
|
*/
|
||||||
function getMetadata<T = any>(key: string | symbol, target: any): T | undefined {
|
function getMetadata<T = any>(
|
||||||
|
key: string | symbol,
|
||||||
|
target: any,
|
||||||
|
): T | undefined {
|
||||||
return metadataStore.get(target)?.get(key);
|
return metadataStore.get(target)?.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,24 +50,28 @@ function getMetadata<T = any>(key: string | symbol, target: any): T | undefined
|
|||||||
* 创建HTTP方法装饰器的工厂函数(TC39标准)
|
* 创建HTTP方法装饰器的工厂函数(TC39标准)
|
||||||
*/
|
*/
|
||||||
function createMethodDecorator(method: HttpMethod) {
|
function createMethodDecorator(method: HttpMethod) {
|
||||||
return function (path: string = '') {
|
return (path: string = '') =>
|
||||||
return function <This, Args extends any[], Return>(
|
<This, Args extends any[], Return>(
|
||||||
target: (this: This, ...args: Args) => Return,
|
target: (this: This, ...args: Args) => Return,
|
||||||
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
|
context: ClassMethodDecoratorContext<
|
||||||
) {
|
This,
|
||||||
|
(this: This, ...args: Args) => Return
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
// 在类初始化时执行
|
// 在类初始化时执行
|
||||||
context.addInitializer(function () {
|
context.addInitializer(function () {
|
||||||
// 使用 this.constructor 时需要类型断言
|
// 使用 this.constructor 时需要类型断言
|
||||||
const ctor = (this as any).constructor;
|
const ctor = (this as any).constructor;
|
||||||
|
|
||||||
// 获取现有的路由元数据
|
// 获取现有的路由元数据
|
||||||
const existingRoutes: RouteMetadata[] = getMetadata(ROUTE_METADATA_KEY, ctor) || [];
|
const existingRoutes: RouteMetadata[] =
|
||||||
|
getMetadata(ROUTE_METADATA_KEY, ctor) || [];
|
||||||
|
|
||||||
// 添加新的路由元数据
|
// 添加新的路由元数据
|
||||||
const newRoute: RouteMetadata = {
|
const newRoute: RouteMetadata = {
|
||||||
method,
|
method,
|
||||||
path,
|
path,
|
||||||
propertyKey: String(context.name)
|
propertyKey: String(context.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
existingRoutes.push(newRoute);
|
existingRoutes.push(newRoute);
|
||||||
@@ -71,7 +82,6 @@ function createMethodDecorator(method: HttpMethod) {
|
|||||||
|
|
||||||
return target;
|
return target;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,10 +119,10 @@ export const Patch = createMethodDecorator('PATCH');
|
|||||||
* @param prefix 路由前缀
|
* @param prefix 路由前缀
|
||||||
*/
|
*/
|
||||||
export function Controller(prefix: string = '') {
|
export function Controller(prefix: string = '') {
|
||||||
return function <T extends abstract new (...args: any) => any>(
|
return <T extends abstract new (...args: any) => any>(
|
||||||
target: T,
|
target: T,
|
||||||
context: ClassDecoratorContext<T>
|
context: ClassDecoratorContext<T>,
|
||||||
) {
|
) => {
|
||||||
// 在类初始化时保存控制器前缀
|
// 在类初始化时保存控制器前缀
|
||||||
context.addInitializer(function () {
|
context.addInitializer(function () {
|
||||||
setMetadata('prefix', prefix, this);
|
setMetadata('prefix', prefix, this);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -13,32 +12,32 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
import * as Prisma from './internal/prismaNamespaceBrowser.ts';
|
||||||
export { Prisma }
|
export { Prisma };
|
||||||
export * as $Enums from './enums.ts'
|
export * as $Enums from './enums.ts';
|
||||||
export * from './enums.ts';
|
export * from './enums.ts';
|
||||||
/**
|
/**
|
||||||
* Model Project
|
* Model Project
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Project = Prisma.ProjectModel
|
export type Project = Prisma.ProjectModel;
|
||||||
/**
|
/**
|
||||||
* Model User
|
* Model User
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type User = Prisma.UserModel
|
export type User = Prisma.UserModel;
|
||||||
/**
|
/**
|
||||||
* Model Pipeline
|
* Model Pipeline
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Pipeline = Prisma.PipelineModel
|
export type Pipeline = Prisma.PipelineModel;
|
||||||
/**
|
/**
|
||||||
* Model Step
|
* Model Step
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Step = Prisma.StepModel
|
export type Step = Prisma.StepModel;
|
||||||
/**
|
/**
|
||||||
* Model Deployment
|
* Model Deployment
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Deployment = Prisma.DeploymentModel
|
export type Deployment = Prisma.DeploymentModel;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -10,18 +9,19 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as process from 'node:process'
|
import * as path from 'node:path';
|
||||||
import * as path from 'node:path'
|
import * as process from 'node:process';
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url';
|
||||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/client"
|
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url));
|
||||||
import * as $Enums from "./enums.ts"
|
|
||||||
import * as $Class from "./internal/class.ts"
|
|
||||||
import * as Prisma from "./internal/prismaNamespace.ts"
|
|
||||||
|
|
||||||
export * as $Enums from './enums.ts'
|
import * as runtime from '@prisma/client/runtime/client';
|
||||||
export * from "./enums.ts"
|
import * as $Enums from './enums.ts';
|
||||||
|
import * as $Class from './internal/class.ts';
|
||||||
|
import * as Prisma from './internal/prismaNamespace.ts';
|
||||||
|
|
||||||
|
export * as $Enums from './enums.ts';
|
||||||
|
export * from './enums.ts';
|
||||||
/**
|
/**
|
||||||
* ## Prisma Client
|
* ## Prisma Client
|
||||||
*
|
*
|
||||||
@@ -35,32 +35,38 @@ export * from "./enums.ts"
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
|
||||||
*/
|
*/
|
||||||
export const PrismaClient = $Class.getPrismaClientClass()
|
export const PrismaClient = $Class.getPrismaClientClass();
|
||||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
export type PrismaClient<
|
||||||
export { Prisma }
|
LogOpts extends Prisma.LogLevel = never,
|
||||||
|
OmitOpts extends
|
||||||
|
Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'],
|
||||||
|
ExtArgs extends
|
||||||
|
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||||
|
> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>;
|
||||||
|
export { Prisma };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model Project
|
* Model Project
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Project = Prisma.ProjectModel
|
export type Project = Prisma.ProjectModel;
|
||||||
/**
|
/**
|
||||||
* Model User
|
* Model User
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type User = Prisma.UserModel
|
export type User = Prisma.UserModel;
|
||||||
/**
|
/**
|
||||||
* Model Pipeline
|
* Model Pipeline
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Pipeline = Prisma.PipelineModel
|
export type Pipeline = Prisma.PipelineModel;
|
||||||
/**
|
/**
|
||||||
* Model Step
|
* Model Step
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Step = Prisma.StepModel
|
export type Step = Prisma.StepModel;
|
||||||
/**
|
/**
|
||||||
* Model Deployment
|
* Model Deployment
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Deployment = Prisma.DeploymentModel
|
export type Deployment = Prisma.DeploymentModel;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -9,394 +8,419 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as runtime from "@prisma/client/runtime/client"
|
import type * as runtime from '@prisma/client/runtime/client';
|
||||||
import * as $Enums from "./enums.ts"
|
import * as $Enums from './enums.ts';
|
||||||
import type * as Prisma from "./internal/prismaNamespace.ts"
|
import type * as Prisma from './internal/prismaNamespace.ts';
|
||||||
|
|
||||||
|
|
||||||
export type IntFilter<$PrismaModel = never> = {
|
export type IntFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringFilter<$PrismaModel = never> = {
|
export type StringFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringNullableFilter<$PrismaModel = never> = {
|
export type StringNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type DateTimeFilter<$PrismaModel = never> = {
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type SortOrderInput = {
|
export type SortOrderInput = {
|
||||||
sort: Prisma.SortOrder
|
sort: Prisma.SortOrder;
|
||||||
nulls?: Prisma.NullsOrder
|
nulls?: Prisma.NullsOrder;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| string
|
||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| null;
|
||||||
}
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type BoolFilter<$PrismaModel = never> = {
|
export type BoolFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IntNullableFilter<$PrismaModel = never> = {
|
export type IntNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
| number
|
||||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| null;
|
||||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||||
}
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
export type DateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
}
|
| Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
| Date
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
|
||||||
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| Date
|
||||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| string
|
||||||
}
|
| null;
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedIntFilter<$PrismaModel = never> = {
|
export type NestedIntFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringFilter<$PrismaModel = never> = {
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>;
|
||||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
_sum?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
_min?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
_max?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
in?: number[]
|
in?: number[];
|
||||||
notIn?: number[]
|
notIn?: number[];
|
||||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
not?: Prisma.NestedFloatFilter<$PrismaModel> | number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
in?: string[]
|
in?: string[];
|
||||||
notIn?: string[]
|
notIn?: string[];
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
_min?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
_max?: Prisma.NestedStringFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null;
|
||||||
in?: string[] | null
|
in?: string[] | null;
|
||||||
notIn?: string[] | null
|
notIn?: string[] | null;
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| string
|
||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
| null;
|
||||||
}
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[];
|
||||||
notIn?: Date[] | string[]
|
notIn?: Date[] | string[];
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedBoolFilter<$PrismaModel = never> = {
|
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean;
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
_count?: Prisma.NestedIntFilter<$PrismaModel>;
|
||||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
|
| number
|
||||||
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| null;
|
||||||
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>;
|
||||||
}
|
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
export type NestedFloatNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null;
|
||||||
in?: number[] | null
|
in?: number[] | null;
|
||||||
notIn?: number[] | null
|
notIn?: number[] | null;
|
||||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
}
|
| Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
||||||
|
| Date
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null;
|
||||||
in?: Date[] | string[] | null
|
in?: Date[] | string[] | null;
|
||||||
notIn?: Date[] | string[] | null
|
notIn?: Date[] | string[] | null;
|
||||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>;
|
||||||
not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null
|
not?:
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
| Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel>
|
||||||
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| Date
|
||||||
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
|
| string
|
||||||
}
|
| null;
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
|
||||||
|
_min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -9,7 +8,5 @@
|
|||||||
* 🟢 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,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -11,44 +10,53 @@
|
|||||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/client"
|
import * as runtime from '@prisma/client/runtime/client';
|
||||||
import type * as Prisma from "./prismaNamespace.ts"
|
import type * as Prisma from './prismaNamespace.ts';
|
||||||
|
|
||||||
|
|
||||||
const config: runtime.GetPrismaClientConfig = {
|
const config: runtime.GetPrismaClientConfig = {
|
||||||
"previewFeatures": [],
|
previewFeatures: [],
|
||||||
"clientVersion": "7.0.0",
|
clientVersion: '7.0.0',
|
||||||
"engineVersion": "0c19ccc313cf9911a90d99d2ac2eb0280c76c513",
|
engineVersion: '0c19ccc313cf9911a90d99d2ac2eb0280c76c513',
|
||||||
"activeProvider": "sqlite",
|
activeProvider: 'sqlite',
|
||||||
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n projectDir String @unique // 项目工作目录路径(必填)\n // Relations\n deployments Deployment[]\n pipelines Pipeline[]\n\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n username String\n login String\n email String\n avatar_url String?\n active Boolean @default(true)\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String @default(\"system\")\n updatedBy String @default(\"system\")\n}\n\nmodel Pipeline {\n id Int @id @default(autoincrement())\n name String\n description String?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n // Relations\n projectId Int?\n Project Project? @relation(fields: [projectId], references: [id])\n steps Step[]\n}\n\nmodel Step {\n id Int @id @default(autoincrement())\n name String\n order Int\n script String // 执行的脚本命令\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n pipelineId Int\n pipeline Pipeline @relation(fields: [pipelineId], references: [id])\n}\n\nmodel Deployment {\n id Int @id @default(autoincrement())\n branch String\n env String?\n status String // pending, running, success, failed, cancelled\n commitHash String?\n commitMessage String?\n buildLog String?\n sparseCheckoutPaths String? // 稀疏检出路径,用于monorepo项目\n startedAt DateTime @default(now())\n finishedAt DateTime?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n projectId Int\n Project Project? @relation(fields: [projectId], references: [id])\n pipelineId Int\n}\n",
|
inlineSchema:
|
||||||
"runtimeDataModel": {
|
'// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = "prisma-client"\n output = "../generated"\n}\n\ndatasource db {\n provider = "sqlite"\n}\n\nmodel Project {\n id Int @id @default(autoincrement())\n name String\n description String?\n repository String\n projectDir String @unique // 项目工作目录路径(必填)\n envPresets String? // 环境预设配置(JSON格式)\n // Relations\n deployments Deployment[]\n pipelines Pipeline[]\n\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n username String\n login String\n email String\n avatar_url String?\n active Boolean @default(true)\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String @default("system")\n updatedBy String @default("system")\n}\n\nmodel Pipeline {\n id Int @id @default(autoincrement())\n name String\n description String?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n // Relations\n projectId Int?\n Project Project? @relation(fields: [projectId], references: [id])\n steps Step[]\n}\n\nmodel Step {\n id Int @id @default(autoincrement())\n name String\n order Int\n script String // 执行的脚本命令\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n pipelineId Int\n pipeline Pipeline @relation(fields: [pipelineId], references: [id])\n}\n\nmodel Deployment {\n id Int @id @default(autoincrement())\n branch String\n envVars String? // 环境变量(JSON格式),统一存储所有配置\n status String // pending, running, success, failed, cancelled\n commitHash String?\n commitMessage String?\n buildLog String?\n sparseCheckoutPaths String? // 稀疏检出路径,用于monorepo项目\n startedAt DateTime @default(now())\n finishedAt DateTime?\n valid Int @default(1)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n createdBy String\n updatedBy String\n\n projectId Int\n Project Project? @relation(fields: [projectId], references: [id])\n pipelineId Int\n}\n',
|
||||||
"models": {},
|
runtimeDataModel: {
|
||||||
"enums": {},
|
models: {},
|
||||||
"types": {}
|
enums: {},
|
||||||
}
|
types: {},
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
|
||||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"Project\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"repository\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectDir\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"deployments\",\"kind\":\"object\",\"type\":\"Deployment\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelines\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToProject\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"login\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"avatar_url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Pipeline\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"PipelineToProject\"},{\"name\":\"steps\",\"kind\":\"object\",\"type\":\"Step\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Step\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"order\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"script\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"pipeline\",\"kind\":\"object\",\"type\":\"Pipeline\",\"relationName\":\"PipelineToStep\"}],\"dbName\":null},\"Deployment\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"branch\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"env\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitHash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"commitMessage\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"buildLog\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sparseCheckoutPaths\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"startedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"finishedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"valid\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"updatedBy\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"projectId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"Project\",\"kind\":\"object\",\"type\":\"Project\",\"relationName\":\"DeploymentToProject\"},{\"name\":\"pipelineId\",\"kind\":\"scalar\",\"type\":\"Int\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
config.runtimeDataModel = JSON.parse(
|
||||||
|
'{"models":{"Project":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"name","kind":"scalar","type":"String"},{"name":"description","kind":"scalar","type":"String"},{"name":"repository","kind":"scalar","type":"String"},{"name":"projectDir","kind":"scalar","type":"String"},{"name":"envPresets","kind":"scalar","type":"String"},{"name":"deployments","kind":"object","type":"Deployment","relationName":"DeploymentToProject"},{"name":"pipelines","kind":"object","type":"Pipeline","relationName":"PipelineToProject"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"}],"dbName":null},"User":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"username","kind":"scalar","type":"String"},{"name":"login","kind":"scalar","type":"String"},{"name":"email","kind":"scalar","type":"String"},{"name":"avatar_url","kind":"scalar","type":"String"},{"name":"active","kind":"scalar","type":"Boolean"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"}],"dbName":null},"Pipeline":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"name","kind":"scalar","type":"String"},{"name":"description","kind":"scalar","type":"String"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"},{"name":"projectId","kind":"scalar","type":"Int"},{"name":"Project","kind":"object","type":"Project","relationName":"PipelineToProject"},{"name":"steps","kind":"object","type":"Step","relationName":"PipelineToStep"}],"dbName":null},"Step":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"name","kind":"scalar","type":"String"},{"name":"order","kind":"scalar","type":"Int"},{"name":"script","kind":"scalar","type":"String"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"},{"name":"pipelineId","kind":"scalar","type":"Int"},{"name":"pipeline","kind":"object","type":"Pipeline","relationName":"PipelineToStep"}],"dbName":null},"Deployment":{"fields":[{"name":"id","kind":"scalar","type":"Int"},{"name":"branch","kind":"scalar","type":"String"},{"name":"envVars","kind":"scalar","type":"String"},{"name":"status","kind":"scalar","type":"String"},{"name":"commitHash","kind":"scalar","type":"String"},{"name":"commitMessage","kind":"scalar","type":"String"},{"name":"buildLog","kind":"scalar","type":"String"},{"name":"sparseCheckoutPaths","kind":"scalar","type":"String"},{"name":"startedAt","kind":"scalar","type":"DateTime"},{"name":"finishedAt","kind":"scalar","type":"DateTime"},{"name":"valid","kind":"scalar","type":"Int"},{"name":"createdAt","kind":"scalar","type":"DateTime"},{"name":"updatedAt","kind":"scalar","type":"DateTime"},{"name":"createdBy","kind":"scalar","type":"String"},{"name":"updatedBy","kind":"scalar","type":"String"},{"name":"projectId","kind":"scalar","type":"Int"},{"name":"Project","kind":"object","type":"Project","relationName":"DeploymentToProject"},{"name":"pipelineId","kind":"scalar","type":"Int"}],"dbName":null}},"enums":{},"types":{}}',
|
||||||
|
);
|
||||||
|
|
||||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
async function decodeBase64AsWasm(
|
||||||
const { Buffer } = await import('node:buffer')
|
wasmBase64: string,
|
||||||
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
): Promise<WebAssembly.Module> {
|
||||||
return new WebAssembly.Module(wasmArray)
|
const { Buffer } = await import('node:buffer');
|
||||||
|
const wasmArray = Buffer.from(wasmBase64, 'base64');
|
||||||
|
return new WebAssembly.Module(wasmArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.compilerWasm = {
|
config.compilerWasm = {
|
||||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.sqlite.mjs"),
|
getRuntime: async () =>
|
||||||
|
await import('@prisma/client/runtime/query_compiler_bg.sqlite.mjs'),
|
||||||
|
|
||||||
getQueryCompilerWasmModule: async () => {
|
getQueryCompilerWasmModule: async () => {
|
||||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs")
|
const { wasm } = await import(
|
||||||
return await decodeBase64AsWasm(wasm)
|
'@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs'
|
||||||
}
|
);
|
||||||
}
|
return await decodeBase64AsWasm(wasm);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||||
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
'log' extends keyof ClientOptions
|
||||||
|
? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
|
||||||
|
? Prisma.GetEvents<ClientOptions['log']>
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
export interface PrismaClientConstructor {
|
export interface PrismaClientConstructor {
|
||||||
/**
|
/**
|
||||||
@@ -68,9 +76,16 @@ export interface PrismaClientConstructor {
|
|||||||
new <
|
new <
|
||||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends {
|
||||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
omit: infer U;
|
||||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
}
|
||||||
|
? U
|
||||||
|
: Prisma.PrismaClientOptions['omit'],
|
||||||
|
ExtArgs extends
|
||||||
|
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||||
|
>(
|
||||||
|
options: Prisma.Subset<Options, Prisma.PrismaClientOptions>,
|
||||||
|
): PrismaClient<LogOpts, OmitOpts, ExtArgs>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,11 +105,17 @@ export interface PrismaClientConstructor {
|
|||||||
export interface PrismaClient<
|
export interface PrismaClient<
|
||||||
in LogOpts extends Prisma.LogLevel = never,
|
in LogOpts extends Prisma.LogLevel = never,
|
||||||
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
in out ExtArgs extends
|
||||||
|
runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||||
> {
|
> {
|
||||||
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] };
|
||||||
|
|
||||||
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
$on<V extends LogOpts>(
|
||||||
|
eventType: V,
|
||||||
|
callback: (
|
||||||
|
event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent,
|
||||||
|
) => void,
|
||||||
|
): PrismaClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect with the database
|
* Connect with the database
|
||||||
@@ -115,7 +136,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
$executeRaw<T = unknown>(
|
||||||
|
query: TemplateStringsArray | Prisma.Sql,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a raw query and returns the number of affected rows.
|
* Executes a raw query and returns the number of affected rows.
|
||||||
@@ -127,7 +151,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
$executeRawUnsafe<T = unknown>(
|
||||||
|
query: string,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a prepared raw query and returns the `SELECT` data.
|
* Performs a prepared raw query and returns the `SELECT` data.
|
||||||
@@ -138,7 +165,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
$queryRaw<T = unknown>(
|
||||||
|
query: TemplateStringsArray | Prisma.Sql,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a raw query and returns the `SELECT` data.
|
* Performs a raw query and returns the `SELECT` data.
|
||||||
@@ -150,8 +180,10 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
|
||||||
*/
|
*/
|
||||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
$queryRawUnsafe<T = unknown>(
|
||||||
|
query: string,
|
||||||
|
...values: any[]
|
||||||
|
): Prisma.PrismaPromise<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||||
@@ -166,13 +198,33 @@ export interface PrismaClient<
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||||
*/
|
*/
|
||||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
$transaction<P extends Prisma.PrismaPromise<any>[]>(
|
||||||
|
arg: [...P],
|
||||||
|
options?: { isolationLevel?: Prisma.TransactionIsolationLevel },
|
||||||
|
): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>;
|
||||||
|
|
||||||
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
$transaction<R>(
|
||||||
|
fn: (
|
||||||
|
prisma: Omit<PrismaClient, runtime.ITXClientDenyList>,
|
||||||
|
) => runtime.Types.Utils.JsPromise<R>,
|
||||||
|
options?: {
|
||||||
|
maxWait?: number;
|
||||||
|
timeout?: number;
|
||||||
|
isolationLevel?: Prisma.TransactionIsolationLevel;
|
||||||
|
},
|
||||||
|
): runtime.Types.Utils.JsPromise<R>;
|
||||||
|
|
||||||
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
$extends: runtime.Types.Extensions.ExtendsHook<
|
||||||
extArgs: ExtArgs
|
'extends',
|
||||||
}>>
|
Prisma.TypeMapCb<OmitOpts>,
|
||||||
|
ExtArgs,
|
||||||
|
runtime.Types.Utils.Call<
|
||||||
|
Prisma.TypeMapCb<OmitOpts>,
|
||||||
|
{
|
||||||
|
extArgs: ExtArgs;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `prisma.project`: Exposes CRUD operations for the **Project** model.
|
* `prisma.project`: Exposes CRUD operations for the **Project** model.
|
||||||
@@ -226,5 +278,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,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -15,61 +14,65 @@
|
|||||||
* model files in the `model` directory!
|
* model files in the `model` directory!
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
import * as runtime from '@prisma/client/runtime/index-browser';
|
||||||
|
|
||||||
export type * from '../models.ts'
|
export type * from '../models.ts';
|
||||||
export type * from './prismaNamespace.ts'
|
export type * from './prismaNamespace.ts';
|
||||||
|
|
||||||
export const Decimal = runtime.Decimal
|
|
||||||
|
|
||||||
|
export const Decimal = runtime.Decimal;
|
||||||
|
|
||||||
export const NullTypes = {
|
export const NullTypes = {
|
||||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
DbNull: runtime.NullTypes.DbNull as new (
|
||||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
secret: never,
|
||||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
) => typeof runtime.DbNull,
|
||||||
}
|
JsonNull: runtime.NullTypes.JsonNull as new (
|
||||||
|
secret: never,
|
||||||
|
) => typeof runtime.JsonNull,
|
||||||
|
AnyNull: runtime.NullTypes.AnyNull as new (
|
||||||
|
secret: never,
|
||||||
|
) => typeof runtime.AnyNull,
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const DbNull = runtime.DbNull
|
export const DbNull = runtime.DbNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const JsonNull = runtime.JsonNull
|
export const JsonNull = runtime.JsonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const AnyNull = runtime.AnyNull
|
export const AnyNull = runtime.AnyNull;
|
||||||
|
|
||||||
|
|
||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
Project: 'Project',
|
Project: 'Project',
|
||||||
User: 'User',
|
User: 'User',
|
||||||
Pipeline: 'Pipeline',
|
Pipeline: 'Pipeline',
|
||||||
Step: 'Step',
|
Step: 'Step',
|
||||||
Deployment: 'Deployment'
|
Deployment: 'Deployment',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enums
|
* Enums
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const TransactionIsolationLevel = {
|
export const TransactionIsolationLevel = {
|
||||||
Serializable: 'Serializable'
|
Serializable: 'Serializable',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel =
|
||||||
|
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel];
|
||||||
|
|
||||||
export const ProjectScalarFieldEnum = {
|
export const ProjectScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -77,15 +80,16 @@ export const ProjectScalarFieldEnum = {
|
|||||||
description: 'description',
|
description: 'description',
|
||||||
repository: 'repository',
|
repository: 'repository',
|
||||||
projectDir: 'projectDir',
|
projectDir: 'projectDir',
|
||||||
|
envPresets: 'envPresets',
|
||||||
valid: 'valid',
|
valid: 'valid',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy'
|
updatedBy: 'updatedBy',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type ProjectScalarFieldEnum = (typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type ProjectScalarFieldEnum =
|
||||||
|
(typeof ProjectScalarFieldEnum)[keyof typeof ProjectScalarFieldEnum];
|
||||||
|
|
||||||
export const UserScalarFieldEnum = {
|
export const UserScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -98,11 +102,11 @@ export const UserScalarFieldEnum = {
|
|||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy'
|
updatedBy: 'updatedBy',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type UserScalarFieldEnum =
|
||||||
|
(typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum];
|
||||||
|
|
||||||
export const PipelineScalarFieldEnum = {
|
export const PipelineScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -113,11 +117,11 @@ export const PipelineScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy',
|
updatedBy: 'updatedBy',
|
||||||
projectId: 'projectId'
|
projectId: 'projectId',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type PipelineScalarFieldEnum = (typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type PipelineScalarFieldEnum =
|
||||||
|
(typeof PipelineScalarFieldEnum)[keyof typeof PipelineScalarFieldEnum];
|
||||||
|
|
||||||
export const StepScalarFieldEnum = {
|
export const StepScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
@@ -129,16 +133,16 @@ export const StepScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy',
|
updatedBy: 'updatedBy',
|
||||||
pipelineId: 'pipelineId'
|
pipelineId: 'pipelineId',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type StepScalarFieldEnum = (typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type StepScalarFieldEnum =
|
||||||
|
(typeof StepScalarFieldEnum)[keyof typeof StepScalarFieldEnum];
|
||||||
|
|
||||||
export const DeploymentScalarFieldEnum = {
|
export const DeploymentScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
branch: 'branch',
|
branch: 'branch',
|
||||||
env: 'env',
|
envVars: 'envVars',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
commitHash: 'commitHash',
|
commitHash: 'commitHash',
|
||||||
commitMessage: 'commitMessage',
|
commitMessage: 'commitMessage',
|
||||||
@@ -152,24 +156,22 @@ export const DeploymentScalarFieldEnum = {
|
|||||||
createdBy: 'createdBy',
|
createdBy: 'createdBy',
|
||||||
updatedBy: 'updatedBy',
|
updatedBy: 'updatedBy',
|
||||||
projectId: 'projectId',
|
projectId: 'projectId',
|
||||||
pipelineId: 'pipelineId'
|
pipelineId: 'pipelineId',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type DeploymentScalarFieldEnum = (typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type DeploymentScalarFieldEnum =
|
||||||
|
(typeof DeploymentScalarFieldEnum)[keyof typeof DeploymentScalarFieldEnum];
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
|
||||||
|
|
||||||
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder];
|
||||||
|
|
||||||
export const NullsOrder = {
|
export const NullsOrder = {
|
||||||
first: 'first',
|
first: 'first',
|
||||||
last: 'last'
|
last: 'last',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
|
||||||
|
|
||||||
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder];
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
export type * from './commonInputTypes.ts';
|
||||||
|
export type * from './models/Deployment.ts';
|
||||||
|
export type * from './models/Pipeline.ts';
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -8,9 +10,6 @@
|
|||||||
*
|
*
|
||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
export type * from './models/Project.ts'
|
export type * from './models/Project.ts';
|
||||||
export type * from './models/User.ts'
|
export type * from './models/Step.ts';
|
||||||
export type * from './models/Pipeline.ts'
|
export type * from './models/User.ts';
|
||||||
export type * from './models/Step.ts'
|
|
||||||
export type * from './models/Deployment.ts'
|
|
||||||
export type * from './commonInputTypes.ts'
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
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>();
|
||||||
|
|
||||||
@@ -40,14 +42,14 @@ export class ExecutionQueue {
|
|||||||
* 初始化执行队列,包括恢复未完成的任务
|
* 初始化执行队列,包括恢复未完成的任务
|
||||||
*/
|
*/
|
||||||
public async initialize(): Promise<void> {
|
public async initialize(): Promise<void> {
|
||||||
console.log('Initializing execution queue...');
|
log.info(TAG, 'Initializing execution queue...');
|
||||||
// 恢复未完成的任务
|
// 恢复未完成的任务
|
||||||
await this.recoverPendingDeployments();
|
await this.recoverPendingDeployments();
|
||||||
|
|
||||||
// 启动定时轮询
|
// 启动定时轮询
|
||||||
this.startPolling();
|
this.startPolling();
|
||||||
|
|
||||||
console.log('Execution queue initialized');
|
log.info(TAG, 'Execution queue initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +57,7 @@ export class ExecutionQueue {
|
|||||||
*/
|
*/
|
||||||
private async recoverPendingDeployments(): Promise<void> {
|
private async recoverPendingDeployments(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Recovering pending deployments from database...');
|
log.info(TAG, 'Recovering pending deployments from database...');
|
||||||
|
|
||||||
// 查询数据库中状态为pending的部署任务
|
// 查询数据库中状态为pending的部署任务
|
||||||
const pendingDeployments = await prisma.deployment.findMany({
|
const pendingDeployments = await prisma.deployment.findMany({
|
||||||
@@ -69,16 +71,16 @@ export class ExecutionQueue {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Found ${pendingDeployments.length} pending deployments`);
|
log.info(TAG, `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);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Pending deployments recovery completed');
|
log.info(TAG, 'Pending deployments recovery completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to recover pending deployments:', error);
|
log.error(TAG, 'Failed to recover pending deployments:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +89,12 @@ export class ExecutionQueue {
|
|||||||
*/
|
*/
|
||||||
private startPolling(): void {
|
private startPolling(): void {
|
||||||
if (this.isPolling) {
|
if (this.isPolling) {
|
||||||
console.log('Polling is already running');
|
log.info(TAG, 'Polling is already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isPolling = true;
|
this.isPolling = true;
|
||||||
console.log(`Starting polling with interval ${POLLING_INTERVAL}ms`);
|
log.info(TAG, `Starting polling with interval ${POLLING_INTERVAL}ms`);
|
||||||
|
|
||||||
// 立即执行一次检查
|
// 立即执行一次检查
|
||||||
this.checkPendingDeployments();
|
this.checkPendingDeployments();
|
||||||
@@ -111,7 +113,7 @@ export class ExecutionQueue {
|
|||||||
clearInterval(pollingTimer);
|
clearInterval(pollingTimer);
|
||||||
pollingTimer = null;
|
pollingTimer = null;
|
||||||
this.isPolling = false;
|
this.isPolling = false;
|
||||||
console.log('Polling stopped');
|
log.info(TAG, 'Polling stopped');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ export class ExecutionQueue {
|
|||||||
*/
|
*/
|
||||||
private async checkPendingDeployments(): Promise<void> {
|
private async checkPendingDeployments(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Checking for pending deployments in database...');
|
log.info(TAG, 'Checking for pending deployments in database...');
|
||||||
|
|
||||||
// 查询数据库中状态为pending的部署任务
|
// 查询数据库中状态为pending的部署任务
|
||||||
const pendingDeployments = await prisma.deployment.findMany({
|
const pendingDeployments = await prisma.deployment.findMany({
|
||||||
@@ -134,7 +136,8 @@ export class ExecutionQueue {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
log.info(
|
||||||
|
TAG,
|
||||||
`Found ${pendingDeployments.length} pending deployments in polling`,
|
`Found ${pendingDeployments.length} pending deployments in polling`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,14 +145,15 @@ export class ExecutionQueue {
|
|||||||
for (const deployment of pendingDeployments) {
|
for (const deployment of pendingDeployments) {
|
||||||
// 检查是否已经在运行队列中
|
// 检查是否已经在运行队列中
|
||||||
if (!runningDeployments.has(deployment.id)) {
|
if (!runningDeployments.has(deployment.id)) {
|
||||||
console.log(
|
log.info(
|
||||||
|
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) {
|
||||||
console.error('Failed to check pending deployments:', error);
|
log.error(TAG, 'Failed to check pending deployments:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +168,7 @@ export class ExecutionQueue {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 检查是否已经在运行队列中
|
// 检查是否已经在运行队列中
|
||||||
if (runningDeployments.has(deploymentId)) {
|
if (runningDeployments.has(deploymentId)) {
|
||||||
console.log(`Deployment ${deploymentId} is already queued or running`);
|
log.info(TAG, `Deployment ${deploymentId} is already queued or running`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +198,7 @@ export class ExecutionQueue {
|
|||||||
// 执行流水线
|
// 执行流水线
|
||||||
await this.executePipeline(task.deploymentId, task.pipelineId);
|
await this.executePipeline(task.deploymentId, task.pipelineId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('执行流水线失败:', error);
|
log.error(TAG, '执行流水线失败:', error);
|
||||||
// 这里可以添加更多的错误处理逻辑
|
// 这里可以添加更多的错误处理逻辑
|
||||||
} finally {
|
} finally {
|
||||||
// 从运行队列中移除
|
// 从运行队列中移除
|
||||||
@@ -245,7 +249,7 @@ export class ExecutionQueue {
|
|||||||
);
|
);
|
||||||
await runner.run(pipelineId);
|
await runner.run(pipelineId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('执行流水线失败:', error);
|
log.error(TAG, '执行流水线失败:', 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { log } from './logger.ts';
|
||||||
|
|
||||||
|
const TAG = 'Gitea';
|
||||||
|
|
||||||
interface TokenResponse {
|
interface TokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
@@ -38,15 +42,13 @@ class Gitea {
|
|||||||
clientId: process.env.GITEA_CLIENT_ID!,
|
clientId: process.env.GITEA_CLIENT_ID!,
|
||||||
clientSecret: process.env.GITEA_CLIENT_SECRET!,
|
clientSecret: process.env.GITEA_CLIENT_SECRET!,
|
||||||
redirectUri: process.env.GITEA_REDIRECT_URI!,
|
redirectUri: process.env.GITEA_REDIRECT_URI!,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getToken(code: string) {
|
async getToken(code: string) {
|
||||||
const { giteaUrl, clientId, clientSecret, redirectUri } = this.config;
|
const { giteaUrl, clientId, clientSecret, redirectUri } = this.config;
|
||||||
console.log('this.config', this.config);
|
log.debug(TAG, 'Gitea token request started');
|
||||||
const response = await fetch(
|
const response = await fetch(`${giteaUrl}/login/oauth/access_token`, {
|
||||||
`${giteaUrl}/login/oauth/access_token`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -56,10 +58,15 @@ class Gitea {
|
|||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
}),
|
}),
|
||||||
},
|
});
|
||||||
);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.log(await response.json());
|
const payload = await response.json().catch(() => null as unknown);
|
||||||
|
log.error(
|
||||||
|
TAG,
|
||||||
|
'Gitea token request failed: status=%d payload=%o',
|
||||||
|
response.status,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
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;
|
||||||
@@ -108,19 +115,23 @@ class Gitea {
|
|||||||
* @param accessToken 访问令牌
|
* @param accessToken 访问令牌
|
||||||
* @param sha 分支名称或提交SHA
|
* @param sha 分支名称或提交SHA
|
||||||
*/
|
*/
|
||||||
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) {
|
async getCommits(
|
||||||
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
accessToken: string,
|
||||||
|
sha?: string,
|
||||||
|
) {
|
||||||
|
const url = new URL(
|
||||||
|
`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`,
|
||||||
|
);
|
||||||
if (sha) {
|
if (sha) {
|
||||||
url.searchParams.append('sha', sha);
|
url.searchParams.append('sha', sha);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(url.toString(), {
|
||||||
url.toString(),
|
|
||||||
{
|
|
||||||
method: 'GET',
|
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}`);
|
||||||
}
|
}
|
||||||
@@ -133,7 +144,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,4 +1,7 @@
|
|||||||
import { prisma } from './prisma.ts';
|
import { prisma } from './prisma.ts';
|
||||||
|
import { log } from './logger.ts';
|
||||||
|
|
||||||
|
const TAG = 'PipelineTemplate';
|
||||||
|
|
||||||
// 默认流水线模板
|
// 默认流水线模板
|
||||||
export interface PipelineTemplate {
|
export interface PipelineTemplate {
|
||||||
@@ -17,11 +20,6 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
|||||||
name: 'Git Clone Pipeline',
|
name: 'Git Clone Pipeline',
|
||||||
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
||||||
steps: [
|
steps: [
|
||||||
{
|
|
||||||
name: 'Clone Repository',
|
|
||||||
order: 0,
|
|
||||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Install Dependencies',
|
name: 'Install Dependencies',
|
||||||
order: 1,
|
order: 1,
|
||||||
@@ -36,73 +34,43 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
|||||||
name: 'Build Project',
|
name: 'Build Project',
|
||||||
order: 3,
|
order: 3,
|
||||||
script: '# 构建项目\nnpm run build',
|
script: '# 构建项目\nnpm run build',
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
name: 'Sparse Checkout Pipeline',
|
|
||||||
description: '稀疏检出流水线,适用于monorepo项目,只获取指定目录的代码',
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
name: 'Sparse Checkout Repository',
|
|
||||||
order: 0,
|
|
||||||
script: '# 进行稀疏检出指定目录的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit config core.sparseCheckout true\necho "$SPARSE_CHECKOUT_PATHS" > .git/info/sparse-checkout\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Install Dependencies',
|
|
||||||
order: 1,
|
|
||||||
script: '# 安装项目依赖\nnpm install',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Run Tests',
|
|
||||||
order: 2,
|
|
||||||
script: '# 运行测试\nnpm test',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Build Project',
|
|
||||||
order: 3,
|
|
||||||
script: '# 构建项目\nnpm run build',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Simple Deploy Pipeline',
|
name: 'Simple Deploy Pipeline',
|
||||||
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
||||||
steps: [
|
steps: [
|
||||||
{
|
|
||||||
name: 'Clone Repository',
|
|
||||||
order: 0,
|
|
||||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Build and Deploy',
|
name: 'Build and Deploy',
|
||||||
order: 1,
|
order: 1,
|
||||||
script: '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
script:
|
||||||
}
|
'# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
||||||
]
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化系统默认流水线模板
|
* 初始化系统默认流水线模板
|
||||||
*/
|
*/
|
||||||
export async function initializePipelineTemplates(): Promise<void> {
|
export async function initializePipelineTemplates(): Promise<void> {
|
||||||
console.log('Initializing pipeline templates...');
|
log.info(TAG, '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) {
|
||||||
console.log('Creating default pipeline templates...');
|
log.info(TAG, 'Creating default pipeline templates...');
|
||||||
|
|
||||||
for (const template of DEFAULT_PIPELINE_TEMPLATES) {
|
for (const template of DEFAULT_PIPELINE_TEMPLATES) {
|
||||||
// 创建模板流水线(使用负数ID表示模板)
|
// 创建模板流水线(使用负数ID表示模板)
|
||||||
@@ -113,8 +81,8 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1,
|
valid: 1,
|
||||||
projectId: null // 模板不属于任何特定项目
|
projectId: null, // 模板不属于任何特定项目
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建模板步骤
|
// 创建模板步骤
|
||||||
@@ -127,20 +95,23 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
pipelineId: pipeline.id,
|
pipelineId: pipeline.id,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1
|
valid: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Created template: ${template.name}`);
|
log.info(TAG, `Created template: ${template.name}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Pipeline templates already exist, skipping initialization');
|
log.info(
|
||||||
|
TAG,
|
||||||
|
'Pipeline templates already exist, skipping initialization',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Pipeline templates initialization completed');
|
log.info(TAG, 'Pipeline templates initialization completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize pipeline templates:', error);
|
log.error(TAG, 'Failed to initialize pipeline templates:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,28 +119,30 @@ export async function initializePipelineTemplates(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* 获取所有可用的流水线模板
|
* 获取所有可用的流水线模板
|
||||||
*/
|
*/
|
||||||
export async function getAvailableTemplates(): Promise<Array<{id: number, name: string, description: string}>> {
|
export async function getAvailableTemplates(): Promise<
|
||||||
|
Array<{ id: number; name: string; description: string }>
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const templates = await prisma.pipeline.findMany({
|
const templates = await prisma.pipeline.findMany({
|
||||||
where: {
|
where: {
|
||||||
projectId: null, // 模板流水线没有关联的项目
|
projectId: null, // 模板流水线没有关联的项目
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
description: true
|
description: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理可能为null的description字段
|
// 处理可能为null的description字段
|
||||||
return templates.map(template => ({
|
return templates.map((template) => ({
|
||||||
id: template.id,
|
id: template.id,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
description: template.description || ''
|
description: template.description || '',
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get pipeline templates:', error);
|
log.error(TAG, 'Failed to get pipeline templates:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +158,7 @@ export async function createPipelineFromTemplate(
|
|||||||
templateId: number,
|
templateId: number,
|
||||||
projectId: number,
|
projectId: number,
|
||||||
pipelineName: string,
|
pipelineName: string,
|
||||||
pipelineDescription: string
|
pipelineDescription: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
// 获取模板流水线及其步骤
|
// 获取模板流水线及其步骤
|
||||||
@@ -193,18 +166,18 @@ export async function createPipelineFromTemplate(
|
|||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
projectId: null, // 确保是模板流水线
|
projectId: null, // 确保是模板流水线
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
steps: {
|
steps: {
|
||||||
where: {
|
where: {
|
||||||
valid: 1
|
valid: 1,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
order: 'asc'
|
order: 'asc',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!templatePipeline) {
|
if (!templatePipeline) {
|
||||||
@@ -219,8 +192,8 @@ export async function createPipelineFromTemplate(
|
|||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1
|
valid: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 复制模板步骤到新流水线
|
// 复制模板步骤到新流水线
|
||||||
@@ -233,15 +206,18 @@ export async function createPipelineFromTemplate(
|
|||||||
pipelineId: newPipeline.id,
|
pipelineId: newPipeline.id,
|
||||||
createdBy: 'system',
|
createdBy: 'system',
|
||||||
updatedBy: 'system',
|
updatedBy: 'system',
|
||||||
valid: 1
|
valid: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Created pipeline from template ${templateId}: ${newPipeline.name}`);
|
log.info(
|
||||||
|
TAG,
|
||||||
|
`Created pipeline from template ${templateId}: ${newPipeline.name}`,
|
||||||
|
);
|
||||||
return newPipeline.id;
|
return newPipeline.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create pipeline from template:', error);
|
log.error(TAG, 'Failed to create pipeline from template:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import type Koa from 'koa';
|
|
||||||
import KoaRouter from '@koa/router';
|
import KoaRouter from '@koa/router';
|
||||||
import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts';
|
import type Koa from 'koa';
|
||||||
|
import {
|
||||||
|
getControllerPrefix,
|
||||||
|
getRouteMetadata,
|
||||||
|
type RouteMetadata,
|
||||||
|
} from '../decorators/route.ts';
|
||||||
import { createSuccessResponse } from '../middlewares/exception.ts';
|
import { createSuccessResponse } from '../middlewares/exception.ts';
|
||||||
|
import { log } from './logger.ts';
|
||||||
|
|
||||||
|
const TAG = 'RouteScanner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制器类型
|
* 控制器类型
|
||||||
@@ -33,7 +40,7 @@ export class RouteScanner {
|
|||||||
* 注册多个控制器类
|
* 注册多个控制器类
|
||||||
*/
|
*/
|
||||||
registerControllers(controllers: ControllerClass[]): void {
|
registerControllers(controllers: ControllerClass[]): void {
|
||||||
controllers.forEach(controller => this.registerController(controller));
|
controllers.forEach((controller) => this.registerController(controller));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,9 +57,12 @@ export class RouteScanner {
|
|||||||
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
|
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
|
||||||
|
|
||||||
// 注册每个路由
|
// 注册每个路由
|
||||||
routes.forEach(route => {
|
routes.forEach((route) => {
|
||||||
const fullPath = this.buildFullPath(controllerPrefix, route.path);
|
const fullPath = this.buildFullPath(controllerPrefix, route.path);
|
||||||
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey);
|
const handler = this.wrapControllerMethod(
|
||||||
|
controllerInstance,
|
||||||
|
route.propertyKey,
|
||||||
|
);
|
||||||
|
|
||||||
// 根据HTTP方法注册路由
|
// 根据HTTP方法注册路由
|
||||||
switch (route.method) {
|
switch (route.method) {
|
||||||
@@ -72,7 +82,7 @@ export class RouteScanner {
|
|||||||
this.router.patch(fullPath, handler);
|
this.router.patch(fullPath, handler);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`未支持的HTTP方法: ${route.method}`);
|
log.info(TAG, `未支持的HTTP方法: ${route.method}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -87,10 +97,10 @@ export class RouteScanner {
|
|||||||
|
|
||||||
let fullPath = '';
|
let fullPath = '';
|
||||||
if (cleanControllerPrefix) {
|
if (cleanControllerPrefix) {
|
||||||
fullPath += '/' + cleanControllerPrefix;
|
fullPath += `/${cleanControllerPrefix}`;
|
||||||
}
|
}
|
||||||
if (cleanRoutePath) {
|
if (cleanRoutePath) {
|
||||||
fullPath += '/' + cleanRoutePath;
|
fullPath += `/${cleanRoutePath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果路径为空,返回根路径
|
// 如果路径为空,返回根路径
|
||||||
@@ -105,11 +115,11 @@ export class RouteScanner {
|
|||||||
// 调用控制器方法
|
// 调用控制器方法
|
||||||
const method = instance[methodName];
|
const method = instance[methodName];
|
||||||
if (typeof method !== 'function') {
|
if (typeof method !== 'function') {
|
||||||
ctx.throw(401, 'Not Found')
|
ctx.throw(401, 'Not Found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定this并调用方法
|
// 绑定this并调用方法
|
||||||
const result = await method.call(instance, ctx, next) ?? null;
|
const result = (await method.call(instance, ctx, next)) ?? null;
|
||||||
|
|
||||||
ctx.body = createSuccessResponse(result);
|
ctx.body = createSuccessResponse(result);
|
||||||
};
|
};
|
||||||
@@ -133,19 +143,29 @@ export class RouteScanner {
|
|||||||
/**
|
/**
|
||||||
* 获取已注册的路由信息(用于调试)
|
* 获取已注册的路由信息(用于调试)
|
||||||
*/
|
*/
|
||||||
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> {
|
getRegisteredRoutes(): Array<{
|
||||||
const routes: Array<{ method: string; path: string; controller: string; action: string }> = [];
|
method: string;
|
||||||
|
path: string;
|
||||||
|
controller: string;
|
||||||
|
action: string;
|
||||||
|
}> {
|
||||||
|
const routes: Array<{
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
controller: string;
|
||||||
|
action: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
this.controllers.forEach(ControllerClass => {
|
this.controllers.forEach((ControllerClass) => {
|
||||||
const controllerPrefix = getControllerPrefix(ControllerClass);
|
const controllerPrefix = getControllerPrefix(ControllerClass);
|
||||||
const routeMetadata = getRouteMetadata(ControllerClass);
|
const routeMetadata = getRouteMetadata(ControllerClass);
|
||||||
|
|
||||||
routeMetadata.forEach(route => {
|
routeMetadata.forEach((route) => {
|
||||||
routes.push({
|
routes.push({
|
||||||
method: route.method,
|
method: route.method,
|
||||||
path: this.buildFullPath(controllerPrefix, route.path),
|
path: this.buildFullPath(controllerPrefix, route.path),
|
||||||
controller: ControllerClass.name,
|
controller: ControllerClass.name,
|
||||||
action: route.propertyKey
|
action: route.propertyKey,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import bodyParser from 'koa-bodyparser';
|
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
|
import bodyParser from 'koa-bodyparser';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { Middleware } from './types.ts';
|
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一响应体结构
|
* 统一响应体结构
|
||||||
@@ -58,15 +58,26 @@ export class Exception implements Middleware {
|
|||||||
const errorMessage = firstError?.message || '参数验证失败';
|
const errorMessage = firstError?.message || '参数验证失败';
|
||||||
const fieldPath = firstError?.path?.join('.') || 'unknown';
|
const fieldPath = firstError?.path?.join('.') || 'unknown';
|
||||||
|
|
||||||
log.info('Exception', 'Zod validation failed: %s at %s', errorMessage, fieldPath);
|
log.info(
|
||||||
this.sendResponse(ctx, 1003, errorMessage, {
|
'Exception',
|
||||||
|
'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,4 +1,5 @@
|
|||||||
import Koa, { type Context } from 'koa';
|
import type Koa from 'koa';
|
||||||
|
import type { Context } from 'koa';
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ export class HttpLogger implements Middleware {
|
|||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
await next();
|
await next();
|
||||||
const ms = Date.now() - start;
|
const ms = Date.now() - start;
|
||||||
log.info('HTTP', `${ctx.method} ${ctx.url} - ${ms}ms`)
|
log.info('HTTP', `${ctx.method} ${ctx.url} - ${ms}ms`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import KoaRouter from '@koa/router';
|
import KoaRouter from '@koa/router';
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
import type { Middleware } from './types.ts';
|
|
||||||
import { RouteScanner } from '../libs/route-scanner.ts';
|
|
||||||
import {
|
import {
|
||||||
ProjectController,
|
|
||||||
UserController,
|
|
||||||
AuthController,
|
AuthController,
|
||||||
DeploymentController,
|
DeploymentController,
|
||||||
|
GitController,
|
||||||
PipelineController,
|
PipelineController,
|
||||||
|
ProjectController,
|
||||||
StepController,
|
StepController,
|
||||||
GitController
|
UserController,
|
||||||
} from '../controllers/index.ts';
|
} from '../controllers/index.ts';
|
||||||
import { log } from '../libs/logger.ts';
|
import { log } from '../libs/logger.ts';
|
||||||
|
import { RouteScanner } from '../libs/route-scanner.ts';
|
||||||
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
export class Router implements Middleware {
|
export class Router implements Middleware {
|
||||||
private router: KoaRouter;
|
private router: KoaRouter;
|
||||||
@@ -45,7 +45,7 @@ export class Router implements Middleware {
|
|||||||
DeploymentController,
|
DeploymentController,
|
||||||
PipelineController,
|
PipelineController,
|
||||||
StepController,
|
StepController,
|
||||||
GitController
|
GitController,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 输出注册的路由信息
|
// 输出注册的路由信息
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import session from 'koa-session';
|
|
||||||
import type Koa from 'koa';
|
import type Koa from 'koa';
|
||||||
|
import session from 'koa-session';
|
||||||
import type { Middleware } from './types.ts';
|
import type { Middleware } from './types.ts';
|
||||||
|
|
||||||
export class Session implements Middleware {
|
export class Session implements Middleware {
|
||||||
apply(app: Koa): void {
|
apply(app: Koa): void {
|
||||||
app.keys = ['foka-ci'];
|
app.keys = ['mini-ci'];
|
||||||
app.use(
|
app.use(
|
||||||
session(
|
session(
|
||||||
{
|
{
|
||||||
key: 'foka.sid',
|
key: 'mini-ci.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,6 +16,7 @@ model Project {
|
|||||||
description String?
|
description String?
|
||||||
repository String
|
repository String
|
||||||
projectDir String @unique // 项目工作目录路径(必填)
|
projectDir String @unique // 项目工作目录路径(必填)
|
||||||
|
envPresets String? // 环境预设配置(JSON格式)
|
||||||
// Relations
|
// Relations
|
||||||
deployments Deployment[]
|
deployments Deployment[]
|
||||||
pipelines Pipeline[]
|
pipelines Pipeline[]
|
||||||
@@ -75,7 +76,7 @@ model Step {
|
|||||||
model Deployment {
|
model Deployment {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
branch String
|
branch String
|
||||||
env String?
|
envVars String? // 环境变量(JSON格式),统一存储所有配置
|
||||||
status String // pending, running, success, failed, cancelled
|
status String // pending, running, success, failed, cancelled
|
||||||
commitHash String?
|
commitHash String?
|
||||||
commitMessage String?
|
commitMessage String?
|
||||||
|
|||||||
@@ -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,11 +66,14 @@ 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 = `[${new Date().toISOString()}] 开始执行步骤 ${index + 1}/${pipeline.steps.length}: ${step.name}\n`;
|
const startLog = this.addTimestamp(
|
||||||
|
`${progress} 开始执行: ${step.name}`,
|
||||||
|
);
|
||||||
logs += startLog;
|
logs += startLog;
|
||||||
|
|
||||||
// 实时更新日志
|
// 实时更新日志
|
||||||
@@ -81,10 +84,10 @@ export class PipelineRunner {
|
|||||||
|
|
||||||
// 执行步骤
|
// 执行步骤
|
||||||
const stepLog = await this.executeStep(step, envVars);
|
const stepLog = await this.executeStep(step, envVars);
|
||||||
logs += stepLog + '\n';
|
logs += stepLog;
|
||||||
|
|
||||||
// 记录步骤执行完成的日志
|
// 记录步骤执行完成的日志
|
||||||
const endLog = `[${new Date().toISOString()}] 步骤 "${step.name}" 执行完成\n`;
|
const endLog = this.addTimestamp(`${progress} 执行完成: ${step.name}`);
|
||||||
logs += endLog;
|
logs += endLog;
|
||||||
|
|
||||||
// 实时更新日志
|
// 实时更新日志
|
||||||
@@ -93,9 +96,16 @@ 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) {
|
||||||
hasError = true;
|
const errorMsg = this.addTimestamp(`${(error as Error).message}`);
|
||||||
const errorMsg = `[${new Date().toISOString()}] Error: ${(error as Error).message}\n`;
|
|
||||||
logs += errorMsg;
|
logs += errorMsg;
|
||||||
|
|
||||||
log.error(
|
log.error(
|
||||||
@@ -116,18 +126,6 @@ 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(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,21 +139,20 @@ export class PipelineRunner {
|
|||||||
branch: string,
|
branch: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
let logs = '';
|
let logs = '';
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logs += `[${timestamp}] 检查工作目录状态: ${this.projectDir}\n`;
|
logs += this.addTimestamp(`检查工作目录状态: ${this.projectDir}`);
|
||||||
|
|
||||||
// 检查工作目录状态
|
// 检查工作目录状态
|
||||||
const status = await GitManager.checkWorkspaceStatus(this.projectDir);
|
const status = await GitManager.checkWorkspaceStatus(this.projectDir);
|
||||||
logs += `[${new Date().toISOString()}] 工作目录状态: ${status.status}\n`;
|
logs += this.addTimestamp(`工作目录状态: ${status.status}`);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
status.status === WorkspaceDirStatus.NOT_CREATED ||
|
status.status === WorkspaceDirStatus.NOT_CREATED ||
|
||||||
status.status === WorkspaceDirStatus.EMPTY
|
status.status === WorkspaceDirStatus.EMPTY
|
||||||
) {
|
) {
|
||||||
// 目录不存在或为空,需要克隆
|
// 目录不存在或为空,需要克隆
|
||||||
logs += `[${new Date().toISOString()}] 工作目录不存在或为空,开始克隆仓库\n`;
|
logs += this.addTimestamp('工作目录不存在或为空,开始克隆仓库');
|
||||||
|
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
await GitManager.ensureDirectory(this.projectDir);
|
await GitManager.ensureDirectory(this.projectDir);
|
||||||
@@ -168,7 +165,7 @@ export class PipelineRunner {
|
|||||||
// TODO: 添加 token 支持
|
// TODO: 添加 token 支持
|
||||||
);
|
);
|
||||||
|
|
||||||
logs += `[${new Date().toISOString()}] 仓库克隆成功\n`;
|
logs += this.addTimestamp('仓库克隆成功');
|
||||||
} else if (status.status === WorkspaceDirStatus.NO_GIT) {
|
} else if (status.status === WorkspaceDirStatus.NO_GIT) {
|
||||||
// 目录存在但不是 Git 仓库
|
// 目录存在但不是 Git 仓库
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -176,14 +173,16 @@ export class PipelineRunner {
|
|||||||
);
|
);
|
||||||
} else if (status.status === WorkspaceDirStatus.READY) {
|
} else if (status.status === WorkspaceDirStatus.READY) {
|
||||||
// 已存在 Git 仓库,更新代码
|
// 已存在 Git 仓库,更新代码
|
||||||
logs += `[${new Date().toISOString()}] 工作目录已存在 Git 仓库,开始更新代码\n`;
|
logs += this.addTimestamp('工作目录已存在 Git 仓库,开始更新代码');
|
||||||
await GitManager.updateRepository(this.projectDir, branch);
|
await GitManager.updateRepository(this.projectDir, branch);
|
||||||
logs += `[${new Date().toISOString()}] 代码更新成功\n`;
|
logs += this.addTimestamp('代码更新成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
return logs;
|
return logs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorLog = `[${new Date().toISOString()}] 准备工作目录失败: ${(error as Error).message}\n`;
|
const errorLog = this.addTimestamp(
|
||||||
|
`准备工作目录失败: ${(error as Error).message}`,
|
||||||
|
);
|
||||||
logs += errorLog;
|
logs += errorLog;
|
||||||
log.error(
|
log.error(
|
||||||
this.TAG,
|
this.TAG,
|
||||||
@@ -215,12 +214,18 @@ export class PipelineRunner {
|
|||||||
envVars.BRANCH_NAME = deployment.branch || '';
|
envVars.BRANCH_NAME = deployment.branch || '';
|
||||||
envVars.COMMIT_HASH = deployment.commitHash || '';
|
envVars.COMMIT_HASH = deployment.commitHash || '';
|
||||||
|
|
||||||
// 稀疏检出路径(如果有配置的话)
|
// 注入用户配置的环境变量
|
||||||
envVars.SPARSE_CHECKOUT_PATHS = deployment.sparseCheckoutPaths || '';
|
if (deployment.envVars) {
|
||||||
|
try {
|
||||||
|
const userEnvVars = JSON.parse(deployment.envVars);
|
||||||
|
Object.assign(envVars, userEnvVars);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(this.TAG, '解析环境变量失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 工作空间路径(使用配置的项目目录)
|
// 工作空间路径(使用配置的项目目录)
|
||||||
envVars.WORKSPACE = this.projectDir;
|
envVars.WORKSPACE = this.projectDir;
|
||||||
envVars.PROJECT_DIR = this.projectDir;
|
|
||||||
|
|
||||||
return envVars;
|
return envVars;
|
||||||
}
|
}
|
||||||
@@ -231,30 +236,9 @@ export class PipelineRunner {
|
|||||||
* @param isError 是否为错误日志
|
* @param isError 是否为错误日志
|
||||||
* @returns 带时间戳的日志消息
|
* @returns 带时间戳的日志消息
|
||||||
*/
|
*/
|
||||||
private addTimestamp(message: string, isError = false): string {
|
private addTimestamp(message: string): string {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
if (isError) {
|
return `[${timestamp}] [ERROR] ${message}\n`;
|
||||||
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'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,35 +252,21 @@ 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) {
|
||||||
// 为stdout中的每一行添加时间戳
|
logs += this.addTimestamp(`\n${result.stdout}`);
|
||||||
logs += this.addTimestampToLines(result.stdout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.stderr) {
|
if (result.stderr) {
|
||||||
// 为stderr中的每一行添加时间戳和错误标记
|
logs += this.addTimestamp(`\n${result.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: 'Foka CI',
|
title: 'Mini CI',
|
||||||
},
|
},
|
||||||
source: {
|
source: {
|
||||||
define: {
|
define: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useEffect, useCallback } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export function useAsyncEffect(
|
export function useAsyncEffect(
|
||||||
effect: () => Promise<void | (() => void)>,
|
effect: () => Promise<any | (() => void)>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
) {
|
) {
|
||||||
const callback = useCallback(effect, [...deps]);
|
const callback = useCallback(effect, [...deps]);
|
||||||
@@ -11,7 +11,7 @@ export function useAsyncEffect(
|
|||||||
const cleanupPromise = callback();
|
const cleanupPromise = callback();
|
||||||
return () => {
|
return () => {
|
||||||
if (cleanupPromise instanceof Promise) {
|
if (cleanupPromise instanceof Promise) {
|
||||||
cleanupPromise.then(cleanup => cleanup && cleanup());
|
cleanupPromise.then((cleanup) => cleanup?.());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [callback]);
|
}, [callback]);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
5
apps/web/src/pages/env/index.tsx
vendored
5
apps/web/src/pages/env/index.tsx
vendored
@@ -1,5 +0,0 @@
|
|||||||
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">Foka CI</h2>}
|
{!collapsed && <h2 className="ml-4 text-xl font-medium">Mini CI</h2>}
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<Menu
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -45,12 +45,7 @@ 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 '@shared';
|
import { net } from '../../utils';
|
||||||
import type { NavigateFunction } from 'react-router';
|
import type { NavigateFunction } from 'react-router';
|
||||||
import { useGlobalStore } from '../../stores/global';
|
import { useGlobalStore } from '../../stores/global';
|
||||||
import type { AuthLoginResponse, AuthURLResponse } from './types';
|
import type { AuthURL, User } from './types';
|
||||||
|
|
||||||
class LoginService {
|
class LoginService {
|
||||||
async getAuthUrl() {
|
async getAuthUrl() {
|
||||||
const { code, data } = await net.request<AuthURLResponse>({
|
const { code, data } = await net.request<AuthURL>({
|
||||||
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<AuthLoginResponse>({
|
const { data, code } = await net.request<User>({
|
||||||
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<AuthURLResponse>({
|
const { code } = await net.request<null>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/logout',
|
url: '/api/auth/logout',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { APIResponse } from '@shared';
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,8 +6,6 @@ export interface User {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthURLResponse = APIResponse<{
|
export interface AuthURL {
|
||||||
url: string;
|
url: string;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
export type AuthLoginResponse = APIResponse<User>;
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import {
|
import { Form, Input, Message, Modal, Select } from '@arco-design/web-react';
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Message,
|
|
||||||
Modal,
|
|
||||||
Select,
|
|
||||||
} from '@arco-design/web-react';
|
|
||||||
import { formatDateTime } from '../../../../utils/time';
|
|
||||||
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import type { Branch, Commit, Pipeline } from '../../types';
|
import { formatDateTime } from '../../../../utils/time';
|
||||||
|
import type { Branch, Commit, Pipeline, Project } from '../../types';
|
||||||
import { detailService } from '../service';
|
import { detailService } from '../service';
|
||||||
|
|
||||||
|
interface EnvPreset {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'select' | 'multiselect' | 'input';
|
||||||
|
required?: boolean;
|
||||||
|
options?: Array<{ label: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
interface DeployModalProps {
|
interface DeployModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onOk: () => void;
|
onOk: () => void;
|
||||||
pipelines: Pipeline[];
|
pipelines: Pipeline[];
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
project?: Project | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeployModal({
|
function DeployModal({
|
||||||
@@ -26,12 +27,29 @@ function DeployModal({
|
|||||||
onOk,
|
onOk,
|
||||||
pipelines,
|
pipelines,
|
||||||
projectId,
|
projectId,
|
||||||
|
project,
|
||||||
}: DeployModalProps) {
|
}: DeployModalProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [branches, setBranches] = useState<Branch[]>([]);
|
const [branches, setBranches] = useState<Branch[]>([]);
|
||||||
const [commits, setCommits] = useState<Commit[]>([]);
|
const [commits, setCommits] = useState<Commit[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [branchLoading, setBranchLoading] = useState(false);
|
const [branchLoading, setBranchLoading] = useState(false);
|
||||||
|
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
|
||||||
|
|
||||||
|
// 解析项目环境预设
|
||||||
|
useEffect(() => {
|
||||||
|
if (project?.envPresets) {
|
||||||
|
try {
|
||||||
|
const presets = JSON.parse(project.envPresets);
|
||||||
|
setEnvPresets(presets);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析环境预设失败:', error);
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
}, [project]);
|
||||||
|
|
||||||
const fetchCommits = useCallback(
|
const fetchCommits = useCallback(
|
||||||
async (branch: string) => {
|
async (branch: string) => {
|
||||||
@@ -91,16 +109,27 @@ function DeployModal({
|
|||||||
try {
|
try {
|
||||||
const values = await form.validate();
|
const values = await form.validate();
|
||||||
const selectedCommit = commits.find((c) => c.sha === values.commitHash);
|
const selectedCommit = commits.find((c) => c.sha === values.commitHash);
|
||||||
const selectedPipeline = pipelines.find((p) => p.id === values.pipelineId);
|
const selectedPipeline = pipelines.find(
|
||||||
|
(p) => p.id === values.pipelineId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!selectedCommit || !selectedPipeline) {
|
if (!selectedCommit || !selectedPipeline) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化环境变量
|
// 收集所有环境变量(从预设项中提取)
|
||||||
const env = values.envVars
|
const envVars: Record<string, string> = {};
|
||||||
?.map((item: { key: string; value: string }) => `${item.key}=${item.value}`)
|
for (const preset of envPresets) {
|
||||||
.join('\n');
|
const value = values[preset.key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
// 对于 multiselect,将数组转为逗号分隔的字符串
|
||||||
|
if (preset.type === 'multiselect' && Array.isArray(value)) {
|
||||||
|
envVars[preset.key] = value.join(',');
|
||||||
|
} else {
|
||||||
|
envVars[preset.key] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await detailService.createDeployment({
|
await detailService.createDeployment({
|
||||||
projectId,
|
projectId,
|
||||||
@@ -108,8 +137,7 @@ function DeployModal({
|
|||||||
branch: values.branch,
|
branch: values.branch,
|
||||||
commitHash: selectedCommit.sha,
|
commitHash: selectedCommit.sha,
|
||||||
commitMessage: selectedCommit.commit.message,
|
commitMessage: selectedCommit.commit.message,
|
||||||
env: env,
|
envVars, // 提交所有环境变量
|
||||||
sparseCheckoutPaths: values.sparseCheckoutPaths,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Message.success('部署任务已创建');
|
Message.success('部署任务已创建');
|
||||||
@@ -128,8 +156,15 @@ 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"
|
||||||
@@ -197,57 +232,86 @@ function DeployModal({
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="稀疏检出路径(用于monorepo项目,每行一个路径)"
|
|
||||||
field="sparseCheckoutPaths"
|
|
||||||
tooltip="在monorepo项目中,指定需要检出的目录路径,每行一个路径。留空则检出整个仓库。"
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
placeholder={`例如:\n/packages/frontend\n/packages/backend`}
|
|
||||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<div className="mb-2 font-medium text-gray-700">环境变量</div>
|
|
||||||
<Form.List field="envVars">
|
|
||||||
{(fields, { add, remove }) => (
|
|
||||||
<div>
|
|
||||||
{fields.map((item, index) => (
|
|
||||||
<div key={item.key} className="flex items-center gap-2 mb-2">
|
|
||||||
<Form.Item
|
|
||||||
field={`${item.field}.key`}
|
|
||||||
noStyle
|
|
||||||
rules={[{ required: true, message: '请输入变量名' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="变量名" />
|
|
||||||
</Form.Item>
|
|
||||||
<span className="text-gray-400">=</span>
|
|
||||||
<Form.Item
|
|
||||||
field={`${item.field}.value`}
|
|
||||||
noStyle
|
|
||||||
rules={[{ required: true, message: '请输入变量值' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="变量值" />
|
|
||||||
</Form.Item>
|
|
||||||
<Button
|
|
||||||
icon={<IconDelete />}
|
|
||||||
status="danger"
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
<Button
|
{/* 环境变量预设 */}
|
||||||
type="dashed"
|
{envPresets.length > 0 && (
|
||||||
long
|
<div>
|
||||||
onClick={() => add()}
|
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||||
icon={<IconPlus />}
|
环境变量
|
||||||
|
</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}`}>
|
||||||
</Button>
|
{preset.options.map((option) => (
|
||||||
|
<Select.Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.type === 'multiselect' && preset.options) {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={preset.key}
|
||||||
|
label={preset.label}
|
||||||
|
field={preset.key}
|
||||||
|
rules={
|
||||||
|
preset.required
|
||||||
|
? [{ required: true, message: `请选择${preset.label}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder={`请选择${preset.label}`}
|
||||||
|
allowClear
|
||||||
|
>
|
||||||
|
{preset.options.map((option) => (
|
||||||
|
<Select.Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset.type === 'input') {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={preset.key}
|
||||||
|
label={preset.label}
|
||||||
|
field={preset.key}
|
||||||
|
rules={
|
||||||
|
preset.required
|
||||||
|
? [{ required: true, message: `请输入${preset.label}` }]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input placeholder={`请输入${preset.label}`} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,17 +25,6 @@ function DeployRecordItem({
|
|||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 环境标签渲染函数
|
|
||||||
const getEnvTag = (env: string) => {
|
|
||||||
const envMap: Record<string, { color: string; text: string }> = {
|
|
||||||
production: { color: 'red', text: '生产环境' },
|
|
||||||
staging: { color: 'orange', text: '预发布环境' },
|
|
||||||
development: { color: 'blue', text: '开发环境' },
|
|
||||||
};
|
|
||||||
const config = envMap[env] || { color: 'gray', text: env };
|
|
||||||
return <Tag color={config.color}>{config.text}</Tag>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -68,9 +57,6 @@ function DeployRecordItem({
|
|||||||
分支:{' '}
|
分支:{' '}
|
||||||
<span className="font-medium text-gray-700">{item.branch}</span>
|
<span className="font-medium text-gray-700">{item.branch}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
环境: {getEnvTag(item.env || 'unknown')}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
状态: {getStatusTag(item.status)}
|
状态: {getStatusTag(item.status)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import { Button, Checkbox, Input, Select, Space } from '@arco-design/web-react';
|
||||||
|
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export interface EnvPreset {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: 'select' | 'multiselect' | 'input';
|
||||||
|
required?: boolean; // 是否必填
|
||||||
|
options?: Array<{ label: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnvPresetsEditorProps {
|
||||||
|
value?: EnvPreset[];
|
||||||
|
onChange?: (value: EnvPreset[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EnvPresetsEditor({ value = [], onChange }: EnvPresetsEditorProps) {
|
||||||
|
const [presets, setPresets] = useState<EnvPreset[]>(value);
|
||||||
|
|
||||||
|
// 当外部 value 变化时同步到内部状态
|
||||||
|
useEffect(() => {
|
||||||
|
setPresets(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleAddPreset = () => {
|
||||||
|
const newPreset: EnvPreset = {
|
||||||
|
key: '',
|
||||||
|
label: '',
|
||||||
|
type: 'select',
|
||||||
|
options: [{ label: '', value: '' }],
|
||||||
|
};
|
||||||
|
const newPresets = [...presets, newPreset];
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemovePreset = (index: number) => {
|
||||||
|
const newPresets = presets.filter((_, i) => i !== index);
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePresetChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof EnvPreset,
|
||||||
|
val: string | boolean | EnvPreset['type'] | EnvPreset['options'],
|
||||||
|
) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
newPresets[index] = { ...newPresets[index], [field]: val };
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddOption = (presetIndex: number) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
if (!newPresets[presetIndex].options) {
|
||||||
|
newPresets[presetIndex].options = [];
|
||||||
|
}
|
||||||
|
newPresets[presetIndex].options?.push({ label: '', value: '' });
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveOption = (presetIndex: number, optionIndex: number) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
newPresets[presetIndex].options = newPresets[presetIndex].options?.filter(
|
||||||
|
(_, i) => i !== optionIndex,
|
||||||
|
);
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOptionChange = (
|
||||||
|
presetIndex: number,
|
||||||
|
optionIndex: number,
|
||||||
|
field: 'label' | 'value',
|
||||||
|
val: string,
|
||||||
|
) => {
|
||||||
|
const newPresets = [...presets];
|
||||||
|
if (newPresets[presetIndex].options) {
|
||||||
|
newPresets[presetIndex].options![optionIndex][field] = val;
|
||||||
|
setPresets(newPresets);
|
||||||
|
onChange?.(newPresets);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{presets.map((preset, presetIndex) => (
|
||||||
|
<div
|
||||||
|
key={`preset-${preset.key || presetIndex}`}
|
||||||
|
className="border border-gray-200 rounded p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="font-medium text-gray-700">
|
||||||
|
预设项 #{presetIndex + 1}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
status="danger"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
onClick={() => handleRemovePreset(presetIndex)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="变量名 (key)"
|
||||||
|
value={preset.key}
|
||||||
|
onChange={(val) => handlePresetChange(presetIndex, 'key', val)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="显示名称 (label)"
|
||||||
|
value={preset.label}
|
||||||
|
onChange={(val) =>
|
||||||
|
handlePresetChange(presetIndex, 'label', val)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Select
|
||||||
|
placeholder="选择类型"
|
||||||
|
value={preset.type}
|
||||||
|
onChange={(val) => handlePresetChange(presetIndex, 'type', val)}
|
||||||
|
>
|
||||||
|
<Select.Option value="select">单选</Select.Option>
|
||||||
|
<Select.Option value="multiselect">多选</Select.Option>
|
||||||
|
<Select.Option value="input">输入框</Select.Option>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={preset.required || false}
|
||||||
|
onChange={(checked) =>
|
||||||
|
handlePresetChange(presetIndex, 'required', checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
必填项
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(preset.type === 'select' || preset.type === 'multiselect') && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="text-sm text-gray-600 mb-2">选项:</div>
|
||||||
|
{preset.options?.map((option, optionIndex) => (
|
||||||
|
<div
|
||||||
|
key={`option-${option.value || optionIndex}`}
|
||||||
|
className="flex items-center gap-2 mb-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder="显示文本"
|
||||||
|
value={option.label}
|
||||||
|
onChange={(val) =>
|
||||||
|
handleOptionChange(
|
||||||
|
presetIndex,
|
||||||
|
optionIndex,
|
||||||
|
'label',
|
||||||
|
val,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder="值"
|
||||||
|
value={option.value}
|
||||||
|
onChange={(val) =>
|
||||||
|
handleOptionChange(
|
||||||
|
presetIndex,
|
||||||
|
optionIndex,
|
||||||
|
'value',
|
||||||
|
val,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
status="danger"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
onClick={() =>
|
||||||
|
handleRemoveOption(presetIndex, optionIndex)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="dashed"
|
||||||
|
long
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => handleAddOption(presetIndex)}
|
||||||
|
>
|
||||||
|
添加选项
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button type="dashed" long icon={<IconPlus />} onClick={handleAddPreset}>
|
||||||
|
添加环境预设
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnvPresetsEditor;
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Message,
|
Message,
|
||||||
Modal,
|
Modal,
|
||||||
|
Pagination,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
IconCode,
|
IconCode,
|
||||||
|
IconCommand,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
@@ -49,9 +51,12 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
import { formatDateTime } from '../../../utils/time';
|
import { formatDateTime } from '../../../utils/time';
|
||||||
import type { Deployment, Pipeline, Project, Step, WorkspaceDirStatus, WorkspaceStatus } from '../types';
|
import type { Deployment, Pipeline, Project, Step } from '../types';
|
||||||
import DeployModal from './components/DeployModal';
|
import DeployModal from './components/DeployModal';
|
||||||
import DeployRecordItem from './components/DeployRecordItem';
|
import DeployRecordItem from './components/DeployRecordItem';
|
||||||
|
import EnvPresetsEditor, {
|
||||||
|
type EnvPreset,
|
||||||
|
} from './components/EnvPresetsEditor';
|
||||||
import PipelineStepItem from './components/PipelineStepItem';
|
import PipelineStepItem from './components/PipelineStepItem';
|
||||||
import { detailService } from './service';
|
import { detailService } from './service';
|
||||||
|
|
||||||
@@ -84,20 +89,32 @@ function ProjectDetailPage() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
||||||
const [editingPipeline, setEditingPipeline] = useState<PipelineWithEnabled | null>(null);
|
const [editingPipeline, setEditingPipeline] =
|
||||||
|
useState<PipelineWithEnabled | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [pipelineForm] = Form.useForm();
|
const [pipelineForm] = Form.useForm();
|
||||||
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
||||||
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>(null);
|
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(
|
||||||
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
null,
|
||||||
|
);
|
||||||
|
const [templates, setTemplates] = useState<
|
||||||
|
Array<{ id: number; name: string; description: string }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
// 项目设置相关状态
|
// 项目设置相关状态
|
||||||
const [projectEditModalVisible, setProjectEditModalVisible] = useState(false);
|
const [isEditingProject, setIsEditingProject] = useState(false);
|
||||||
const [projectForm] = Form.useForm();
|
const [projectForm] = Form.useForm();
|
||||||
|
const [envPresets, setEnvPresets] = useState<EnvPreset[]>([]);
|
||||||
|
const [envPresetsLoading, setEnvPresetsLoading] = useState(false);
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
@@ -142,10 +159,15 @@ function ProjectDetailPage() {
|
|||||||
|
|
||||||
// 获取部署记录
|
// 获取部署记录
|
||||||
try {
|
try {
|
||||||
const records = await detailService.getDeployments(Number(id));
|
const res = await detailService.getDeployments(
|
||||||
setDeployRecords(records);
|
Number(id),
|
||||||
if (records.length > 0) {
|
1,
|
||||||
setSelectedRecordId(records[0].id);
|
pagination.pageSize,
|
||||||
|
);
|
||||||
|
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);
|
||||||
@@ -164,26 +186,40 @@ function ProjectDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 定期轮询部署记录以更新状态和日志
|
// 定期轮询部署记录以更新状态和日志
|
||||||
useAsyncEffect(async () => {
|
useEffect(() => {
|
||||||
const interval = setInterval(async () => {
|
if (!id) return;
|
||||||
if (id) {
|
|
||||||
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
const records = await detailService.getDeployments(Number(id));
|
const res = await detailService.getDeployments(
|
||||||
setDeployRecords(records);
|
Number(id),
|
||||||
|
pagination.current,
|
||||||
|
pagination.pageSize,
|
||||||
|
);
|
||||||
|
setDeployRecords(res.list);
|
||||||
|
setPagination((prev) => ({ ...prev, total: res.total }));
|
||||||
|
|
||||||
// 如果当前选中的记录正在运行,则更新选中记录
|
// 如果当前选中的记录正在运行,则更新选中记录
|
||||||
const selectedRecord = records.find((r: Deployment) => r.id === selectedRecordId);
|
const selectedRecord = res.list.find(
|
||||||
if (selectedRecord && (selectedRecord.status === 'running' || selectedRecord.status === 'pending')) {
|
(r: Deployment) => r.id === selectedRecordId,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
selectedRecord &&
|
||||||
|
(selectedRecord.status === 'running' ||
|
||||||
|
selectedRecord.status === 'pending')
|
||||||
|
) {
|
||||||
// 保持当前选中状态,但更新数据
|
// 保持当前选中状态,但更新数据
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('轮询部署记录失败:', error);
|
console.error('轮询部署记录失败:', error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, 3000); // 每3秒轮询一次
|
|
||||||
|
poll(); // 立即执行一次
|
||||||
|
const interval = setInterval(poll, 3000); // 每3秒轮询一次
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [id, selectedRecordId]);
|
}, [id, selectedRecordId, pagination.current, pagination.pageSize]);
|
||||||
|
|
||||||
// 触发部署
|
// 触发部署
|
||||||
const handleDeploy = () => {
|
const handleDeploy = () => {
|
||||||
@@ -354,13 +390,14 @@ function ProjectDetailPage() {
|
|||||||
selectedTemplateId,
|
selectedTemplateId,
|
||||||
Number(id),
|
Number(id),
|
||||||
values.name,
|
values.name,
|
||||||
values.description || ''
|
values.description || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新本地状态 - 需要转换步骤数据结构
|
// 更新本地状态 - 需要转换步骤数据结构
|
||||||
const transformedSteps = newPipeline.steps?.map(step => ({
|
const transformedSteps =
|
||||||
|
newPipeline.steps?.map((step) => ({
|
||||||
...step,
|
...step,
|
||||||
enabled: step.valid === 1
|
enabled: step.valid === 1,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const pipelineWithDefaults = {
|
const pipelineWithDefaults = {
|
||||||
@@ -583,8 +620,13 @@ function ProjectDetailPage() {
|
|||||||
|
|
||||||
// 刷新部署记录
|
// 刷新部署记录
|
||||||
if (id) {
|
if (id) {
|
||||||
const records = await detailService.getDeployments(Number(id));
|
const res = await detailService.getDeployments(
|
||||||
setDeployRecords(records);
|
Number(id),
|
||||||
|
pagination.current,
|
||||||
|
pagination.pageSize,
|
||||||
|
);
|
||||||
|
setDeployRecords(res.list);
|
||||||
|
setPagination((prev) => ({ ...prev, total: res.total }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重新执行部署失败:', error);
|
console.error('重新执行部署失败:', error);
|
||||||
@@ -592,6 +634,21 @@ function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 解析环境变量预设
|
||||||
|
useEffect(() => {
|
||||||
|
if (detail?.envPresets) {
|
||||||
|
try {
|
||||||
|
const presets = JSON.parse(detail.envPresets);
|
||||||
|
setEnvPresets(presets);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析环境变量预设失败:', error);
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEnvPresets([]);
|
||||||
|
}
|
||||||
|
}, [detail]);
|
||||||
|
|
||||||
// 项目设置相关函数
|
// 项目设置相关函数
|
||||||
const handleEditProject = () => {
|
const handleEditProject = () => {
|
||||||
if (detail) {
|
if (detail) {
|
||||||
@@ -600,16 +657,21 @@ function ProjectDetailPage() {
|
|||||||
description: detail.description,
|
description: detail.description,
|
||||||
repository: detail.repository,
|
repository: detail.repository,
|
||||||
});
|
});
|
||||||
setProjectEditModalVisible(true);
|
setIsEditingProject(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProjectEditSuccess = async () => {
|
const handleCancelEditProject = () => {
|
||||||
|
setIsEditingProject(false);
|
||||||
|
projectForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveProject = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await projectForm.validate();
|
const values = await projectForm.validate();
|
||||||
await detailService.updateProject(Number(id), values);
|
await detailService.updateProject(Number(id), values);
|
||||||
Message.success('项目更新成功');
|
Message.success('项目更新成功');
|
||||||
setProjectEditModalVisible(false);
|
setIsEditingProject(false);
|
||||||
|
|
||||||
// 刷新项目详情
|
// 刷新项目详情
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -622,6 +684,27 @@ function ProjectDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveEnvPresets = async () => {
|
||||||
|
try {
|
||||||
|
setEnvPresetsLoading(true);
|
||||||
|
await detailService.updateProject(Number(id), {
|
||||||
|
envPresets: JSON.stringify(envPresets),
|
||||||
|
});
|
||||||
|
Message.success('环境变量预设保存成功');
|
||||||
|
|
||||||
|
// 刷新项目详情
|
||||||
|
if (id) {
|
||||||
|
const projectDetail = await detailService.getProjectDetail(Number(id));
|
||||||
|
setDetail(projectDetail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存环境变量预设失败:', error);
|
||||||
|
Message.error('保存环境变量预设失败');
|
||||||
|
} finally {
|
||||||
|
setEnvPresetsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteProject = () => {
|
const handleDeleteProject = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '删除项目',
|
title: '删除项目',
|
||||||
@@ -666,26 +749,13 @@ 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 = (status: string): { text: string; color: string } => {
|
const getWorkspaceStatusTag = (
|
||||||
|
status: string,
|
||||||
|
): { text: string; color: string } => {
|
||||||
const statusMap: Record<string, { text: string; color: string }> = {
|
const statusMap: Record<string, { text: string; color: string }> = {
|
||||||
not_created: { text: '未创建', color: 'gray' },
|
not_created: { text: '未创建', color: 'gray' },
|
||||||
empty: { text: '空目录', color: 'orange' },
|
empty: { text: '空目录', color: 'orange' },
|
||||||
@@ -703,7 +773,15 @@ function ProjectDetailPage() {
|
|||||||
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mb-6" title={<Space><IconFolder />工作目录状态</Space>}>
|
<Card
|
||||||
|
className="mb-6"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconFolder />
|
||||||
|
工作目录状态
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Descriptions
|
<Descriptions
|
||||||
column={2}
|
column={2}
|
||||||
data={[
|
data={[
|
||||||
@@ -715,10 +793,6 @@ 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 || '-',
|
||||||
@@ -726,17 +800,25 @@ function ProjectDetailPage() {
|
|||||||
{
|
{
|
||||||
label: '最后提交',
|
label: '最后提交',
|
||||||
value: workspaceStatus.gitInfo?.lastCommit ? (
|
value: workspaceStatus.gitInfo?.lastCommit ? (
|
||||||
<Space direction="vertical" size="mini">
|
<Space size="small">
|
||||||
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text>
|
<Typography.Text code>
|
||||||
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text>
|
{workspaceStatus.gitInfo.lastCommit}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{workspaceStatus.gitInfo.lastCommitMessage}
|
||||||
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
) : '-',
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{workspaceStatus.error && (
|
{workspaceStatus.error && (
|
||||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
||||||
<Typography.Text type="danger">{workspaceStatus.error}</Typography.Text>
|
<Typography.Text type="error">
|
||||||
|
{workspaceStatus.error}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -763,7 +845,15 @@ function ProjectDetailPage() {
|
|||||||
size="large"
|
size="large"
|
||||||
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
||||||
>
|
>
|
||||||
<Tabs.TabPane title={<Space><IconHistory />部署记录</Space>} key="deployRecords">
|
<Tabs.TabPane
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconHistory />
|
||||||
|
部署记录
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
key="deployRecords"
|
||||||
|
>
|
||||||
<div className="grid grid-cols-5 gap-6 h-full">
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
{/* 左侧部署记录列表 */}
|
{/* 左侧部署记录列表 */}
|
||||||
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
||||||
@@ -775,7 +865,8 @@ function ProjectDetailPage() {
|
|||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0 flex flex-col">
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
{deployRecords.length > 0 ? (
|
{deployRecords.length > 0 ? (
|
||||||
<List
|
<List
|
||||||
className="bg-white rounded-lg border"
|
className="bg-white rounded-lg border"
|
||||||
@@ -789,6 +880,19 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-2 text-right">
|
||||||
|
<Pagination
|
||||||
|
total={pagination.total}
|
||||||
|
current={pagination.current}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
size="small"
|
||||||
|
simple
|
||||||
|
onChange={(page) =>
|
||||||
|
setPagination((prev) => ({ ...prev, current: page }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧构建日志 */}
|
{/* 右侧构建日志 */}
|
||||||
@@ -801,7 +905,7 @@ function ProjectDetailPage() {
|
|||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
{selectedRecord && (
|
{selectedRecord && (
|
||||||
<Typography.Text type="secondary" className="text-sm">
|
<Typography.Text type="secondary" className="text-sm">
|
||||||
{selectedRecord.branch} · {selectedRecord.env} ·{' '}
|
{selectedRecord.branch}
|
||||||
{formatDateTime(selectedRecord.createdAt)}
|
{formatDateTime(selectedRecord.createdAt)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
@@ -813,7 +917,9 @@ function ProjectDetailPage() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
icon={<IconRefresh />}
|
icon={<IconRefresh />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleRetryDeployment(selectedRecord.id)}
|
onClick={() =>
|
||||||
|
handleRetryDeployment(selectedRecord.id)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
重新执行
|
重新执行
|
||||||
</Button>
|
</Button>
|
||||||
@@ -838,7 +944,15 @@ function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane title={<Space><IconCode />流水线</Space>} key="pipeline">
|
<Tabs.TabPane
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconCode />
|
||||||
|
流水线
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
key="pipeline"
|
||||||
|
>
|
||||||
<div className="grid grid-cols-5 gap-6 h-full">
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
{/* 左侧流水线列表 */}
|
{/* 左侧流水线列表 */}
|
||||||
<div className="col-span-2 space-y-4">
|
<div className="col-span-2 space-y-4">
|
||||||
@@ -951,9 +1065,7 @@ function ProjectDetailPage() {
|
|||||||
{pipeline.description}
|
{pipeline.description}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||||
<span>
|
<span>{pipeline.steps?.length || 0} 个步骤</span>
|
||||||
{pipeline.steps?.length || 0} 个步骤
|
|
||||||
</span>
|
|
||||||
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1005,7 +1117,11 @@ function ProjectDetailPage() {
|
|||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={selectedPipeline.steps?.map(step => step.id) || []}
|
items={
|
||||||
|
selectedPipeline.steps?.map(
|
||||||
|
(step) => step.id,
|
||||||
|
) || []
|
||||||
|
}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
<div className="space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||||
@@ -1046,9 +1162,19 @@ function ProjectDetailPage() {
|
|||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|
||||||
{/* 项目设置标签页 */}
|
{/* 项目设置标签页 */}
|
||||||
<Tabs.TabPane key="settings" title={<Space><IconSettings />项目设置</Space>}>
|
<Tabs.TabPane
|
||||||
|
key="settings"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<IconSettings />
|
||||||
|
项目设置
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<Card title="项目信息" className="mb-4">
|
<Card title="项目信息" className="mb-4">
|
||||||
|
{!isEditingProject ? (
|
||||||
|
<>
|
||||||
<Descriptions
|
<Descriptions
|
||||||
column={1}
|
column={1}
|
||||||
data={[
|
data={[
|
||||||
@@ -1082,12 +1208,94 @@ 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>
|
||||||
|
|
||||||
@@ -1139,7 +1347,9 @@ function ProjectDetailPage() {
|
|||||||
<Select.Option key={template.id} value={template.id}>
|
<Select.Option key={template.id} value={template.id}>
|
||||||
<div>
|
<div>
|
||||||
<div>{template.name}</div>
|
<div>{template.name}</div>
|
||||||
<div className="text-xs text-gray-500">{template.description}</div>
|
<div className="text-xs text-gray-500">
|
||||||
|
{template.description}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
@@ -1155,10 +1365,7 @@ function ProjectDetailPage() {
|
|||||||
>
|
>
|
||||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item field="description" label="流水线描述">
|
||||||
field="description"
|
|
||||||
label="流水线描述"
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="描述这个流水线的用途和特点..."
|
placeholder="描述这个流水线的用途和特点..."
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -1176,10 +1383,7 @@ function ProjectDetailPage() {
|
|||||||
>
|
>
|
||||||
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item field="description" label="流水线描述">
|
||||||
field="description"
|
|
||||||
label="流水线描述"
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="描述这个流水线的用途和特点..."
|
placeholder="描述这个流水线的用途和特点..."
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -1217,55 +1421,7 @@ 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>
|
||||||
|
|
||||||
@@ -1276,16 +1432,24 @@ function ProjectDetailPage() {
|
|||||||
setDeployModalVisible(false);
|
setDeployModalVisible(false);
|
||||||
// 刷新部署记录
|
// 刷新部署记录
|
||||||
if (id) {
|
if (id) {
|
||||||
detailService.getDeployments(Number(id)).then((records) => {
|
detailService
|
||||||
setDeployRecords(records);
|
.getDeployments(Number(id), 1, pagination.pageSize)
|
||||||
if (records.length > 0) {
|
.then((res) => {
|
||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
pipelines={pipelines}
|
pipelines={pipelines}
|
||||||
projectId={Number(id)}
|
projectId={Number(id)}
|
||||||
|
project={detail}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { type APIResponse, net } from '@shared';
|
import { net } from '../../../utils';
|
||||||
import type { Branch, Commit, Deployment, Pipeline, Project, Step, CreateDeploymentRequest } from '../types';
|
import type {
|
||||||
|
Branch,
|
||||||
|
Commit,
|
||||||
|
CreateDeploymentRequest,
|
||||||
|
Deployment,
|
||||||
|
Pipeline,
|
||||||
|
Project,
|
||||||
|
Step,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
class DetailService {
|
class DetailService {
|
||||||
async getProject(id: string) {
|
async getProject(id: string) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -11,26 +19,32 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目的所有流水线
|
// 获取项目的所有流水线
|
||||||
async getPipelines(projectId: number) {
|
async getPipelines(projectId: number) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline[]>>({
|
const { data } = await net.request<Pipeline[] | { list: Pipeline[] }>({
|
||||||
url: `/api/pipelines?projectId=${projectId}`,
|
url: `/api/pipelines?projectId=${projectId}`,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取可用的流水线模板
|
// 获取可用的流水线模板
|
||||||
async getPipelineTemplates() {
|
async getPipelineTemplates() {
|
||||||
const { data } = await net.request<APIResponse<{id: number, name: string, description: string}[]>>({
|
const { data } = await net.request<
|
||||||
|
| { id: number; name: string; description: string }[]
|
||||||
|
| { list: { id: number; name: string; description: string }[] }
|
||||||
|
>({
|
||||||
url: '/api/pipelines/templates',
|
url: '/api/pipelines/templates',
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取项目的部署记录
|
async getDeployments(
|
||||||
async getDeployments(projectId: number) {
|
projectId: number,
|
||||||
const { data } = await net.request<any>({
|
page: number = 1,
|
||||||
url: `/api/deployments?projectId=${projectId}`,
|
pageSize: number = 10,
|
||||||
|
) {
|
||||||
|
const { data } = await net.request<DeploymentListResponse>({
|
||||||
|
url: `/api/deployments?projectId=${projectId}&page=${page}&pageSize=${pageSize}`,
|
||||||
});
|
});
|
||||||
return data.data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建流水线
|
// 创建流水线
|
||||||
@@ -46,7 +60,7 @@ class DetailService {
|
|||||||
| 'steps'
|
| 'steps'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
const { data } = await net.request<Pipeline>({
|
||||||
url: '/api/pipelines',
|
url: '/api/pipelines',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: pipeline,
|
data: pipeline,
|
||||||
@@ -59,16 +73,16 @@ class DetailService {
|
|||||||
templateId: number,
|
templateId: number,
|
||||||
projectId: number,
|
projectId: number,
|
||||||
name: string,
|
name: string,
|
||||||
description?: string
|
description?: string,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
const { data } = await net.request<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;
|
||||||
@@ -90,7 +104,7 @@ class DetailService {
|
|||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
const { data } = await net.request<Pipeline>({
|
||||||
url: `/api/pipelines/${id}`,
|
url: `/api/pipelines/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: pipeline,
|
data: pipeline,
|
||||||
@@ -100,7 +114,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 删除流水线
|
// 删除流水线
|
||||||
async deletePipeline(id: number) {
|
async deletePipeline(id: number) {
|
||||||
const { data } = await net.request<APIResponse<null>>({
|
const { data } = await net.request<null>({
|
||||||
url: `/api/pipelines/${id}`,
|
url: `/api/pipelines/${id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -109,10 +123,10 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取流水线的所有步骤
|
// 获取流水线的所有步骤
|
||||||
async getSteps(pipelineId: number) {
|
async getSteps(pipelineId: number) {
|
||||||
const { data } = await net.request<APIResponse<Step[]>>({
|
const { data } = await net.request<Step[] | { list: Step[] }>({
|
||||||
url: `/api/steps?pipelineId=${pipelineId}`,
|
url: `/api/steps?pipelineId=${pipelineId}`,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建步骤
|
// 创建步骤
|
||||||
@@ -122,7 +136,7 @@ class DetailService {
|
|||||||
'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid'
|
'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Step>>({
|
const { data } = await net.request<Step>({
|
||||||
url: '/api/steps',
|
url: '/api/steps',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: step,
|
data: step,
|
||||||
@@ -140,7 +154,7 @@ class DetailService {
|
|||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Step>>({
|
const { data } = await net.request<Step>({
|
||||||
url: `/api/steps/${id}`,
|
url: `/api/steps/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: step,
|
data: step,
|
||||||
@@ -151,7 +165,7 @@ class DetailService {
|
|||||||
// 删除步骤
|
// 删除步骤
|
||||||
async deleteStep(id: number) {
|
async deleteStep(id: number) {
|
||||||
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
||||||
const { data } = await net.request<APIResponse<null>>({
|
const { data } = await net.request<null>({
|
||||||
url: `/api/steps/${id}`,
|
url: `/api/steps/${id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -160,23 +174,23 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目的提交记录
|
// 获取项目的提交记录
|
||||||
async getCommits(projectId: number, branch?: string) {
|
async getCommits(projectId: number, branch?: string) {
|
||||||
const { data } = await net.request<APIResponse<Commit[]>>({
|
const { data } = await net.request<Commit[] | { list: Commit[] }>({
|
||||||
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取项目的分支列表
|
// 获取项目的分支列表
|
||||||
async getBranches(projectId: number) {
|
async getBranches(projectId: number) {
|
||||||
const { data } = await net.request<APIResponse<Branch[]>>({
|
const { data } = await net.request<Branch[] | { list: Branch[] }>({
|
||||||
url: `/api/git/branches?projectId=${projectId}`,
|
url: `/api/git/branches?projectId=${projectId}`,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建部署
|
// 创建部署
|
||||||
async createDeployment(deployment: CreateDeploymentRequest) {
|
async createDeployment(deployment: CreateDeploymentRequest) {
|
||||||
const { data } = await net.request<APIResponse<Deployment>>({
|
const { data } = await net.request<Deployment>({
|
||||||
url: '/api/deployments',
|
url: '/api/deployments',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: deployment,
|
data: deployment,
|
||||||
@@ -186,7 +200,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 重新执行部署
|
// 重新执行部署
|
||||||
async retryDeployment(deploymentId: number) {
|
async retryDeployment(deploymentId: number) {
|
||||||
const { data } = await net.request<APIResponse<Deployment>>({
|
const { data } = await net.request<Deployment>({
|
||||||
url: `/api/deployments/${deploymentId}/retry`,
|
url: `/api/deployments/${deploymentId}/retry`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
@@ -195,18 +209,15 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目详情(包含工作目录状态)
|
// 获取项目详情(包含工作目录状态)
|
||||||
async getProjectDetail(id: number) {
|
async getProjectDetail(id: number) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新项目
|
// 更新项目
|
||||||
async updateProject(
|
async updateProject(id: number, project: Partial<Project>) {
|
||||||
id: number,
|
const { data } = await net.request<Project>({
|
||||||
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,
|
||||||
@@ -224,3 +235,10 @@ 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 { projectService } from '../service';
|
|
||||||
import type { Project } from '../../types';
|
import type { Project } from '../../types';
|
||||||
|
import { projectService } from '../service';
|
||||||
|
|
||||||
interface CreateProjectModalProps {
|
interface CreateProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -114,7 +114,9 @@ function CreateProjectModal({
|
|||||||
if (value.includes('..') || value.includes('~')) {
|
if (value.includes('..') || value.includes('~')) {
|
||||||
return cb('不能包含路径遍历字符(.. 或 ~)');
|
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||||
}
|
}
|
||||||
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
// 检查非法字符(控制字符 0x00-0x1F)
|
||||||
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: 需要检测路径中的控制字符
|
||||||
|
if (/[<>:"|?*\u0000-\u001f]/.test(value)) {
|
||||||
return cb('路径包含非法字符');
|
return cb('路径包含非法字符');
|
||||||
}
|
}
|
||||||
cb();
|
cb();
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
import {
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Message,
|
||||||
|
Modal,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import EnvPresetsEditor, {
|
||||||
|
type EnvPreset,
|
||||||
|
} from '../../detail/components/EnvPresetsEditor';
|
||||||
|
import type { Project } from '../../types';
|
||||||
import { projectService } from '../service';
|
import { projectService } from '../service';
|
||||||
import type { Project } from '../types';
|
|
||||||
|
|
||||||
interface EditProjectModalProps {
|
interface EditProjectModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -22,10 +32,20 @@ function EditProjectModal({
|
|||||||
// 当项目信息变化时,更新表单数据
|
// 当项目信息变化时,更新表单数据
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (project && visible) {
|
if (project && visible) {
|
||||||
|
let envPresets: EnvPreset[] = [];
|
||||||
|
try {
|
||||||
|
if (project.envPresets) {
|
||||||
|
envPresets = JSON.parse(project.envPresets);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析环境预设失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: project.name,
|
name: project.name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
repository: project.repository,
|
repository: project.repository,
|
||||||
|
envPresets,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [project, visible, form]);
|
}, [project, visible, form]);
|
||||||
@@ -37,7 +57,18 @@ function EditProjectModal({
|
|||||||
|
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
const updatedProject = await projectService.update(project.id, values);
|
// 序列化环境预设
|
||||||
|
const submitData = {
|
||||||
|
...values,
|
||||||
|
envPresets: values.envPresets
|
||||||
|
? JSON.stringify(values.envPresets)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedProject = await projectService.update(
|
||||||
|
project.id,
|
||||||
|
submitData,
|
||||||
|
);
|
||||||
|
|
||||||
Message.success('项目更新成功');
|
Message.success('项目更新成功');
|
||||||
onSuccess(updatedProject);
|
onSuccess(updatedProject);
|
||||||
@@ -111,6 +142,14 @@ function EditProjectModal({
|
|||||||
>
|
>
|
||||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Collapse defaultActiveKey={[]} style={{ marginTop: 16 }}>
|
||||||
|
<Collapse.Item header="环境变量预设配置" name="envPresets">
|
||||||
|
<Form.Item field="envPresets" noStyle>
|
||||||
|
<EnvPresetsEditor />
|
||||||
|
</Form.Item>
|
||||||
|
</Collapse.Item>
|
||||||
|
</Collapse>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Space,
|
Space,
|
||||||
Tag,
|
Tag,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@arco-design/web-react';
|
} from '@arco-design/web-react';
|
||||||
import {
|
import {
|
||||||
@@ -58,7 +59,7 @@ function ProjectCard({ project }: ProjectCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="foka-card !rounded-xl border border-gray-200 h-[280px] cursor-pointer"
|
className="!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, Message, Typography } from '@arco-design/web-react';
|
import { Button, Grid, Typography } from '@arco-design/web-react';
|
||||||
import { IconPlus } from '@arco-design/web-react/icon';
|
import { IconPlus } from '@arco-design/web-react/icon';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
@@ -15,7 +15,7 @@ function ProjectPage() {
|
|||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
const response = await projectService.list();
|
const response = await projectService.list();
|
||||||
setProjects(response.data);
|
setProjects(response.list);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateProject = () => {
|
const handleCreateProject = () => {
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { type APIResponse, net } from '@shared';
|
import { net } from '../../../utils';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../types';
|
||||||
|
|
||||||
class ProjectService {
|
class ProjectService {
|
||||||
async list(params?: ProjectQueryParams) {
|
async list(params?: ProjectQueryParams) {
|
||||||
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
const { data } = await net.request<Project[] | ProjectListResponse>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/projects',
|
url: '/api/projects',
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(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<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
@@ -24,7 +26,7 @@ class ProjectService {
|
|||||||
description?: string;
|
description?: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
}) {
|
}) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/projects',
|
url: '/api/projects',
|
||||||
data: project,
|
data: project,
|
||||||
@@ -36,7 +38,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<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
data: project,
|
data: project,
|
||||||
@@ -56,17 +58,14 @@ class ProjectService {
|
|||||||
export const projectService = new ProjectService();
|
export const projectService = new ProjectService();
|
||||||
|
|
||||||
interface ProjectListResponse {
|
interface ProjectListResponse {
|
||||||
data: Project[];
|
list: Project[];
|
||||||
pagination: {
|
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
pageSize: number;
|
||||||
total: number;
|
total: number;
|
||||||
totalPages: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectQueryParams {
|
interface ProjectQueryParams {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
pageSize?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface Project {
|
|||||||
description: string;
|
description: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
projectDir: string; // 项目工作目录路径(必填)
|
projectDir: string; // 项目工作目录路径(必填)
|
||||||
|
envPresets?: string; // 环境预设配置(JSON格式)
|
||||||
valid: number;
|
valid: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -77,12 +78,11 @@ export interface Pipeline {
|
|||||||
export interface Deployment {
|
export interface Deployment {
|
||||||
id: number;
|
id: number;
|
||||||
branch: string;
|
branch: string;
|
||||||
env?: string;
|
envVars?: string; // JSON 字符串
|
||||||
status: string;
|
status: string;
|
||||||
commitHash?: string;
|
commitHash?: string;
|
||||||
commitMessage?: string;
|
commitMessage?: string;
|
||||||
buildLog?: string;
|
buildLog?: string;
|
||||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
finishedAt?: string;
|
finishedAt?: string;
|
||||||
valid: number;
|
valid: number;
|
||||||
@@ -127,6 +127,5 @@ export interface CreateDeploymentRequest {
|
|||||||
branch: string;
|
branch: string;
|
||||||
commitHash: string;
|
commitHash: string;
|
||||||
commitMessage: string;
|
commitMessage: string;
|
||||||
env?: string;
|
envVars?: Record<string, string>; // 环境变量 key-value 对象
|
||||||
sparseCheckoutPaths?: string; // 稀疏检出路径,用于monorepo项目
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
import { type APIResponse, net } from '@shared';
|
import { net } from '@utils';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import type { GlobalStore } from './types';
|
||||||
interface User {
|
import type { User } from '@pages/login/types';
|
||||||
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<APIResponse<User>>({
|
const { data } = await net.request<User>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/info',
|
url: '/api/auth/info',
|
||||||
});
|
});
|
||||||
|
|||||||
13
apps/web/src/stores/types.ts
Normal file
13
apps/web/src/stores/types.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
@@ -20,7 +20,11 @@ class Net {
|
|||||||
(error) => {
|
(error) => {
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
// 对于DELETE请求返回204状态码的情况,视为成功
|
// 对于DELETE请求返回204状态码的情况,视为成功
|
||||||
if (error.response && error.response.status === 204 && error.config.method === 'delete') {
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.status === 204 &&
|
||||||
|
error.config.method === 'delete'
|
||||||
|
) {
|
||||||
// 创建一个模拟的成功响应
|
// 创建一个模拟的成功响应
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
...error.response,
|
...error.response,
|
||||||
@@ -38,9 +42,9 @@ class Net {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async request<T>(config: AxiosRequestConfig): Promise<T> {
|
async request<T>(config: AxiosRequestConfig): Promise<APIResponse<T>> {
|
||||||
try {
|
try {
|
||||||
const response = await this.instance.request<T>(config);
|
const response = await this.instance.request<APIResponse<T>>(config);
|
||||||
if (!response || !response.data) {
|
if (!response || !response.data) {
|
||||||
throw new Error('Invalid response');
|
throw new Error('Invalid response');
|
||||||
}
|
}
|
||||||
@@ -52,6 +56,13 @@ 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;
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
"@pages/*": ["./src/pages/*"],
|
"@pages/*": ["./src/pages/*"],
|
||||||
"@styles/*": ["./src/styles/*"],
|
"@styles/*": ["./src/styles/*"],
|
||||||
"@assets/*": ["./src/assets/*"],
|
"@assets/*": ["./src/assets/*"],
|
||||||
"@shared": ["./src/shared"]
|
"@utils/*": ["./src/utils/*"],
|
||||||
|
"@utils": ["./src/utils"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
22
docs/README.md
Normal file
22
docs/README.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 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
docs/ai.md
Normal file
21
docs/ai.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 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` 是否能正常编译通过。
|
||||||
29
docs/architecture.md
Normal file
29
docs/architecture.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 系统架构
|
||||||
|
|
||||||
|
## 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`。
|
||||||
20
docs/constraints.md
Normal file
20
docs/constraints.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 约束与禁区
|
||||||
|
|
||||||
|
## 1. 路由规范
|
||||||
|
|
||||||
|
- **禁止** 在 `app.ts` 中手动编写 `router.get/post`。
|
||||||
|
- **必须** 使用 `@Controller` 和 `@Get/Post` 装饰器,并放在 `controllers/` 目录下。
|
||||||
|
|
||||||
|
## 2. 数据库安全
|
||||||
|
|
||||||
|
- **禁止** 绕过 Prisma 直接操作数据库文件。
|
||||||
|
- **禁止** 在生产环境中手动修改 `dev.db`。
|
||||||
|
|
||||||
|
## 3. 环境变量
|
||||||
|
|
||||||
|
- **禁区**: 严禁将敏感 Token 或密钥直接硬编码在代码或 `schema.prisma` 中。
|
||||||
|
- 请使用 `.env` 文件配合 `dotenv` 加载。
|
||||||
|
|
||||||
|
## 4. 依赖管理
|
||||||
|
|
||||||
|
- **禁止** 混合使用 npm/yarn,必须统一使用 `pnpm`。
|
||||||
86
docs/conventions.md
Normal file
86
docs/conventions.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 编码规范
|
||||||
|
|
||||||
|
## 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`。
|
||||||
17
docs/decisions/0001-tech-stack.md
Normal file
17
docs/decisions/0001-tech-stack.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# ADR 0001: 技术选型
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
需要构建一个轻量级、易扩展且易于本地部署的 CI 系统。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
- **语言**: 全栈 TypeScript,确保模型定义在前后端的一致性。
|
||||||
|
- **后端框架**: Koa。相比 Express 更加轻量,利用 async/await 处理异步中间件更优雅。
|
||||||
|
- **数据库**: SQLite。CI 系统通常是单机或小规模使用,SQLite 无需独立服务,运维成本极低。
|
||||||
|
- **执行工具**: `zx`。相比原生的 `child_process`,`zx` 处理 Shell 交互更加直观和安全。
|
||||||
|
|
||||||
|
## 后果
|
||||||
|
|
||||||
|
- 优势:开发效率极高,部署简单。
|
||||||
|
- 挑战:SQLite 在极高并发写入(如数百个任务同时输出日志)时可能存在性能瓶颈。
|
||||||
18
docs/decisions/0002-state.md
Normal file
18
docs/decisions/0002-state.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ADR 0002: 状态管理
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
需要在前端管理用户信息、全局配置以及各页面的复杂 UI 状态。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
- **全局状态**: 使用 Zustand。
|
||||||
|
- **理由**:
|
||||||
|
- 相比 Redux 模板代码极少。
|
||||||
|
- 相比 Context API 性能更好且不引起全量重绘。
|
||||||
|
- 符合 React 19 的 Concurrent 模式。
|
||||||
|
- **持久化**: 对关键状态(如 Token)使用 Zustand 的 persist 中间件。
|
||||||
|
|
||||||
|
## 后果
|
||||||
|
|
||||||
|
状态管理逻辑高度内聚在 `apps/web/src/stores` 中。
|
||||||
15
docs/decisions/0003-pipeline-execution.md
Normal file
15
docs/decisions/0003-pipeline-execution.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# ADR 0003: 流水线执行策略
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
如何确保流水线执行的隔离性与可靠性。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
- **工作目录**: 每个项目在服务器上拥有独立的 `projectDir`。
|
||||||
|
- **执行器**: 采用线性执行。目前不支持多步骤并行,以确保日志顺序的确定性。
|
||||||
|
- **队列**: 使用内存队列 + 数据库扫描实现。系统重启后能通过数据库中的 `pending` 状态恢复任务。
|
||||||
|
|
||||||
|
## 后果
|
||||||
|
|
||||||
|
目前的隔离级别为目录级。未来可能需要引入 Docker 容器化执行以增强安全性。
|
||||||
21
docs/pitfalls.md
Normal file
21
docs/pitfalls.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 踩坑指南 (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。
|
||||||
33
docs/requirements/0001-Fix-Response-structure.md
Normal file
33
docs/requirements/0001-Fix-Response-structure.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 标准化响应结构
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/status.md
Normal file
23
docs/status.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 当前进度 (Status)
|
||||||
|
|
||||||
|
## 已完成 ✅
|
||||||
|
|
||||||
|
- 基础 Monorepo 框架搭设
|
||||||
|
- TC39 装饰器路由系统
|
||||||
|
- 项目管理 (CRUD)
|
||||||
|
- 基础流水线执行流程 (Git Clone -> zx Run -> Log Update)
|
||||||
|
- 前端项目列表与详情页预览
|
||||||
|
- 优化: 移除菜单环境管理及页面(目前无用)
|
||||||
|
- 优化:移除创建 Step 弹窗可用环境变量区域
|
||||||
|
- 优化: 部署记录的分页查询每页 10 条
|
||||||
|
|
||||||
|
## 进行中 🚧
|
||||||
|
|
||||||
|
- [标准化接口响应结构](./requirements/0001-Fix-Response-structure.md)
|
||||||
|
|
||||||
|
## 待办 📅
|
||||||
|
|
||||||
|
- [ ] Gitea Webhook 自动触发
|
||||||
|
- [ ] 用户权限管理 (RBAC)
|
||||||
|
- [ ] 修复: 表单必填项,*号和 label 不在一行
|
||||||
|
- [ ] 修复:项目详情页,未选中 tab【部署记录】还会拉取日志信息
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "ark-ci",
|
"name": "MiniCI",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,7 +8,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.6"
|
"@biomejs/biome": "2.0.6"
|
||||||
},
|
},
|
||||||
"keywords": ["ci", "ark", "ark-ci"],
|
"keywords": [
|
||||||
|
"ci",
|
||||||
|
"ark",
|
||||||
|
"ark-ci"
|
||||||
|
],
|
||||||
"author": "hurole",
|
"author": "hurole",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
|
"packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
|
||||||
|
|||||||
Reference in New Issue
Block a user