From 0348cc2f4e10f541855a6b4a48d7a44f0549e5fd Mon Sep 17 00:00:00 2001 From: hukdoesn Date: Mon, 30 Jun 2025 15:45:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=8F=82=E6=95=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/apps/models.py | 18 +++ backend/apps/utils/builder.py | 14 +- backend/apps/views/build.py | 62 ++++++++- web/src/views/build/BuildTaskEdit.vue | 183 ++++++++++++++++++++++++++ web/src/views/build/BuildTasks.vue | 56 ++++++-- 5 files changed, 320 insertions(+), 13 deletions(-) diff --git a/backend/apps/models.py b/backend/apps/models.py index ece8c15..b3527f7 100644 --- a/backend/apps/models.py +++ b/backend/apps/models.py @@ -225,6 +225,18 @@ class BuildTask(models.Model): # 构建阶段 stages = models.JSONField(default=list, verbose_name='构建阶段') + # 构建参数配置 + parameters = models.JSONField(default=list, verbose_name='构建参数配置', help_text=''' + [ + { + "name": "MY_SERVICES", + "description": "选择要部署的服务", + "choices": ["user-service", "order-service", "payment-service"], + "default_values": ["user-service"] + } + ] + ''') + # 外部脚本库配置 use_external_script = models.BooleanField(default=False, verbose_name='使用外部脚本库') external_script_config = models.JSONField(default=dict, verbose_name='外部脚本库配置', help_text=''' @@ -287,6 +299,12 @@ class BuildHistory(models.Model): requirement = models.TextField(null=True, blank=True, verbose_name='构建需求描述') build_log = models.TextField(null=True, blank=True, verbose_name='构建日志') stages = models.JSONField(default=list, verbose_name='构建阶段') + parameter_values = models.JSONField(default=dict, verbose_name='构建参数值', help_text=''' + { + "MY_SERVICES": ["user-service", "order-service"], + "FEATURE_FLAGS": ["enable-cache"] + } + ''') build_time = models.JSONField(default=dict, verbose_name='构建时间信息', help_text=''' { "total_duration": "300", # 总耗时(秒) diff --git a/backend/apps/utils/builder.py b/backend/apps/utils/builder.py index a119db9..cb8588e 100644 --- a/backend/apps/utils/builder.py +++ b/backend/apps/utils/builder.py @@ -462,11 +462,19 @@ class Builder: 'LANG': 'POSIX', } - combined_env = {**os.environ, **system_variables} + # 添加自定义参数变量 + custom_parameters = {} + if self.history.parameter_values: + for param_name, selected_values in self.history.parameter_values.items(): + custom_parameters[param_name] = ','.join(selected_values) + self.send_log(f"设置参数变量: {param_name}={custom_parameters[param_name]}", "Parameters") + + combined_env = {**os.environ, **system_variables, **custom_parameters} stage_executor.env = combined_env - # 保存系统变量到文件 - stage_executor._save_variables_to_file(system_variables) + # 保存系统变量和自定义参数到文件 + all_variables = {**system_variables, **custom_parameters} + stage_executor._save_variables_to_file(all_variables) # 执行构建阶段 success = self.execute_stages(stage_executor) diff --git a/backend/apps/views/build.py b/backend/apps/views/build.py index 5c6cb60..1cece72 100644 --- a/backend/apps/views/build.py +++ b/backend/apps/views/build.py @@ -145,6 +145,7 @@ class BuildTaskView(View): 'name': task.git_token.name } if task.git_token else None, 'stages': task.stages, + 'parameters': task.parameters, 'notification_channels': task.notification_channels, 'notification_robots': notification_robots, # 外部脚本库配置 @@ -288,7 +289,8 @@ class BuildTaskView(View): 'description': task.description, 'branch': task.branch, 'status': task.status, - 'building_status': task.building_status, # 添加构建状态字段 + 'building_status': task.building_status, + 'parameters': task.parameters, 'version': task.version, 'last_build_number': task.last_build_number, 'total_builds': task.total_builds, @@ -334,6 +336,7 @@ class BuildTaskView(View): branch = data.get('branch', 'main') git_token_id = data.get('git_token_id') stages = data.get('stages', []) + parameters = data.get('parameters', []) notification_channels = data.get('notification_channels', []) # 外部脚本库配置 @@ -379,6 +382,23 @@ class BuildTaskView(View): 'message': '任务名称、项目和环境不能为空' }) + # 验证参数配置格式 + if parameters: + import re + for param in parameters: + if not param.get('name') or not param.get('choices'): + return JsonResponse({ + 'code': 400, + 'message': '参数名称和可选值不能为空' + }) + + # 验证参数名格式(大写字母、数字、下划线) + if not re.match(r'^[A-Z_][A-Z0-9_]*$', param['name']): + return JsonResponse({ + 'code': 400, + 'message': f'参数名"{param["name"]}"格式不正确,只能包含大写字母、数字和下划线,且必须以字母或下划线开头' + }) + # 验证通知机器人是否存在 if notification_channels: existing_robots = set(NotificationRobot.objects.filter( @@ -431,6 +451,7 @@ class BuildTaskView(View): branch=branch, git_token=git_token, stages=stages, + parameters=parameters, notification_channels=notification_channels, use_external_script=use_external_script, external_script_config=external_script_config, @@ -473,6 +494,7 @@ class BuildTaskView(View): branch = data.get('branch') git_token_id = data.get('git_token_id') stages = data.get('stages') + parameters = data.get('parameters') notification_channels = data.get('notification_channels') status = data.get('status') @@ -512,6 +534,23 @@ class BuildTaskView(View): else: external_script_config = {} + # 验证参数配置格式 + if parameters: + import re + for param in parameters: + if not param.get('name') or not param.get('choices'): + return JsonResponse({ + 'code': 400, + 'message': '参数名称和可选值不能为空' + }) + + # 验证参数名格式(大写字母、数字、下划线) + if not re.match(r'^[A-Z_][A-Z0-9_]*$', param['name']): + return JsonResponse({ + 'code': 400, + 'message': f'参数名"{param["name"]}"格式不正确,只能包含大写字母、数字和下划线,且必须以字母或下划线开头' + }) + if not task_id: return JsonResponse({ 'code': 400, @@ -617,6 +656,8 @@ class BuildTaskView(View): task.branch = branch if 'stages' in data: task.stages = stages + if 'parameters' in data: + task.parameters = parameters if 'notification_channels' in data: # 验证通知机器人是否存在 existing_robots = set(NotificationRobot.objects.filter( @@ -710,6 +751,7 @@ class BuildExecuteView(View): commit_id = data.get('commit_id') version = data.get('version') requirement = data.get('requirement') + parameter_values = data.get('parameter_values', {}) if not task_id: return JsonResponse({ @@ -785,6 +827,23 @@ class BuildExecuteView(View): 'message': '构建需求描述不能为空' }) + # 验证参数值是否合法 + if task.parameters and parameter_values: + task_parameters = {p['name']: p['choices'] for p in task.parameters} + for param_name, selected_values in parameter_values.items(): + if param_name not in task_parameters: + return JsonResponse({ + 'code': 400, + 'message': f'未定义的参数: {param_name}' + }) + + for value in selected_values: + if value not in task_parameters[param_name]: + return JsonResponse({ + 'code': 400, + 'message': f'参数{param_name}的值"{value}"不在可选范围内' + }) + # 检查任务的构建状态 if task.building_status == 'building': return JsonResponse({ @@ -824,6 +883,7 @@ class BuildExecuteView(View): version=version if version else None, # 对于预发布和生产环境,使用传入的版本号 status='pending', # 初始状态为等待中 requirement=requirement, + parameter_values=parameter_values, operator=User.objects.get(user_id=request.user_id) # 记录构建人 ) diff --git a/web/src/views/build/BuildTaskEdit.vue b/web/src/views/build/BuildTaskEdit.vue index c6b011d..7b97fde 100644 --- a/web/src/views/build/BuildTaskEdit.vue +++ b/web/src/views/build/BuildTaskEdit.vue @@ -134,6 +134,84 @@ + + 构建参数 +
+
+
+ + 参数 {{ index + 1 }} + + + + + + + + +
+ + + + + + +
参数名只能包含大写字母、数字和下划线
+
+
+ + + + + + + + +
多个默认值用英文逗号分隔
+
+
+
+ + + + +
每行输入一个选项值
+
+
+
+
+
+ +
+ + 添加构建参数 + +
+ + 构建阶段
@@ -404,6 +482,7 @@ const formState = reactive({ script: '', } ], + parameters: [], notification_channels: [], }); @@ -509,6 +588,51 @@ const removeStage = (index) => { formState.stages.splice(index, 1); }; +// 添加构建参数 +const addParameter = () => { + formState.parameters.push({ + name: '', + description: '', + choices: [], + choicesText: '', + choiceOptions: [], + default_values: [], + defaultValuesText: '', + }); +}; + +const removeParameter = (index) => { + formState.parameters.splice(index, 1); +}; + +// 更新选项 +const updateChoices = (param, index) => { + if (param.choicesText) { + const choices = param.choicesText.split('\n').filter(line => line.trim()).map(line => line.trim()); + param.choices = choices; + param.choiceOptions = choices.map(choice => ({ + label: choice, + value: choice + })); + + param.default_values = param.default_values.filter(value => choices.includes(value)); + } else { + param.choices = []; + param.choiceOptions = []; + param.default_values = []; + } +}; + +// 更新默认值 +const updateDefaultValues = (param, index) => { + if (param.defaultValuesText) { + const values = param.defaultValuesText.split(',').filter(val => val.trim()).map(val => val.trim()); + param.default_values = values; + } else { + param.default_values = []; + } +}; + // 处理返回 const handleBack = () => { router.back(); @@ -526,6 +650,14 @@ const handleSubmit = async () => { // 构建提交数据 const submitData = { ...formState }; + // 处理参数数据 + submitData.parameters = formState.parameters.map(param => ({ + name: param.name, + description: param.description, + choices: param.choices, + default_values: param.default_values + })); + if (!submitData.use_external_script) { submitData.external_script_repo_url = ''; submitData.external_script_directory = ''; @@ -589,6 +721,25 @@ const loadTaskDetail = async (taskId) => { script: '', }); } + + // 加载参数配置 + const parameters = response.data.data.parameters || []; + formState.parameters = parameters.map(param => { + const choicesText = (param.choices || []).join('\n'); + const defaultValuesText = (param.default_values || []).join(','); + return { + name: param.name || '', + description: param.description || '', + choices: param.choices || [], + choicesText: choicesText, + choiceOptions: (param.choices || []).map(choice => ({ + label: choice, + value: choice + })), + default_values: param.default_values || [], + defaultValuesText: defaultValuesText, + }; + }); formState.notification_channels = response.data.data.notification_channels || []; @@ -790,6 +941,38 @@ onMounted(async () => { margin-top: 16px; } +.parameters-list { + margin-top: 16px; +} + +.parameter-item { + padding: 16px; + background: transparent; + border-radius: 4px; + margin-bottom: 16px; + border: 1px solid #e8e8e8; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.parameter-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #e8e8e8; +} + +.parameter-number { + font-size: 14px; + font-weight: 500; + color: rgba(0, 0, 0, 0.85); +} + +.parameter-actions { + margin-top: 16px; +} + .form-footer { margin-top: 24px; text-align: center; diff --git a/web/src/views/build/BuildTasks.vue b/web/src/views/build/BuildTasks.vue index 0dd2c27..5e3d8b2 100644 --- a/web/src/views/build/BuildTasks.vue +++ b/web/src/views/build/BuildTasks.vue @@ -362,6 +362,21 @@
+ +
+ 构建参数 + +
+ + +
{{ param.description }}
+
+
+
+ { buildForm.commit_id = ''; buildForm.requirement = ''; buildForm.version = ''; + buildForm.parameterValues = {}; + + // 初始化参数默认值 + if (record.parameters && record.parameters.length > 0) { + record.parameters.forEach(param => { + // 处理参数选项 + param.choiceOptions = (param.choices || []).map(choice => ({ + label: choice, + value: choice + })); + + // 设置默认值 + buildForm.parameterValues[param.name] = [...(param.default_values || [])]; + }); + } // 显示构建对话框 buildModalVisible.value = true; @@ -1213,7 +1244,8 @@ const confirmBuild = async () => { // 构建请求参数 const requestData = { task_id: selectedTask.value.task_id, - requirement: buildForm.requirement + requirement: buildForm.requirement, + parameter_values: buildForm.parameterValues }; if (isDevOrTestEnv.value) { @@ -1326,14 +1358,6 @@ const isStagingOrProdEnv = computed(() => { // 构建对话框标题 const buildModalTitle = computed(() => { - if (!selectedTask.value) return '构建配置'; - - const envType = selectedTask.value.environment?.type; - if (envType === 'development') return '开发环境构建配置'; - if (envType === 'testing') return '测试环境构建配置'; - if (envType === 'staging') return '预发布环境构建配置'; - if (envType === 'production') return '生产环境构建配置'; - return '构建配置'; }); @@ -1698,4 +1722,18 @@ const handleViewBuildDetail = (record) => { font-size: 12px; color: #999; } + +.build-parameters { + margin: 16px 0; +} + +.parameter-group { + margin-bottom: 16px; +} + +.parameter-help { + font-size: 12px; + color: rgba(0, 0, 0, 0.45); + margin-top: 4px; +} \ No newline at end of file