| name | 26-pipeline-variable-extension |
| description | 流水线变量字段扩展指南,涵盖变量字段定义、类型扩展、变量作用域、变量继承、自定义变量处理。当用户扩展流水线变量、添加新变量字段、处理变量作用域或实现变量继承时使用。 |
Skill 26: 流水线变量字段扩展
适用场景
当需要为流水线变量(Pipeline Variable)新增字段时,本指南提供完整的改动路径和规范。
核心概念
两种变量模型
BK-CI 中流水线变量存在两种数据模型的双向转换:
BuildFormProperty(后端内部模型)
- 位置:
common-pipeline/src/main/kotlin/.../BuildFormProperty.kt - 用途:微服务内部使用,数据库存储格式
- 特点:完整的字段定义,包含 Swagger 注解
- 位置:
Variable(YAML 模型)
- 位置:
common-pipeline-yaml/src/main/kotlin/.../yaml/v3/models/Variable.kt - 用途:YAML 流水线定义,对外 API 交互
- 特点:Jackson 注解,支持 JSON 序列化
- 位置:
转换器(VariableTransfer)
- 位置:
common-pipeline-yaml/src/main/kotlin/.../yaml/transfer/VariableTransfer.kt - 职责:实现
BuildFormProperty↔Variable的双向转换 - 核心方法:
makeVariableFromModel()- Model → YAMLmakeVariableFromYaml()- YAML → Model
改动清单(按顺序执行)
1. 定义 YAML 模型字段
文件:src/backend/ci/core/common/common-pipeline-yaml/src/main/kotlin/com/tencent/devops/process/yaml/v3/models/Variable.kt
操作:在 Variable 或 VariableProps data class 中添加字段
规范:
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
data class Variable(
// ... 现有字段
@get:JsonProperty("new-field-name") // YAML 中的字段名(kebab-case)
@get:Schema(title = "字段中文描述", required = false)
var newFieldName: Boolean? = null, // Kotlin 属性名(camelCase)
// 注意事项:
// 1. 使用可空类型(Type?)避免破坏已有数据
// 2. @JsonProperty 注解指定 YAML 序列化名称
// 3. @Schema 提供 API 文档描述
)
实际案例(#12471):
data class Variable(
// ... 其他字段
@get:JsonProperty("as-instance-input")
@get:Schema(title = "默认为实例入参,只有模版才有值,流水线没有值", required = false)
var asInstanceInput: Boolean? = null,
)
2. 定义内部模型字段
文件:src/backend/ci/core/common/common-pipeline/src/main/kotlin/com/tencent/devops/common/pipeline/pojo/BuildFormProperty.kt
操作:在 BuildFormProperty data class 中添加对应字段
规范:
@Schema(title = "构建模型-表单元素属性")
data class BuildFormProperty(
// ... 现有字段
@get:Schema(
title = "字段中文描述,说明用途和作用范围",
required = false
)
var newFieldName: Boolean? = null
)
注意事项:
- 字段名与 Variable 中保持一致(camelCase)
- 使用 Swagger v3 注解(
@Schema) - 参数顺序:建议放在 data class 末尾,避免影响现有构造函数调用
实际案例(#12471):
data class BuildFormProperty(
// ... 其他字段
@get:Schema(
title = "在新增实例、以及新增变量时作用,控制实例化页面「实例入参」按钮, 当required:true时,值才生效",
required = false
)
var asInstanceInput: Boolean? = null
)
3. 实现 Model → YAML 转换逻辑
文件:src/backend/ci/core/common/common-pipeline-yaml/src/main/kotlin/com/tencent/devops/process/yaml/transfer/VariableTransfer.kt
方法:makeVariableFromModel(triggerContainer: TriggerContainer?): Map<String, Variable>?
操作:在构建 Variable 对象时,添加字段映射逻辑
规范:
fun makeVariableFromModel(triggerContainer: TriggerContainer?): Map<String, Variable>? {
// ... 现有代码
triggerContainer?.params?.forEach {
// ... 处理 props
result[it.id] = Variable(
value = ...,
readonly = ...,
// 新增字段转换逻辑(根据业务规则)
newFieldName = if (/* 条件判断 */) {
it.newFieldName.nullIfDefault(defaultValue)
} else null,
// ... 其他字段
)
}
}
实际案例(#12471):
result[it.id] = Variable(
value = ...,
readonly = ...,
allowModifyAtStartup = if (const != true) it.required.nullIfDefault(true) else null,
// 新增 asInstanceInput 转换:仅在非常量且 required=true 时转换
asInstanceInput = if (const != true && it.required) {
it.asInstanceInput.nullIfDefault(true)
} else null,
const = const,
// ...
)
关键技巧:
- 使用
.nullIfDefault(默认值)扩展方法省略默认值(减少 YAML 冗余) - 根据业务逻辑判断是否需要序列化该字段
- 常见条件:
if (const != true)(非常量时才处理)
4. 实现 YAML → Model 转换逻辑
文件:同上 VariableTransfer.kt
方法:makeVariableFromYaml(variables: Map<String, Variable>?): List<BuildFormProperty>
操作:在构建 BuildFormProperty 时,添加字段赋值
规范:
fun makeVariableFromYaml(variables: Map<String, Variable>?): List<BuildFormProperty> {
// ... 现有代码
variables.forEach { (key, variable) ->
// 计算派生字段(如有必要)
val allowModifyAtStartup = variable.allowModifyAtStartup ?: true
buildFormProperties.add(
BuildFormProperty(
id = key,
// ... 其他字段
// 新增字段赋值(可能需要计算逻辑)
newFieldName = if (/* 条件 */) {
variable.newFieldName ?: defaultValue
} else null
)
)
}
}
实际案例(#12471):
val allowModifyAtStartup = variable.allowModifyAtStartup ?: true
buildFormProperties.add(
BuildFormProperty(
id = key,
required = allowModifyAtStartup,
// ...
// 仅在 allowModifyAtStartup=true 时赋值,否则为 null
asInstanceInput = if (allowModifyAtStartup) {
variable.asInstanceInput ?: true
} else null
)
)
注意事项:
- 考虑字段间的依赖关系(如
asInstanceInput依赖required) - 提供合理的默认值(
?: defaultValue) - 保持与前端约定的语义一致
5. 更新 YAML JSON Schema
文件:src/backend/ci/core/common/common-pipeline-yaml/src/main/resources/schema/V3_0/ci.json
操作:在 Variable 的 JSON Schema 定义中添加字段
路径定位:搜索 "definitions" → "Variable" 或相关对象
规范:
{
"definitions": {
"Variable": {
"type": "object",
"properties": {
"value": { "type": ["string", "number", "boolean", "object"] },
"readonly": { "type": "boolean" },
"allow-modify-at-startup": { "type": "boolean" },
"new-field-name": {
"type": "boolean",
"description": "字段描述"
}
}
}
}
}
实际案例(#12471):
{
"allow-modify-at-startup" : {
"type" : "boolean"
},
"as-instance-input" : {
"type" : "boolean"
},
"value-not-empty" : {
"type" : "boolean"
}
}
作用:
- 为 IDE 提供 YAML 自动补全
- 验证 YAML 文件格式正确性
- 生成 API 文档
6. 传递字段到服务层(可选)
场景:如果需要在业务服务中使用新字段
文件:src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/ParamFacadeService.kt
操作:在构建参数方法中传递字段
规范:
private fun copyFormProperty(
property: BuildFormProperty,
// ... 其他参数
): BuildFormProperty {
return BuildFormProperty(
id = property.id,
required = property.required,
// ... 其他字段
newFieldName = property.newFieldName // 传递新字段
)
}
实际案例(#12471):
return BuildFormProperty(
id = property.id,
// ...
displayCondition = property.displayCondition,
asInstanceInput = property.asInstanceInput // 新增传递
)
国际化(如需要)
7. 添加多语言支持
文件:support-files/i18n/project/message_*.properties
操作:如果字段涉及用户可见文案,需添加翻译
示例:
# message_zh_CN.properties
variable.asInstanceInput=默认为实例入参
# message_en_US.properties
variable.asInstanceInput=Default as Instance Input
# message_ja_JP.properties
variable.asInstanceInput=デフォルトでインスタンス入力
测试建议
单元测试覆盖
测试文件位置:src/backend/ci/core/common/common-pipeline-yaml/src/test/kotlin/.../VariableTransferTest.kt
测试用例类型:
@Test
fun `test makeVariableFromModel with new field`() {
// 测试 Model → YAML 转换
val property = BuildFormProperty(
id = "testVar",
newFieldName = true,
// ...
)
val result = variableTransfer.makeVariableFromModel(...)
assertEquals(true, result["testVar"]?.newFieldName)
}
@Test
fun `test makeVariableFromYaml with new field`() {
// 测试 YAML → Model 转换
val variable = Variable(
value = "test",
newFieldName = false
)
val result = variableTransfer.makeVariableFromYaml(mapOf("testVar" to variable))
assertEquals(false, result[0].newFieldName)
}
@Test
fun `test new field default value`() {
// 测试默认值处理
val variable = Variable(value = "test", newFieldName = null)
val result = variableTransfer.makeVariableFromYaml(mapOf("testVar" to variable))
assertEquals(expectedDefault, result[0].newFieldName)
}
最佳实践
DO ✅
字段命名一致性
- YAML:
kebab-case(如as-instance-input) - Kotlin:
camelCase(如asInstanceInput) - 使用
@JsonProperty映射
- YAML:
可空类型优先
var newField: Boolean? = null // ✅ 推荐 var newField: Boolean = false // ❌ 避免(破坏兼容性)省略默认值
// 使用 nullIfDefault 减少 YAML 冗余 newField = variable.newField.nullIfDefault(true)条件序列化
// 仅在特定条件下才序列化字段 asInstanceInput = if (const != true && required) { property.asInstanceInput.nullIfDefault(true) } else null注释清晰
// 仅在非常量且必填时生效 @get:Schema(title = "默认为实例入参,只有模版才有值,流水线没有值", required = false)
DON'T ❌
直接修改字段顺序
- 避免修改 data class 已有字段的顺序
- 新字段追加到末尾
忽略向后兼容
// ❌ 错误:强制要求非空 var newField: Boolean // ✅ 正确:可空类型 var newField: Boolean? = null缺少 Schema 注解
// ❌ 缺少文档 var newField: Boolean? = null // ✅ 完整注解 @get:Schema(title = "字段说明", required = false) var newField: Boolean? = null忘记更新 JSON Schema
- 会导致 IDE 自动补全失效
- YAML 验证可能不准确
常见问题
Q1: 字段应该放在 Variable 还是 VariableProps?
判断标准:
- Variable:核心字段(值、只读、常量等)
- VariableProps:UI 相关、类型特定属性(标签、选项、描述等)
案例(#12471):
asInstanceInput放在Variable:因为它是模板实例化的核心逻辑label、description在VariableProps:UI 展示属性
Q2: 如何处理字段间的依赖关系?
场景:asInstanceInput 依赖 required 字段
解决方案:
// 先计算依赖字段
val allowModifyAtStartup = variable.allowModifyAtStartup ?: true
// 再使用条件赋值
asInstanceInput = if (allowModifyAtStartup) {
variable.asInstanceInput ?: true
} else null
Q3: 什么时候需要更新 ParamFacadeService?
场景判断:
- ✅ 需要:字段在业务逻辑中使用(如实例化、执行时参数校验)
- ❌ 不需要:纯粹的存储字段(如 UI 配置)
检查清单
变更完成后,逐项确认:
- Variable.kt 新增字段(含 @JsonProperty、@Schema)
- BuildFormProperty.kt 新增字段(含 @Schema)
- VariableTransfer.makeVariableFromModel() 添加转换逻辑
- VariableTransfer.makeVariableFromYaml() 添加转换逻辑
- ci.json JSON Schema 更新
- ParamFacadeService.kt 字段传递(如需要)
- 国际化文件添加翻译(如需要)
- 单元测试覆盖双向转换和默认值场景
- 代码通过 Detekt 静态检查
- 提交信息符合规范(
feat: 变量支持xxx字段 #issue)
参考案例
完整实现:asInstanceInput 字段(#12471)
需求:模板实例化时控制变量是否作为实例入参
改动文件:
Variable.kt- 添加asInstanceInput字段BuildFormProperty.kt- 添加asInstanceInput字段VariableTransfer.kt- 实现双向转换(共 2 处)ci.json- 添加 Schema 定义ParamFacadeService.kt- 传递字段
核心逻辑:
// Model → YAML: 仅在非常量且必填时转换
asInstanceInput = if (const != true && it.required) {
it.asInstanceInput.nullIfDefault(true)
} else null
// YAML → Model: 依赖 allowModifyAtStartup
asInstanceInput = if (allowModifyAtStartup) {
variable.asInstanceInput ?: true
} else null
总结
新增流水线变量字段的标准流程:
数据模型定义(2个文件)
↓
转换逻辑实现(1个文件,2个方法)
↓
Schema 更新(1个文件)
↓
服务层传递(按需)
↓
国际化支持(按需)
↓
测试验证
遵循本指南可确保字段扩展的完整性、一致性和向后兼容性。
相关文件
Variable.kt:src/backend/ci/core/common/common-pipeline-yaml/src/main/kotlin/com/tencent/devops/process/yaml/v3/models/Variable.ktBuildFormProperty.kt:src/backend/ci/core/common/common-pipeline/src/main/kotlin/com/tencent/devops/common/pipeline/pojo/BuildFormProperty.ktVariableTransfer.kt:src/backend/ci/core/common/common-pipeline-yaml/src/main/kotlin/com/tencent/devops/process/yaml/transfer/VariableTransfer.ktci.json:src/backend/ci/core/common/common-pipeline-yaml/src/main/resources/schema/V3_0/ci.json