yaml-pipeline-transfer
npx skills add https://github.com/tencentblueking/bk-ci --skill yaml-pipeline-transfer
Agent 安装分布
Skill 文档
Skill 22: YAML æµæ°´çº¿è½¬æ¢æå
æ¦è¿°
YAML æµæ°´çº¿è½¬æ¢æ¯ BK-CI çæ ¸å¿åè½ä¹ä¸ï¼æ¯æ Pipeline as Codeï¼PACï¼æ¨¡å¼ãæ¬ Skill 详ç»ä»ç» YAML ä¸ Model ä¹é´çååè½¬æ¢æºå¶ã模æ¿ç³»ç»ã触åå¨é ç½®çå ³é®ææ¯ã
è§¦åæ¡ä»¶
å½ç¨æ·éè¦å®ç°ä»¥ä¸åè½æ¶ï¼ä½¿ç¨æ¤ Skillï¼
- YAML æµæ°´çº¿è§£æä¸è½¬æ¢
- Model 转æ¢ä¸º YAML
- PACï¼Pipeline as Codeï¼å®ç°
- æµæ°´çº¿æ¨¡æ¿å¼ç¨
- YAML è¯æ³æ ¡éª
- èªå®ä¹ YAML å¤çé»è¾
æ ¸å¿ç»ä»¶æ¶æ
1. TransferMapperï¼è½¬æ¢æ å°å¨ï¼
ä½ç½®ï¼common-pipeline-yaml/src/main/kotlin/.../transfer/TransferMapper.kt
èè´£ï¼YAML ä¸å¯¹è±¡ä¹é´çåºåå/ååºååæ ¸å¿å¼æ
å ³é®æ¹æ³
object TransferMapper {
// YAML å符串转对象
fun <T> to(str: String): T
// 对象转 YAML å符串
fun toYaml(bean: Any): String
// ä»»æå¯¹è±¡è½¬æ¢
fun <T> anyTo(any: Any?): T
// æ ¼å¼å YAML
fun formatYaml(yaml: String): String
// åå¹¶ YAMLï¼ä¿ç注éåéç¹ï¼
fun mergeYaml(old: String, new: String): String
// è·å YAML 第ä¸å±çº§çåæ å®ä½
fun getYamlLevelOneIndex(yaml: String): Map<String, TransferMark>
// YAML èç¹ç´¢å¼
fun indexYaml(yaml: String, line: Int, column: Int): NodeIndex?
// æ è®° YAML èç¹ä½ç½®
fun markYaml(index: NodeIndex, yaml: String): TransferMark?
// è·å YAML å·¥å
fun getYamlFactory(): Yaml
// è·å ObjectMapper
fun getObjectMapper(): ObjectMapper
}
èªå®ä¹ç¹æ§
1. èªå®ä¹å符串å¼å·æ£æ¥å¨
è§£å³ YAML on å
³é®åçç¹æ®ç¨æ³ï¼
class CustomStringQuotingChecker : StringQuotingChecker() {
override fun needToQuoteName(name: String): Boolean {
// èªå®ä¹é»è¾ï¼on å
³é®åä¸å å¼å·
if (name == "on") return false
return reservedKeyword(name) || looksLikeYAMLNumber(name)
}
// æ£æµåå
è¿å¶æ°åï¼0xå¼å¤´éè¦å å¼å·ï¼
private fun looksLikeHexNumber(value: String): Boolean {
if (value.length < 3) return false
return value.startsWith("0x", ignoreCase = true)
}
}
2. èªå®ä¹ YAML çæå¨
å»é¤æ¢è¡ç¬¦åçå°¾éç©ºæ ¼ï¼æ¯æ YAML Block è¾åºï¼
override fun writeString(text: String) {
super.writeString(removeTrailingSpaces(text))
}
private fun removeTrailingSpaces(text: String): String {
val result = StringBuilder(text.length)
// éè¡å¤çï¼ç§»é¤æ¯è¡æ«å°¾çç©ºæ ¼
// ...
return result.toString()
}
3. éç¹ï¼Anchorï¼ç®¡ç
// æ¶é YAML ä¸çææéç¹
private fun anchorNode(node: Node, anchors: MutableMap<String, Node>)
// æ¿æ¢ç¸åèç¹ä¸ºéç¹å¼ç¨
private fun replaceAnchor(node: Node, anchors: Map<String, Node>)
// èªå®ä¹éç¹çæå¨ï¼ä¿æåå½åï¼
class CustomAnchorGenerator : AnchorGenerator {
override fun nextAnchor(node: Node): String {
return node.anchor // ä¸éå½å
}
}
4. mergeYaml åè½
æºè½å并两个 YAMLï¼ä¿ç注éåéç¹ï¼
fun mergeYaml(old: String, new: String): String {
if (old.isBlank()) return new
val oldE = getYamlFactory().parse(old.reader()).toList()
val newE = getYamlFactory().parse(new.reader()).toMutableList()
// ä½¿ç¨ Myers Diff ç®æ³è®¡ç®å·®å¼
val patch = DiffUtils.diff(oldE, newE, MeyersDiffWithLinearSpace.factory().create())
// å¤ç注éåéç¹
for (delta in patch.deltas) {
when (delta.type) {
DeltaType.DELETE -> {
// ä¿çæºæä»¶çæ³¨é
val sourceComment = checkCommentEvent(delta.source.lines)
if (sourceComment.isNotEmpty()) {
newE.addAll(delta.target.position, sourceComment)
}
}
DeltaType.INSERT -> {
// ä¿çéç¹ä¿¡æ¯
anchorChecker[delta.source.position]?.let { checker ->
// æ¢å¤éç¹
}
}
}
}
// é建èç¹ï¼æ¢å¤éç¹å¼ç¨
val newNode = eventsComposer(newE).singleNode
replaceAnchor(newNode, anchorNodes)
return getYamlFactory().serialize(newNode)
}
2. ModelTransferï¼Model 转æ¢å¨ï¼
ä½ç½®ï¼common-pipeline-yaml/src/main/kotlin/.../transfer/ModelTransfer.kt
èè´£ï¼YAML â Pipeline Model çæ ¸å¿è½¬æ¢é»è¾
å ³é®æ¹æ³
@Component
class ModelTransfer @Autowired constructor(
val client: Client,
val modelStage: StageTransfer,
val elementTransfer: ElementTransfer,
val variableTransfer: VariableTransfer,
val transferCache: TransferCacheService
) {
// YAML 转 Model
fun yaml2Model(yamlInput: YamlTransferInput): Model
// YAML 转 Setting
fun yaml2Setting(yamlInput: YamlTransferInput): PipelineSetting
// YAML 转 Labels
fun yaml2Labels(yamlInput: YamlTransferInput): List<String>
// Model 转 YAML
fun model2Yaml(input: ModelTransferInput): PreTemplateScriptBuildYamlParser
}
yaml2Model å®ç°æµç¨
fun yaml2Model(yamlInput: YamlTransferInput): Model {
// 1. åç½®åé¢å¤ç
yamlInput.aspectWrapper.setYaml4Yaml(yamlInput.yaml, BEFORE)
// 2. æå»º Model åºç¡ç»æ
val stageList = mutableListOf<Stage>()
val model = Model(
name = yamlInput.yaml.name ?: yamlInput.pipelineInfo?.pipelineName ?: "",
desc = yamlInput.yaml.desc ?: yamlInput.pipelineInfo?.pipelineDesc ?: "",
stages = stageList,
labels = emptyList(),
instanceFromTemplate = false,
pipelineCreator = yamlInput.pipelineInfo?.creator ?: yamlInput.userId
)
val stageIndex = AtomicInteger(0)
// 3. æå»º Trigger Stage
if (!yamlInput.yaml.checkForTemplateUse()) {
stageList.add(modelStage.yaml2TriggerStage(yamlInput, stageIndex.incrementAndGet()))
}
// 4. æå»ºæ®é Stage
formatStage(yamlInput, stageList, stageIndex)
// 5. æå»º Finally Stage
formatFinally(yamlInput, stageList, stageIndex.incrementAndGet())
// 6. å¤ç模æ¿å¼ç¨
formatTemplate(yamlInput, model)
// 7. åç½®åé¢å¤ç
yamlInput.aspectWrapper.setModel4Model(model, AFTER)
return model
}
yaml2Setting å®ç°
fun yaml2Setting(yamlInput: YamlTransferInput): PipelineSetting {
val yaml = yamlInput.yaml
return PipelineSetting(
projectId = yamlInput.pipelineInfo?.projectId ?: "",
pipelineId = yamlInput.pipelineInfo?.pipelineId ?: "",
buildNumRule = yaml.customBuildNum,
pipelineName = yaml.name ?: yamlInput.yamlFileName ?: yamlInput.pipelineInfo?.pipelineName ?: "",
desc = yaml.desc ?: yamlInput.pipelineInfo?.pipelineDesc ?: "",
// å¹¶åæ§å¶
concurrencyGroup = yaml.concurrency?.group ?: PIPELINE_SETTING_CONCURRENCY_GROUP_DEFAULT,
concurrencyCancelInProgress = yaml.concurrency?.cancelInProgress ?: false,
runLockType = when {
yaml.disablePipeline == true -> PipelineRunLockType.LOCK
yaml.concurrency?.group != null -> PipelineRunLockType.GROUP_LOCK
else -> PipelineRunLockType.MULTIPLE
},
waitQueueTimeMinute = yaml.concurrency?.queueTimeoutMinutes ?: DEFAULT_WAIT_QUEUE_TIME_MINUTE,
maxQueueSize = yaml.concurrency?.queueLength ?: DEFAULT_PIPELINE_SETTING_MAX_QUEUE_SIZE,
maxConRunningQueueSize = yaml.concurrency?.maxParallel ?: PIPELINE_SETTING_MAX_CON_QUEUE_SIZE_MAX,
// æ ç¾åæ¹è¨
labels = yaml2Labels(yamlInput),
pipelineAsCodeSettings = yamlSyntaxDialect2Setting(yaml.syntaxDialect),
// éç¥è®¢é
successSubscriptionList = yamlNotice2Setting(
projectId = yamlInput.projectCode,
notices = yaml.notices?.filter { it.checkNotifyForSuccess() }
),
failSubscriptionList = yamlNotice2Setting(
projectId = yamlInput.projectCode,
notices = yaml.notices?.filter { it.checkNotifyForFail() }
),
// å
¶ä»é
ç½®
failIfVariableInvalid = yaml.failIfVariableInvalid.nullIfDefault(false),
buildCancelPolicy = BuildCancelPolicy.codeParse(yaml.cancelPolicy)
)
}
3. ElementTransferï¼å ç´ è½¬æ¢å¨ï¼
ä½ç½®ï¼common-pipeline-yaml/src/main/kotlin/.../transfer/ElementTransfer.kt
èè´£ï¼YAML Step â Model Element 转æ¢
å ç´ ç±»åæ å°
| YAML Step | Model Element | 说æ |
|---|---|---|
uses: checkout@v2 |
GitCheckoutElement |
ä»£ç æ£åº |
uses: <atomCode>@v* |
MarketBuildAtomElement |
ç åååºæä»¶ |
run: <script> |
LinuxScriptElement / WindowsScriptElement |
èæ¬æ§è¡ |
uses: manual-review@v* |
ManualReviewUserTaskElement |
äººå·¥å®¡æ ¸ |
template: <path> |
StepTemplateElement |
æ¥éª¤æ¨¡æ¿ |
å ³é®æ¹æ³
@Component
class ElementTransfer @Autowired constructor(
val client: Client,
val creator: TransferCreator,
val transferCache: TransferCacheService,
val triggerTransfer: TriggerTransfer
) {
// YAML 转触åå¨
fun yaml2Triggers(yamlInput: YamlTransferInput, elements: MutableList<Element>)
// 触åå¨è½¬ YAML
fun baseTriggers2yaml(elements: List<Element>, aspectWrapper: PipelineTransferAspectWrapper): TriggerOn?
// SCM 触åå¨è½¬ YAML
fun scmTriggers2Yaml(elements: List<Element>, projectId: String, aspectWrapper: PipelineTransferAspectWrapper): Map<ScmType, List<TriggerOn>>
// YAML Step 转 Element
fun yaml2Step(step: Step, job: Job, yamlInput: YamlTransferInput): Element
// Element 转 YAML Step
fun element2YamlStep(element: Element, projectId: String): PreStep
}
yaml2Step å®ç°ç¤ºä¾
fun yaml2Step(step: Step, job: Job, yamlInput: YamlTransferInput): Element {
return when {
// checkout æ¥éª¤
step is PreCheckoutStep -> {
GitCheckoutElement(
name = step.name ?: "Checkout",
repositoryHashId = step.with?.get("repository") as? String,
branchName = step.with?.get("ref") as? String,
// ... å
¶ä»åæ°
)
}
// uses: æä»¶æ¥éª¤
step.uses != null -> {
val (atomCode, version) = parseAtomCodeAndVersion(step.uses!!)
MarketBuildAtomElement(
name = step.name ?: atomCode,
atomCode = atomCode,
version = version,
data = step.with ?: emptyMap()
)
}
// run: èæ¬æ¥éª¤
step.run != null -> {
when (job.runsOn) {
JobRunsOnType.WINDOWS -> WindowsScriptElement(
name = step.name ?: "Script",
script = step.run!!,
scriptType = BuildScriptType.BAT
)
else -> LinuxScriptElement(
name = step.name ?: "Script",
script = step.run!!,
scriptType = BuildScriptType.SHELL
)
}
}
// template: æ¥éª¤æ¨¡æ¿
step.template != null -> {
StepTemplateElement(
name = step.name ?: "Template",
templatePath = step.template!!,
parameters = step.with ?: emptyMap()
)
}
else -> throw ModelCreateException("Invalid step definition")
}
}
4. StageTransferï¼Stage 转æ¢å¨ï¼
ä½ç½®ï¼common-pipeline-yaml/src/main/kotlin/.../transfer/StageTransfer.kt
èè´£ï¼YAML Stage â Model Stage 转æ¢
å ³é®æ¹æ³
@Component
class StageTransfer @Autowired constructor(
val containerTransfer: ContainerTransfer,
val elementTransfer: ElementTransfer,
val variableTransfer: VariableTransfer
) {
// YAML 转 Trigger Stage
fun yaml2TriggerStage(yamlInput: YamlTransferInput, stageIndex: Int): Stage
// YAML Stage 转 Model Stage
fun yaml2NormalStage(
stage: IStage,
yamlInput: YamlTransferInput,
stageIndex: Int
): Stage
// Model Stage 转 YAML Stage
fun stage2YamlStage(stage: Stage, projectId: String): PreStage
}
5. ContainerTransferï¼Container 转æ¢å¨ï¼
ä½ç½®ï¼common-pipeline-yaml/src/main/kotlin/.../transfer/ContainerTransfer.kt
èè´£ï¼YAML Job â Model Container 转æ¢
Container ç±»åæ å°
| YAML Job runs-on | Model Container | 说æ |
|---|---|---|
linux |
VMBuildContainer |
Linux èææº |
windows |
VMBuildContainer |
Windows èææº |
macos |
VMBuildContainer |
macOS èææº |
agent-id: <id> |
ThirdPartyAgentIdContainer |
ç¬¬ä¸æ¹æå»ºæºï¼IDï¼ |
agent-name: <name> |
ThirdPartyAgentNameContainer |
ç¬¬ä¸æ¹æå»ºæºï¼åç§°ï¼ |
pool: <pool> |
ThirdPartyAgentNameContainer |
æå»ºæ± |
self-hosted: true |
NormalContainer |
èªæç®¡ç¯å¢ |
6. TriggerTransferï¼è§¦åå¨è½¬æ¢å¨ï¼
ä½ç½®ï¼common-pipeline-yaml/src/main/kotlin/.../transfer/TriggerTransfer.kt
èè´£ï¼å¤çåç§è§¦åå¨ç转æ¢é»è¾
æ¯æç触åå¨ç±»å
enum class TriggerType {
BASE, // åºç¡è§¦åå¨ï¼æå¨ã宿¶ãè¿ç¨ï¼
CODE_GIT, // Git 触åå¨
CODE_TGIT, // TGit 触åå¨
GITHUB, // GitHub 触åå¨
CODE_SVN, // SVN 触åå¨
CODE_P4, // Perforce 触åå¨
CODE_GITLAB, // GitLab 触åå¨
SCM_GIT, // SCM Git 触åå¨
SCM_SVN // SCM SVN 触åå¨
}
触åå¨è½¬æ¢æ¹æ³
@Component
class TriggerTransfer {
// åºç¡è§¦åå¨ï¼æå¨ã宿¶ãè¿ç¨ï¼
fun yaml2TriggerBase(yamlInput: YamlTransferInput, triggerOn: TriggerOn, elements: MutableList<Element>)
// Git 触åå¨
fun yaml2TriggerGit(triggerOn: TriggerOn, elements: MutableList<Element>)
// GitHub 触åå¨
fun yaml2TriggerGithub(triggerOn: TriggerOn, elements: MutableList<Element>)
// 宿¶è§¦åå¨è½¬ YAML
fun timer2YamlTrigger(element: TimerTriggerElement): SchedulesRule
// Git WebHook 转 YAML
fun git2YamlTriggerOn(
elements: List<WebHookTriggerElementChanger>,
projectId: String,
aspectWrapper: PipelineTransferAspectWrapper,
defaultName: String
): List<TriggerOn>
}
7. VariableTransferï¼åé转æ¢å¨ï¼
ä½ç½®ï¼common-pipeline-yaml/src/main/kotlin/.../transfer/VariableTransfer.kt
èè´£ï¼YAML Variable â Model BuildFormProperty 转æ¢
è¯¦è§ æµæ°´çº¿åé管çæå – åéåæ®µæ©å±
YAML æ°æ®æ¨¡å
çæ¬ä½ç³»
BK-CI æ¯æä¸¤ä¸ª YAML çæ¬ï¼
| çæ¬ | æ è¯ | 说æ |
|---|---|---|
| v2.0 | version: v2.0 |
å½åä¸»çæ¬ |
| v3.0 | version: v3.0 |
å¢å¼ºçæ¬ï¼æ¯ææ´å¤ç¹æ§ï¼ |
æ¥å£å®ä¹ï¼
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "version",
defaultImpl = PreTemplateScriptBuildYamlParser::class
)
@JsonSubTypes(
JsonSubTypes.Type(value = PreTemplateScriptBuildYamlV3Parser::class, name = YamlVersion.V3),
JsonSubTypes.Type(value = PreTemplateScriptBuildYamlParser::class, name = YamlVersion.V2)
)
interface IPreTemplateScriptBuildYamlParser : YamlVersionParser {
val version: String?
val name: String?
val desc: String?
val label: List<String>?
val notices: List<Notices>?
var concurrency: Concurrency?
var disablePipeline: Boolean?
var recommendedVersion: RecommendedVersion?
var customBuildNum: String?
var syntaxDialect: String?
var failIfVariableInvalid: Boolean?
var cancelPolicy: String?
fun replaceTemplate(f: (param: ITemplateFilter) -> PreScriptBuildYamlIParser)
fun formatVariables(): Map<String, Variable>
fun formatTriggerOn(default: ScmType): List<Pair<TriggerType, TriggerOn>>
fun formatStages(): List<IStage>
fun formatFinallyStage(): List<IJob>
fun formatResources(): Resources?
fun formatExtends(): Extends?
fun templateFilter(): ITemplateFilter
fun settingGroups(): List<PipelineSettingGroupType>?
fun checkForTemplateUse(): Boolean
}
宿´ YAML ç»æ
version: v2.0 # çæ¬å·ï¼å¿
å¡«ï¼
name: CI Pipeline # æµæ°´çº¿åç§°
desc: æç»éææµæ°´çº¿æè¿° # æè¿°
label: # æ ç¾
- backend
- production
# ========== 触åå¨é
ç½® ==========
on:
push: # æ¨é触å
branches:
- master
- develop
- /^feature\/.*/
paths: # è·¯å¾è¿æ»¤
- src/**
- build.gradle
paths-ignore: # è·¯å¾æé¤
- docs/**
- "*.md"
mr: # å并请æ±è§¦å
target-branches:
- master
action:
- open
- update
- close
block-mr: true # é»å¡ MR
report-commit-check: true # 䏿¥æäº¤æ£æ¥
tag: # Tag 触å
tags:
- /^v.*/
schedules: # 宿¶è§¦å
- cron: "0 2 * * *" # Cron 表达å¼
always: true # æ»æ¯æ§è¡
branches:
- master
- interval: # åºå®æ¶é´è§¦å
week:
- Mon
- Fri
time-points:
- "02:00"
- "14:00"
manual: # æå¨è§¦å
enable: true
use-latest-parameters: true # ä½¿ç¨æè¿ä¸æ¬¡åæ°
remote: # è¿ç¨è§¦å
enable: true
# ========== åéå®ä¹ ==========
variables:
BUILD_TYPE: # ç®ååé
value: release
readonly: false
allow-modify-at-startup: true
as-instance-input: true
DEPLOY_ENV: # æä¸¾åé
value: prod
props:
type: enum
options:
- dev
- test
- prod
label: "é¨ç½²ç¯å¢"
description: "éæ©é¨ç½²çç®æ ç¯å¢"
API_TOKEN: # å¯ç åé
value: ""
props:
type: password
label: "API Token"
VERSION_NUMBER: # æ°ååé
value: 1
props:
type: number
min: 1
max: 100
# ========== å¹¶åæ§å¶ ==========
concurrency:
group: ${{ variables.BUILD_TYPE }} # å¹¶åç»
cancel-in-progress: true # åæ¶è¿è¡ä¸çæå»º
queue-length: 10 # éåé¿åº¦
queue-timeout-minutes: 30 # éåè¶
æ¶ï¼åéï¼
max-parallel: 5 # æå¤§å¹¶åæ°
# ========== èµæºæ± é
ç½® ==========
resources:
repositories: # 代ç åºèµæº
- repository: my-repo
type: github
name: my-org/my-repo
ref: main
pools: # æå»ºæ±
- pool: my-pool
container: linux
# ========== 模æ¿å¼ç¨ ==========
extends:
template: templates/base.yml # 模æ¿è·¯å¾
parameters: # 模æ¿åæ°
BUILD_TYPE: ${{ variables.BUILD_TYPE }}
# ========== Stage å®ä¹ ==========
stages:
- name: Build # Stage åç§°
label: # Stage æ ç¾
- compile
if: ${{ eq(variables.BUILD_TYPE, 'release') }} # æ§è¡æ¡ä»¶
if-modify: # è·¯å¾åæ´æ¡ä»¶
- src/**
check-in: manual # åå
¥å®¡æ ¸
check-out: manual # ååºå®¡æ ¸
fast-kill: true # å¿«éç»æ¢
jobs: # Job å表
compile: # Job ID
name: ç¼è¯æå»º # Job åç§°
runs-on: linux # è¿è¡ç¯å¢
if: success() # æ§è¡æ¡ä»¶
timeout-minutes: 60 # è¶
æ¶æ¶é´
continue-on-error: false # 失败继ç»
strategy: # ç©éµçç¥
matrix:
os: [linux, windows]
node: [14, 16, 18]
fail-fast: true
env: # ç¯å¢åé
NODE_ENV: production
steps: # æ¥éª¤å表
# Checkout æ¥éª¤
- uses: checkout@v2
with:
repository: ${{ resources.repositories.my-repo }}
ref: ${{ on.push.branch }}
fetch-depth: 1
submodules: false
lfs: false
enable-git-clean: true
# èæ¬æ¥éª¤
- name: Build
run: |
echo "Building..."
./gradlew build
if: success()
continue-on-error: false
timeout-minutes: 30
retry-times: 3
# æä»¶æ¥éª¤
- name: Upload Artifact
uses: upload-artifact@v2
with:
name: build-output
path: build/
retention-days: 7
# æ¥éª¤æ¨¡æ¿
- template: templates/deploy-step.yml
parameters:
env: prod
# äººå·¥å®¡æ ¸
- name: Manual Review
uses: manual-review@v1
with:
desc: "è¯·å®¡æ ¸æå»ºäº§ç©"
reviewers:
- user1
- user2
notify-type: [email, wechat]
- name: Deploy
label:
- deployment
depends-on: # ä¾èµç Stage
- Build
jobs:
deploy:
name: é¨ç½²
runs-on:
pool: prod-pool # æå®æå»ºæ±
steps:
- name: Download Artifact
uses: download-artifact@v2
with:
name: build-output
- name: Deploy
run: ./deploy.sh
# ========== Finally Stage ==========
finally: # æç»æ§è¡ï¼æ 论æå失败ï¼
cleanup:
name: æ¸
ç
runs-on: linux
if: always() # æ»æ¯æ§è¡
steps:
- name: Cleanup
run: rm -rf temp/
# ========== éç¥é
ç½® ==========
notices:
- notify-type: [email, wechat] # éç¥ç±»å
notify-when: [fail] # éç¥æ¶æº
notify-group: [å¼åç»] # éç¥ç»
notify-user: [user1] # éç¥ç¨æ·
content: "æå»ºå¤±è´¥ï¼è¯·åæ¶å¤ç"
title: "æµæ°´çº¿å¤±è´¥éç¥"
# ========== å
¶ä»é
ç½® ==========
disable-pipeline: false # ç¦ç¨æµæ°´çº¿
custom-build-num: ${{ variables.VERSION_NUMBER }} # èªå®ä¹æå»ºå·
syntax-dialect: CLASSIC # è¯æ³æ¹è¨ï¼CLASSIC/CONSTRAINTï¼
fail-if-variable-invalid: false # åéæ ææ¶å¤±è´¥
cancel-policy: SIMPLE # åæ¶çç¥
# ========== æ¨èçæ¬ ==========
recommended-version:
enabled: true
version: "1.0.0"
reason: "稳å®çæ¬"
æ ¸å¿æ¦å¿µè¯¦è§£
1. YamlTransferInputï¼è½¬æ¢è¾å ¥ï¼
data class YamlTransferInput(
val userId: String, // ç¨æ· ID
val projectCode: String, // é¡¹ç® ID
val yaml: IPreTemplateScriptBuildYamlParser, // YAML 对象
val pipelineInfo: PipelineInfo? = null, // æµæ°´çº¿ä¿¡æ¯
val yamlFileName: String? = null, // YAML æä»¶å
val defaultScmType: ScmType = ScmType.CODE_GIT, // é»è®¤ SCM ç±»å
val aspectWrapper: PipelineTransferAspectWrapper = PipelineTransferAspectWrapper() // åé¢å
è£
å¨
)
2. ModelTransferInputï¼Model 转æ¢è¾å ¥ï¼
data class ModelTransferInput(
val userId: String,
val projectCode: String,
val model: Model,
val yamlVersion: YamlVersion = YamlVersion.V2_0,
val checkPermission: Boolean = true,
val defaultScmType: ScmType = ScmType.CODE_GIT,
val aspectWrapper: PipelineTransferAspectWrapper = PipelineTransferAspectWrapper()
)
3. TransferMarkï¼ä½ç½®æ è®°ï¼
ç¨äºå¨ YAML æä»¶ä¸å®ä½èç¹ä½ç½®ï¼
data class TransferMark(
val startMark: Mark, // èµ·å§ä½ç½®
val endMark: Mark // ç»æä½ç½®
) {
data class Mark(
val line: Int, // è¡å·
val column: Int // åå·
)
}
4. NodeIndexï¼èç¹ç´¢å¼ï¼
ç¨äºå¨ YAML AST ä¸å®ä½èç¹ï¼
data class NodeIndex(
val key: String? = null, // 对象é®
val index: Int? = null, // æ°ç»ç´¢å¼
val next: NodeIndex? = null // ä¸ä¸çº§èç¹
) {
override fun toString(): String {
return key ?: "array($index)" + (next?.toString() ?: "")
}
}
使ç¨ç¤ºä¾ï¼
// å®ä½ stages[0].jobs.compile.steps[2]
val index = NodeIndex(
key = "stages",
next = NodeIndex(
index = 0,
next = NodeIndex(
key = "jobs",
next = NodeIndex(
key = "compile",
next = NodeIndex(
key = "steps",
next = NodeIndex(
index = 2
)
)
)
)
)
)
使ç¨åºæ¯ä¸ç¤ºä¾
åºæ¯ 1ï¼YAML å¯¼å ¥æµæ°´çº¿
@Service
class PipelineYamlService(
private val modelTransfer: ModelTransfer,
private val pipelineService: PipelineService
) {
fun importFromYaml(
userId: String,
projectId: String,
yaml: String,
yamlFileName: String? = null
): Pipeline {
// 1. è§£æ YAML
val yamlObject = TransferMapper.to<PreTemplateScriptBuildYamlParser>(yaml)
// 2. æ ¡éª YAML
validateYaml(yamlObject)
// 3. æå»ºè½¬æ¢è¾å
¥
val yamlInput = YamlTransferInput(
userId = userId,
projectCode = projectId,
yaml = yamlObject,
yamlFileName = yamlFileName
)
// 4. 转æ¢ä¸º Model
val model = modelTransfer.yaml2Model(yamlInput)
val setting = modelTransfer.yaml2Setting(yamlInput)
// 5. åå»ºæµæ°´çº¿
return pipelineService.createPipeline(
userId = userId,
projectId = projectId,
model = model,
setting = setting
)
}
private fun validateYaml(yaml: IPreTemplateScriptBuildYamlParser) {
if (yaml.name.isNullOrBlank()) {
throw ErrorCodeException(
errorCode = "YAML_NAME_REQUIRED",
defaultMessage = "æµæ°´çº¿åç§°ä¸è½ä¸ºç©º"
)
}
val stages = yaml.formatStages()
if (stages.isEmpty()) {
throw ErrorCodeException(
errorCode = "YAML_STAGES_EMPTY",
defaultMessage = "è³å°éè¦å®ä¹ä¸ä¸ª Stage"
)
}
}
}
åºæ¯ 2ï¼æµæ°´çº¿å¯¼åºä¸º YAML
@Service
class PipelineExportService(
private val modelTransfer: ModelTransfer,
private val pipelineService: PipelineService
) {
fun exportToYaml(
userId: String,
projectId: String,
pipelineId: String,
yamlVersion: YamlVersion = YamlVersion.V2_0
): String {
// 1. è·åæµæ°´çº¿ Model
val model = pipelineService.getModel(userId, projectId, pipelineId)
// 2. æå»ºè½¬æ¢è¾å
¥
val input = ModelTransferInput(
userId = userId,
projectCode = projectId,
model = model,
yamlVersion = yamlVersion
)
// 3. 转æ¢ä¸º YAML 对象
val yamlObject = modelTransfer.model2Yaml(input)
// 4. åºåå为 YAML å符串
return TransferMapper.toYaml(yamlObject)
}
}
åºæ¯ 3ï¼PAC ä»ä»£ç åºåæ¥
@Service
class PacService(
private val repositoryService: RepositoryService,
private val pipelineYamlService: PipelineYamlService
) {
fun syncFromRepo(
userId: String,
projectId: String,
repoUrl: String,
branch: String = "master",
yamlPath: String = ".ci/pipeline.yml"
): Pipeline {
// 1. ä»ä»£ç åºæå YAML æä»¶
val yaml = repositoryService.getFileContent(
repoUrl = repoUrl,
branch = branch,
filePath = yamlPath
)
// 2. 导å
¥æµæ°´çº¿
return pipelineYamlService.importFromYaml(
userId = userId,
projectId = projectId,
yaml = yaml,
yamlFileName = yamlPath
)
}
fun autoSync(
userId: String,
projectId: String,
pipelineId: String
) {
// 1. è·åæµæ°´çº¿ç PAC é
ç½®
val pacConfig = pipelineService.getPacConfig(pipelineId)
// 2. æ£æ¥ä»£ç åºåæ´
val latestCommit = repositoryService.getLatestCommit(
repoUrl = pacConfig.repoUrl,
branch = pacConfig.branch,
filePath = pacConfig.yamlPath
)
if (latestCommit.sha != pacConfig.lastCommitSha) {
// 3. æåææ° YAML
val yaml = repositoryService.getFileContent(
repoUrl = pacConfig.repoUrl,
branch = pacConfig.branch,
filePath = pacConfig.yamlPath
)
// 4. æ´æ°æµæ°´çº¿
val yamlObject = TransferMapper.to<PreTemplateScriptBuildYamlParser>(yaml)
val yamlInput = YamlTransferInput(
userId = userId,
projectCode = projectId,
yaml = yamlObject,
pipelineInfo = PipelineInfo(
pipelineId = pipelineId,
projectId = projectId
)
)
val model = modelTransfer.yaml2Model(yamlInput)
pipelineService.updatePipeline(userId, projectId, pipelineId, model)
// 5. æ´æ°åæ¥è®°å½
pipelineService.updatePacConfig(pipelineId, pacConfig.copy(
lastCommitSha = latestCommit.sha,
lastSyncTime = System.currentTimeMillis()
))
}
}
}
åºæ¯ 4ï¼YAML åå¹¶ï¼ä¿ç注éï¼
@Service
class YamlMergeService {
fun mergeYaml(
oldYaml: String,
newYaml: String
): String {
// ä½¿ç¨ TransferMapper.mergeYaml ä¿ç注éåéç¹
return TransferMapper.mergeYaml(oldYaml, newYaml)
}
fun updatePipelineYaml(
pipelineId: String,
updates: Map<String, Any>
): String {
// 1. è·åå½å YAML
val currentYaml = pipelineService.getPipelineYaml(pipelineId)
// 2. è§£æä¸ºå¯¹è±¡
val yamlObject = TransferMapper.to<PreTemplateScriptBuildYamlParser>(currentYaml)
// 3. åºç¨æ´æ°
val updatedObject = yamlObject.copy(
name = updates["name"] as? String ?: yamlObject.name,
desc = updates["desc"] as? String ?: yamlObject.desc
// ... å
¶ä»å段
)
// 4. 转æ¢ä¸ºæ° YAML
val newYaml = TransferMapper.toYaml(updatedObject)
// 5. åå¹¶ï¼ä¿çå YAML çæ³¨éï¼
return TransferMapper.mergeYaml(currentYaml, newYaml)
}
}
åºæ¯ 5ï¼YAML å ç´ æå ¥ï¼å®ä½æä½ï¼
@Service
class YamlElementInsertService {
fun insertStep(
yaml: String,
stageIndex: Int,
jobId: String,
stepIndex: Int,
newStep: PreStep
): String {
// 1. è§£æ YAML 为对象
val yamlObject = TransferMapper.to<ITemplateFilter>(yaml)
// 2. å®ä½æå
¥ä½ç½®
val position = PositionResponse(
stageIndex = stageIndex,
jobId = jobId,
stepIndex = stepIndex,
type = PositionResponse.PositionType.STEP
)
// 3. 计ç®èç¹ç´¢å¼
val nodeIndex = TransferMapper.indexYaml(
position = position,
pYml = yamlObject,
yml = newStep,
type = ElementInsertBody.ElementInsertType.INSERT
)
// 4. 转æ¢ä¸ºæ° YAML
val newYaml = TransferMapper.toYaml(yamlObject)
// 5. åå¹¶å YAMLï¼ä¿ç注éï¼
return TransferMapper.mergeYaml(yaml, newYaml)
}
fun updateStepByPosition(
yaml: String,
line: Int,
column: Int,
updatedStep: PreStep
): String {
// 1. æ ¹æ®è¡åå·å®ä½èç¹
val nodeIndex = TransferMapper.indexYaml(yaml, line, column)
?: throw ErrorCodeException(errorCode = "INVALID_POSITION")
// 2. è§£æ YAML
val yamlObject = TransferMapper.to<ITemplateFilter>(yaml)
// 3. æ´æ°å¯¹åºèç¹
// ... æ ¹æ® nodeIndex å®ä½å¹¶æ´æ°
// 4. 转æ¢å¹¶åå¹¶
val newYaml = TransferMapper.toYaml(yamlObject)
return TransferMapper.mergeYaml(yaml, newYaml)
}
}
模æ¿ç³»ç»
1. Extends 模æ¿å¼ç¨
# 主 YAML
extends:
template: templates/base.yml
parameters:
BUILD_TYPE: release
DEPLOY_ENV: prod
variables:
CUSTOM_VAR: custom-value
# templates/base.yml
version: v2.0
name: Base Template
variables:
BUILD_TYPE:
value: ${{ parameters.BUILD_TYPE }}
DEPLOY_ENV:
value: ${{ parameters.DEPLOY_ENV }}
stages:
- name: Build
jobs:
build:
runs-on: linux
steps:
- name: Build
run: ./build.sh
2. Step 模æ¿
steps:
- template: templates/deploy-step.yml
parameters:
env: ${{ variables.DEPLOY_ENV }}
# templates/deploy-step.yml
- name: Deploy to ${{ parameters.env }}
run: |
echo "Deploying to ${{ parameters.env }}"
./deploy.sh --env=${{ parameters.env }}
3. Job 模æ¿
stages:
- name: Test
jobs:
test:
template: templates/test-job.yml
parameters:
node-version: 16
4. Stage 模æ¿
stages:
- template: templates/build-stage.yml
parameters:
platform: linux
表达å¼ç³»ç»
1. åéå¼ç¨
# å¼ç¨åé
${{ variables.BUILD_TYPE }}
# å¼ç¨è§¦åå¨ä¿¡æ¯
${{ on.push.branch }}
${{ on.mr.target-branch }}
# å¼ç¨èµæº
${{ resources.repositories.my-repo }}
2. 彿°è°ç¨
// æ¡ä»¶å¤æ
if: ${{ eq(variables.BUILD_TYPE, 'release') }}
if: ${{ ne(variables.DEPLOY_ENV, 'prod') }}
// é»è¾è¿ç®
if: ${{ and(eq(variables.BUILD_TYPE, 'release'), ne(on.push.branch, 'master')) }}
if: ${{ or(eq(on.push.branch, 'master'), eq(on.push.branch, 'develop')) }}
// ç¶æå¤æ
if: success() // ååºæ¥éª¤æå
if: failure() // ååºæ¥éª¤å¤±è´¥
if: always() // æ»æ¯æ§è¡
if: cancelled() // è¢«åæ¶
3. 表达å¼è§£æ
表达å¼è§£æå¨ ParametersExpression å IfField ä¸å¤çï¼
data class IfField(
val mode: Mode, // SIMPLE / COMPLEX
val expression: String // 表达å¼å符串
) {
enum class Mode {
SIMPLE, // ç®å模å¼ï¼success(), failure(), always()
COMPLEX // å¤ææ¨¡å¼ï¼${{ ... }}
}
}
åé¢ç³»ç»ï¼PipelineTransferAspectWrapperï¼
ç¨äºå¨è½¬æ¢è¿ç¨ä¸æ³¨å ¥èªå®ä¹é»è¾ï¼
class PipelineTransferAspectWrapper {
enum class AspectType {
BEFORE, // åç½®å¤ç
AFTER // åç½®å¤ç
}
// 设置 YAML 对象ï¼YAML â Model è¿ç¨ï¼
fun setYaml4Yaml(yaml: IPreTemplateScriptBuildYamlParser, type: AspectType)
// 设置 Model 对象ï¼Model â YAML è¿ç¨ï¼
fun setModel4Model(model: Model, type: AspectType)
// 设置触åå¨
fun setYamlTriggerOn(triggerOn: TriggerOn, type: AspectType)
// 设置å
ç´
fun setModelElement4Model(element: Element, type: AspectType)
}
使ç¨åºæ¯ï¼
- æ·»å èªå®ä¹æ ¡éªé»è¾
- æ³¨å ¥é¢å¤ç转æ¢å¤ç
- è®°å½è½¬æ¢è¿ç¨æ¥å¿
- å®ç°æä»¶åæ©å±
æä½³å®è·µ
1. YAML çæ¬æ§å¶
# â
æ¨èï¼æç¡®æå®çæ¬
version: v2.0
# â 䏿¨èï¼çç¥çæ¬ï¼ä½¿ç¨é»è®¤çæ¬ï¼
2. åéå½åè§è
# â
æ¨èï¼å¤§åä¸å线
variables:
BUILD_TYPE: release
DEPLOY_ENV: prod
# â 䏿¨èï¼é©¼å³°æå°å
variables:
buildType: release
deployenv: prod
3. ä½¿ç¨æ¨¡æ¿å¤ç¨
# â
æ¨èï¼æåå
Œ
±é
ç½®å°æ¨¡æ¿
extends:
template: templates/base.yml
# â 䏿¨èï¼æ¯ä¸ªæµæ°´çº¿éå¤å®ä¹
4. åçä½¿ç¨æ¡ä»¶æ§è¡
# â
æ¨èï¼ä½¿ç¨è¡¨è¾¾å¼æ§å¶æ§è¡
stages:
- name: Deploy
if: ${{ eq(variables.DEPLOY_ENV, 'prod') }}
# â 䏿¨èï¼å¨èæ¬ä¸å¤æ
stages:
- name: Deploy
jobs:
deploy:
steps:
- run: |
if [ "$DEPLOY_ENV" == "prod" ]; then
./deploy.sh
fi
5. ä½¿ç¨ mergeYaml ä¿ç注é
// â
æ¨èï¼ä½¿ç¨ mergeYaml ä¿çå YAML çæ³¨éåéç¹
val merged = TransferMapper.mergeYaml(oldYaml, newYaml)
// â 䏿¨èï¼ç´æ¥è¦çï¼ä¸¢å¤±æ³¨éï¼
val newYaml = TransferMapper.toYaml(yamlObject)
6. æ ¡éª YAML 宿´æ§
// â
æ¨èï¼è½¬æ¢åæ ¡éª
fun importYaml(yaml: String) {
val yamlObject = TransferMapper.to<PreTemplateScriptBuildYamlParser>(yaml)
validateYaml(yamlObject) // æ ¡éª
val model = modelTransfer.yaml2Model(yamlInput)
}
// â 䏿¨èï¼ä¸æ ¡éªç´æ¥è½¬æ¢
fun importYaml(yaml: String) {
val yamlObject = TransferMapper.to<PreTemplateScriptBuildYamlParser>(yaml)
val model = modelTransfer.yaml2Model(yamlInput) // å¯è½æåºå¼å¸¸
}
常è§é®é¢
Q1: å¦ä½å¤ç YAML ä¸çç¹æ®å符ï¼
A: ä½¿ç¨ CustomStringQuotingChecker èªå¨å¤çï¼
# èªå¨å å¼å·
version: "0x123" # åå
è¿å¶æ°å
name: "yes" # å¸å°å
³é®å
# ä¸å å¼å·
on: # on å
³é®åç¹æ®å¤ç
push:
Q2: å¦ä½å¨ YAML ä¸ä½¿ç¨éç¹åå«åï¼
A: TransferMapper èªå¨ç®¡çéç¹ï¼
# å®ä¹éç¹
defaults: &defaults
runs-on: linux
timeout-minutes: 60
# 使ç¨å«å
stages:
- name: Build
jobs:
build:
<<: *defaults
steps:
- run: ./build.sh
Q3: å¦ä½æ©å±èªå®ä¹ç YAML åæ®µï¼
A: éè¿ç»§æ¿ IPreTemplateScriptBuildYamlParser å¹¶ä½¿ç¨ @JsonSubTypesï¼
@JsonSubTypes.Type(value = CustomYamlParser::class, name = "v4.0")
data class CustomYamlParser(
// ç»§æ¿ææå段
override val version: String?,
override val name: String?,
// ... å
¶ä»å段
// æ°å¢èªå®ä¹å段
val customField: String?
) : IPreTemplateScriptBuildYamlParser {
// å®ç°å¿
è¦æ¹æ³
}
Q4: å¦ä½å¤ç大å YAML æä»¶çæ§è½é®é¢ï¼
A:
- ä½¿ç¨æµå¼è§£æï¼
getYamlFactory().parse()ï¼ - ç¼å已解æç模æ¿ï¼
TransferCacheServiceï¼ - åæ¹å¤ç Stage/Job/Step
- 弿¥è½¬æ¢éå ³é®è·¯å¾
Q5: å¦ä½è°è¯ YAML 转æ¢é®é¢ï¼
A:
// 1. å¯ç¨ Debug æ¥å¿
logger.debug("YAML Input: $yaml")
logger.debug("YAML Object: ${JsonUtil.toJson(yamlObject)}")
// 2. 使ç¨åé¢è®°å½è½¬æ¢è¿ç¨
val aspectWrapper = PipelineTransferAspectWrapper()
aspectWrapper.setYaml4Yaml(yamlObject, BEFORE) { yaml ->
logger.info("Before YAML: ${TransferMapper.toYaml(yaml)}")
}
// 3. åæ¥è½¬æ¢ï¼å®ä½é®é¢
val model = modelTransfer.yaml2Model(yamlInput)
logger.info("Model: ${JsonUtil.toJson(model)}")
// 4. ä½¿ç¨ getYamlLevelOneIndex æ£æ¥ç»æ
val index = TransferMapper.getYamlLevelOneIndex(yaml)
logger.info("YAML Index: $index")
æ£æ¥æ¸ å
å¨å®ç° YAML 转æ¢åè½åï¼ç¡®è®¤ï¼
- ç¡®å® YAML çæ¬ï¼v2.0 / v3.0ï¼
- äºè§£ YAML ä¸ Model çæ å°å ³ç³»
- 确认éè¦æ¯æç触åå¨ç±»å
- 确认éè¦æ¯æçå ç´ ç±»å
- 确认æ¯å¦éè¦æ¨¡æ¿å¼ç¨
- 确认æ¯å¦éè¦ä¿ç注éåéç¹ï¼ä½¿ç¨ mergeYamlï¼
- æ·»å YAML æ ¡éªé»è¾
- æ·»å å¼å¸¸å¤çï¼æè·
YamlFormatExceptionãModelCreateExceptionï¼ - æ·»å åå æµè¯è¦ç转æ¢é»è¾
- èèæ§è½ä¼åï¼ç¼åã弿¥ï¼
- æ·»å æ¥å¿è®°å½ï¼Debug 级å«ï¼
ç¸å ³ Skills
- æµæ°´çº¿åéç®¡ç – åé转æ¢é»è¾ï¼åè reference/2-extension.mdï¼
- 01-å端微æå¡å¼å – å¾®æå¡æ¶æ
- 27-è®¾è®¡æ¨¡å¼ – 工忍¡å¼ãçç¥æ¨¡å¼
ç¸å ³æä»¶è·¯å¾
æ ¸å¿è½¬æ¢ç±»
common-pipeline-yaml/src/main/kotlin/.../transfer/TransferMapper.kt– è½¬æ¢æ å°å¨common-pipeline-yaml/src/main/kotlin/.../transfer/ModelTransfer.kt– Model 转æ¢common-pipeline-yaml/src/main/kotlin/.../transfer/ElementTransfer.kt– å ç´ è½¬æ¢common-pipeline-yaml/src/main/kotlin/.../transfer/StageTransfer.kt– Stage 转æ¢common-pipeline-yaml/src/main/kotlin/.../transfer/ContainerTransfer.kt– Container 转æ¢common-pipeline-yaml/src/main/kotlin/.../transfer/TriggerTransfer.kt– 触åå¨è½¬æ¢common-pipeline-yaml/src/main/kotlin/.../transfer/VariableTransfer.kt– åé转æ¢
YAML 模åå®ä¹
common-pipeline-yaml/src/main/kotlin/.../v3/models/PreTemplateScriptBuildYamlParser.kt– v2.0 YAML 模åcommon-pipeline-yaml/src/main/kotlin/.../v3/models/PreTemplateScriptBuildYamlV3Parser.kt– v3.0 YAML 模åcommon-pipeline-yaml/src/main/kotlin/.../v3/models/Variable.kt– å鿍¡åcommon-pipeline-yaml/src/main/kotlin/.../v3/models/Concurrency.kt– å¹¶åæ§å¶common-pipeline-yaml/src/main/kotlin/.../v3/models/Notices.kt– éç¥é ç½®common-pipeline-yaml/src/main/kotlin/.../v3/models/on/TriggerOn.kt– 触åå¨å®ä¹common-pipeline-yaml/src/main/kotlin/.../v3/models/stage/Stage.kt– Stage å®ä¹common-pipeline-yaml/src/main/kotlin/.../v3/models/job/Job.kt– Job å®ä¹common-pipeline-yaml/src/main/kotlin/.../v3/models/step/Step.kt– Step å®ä¹
æµè¯ç¨ä¾
common-pipeline-yaml/src/test/kotlin/.../transfer/MergeYamlTest.kt– YAML åå¹¶æµè¯common-pipeline-yaml/src/test/kotlin/.../parsers/template/YamlTemplateTest.kt– æ¨¡æ¿æµè¯common-pipeline-yaml/src/test/resources/samples/– YAML ç¤ºä¾æä»¶
JSON Schema
common-pipeline-yaml/src/main/resources/schema/V3_0/ci.json– v3.0 Schemacommon-pipeline-yaml/src/main/resources/schema/V2_0/ci.json– v2.0 Schema
æ»ç»
YAML æµæ°´çº¿è½¬æ¢æ¯ BK-CI å®ç° Pipeline as Code çæ ¸å¿ææ¯ï¼æ¶åï¼
- åå转æ¢ï¼YAML â Model ç宿´è½¬æ¢é¾è·¯
- æºè½åå¹¶ï¼ä¿ç注éåéç¹ç YAML åå¹¶
- 模æ¿ç³»ç»ï¼æ¯æ ExtendsãStep/Job/Stage 模æ¿
- 表达å¼ç³»ç»ï¼åéå¼ç¨ã彿°è°ç¨ãæ¡ä»¶å¤æ
- å颿©å±ï¼æ¯æèªå®ä¹è½¬æ¢é»è¾æ³¨å ¥
- èç¹å®ä½ï¼æ¯æç²¾ç¡®ç YAML èç¹æä½
éµå¾ªæ¬æåå¯ç¡®ä¿ YAML 转æ¢çæ£ç¡®æ§ãå¯ç»´æ¤æ§åæ§è½ã