feat: 实现环境变量预设功能 & 移除稀疏检出
## 后端改动 - 添加 Project.envPresets 字段(JSON 格式) - 移除 Deployment.env 字段,统一使用 envVars - 更新部署 DTO,支持 envVars (Record<string, string>) - pipeline-runner 支持解析并注入 envVars 到环境 - 移除稀疏检出模板和相关环境变量 - 优化代码格式(Biome lint & format) ## 前端改动 - 新增 EnvPresetsEditor 组件(支持单选/多选/输入框类型) - 项目创建/编辑界面集成环境预设编辑器 - 部署界面基于预设动态生成环境变量表单 - 移除稀疏检出表单项 - 项目详情页添加环境变量预设配置 tab - 优化部署界面布局(基本参数 & 环境变量分区) ## 文档 - 添加完整文档目录结构(docs/) - 创建设计文档 design-0005(部署流程重构) - 添加 API 文档、架构设计文档等 ## 数据库 - 执行 prisma db push 同步 schema 变更
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
* 封装 Git 操作:克隆、更新、分支切换等
|
||||
*/
|
||||
|
||||
import { $ } from 'zx';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { $ } from 'zx';
|
||||
import { log } from './logger';
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,26 +38,23 @@ class Gitea {
|
||||
clientId: process.env.GITEA_CLIENT_ID!,
|
||||
clientSecret: process.env.GITEA_CLIENT_SECRET!,
|
||||
redirectUri: process.env.GITEA_REDIRECT_URI!,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
const { giteaUrl, clientId, clientSecret, redirectUri } = this.config;
|
||||
console.log('this.config', this.config);
|
||||
const response = await fetch(
|
||||
`${giteaUrl}/login/oauth/access_token`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const response = await fetch(`${giteaUrl}/login/oauth/access_token`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.log(await response.json());
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
@@ -108,19 +105,23 @@ class Gitea {
|
||||
* @param accessToken 访问令牌
|
||||
* @param sha 分支名称或提交SHA
|
||||
*/
|
||||
async getCommits(owner: string, repo: string, accessToken: string, sha?: string) {
|
||||
const url = new URL(`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`);
|
||||
async getCommits(
|
||||
owner: string,
|
||||
repo: string,
|
||||
accessToken: string,
|
||||
sha?: string,
|
||||
) {
|
||||
const url = new URL(
|
||||
`${this.config.giteaUrl}/api/v1/repos/${owner}/${repo}/commits`,
|
||||
);
|
||||
if (sha) {
|
||||
url.searchParams.append('sha', sha);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
url.toString(),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken),
|
||||
},
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
}
|
||||
@@ -133,7 +134,7 @@ class Gitea {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (accessToken) {
|
||||
headers['Authorization'] = `token ${accessToken}`;
|
||||
headers.Authorization = `token ${accessToken}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,6 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
||||
name: 'Git Clone Pipeline',
|
||||
description: '默认的Git克隆流水线,用于从仓库克隆代码',
|
||||
steps: [
|
||||
{
|
||||
name: 'Clone Repository',
|
||||
order: 0,
|
||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
||||
},
|
||||
{
|
||||
name: 'Install Dependencies',
|
||||
order: 1,
|
||||
@@ -36,51 +31,21 @@ export const DEFAULT_PIPELINE_TEMPLATES: PipelineTemplate[] = [
|
||||
name: 'Build Project',
|
||||
order: 3,
|
||||
script: '# 构建项目\nnpm run build',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Sparse Checkout Pipeline',
|
||||
description: '稀疏检出流水线,适用于monorepo项目,只获取指定目录的代码',
|
||||
steps: [
|
||||
{
|
||||
name: 'Sparse Checkout Repository',
|
||||
order: 0,
|
||||
script: '# 进行稀疏检出指定目录的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit config core.sparseCheckout true\necho "$SPARSE_CHECKOUT_PATHS" > .git/info/sparse-checkout\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD\n\n# 显示当前提交信息\ngit log --oneline -1',
|
||||
},
|
||||
{
|
||||
name: 'Install Dependencies',
|
||||
order: 1,
|
||||
script: '# 安装项目依赖\nnpm install',
|
||||
},
|
||||
{
|
||||
name: 'Run Tests',
|
||||
order: 2,
|
||||
script: '# 运行测试\nnpm test',
|
||||
},
|
||||
{
|
||||
name: 'Build Project',
|
||||
order: 3,
|
||||
script: '# 构建项目\nnpm run build',
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Simple Deploy Pipeline',
|
||||
description: '简单的部署流水线,包含基本的构建和部署步骤',
|
||||
steps: [
|
||||
{
|
||||
name: 'Clone Repository',
|
||||
order: 0,
|
||||
script: '# 克隆指定commit的代码\ngit init\ngit remote add origin $REPOSITORY_URL\ngit fetch --depth 1 origin $COMMIT_HASH\ngit checkout -q FETCH_HEAD',
|
||||
},
|
||||
{
|
||||
name: 'Build and Deploy',
|
||||
order: 1,
|
||||
script: '# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
||||
}
|
||||
]
|
||||
}
|
||||
script:
|
||||
'# 构建并部署项目\nnpm run build\n\n# 部署到目标服务器\n# 这里可以添加具体的部署命令',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -94,10 +59,10 @@ export async function initializePipelineTemplates(): Promise<void> {
|
||||
const existingTemplates = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
name: {
|
||||
in: DEFAULT_PIPELINE_TEMPLATES.map(template => template.name)
|
||||
in: DEFAULT_PIPELINE_TEMPLATES.map((template) => template.name),
|
||||
},
|
||||
valid: 1
|
||||
}
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 如果没有现有的模板,则创建默认模板
|
||||
@@ -113,8 +78,8 @@ export async function initializePipelineTemplates(): Promise<void> {
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1,
|
||||
projectId: null // 模板不属于任何特定项目
|
||||
}
|
||||
projectId: null, // 模板不属于任何特定项目
|
||||
},
|
||||
});
|
||||
|
||||
// 创建模板步骤
|
||||
@@ -127,8 +92,8 @@ export async function initializePipelineTemplates(): Promise<void> {
|
||||
pipelineId: pipeline.id,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -148,25 +113,27 @@ export async function initializePipelineTemplates(): Promise<void> {
|
||||
/**
|
||||
* 获取所有可用的流水线模板
|
||||
*/
|
||||
export async function getAvailableTemplates(): Promise<Array<{id: number, name: string, description: string}>> {
|
||||
export async function getAvailableTemplates(): Promise<
|
||||
Array<{ id: number; name: string; description: string }>
|
||||
> {
|
||||
try {
|
||||
const templates = await prisma.pipeline.findMany({
|
||||
where: {
|
||||
projectId: null, // 模板流水线没有关联的项目
|
||||
valid: 1
|
||||
valid: 1,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true
|
||||
}
|
||||
description: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 处理可能为null的description字段
|
||||
return templates.map(template => ({
|
||||
return templates.map((template) => ({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description || ''
|
||||
description: template.description || '',
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to get pipeline templates:', error);
|
||||
@@ -185,7 +152,7 @@ export async function createPipelineFromTemplate(
|
||||
templateId: number,
|
||||
projectId: number,
|
||||
pipelineName: string,
|
||||
pipelineDescription: string
|
||||
pipelineDescription: string,
|
||||
): Promise<number> {
|
||||
try {
|
||||
// 获取模板流水线及其步骤
|
||||
@@ -193,18 +160,18 @@ export async function createPipelineFromTemplate(
|
||||
where: {
|
||||
id: templateId,
|
||||
projectId: null, // 确保是模板流水线
|
||||
valid: 1
|
||||
valid: 1,
|
||||
},
|
||||
include: {
|
||||
steps: {
|
||||
where: {
|
||||
valid: 1
|
||||
valid: 1,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc'
|
||||
}
|
||||
}
|
||||
}
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!templatePipeline) {
|
||||
@@ -219,8 +186,8 @@ export async function createPipelineFromTemplate(
|
||||
projectId: projectId,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// 复制模板步骤到新流水线
|
||||
@@ -233,12 +200,14 @@ export async function createPipelineFromTemplate(
|
||||
pipelineId: newPipeline.id,
|
||||
createdBy: 'system',
|
||||
updatedBy: 'system',
|
||||
valid: 1
|
||||
}
|
||||
valid: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Created pipeline from template ${templateId}: ${newPipeline.name}`);
|
||||
console.log(
|
||||
`Created pipeline from template ${templateId}: ${newPipeline.name}`,
|
||||
);
|
||||
return newPipeline.id;
|
||||
} catch (error) {
|
||||
console.error('Failed to create pipeline from template:', error);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type Koa from 'koa';
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -33,7 +37,7 @@ export class RouteScanner {
|
||||
* 注册多个控制器类
|
||||
*/
|
||||
registerControllers(controllers: ControllerClass[]): void {
|
||||
controllers.forEach(controller => this.registerController(controller));
|
||||
controllers.forEach((controller) => this.registerController(controller));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,9 +54,12 @@ export class RouteScanner {
|
||||
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
|
||||
|
||||
// 注册每个路由
|
||||
routes.forEach(route => {
|
||||
routes.forEach((route) => {
|
||||
const fullPath = this.buildFullPath(controllerPrefix, route.path);
|
||||
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey);
|
||||
const handler = this.wrapControllerMethod(
|
||||
controllerInstance,
|
||||
route.propertyKey,
|
||||
);
|
||||
|
||||
// 根据HTTP方法注册路由
|
||||
switch (route.method) {
|
||||
@@ -87,10 +94,10 @@ export class RouteScanner {
|
||||
|
||||
let fullPath = '';
|
||||
if (cleanControllerPrefix) {
|
||||
fullPath += '/' + cleanControllerPrefix;
|
||||
fullPath += `/${cleanControllerPrefix}`;
|
||||
}
|
||||
if (cleanRoutePath) {
|
||||
fullPath += '/' + cleanRoutePath;
|
||||
fullPath += `/${cleanRoutePath}`;
|
||||
}
|
||||
|
||||
// 如果路径为空,返回根路径
|
||||
@@ -105,11 +112,11 @@ export class RouteScanner {
|
||||
// 调用控制器方法
|
||||
const method = instance[methodName];
|
||||
if (typeof method !== 'function') {
|
||||
ctx.throw(401, 'Not Found')
|
||||
ctx.throw(401, 'Not Found');
|
||||
}
|
||||
|
||||
// 绑定this并调用方法
|
||||
const result = await method.call(instance, ctx, next) ?? null;
|
||||
const result = (await method.call(instance, ctx, next)) ?? null;
|
||||
|
||||
ctx.body = createSuccessResponse(result);
|
||||
};
|
||||
@@ -133,19 +140,29 @@ export class RouteScanner {
|
||||
/**
|
||||
* 获取已注册的路由信息(用于调试)
|
||||
*/
|
||||
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> {
|
||||
const routes: Array<{ method: string; path: string; controller: string; action: string }> = [];
|
||||
getRegisteredRoutes(): Array<{
|
||||
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 routeMetadata = getRouteMetadata(ControllerClass);
|
||||
|
||||
routeMetadata.forEach(route => {
|
||||
routeMetadata.forEach((route) => {
|
||||
routes.push({
|
||||
method: route.method,
|
||||
path: this.buildFullPath(controllerPrefix, route.path),
|
||||
controller: ControllerClass.name,
|
||||
action: route.propertyKey
|
||||
action: route.propertyKey,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user