自托管一个 CI/CD 工具,迟早会撞上一个哲学问题:流水线的定义,到底放在哪儿?
一派是「画布派」:打开网页,拖几个节点,连几条线,构建→测试→部署一目了然,改起来点点鼠标就行。
Jenkins 的老式配置、很多国产 DevOps 平台都是这个路子。
另一派是「代码派」:流水线就是仓库里的一个 yaml 文件,.github/workflows/*.yml、.gitlab-ci.yml、.pipewright.yml……跟代码一起提交、一起 review、一起回滚。GitHub Actions 把这一派彻底带火了。
这两派各有各的爽点,也各有各的死穴。我做自己那个部署工具的时候,一开始只做了画布,
用着用着发现画布有个绕不过去的坎;后来把 yaml 也加上,又踩进「两套定义怎么共存」的坑里。
这篇把两种范式的取舍、以及「同时支持两种」时那个最容易翻车的机制,讲清楚。
一、画布的爽与痛
画布(可视化编排)的爽是真的爽:
- 零学习成本:不用记 yaml 字段,拖拽就会,新人上手快。
- 所见即所得:阶段之间的依赖(谁在谁之后、哪些并行)画成图,一眼看懂,比读一坨缩进的 yaml 直观。
- 改一下立刻能跑:在网页上调个参数,点运行,不用 commit、不用 push。
但画布有两个绕不过去的痛:
- 配置不在版本控制里。你在网页上改了流水线,谁改的、改了啥、为什么改——没记录。
出了事想回到「上周那个能跑的版本」,对不起,画布没有git log。 - 配置和代码分家。代码在仓库里,流水线在数据库里,两个东西各活各的。
一个 feature 分支想临时改改构建步骤?画布做不到——画布是项目级的一套配置,
不跟分支走。你在 main 上改了画布,feature 分支也被连累。
第 2 点尤其致命。GitHub Actions 之所以好用,一大半功劳就在:workflow 文件跟着分支走。
你在 feature 分支改 CI,只影响这个分支,合并前 main 纹丝不动。这是画布给不了的。
二、代码派的答案:把流水线写进.pipewright.yml
代码派的解法很直接——流水线就是仓库根目录的一个 yaml 文件。以我的工具为例,长这样:
version:1stages:-id:stg_srcname:拉源码kind:sourcejobs:-{id:job_src,name:源,type:git_source}-id:stg_buildname:构建kind:buildneeds:[stg_src]# DAG 依赖:构建在拉源码之后jobs:-id:job_compilename:compiletype:scriptscript:image:golang:1.24commands:-go build ./...-id:stg_deployname:部署kind:deploy_sshneeds:[stg_build]gate:true# 人工审批门:部署前卡一下when:branches:[main]# 只在 main 分支部署这里要特意和「GitOps 部署」划清界限——很多人一提「yaml + git」就想到 docker-compose。
但这两个是不同层面的东西:
- docker-compose.yml描述的是「部署产物长什么样」:哪些服务、端口、卷、网络。
.pipewright.yml描述的是「流水线怎么编排」:先拉源码、再构建、再部署,阶段之间谁依赖谁、
哪个要人工审批、哪个只在 main 跑。
前者是「终态」,后者是「过程」。.pipewright.yml里的deploy_ssh步骤可以去执行docker-compose up,但那是它的一个动作,不是同一层的东西。别把两者混为一谈。
一旦流水线进了 yaml,画布那两个痛点全好了:配置进版本控制(有git log、能 review、能回滚)、
配置跟分支走(feature 分支改 yaml 只影响自己)。
三、真正的难题:两种范式怎么共存
到这儿,一个偷懒的做法是「二选一」——要么全画布,要么全 yaml,逼用户站队。
但现实是:老项目已经在画布上画好了,新需求想上 yaml,你不能让人推倒重来。
所以真问题是:同一个项目,既有画布配置、又有仓库 yaml 时,一次运行到底听谁的?
我的做法是一个每项目的开关(默认关):
- 开关关(默认):100% 用画布配置,仓库里就算有
.pipewright.yml也当没看见,
完全不读。老项目行为一点不变。 - 开关开:优先用仓库 yaml。运行时按这次运行的分支去仓库根目录读
.pipewright.yml,
解析出来驱动这一次运行。
关键点有三个,每一个都值得说清楚:
1. 这是「二选一」,不是「合并」。一次运行的流水线定义只来自一个源,要么是仓库 yaml(repo),
要么是画布配置(stored),不存在把两边揉一起。揉一起是灾难——你永远说不清最终跑的是啥。
选一个,清清楚楚。
2. 按运行分支读,这是 yaml 派最大的价值。开关打开后,在 feature 分支改.pipewright.yml、
推上去,这个分支的运行就按新 yaml 跑,main 分支不受影响。这就把画布给不了的
「配置跟分支走」补上了。
3. 出问题静默回退,运行永不中断。yaml 文件不存在?格式错了?是个二进制?——
一律自动回退到画布配置,这次运行照常跑,不会因为 yaml 写错就把流水线卡死。
这是刻意的设计:流水线即代码是「增强」,不该变成「新的单点故障」。
四、回退是安全网,但也是个坑
「静默回退」听起来很贴心,但它有个反面:静默——回退的时候不吭声。
想象这个场景:你以为运行在按仓库 yaml 跑,实际上 yaml 里有个缩进错误,
系统悄悄回退到了画布的老配置。运行绿了,你还以为新 yaml 生效了,其实压根没跑到。
所以我加了一个预览端点:在真跑之前,你可以让系统去仓库把.pipewright.yml拉出来、
解析一遍、告诉你「找到了没(found)/合法吗(valid)/会生成哪几个阶段」。编辑完 yaml 先预览一下,
确认它真能被解析,再依赖它。别让「静默回退」把你的配置错误也一起藏了。
运行详情里也会挂一个来源徽章:这次运行用的是「仓库main· .pipewright.yml」还是「网页配置」,
一眼可见。如果它显示的是「网页配置」而你以为该走 yaml,那就是回退发生了,去查 yaml。
五、诚实说清楚:这套的边界
自己搓的东西,边界得讲明白,免得你按错误的预期用:
- 没有双向同步。画布改了,不会自动生成 yaml;yaml 改了,也不会回写画布。就是单向的:
开关打开时,仓库 yaml → 驱动运行。想从画布「导出」成 yaml?现在没这功能,得手写(或让 AI 帮你写)。 - 改 yaml 必须推代码才生效。它是「推代码触发运行时去读」,不是「存了 yaml 就热加载」。
想让新 yaml 生效,得触发一次新运行。 - yaml 能表达的字段是画布的子集。画布上那些「变量与缓存」「触发配置」的高级 tab,
yaml 目前不一定都覆盖。核心的阶段/任务/依赖/审批门/矩阵/条件都有,但别指望 100% 对齐画布。 - 文件位置写死:仓库根目录、文件名
.pipewright.yml。不支持放子目录或改名。 - 来源是「记录」不是「强制」。徽章告诉你这次用了哪个源,但你不能在单次运行里说
「这次强制走画布」——切换只有项目级开关这一个粒度。
这些不是 bug,是「够用优先」的取舍。把它们讲清楚,你才知道什么时候该用画布、什么时候该上 yaml。
六、那到底该选哪个?
我的经验:
- 画布适合——刚起步、流水线还在频繁试错、团队里有人不写 yaml、想快速点点看效果。
- yaml适合——流水线定型了、想进版本控制和 code review、需要「配置跟分支走」、
想让「改流水线」这件事本身也走 PR 评审。
而「同时支持、每项目一个开关」的意义在于:你不用一开始就站队。
先用画布把流程跑通,定型之后把它固化成.pipewright.yml提交进仓库、打开开关,
平滑迁移过去,画布配置留着当回退安全网。这大概是自托管场景下最不折腾的路径。
七、这些我都做进了工具里
上面这套——画布可视化编排、.pipewright.yml声明式定义、每项目开关、按分支读 yaml、
出错静默回退、预览端点校验、运行详情的来源徽章——我把它连同前几篇讲的
CI 构建、多机部署、Caddy 自动 HTTPS、Per-PR 预览环境、零停机部署+回滚,
一起塞进了我那个 Go 单二进制部署工具Pipewright:
- 两种范式都给:网页画布拖拽编排,或仓库
.pipewright.yml声明式定义,每项目一个开关切换; - 按运行分支读 yaml,feature 分支改流水线只影响自己,配置真正跟分支走;
- yaml 缺失/非法自动回退画布配置,运行永不中断;配一个预览端点先校验再依赖;
- 运行详情挂来源徽章,这次用的是仓库 yaml 还是网页配置一目了然。
开源 MIT,单二进制、无运行时依赖(前端embed.FS打进二进制,默认 SQLite),
定位个人开发者 / 小团队自托管。
仓库:https://github.com/huangchengsir/pipewright
但就算你不用它,这篇的核心结论照样成立:画布 vs yaml 不是「谁更高级」,而是「什么阶段用什么」;
真要两者兼得,关键是想清楚「一次运行只听一个源」,再用一个开关平滑地在两者之间迁移。
欢迎来提 issue 拍砖。