
我第一次看到 “caveman” 这个词被当成一个项目名第一反应是这该不会是个恶搞项目吧结果顺着仓库点进去发现它是一个正经到让人意外的持续集成工具而且定位极其明确simple CI一个可以跑在你自己的服务器上、配置简单到像穴居人敲石头一样的自动化构建系统。我前后在几个项目里用过它跑构建、做定时任务、挂 Webhook 自动触发测试加起来也有半年多今天想把真实的体验、踩过的坑和完整配置思路整理出来。无论你是一个人的独立开发者还是三五个人的小团队只要你手上有多余的服务器、受够了 Jenkins 的插件负担、又不想让整个 CI 流程被某个托管平台绑死这篇文章应该能帮你省下不少折腾时间。1. 先聊聊 caveman 到底是什么1.1 一个被名字耽误的持续集成工具Caveman 是一个用 Python 写的极简 CI 工具核心使用方式是你写一个 YAML 配置文件描述哪些任务要跑、跑的顺序是什么、什么条件下触发然后它帮你排队、执行、收集输出和上报结果。听起来和 Jenkins、GitHub Actions、GitLab CI 没什么区别但它的气质完全不同。Caveman 的整套设计都在做减法没有插件市场、没有图形配置界面、没有一堆概念需要消化你只需要理解 pipeline、task 和 trigger 这三个词基本就能上手。我自己的感受是小项目或者中大型项目的某些特定环节根本不需要一个重量级 CI 平台。你需要的是“代码推上去之后自动跑一下测试每天晚上自动打个包失败了有人知道”就这么点事。Caveman 恰好把这点事情做得很朴素很稳。它适合谁适合那些对服务器有掌控欲、愿意花二十分钟写一个 YAML、不喜欢在网页上点来点去的人。反过来如果你需要在大量项目之间做复杂的可视化编排或者需要企业级的审计、权限矩阵那它不太适合你。1.2 它和 Jenkins、GitHub Actions 的根本差异要理解 Caveman 为什么值得关注得先看清主流 CI 工具的复杂本质。Jenkins 本身强大但强大带来的副作用是你得维护一套 JVM 服务、隔三差五处理插件兼容性、写 Groovy 脚本、还要操心节点管理。GitHub Actions 倒是省心但你的项目只要不在 GitHub 上或者你希望构建产物和缓存完全掌握在自己手里它就有天然的边界。Caveman 选择了另一条路把 CI 的调度核心做得足够轻然后用 Shell 命令解决一切。它不发明新的 DSL不搞标签系统不搞分布式调度宇宙。构建环境就是你的服务器任务就是你在终端里本来就会敲的那些命令。对比维度CavemanJenkinsGitHub Actions部署成本Python 库 单个服务分钟级JVM 环境 插件体系小时级零部署但受平台约束配置方式一个 YAML 文件界面 Groovy XMLYAML 平台专用语法任务粒度Shell/Python 命令插件封装好的 StepAction 和容器组合可移植性配置跟着项目走换机器即跑配置迁移成本较高几乎绑死 GitHub对服务器的依赖需要自备需要自备不需要但数据在云端适合场景中小规模、自托管、极简需求大规模、复杂权限开源项目、GitHub 全家桶用户这些差异背后其实反映了一个理念CI 的本质就是“定时或触发地执行一些命令”。Caveman 坚决不偏离这个本质也算是一种精神洁癖。我挺吃这一套的。2. 核心配置与任务模型拆解2.1 Pipeline、Task、Trigger 是怎么配合的Caveman 的配置模型我自己的理解是一个服务里可以定义很多个 pipeline流水线每个 pipeline 下面挂着若干 task任务每个 task 可以做三件事——跑命令、声明自己依赖谁、设置独立的触发条件。Pipeline 是一个相对完整的执行单元比如“构建并测试”Task 则是单元里的具体步骤比如“安装依赖”“跑单元测试”“构建镜像”。任务之间通过依赖关系决定执行顺序。这里不需要像某些工具那样去画 DAG 图你只需要写清楚needs依赖谁调度器自然会按顺序跑。Trigger触发器是让我觉得这个工具很灵活的地方。你可以让整个 pipeline 被 Webhook 触发也可以让某个 task 按 cron 定时执行两者还互不冲突。这意味着同一个项目里push 触发的“构建测试”和每天晚上执行的“自动打包清理缓存”可以写在同一个配置里结构非常干净。类比一下Pipeline 像一条生产线Task 是线上不同工位Trigger 则是“什么时候开工”的指令。你不需要为了定时任务单独搭一台机器也不需要为了 Webhook 在业务代码里写接口全都在 YAML 里声明出来。2.2 一个可以直接上手改的完整配置示例我不太喜欢引用那种只有三行、毫无参考价值的“hello world”示例下面这个配置是结合我自己项目的实际需求整理的。它覆盖了依赖安装、测试、失败的邮件通知、晚上的自动备份一个文件搞定。pipelines: main: title: 构建并测试主分支 triggers: on_push: branch: main tasks: - name: install cmd: pip install -r requirements.txt -q - name: test cmd: pytest tests/ needs: [install] - name: report cmd: python scripts/notify.py --status {{ pipeline.status }} needs: [test] on_failure: true nightly: title: 每日备份与缓存清理 triggers: cron: 0 2 * * * tasks: - name: backup cmd: bash scripts/backup.sh - name: cleanup cmd: bash scripts/cleanup_cache.sh needs: [backup]注意看几个关键点on_push.branch把触发范围限定在主干分支避免每个人推分支时都跑一遍needs声明了任务间的顺序on_failure: true表示即使前任务失败也能发送报告。{{ pipeline.status }}是模板变量caveman 会把它替换成实际执行状态这是我后来在实践里发现的实用小技巧。对于 Node.js 项目思路完全一致只是命令换成 npm。不要把 caveman 的配置理解成新的编排语言它本质上就是把“你在服务器上手工会跑的命令”搬进 YAML然后加触发条件和依赖关系。2.3 命令执行背后的几个关键细节关于每个 task 中命令到底怎么跑这里有几个容易踩坑的细节值得单独说。第一个细节是工作目录。Caveman 默认会在你的项目根目录执行命令但如果你把配置放在config/caveman.yaml项目根目录怎么定义就要留意了。建议在项目根目录放配置文件或者使用绝对路径的脚本否则新手很容易遇到“命令明明存在但执行时找不到文件”的诡异问题。第二个细节是环境变量。Caveman 会继承服务进程的环境变量所以你在 systemd 或者 docker-compose 里定义的变量在任务里是能直接读到的。但这同时也意味着你写在配置里的命令不应该硬编码任何密钥。我的习惯是密钥全部通过环境变量注入YAML 里只写变量名。第三个细节是退出码。Caveman 判断任务成不成功只看退出码是不是 0。如果你在命令里写了多个子命令最好在关键步骤之间检查退出码比如python scripts/prepare.py pytest tests/这条命令中如果 prepare.py 失败后面的 pytest 根本不会执行整条命令退出码也是非 0 的。这样就能很自然地传递失败状态。反过来如果用了;连接命令即使前面失败了后面还是会跑最后的退出码取决于最后一条命令很容易掩盖问题。3. 实操从零搭起一个能用的 caveman 服务3.1 安装和启动全流程记录整个安装过程比我想象中顺利很多。在一台 Ubuntu 22.04 服务器上只需要确保 Python 3.10 以上可用然后执行pip install caveman安装完成后建议先写一个最简单的配置文件确认服务能正常启动再逐步加内容。启动服务有两种常见方式前台运行方便看日志后台运行适合长期使用。# 前台运行CtrlC 可退出 caveman serve --config .caveman.yaml # 后台运行日志写入文件 nohup caveman serve --config .caveman.yaml /var/log/caveman.log 21 正式部署我建议用 systemd 托管这样开机自启和崩溃拉起都由系统负责。下面是一个最小可用的 service 文件[Unit] DescriptionCaveman CI service Afternetwork.target [Service] Userdeploy WorkingDirectory/home/deploy/myapp EnvironmentFile/home/deploy/.env ExecStart/usr/local/bin/caveman serve --config /home/deploy/myapp/.caveman.yaml Restartalways RestartSec5 [Install] WantedBymulti-user.target这里有个细节容易被忽略EnvironmentFile指定的文件里可以放各种环境变量caveman 的任务进程会直接继承。这样你在 YAML 配置里就不会出现任何明文密钥通知 Webhook 的 Token 也全都在环境文件里。首次启动后建议先检查两件事一是日志是否正常输出二是服务监听的端口是否能访问。默认端口如果被占用可以通过参数调整但我是直接用本机回环地址加反向代理不额外暴露端口这样更安全也更清爽。3.2 接入 Git 仓库的 Webhook 自动触发Caveman 的 Webhook 触发机制很简单你在配置里声明on_push触发器它会在服务里开一个 HTTP 端点然后你在 Git 仓库后端配置一个 Webhook把请求发到这个端点即可。以 Gitea 为例在仓库设置里添加 WebhookURL 填http://你的服务器地址:端口/ci/triggerContent Type 选 JSON触发事件勾选 Push。保存后可以先手动测试一次观察 caveman 日志里是否收到请求。我遇到过的一个重要问题是触发安全。如果端点没有校验别人知道你地址后可以伪造请求反复触发构建既不安全也浪费资源。所以在生产环境必须打开 token 校验caveman 通常支持在触发端点要求携带固定参数配置大致如下triggers: on_push: branch: main token: ${CAVEMAN_WEBHOOK_TOKEN}这里的${CAVEMAN_WEBHOOK_TOKEN}会被解析成环境变量值。Git 仓库端 Webhook 配置里添加一个请求头或参数把同样的 Token 带上caveman 校验通过才会真正触发任务。为了验证链路是否通畅可以先用 curl 模拟一次curl -X POST http://127.0.0.1:5000/ci/trigger \ -H Content-Type: application/json \ -H X-Caveman-Token: your-token \ -d {ref: refs/heads/main}如果日志里出现“trigger accepted”之类的信息说明 Webhook 链路已经打通。这里有一个排错技巧很多团队发现 Webhook 不触发问题往往不在 caveman而在 Git 平台默认不给localhost或内网 IP 发请求需要在平台设置里允许内部或自定义网络地址。3.3 把执行结果通知到人自动化的最后一步是让正确的人在正确的时间知道结果。Caveman 本身把“执行任务”和“上报结果”都当成普通任务处理所以你完全可以在一个 pipeline 的最后挂一个通知任务用一段脚本把结果推到办公协作软件里。我自己的做法是写一个可以复用的通知脚本核心逻辑就是拼接文本再通过 Webhook 发出去。它接收两个参数任务名和状态其他信息从环境变量读。#!/usr/bin/env bash # scripts/notify.sh set -euo pipefail TASK_NAME${1:-unknown} TASK_STATUS${2:-unknown} PROJECT_NAME${PROJECT_NAME:-my-project} WEBHOOK_URL${NOTIFY_WEBHOOK_URL:-} if [ -z ${WEBHOOK_URL} ]; then echo [notify] no webhook url, skip exit 0 fi curl -s -X POST ${WEBHOOK_URL} \ -H Content-Type: application/json \ -d { \msg_type\: \text\, \text\: { \content\: \[Caveman] ${PROJECT_NAME} ${TASK_NAME} ${TASK_STATUS}\ } }在 YAML 里调用时建议用on_failure和on_success这类钩子做区分。比如- name: notify cmd: bash scripts/notify.sh test failed on_failure: true needs: [test]这里的关键原则是通知脚本本身不能影响构建结果。哪怕通知发送失败也不应该让整个 pipeline 显示为失败。所以脚本里如果通知环节出错要么用|| true要么在脚本内部捕获异常并返回 0。我把这个教训写进这里也是因为曾经踩过一次通知接口超时导致整个构建被标记为失败而真正的测试其实是全部通过的。3.4 跑定时任务时的常见习惯定时任务是 Caveman 很常用的能力比如每天凌晨备份数据库、每周清理构建缓存、定期跑一次完整的回归测试。定时配置写在 pipeline 的triggers.cron里采用标准的 cron 表达式。pipelines: weekly_cleanup: triggers: cron: 0 4 * * 1 tasks: - name: cleanup cmd: bash scripts/cleanup_docker_cache.sh关于 cron 我有两个建议第一尽量避开业务高峰时段凌晨三四点通常是很好的选择第二任务执行时间如果可能超过间隔需要自行维护一个状态文件避免同一个任务还没跑完又启动另一轮。Caveman 似乎不会自动处理重叠执行这是个需要注意的边界问题。4. 常见问题与排查技巧实录4.1 任务一直排队不执行这个现象我遇到不止一次最典型的场景是服务正常启动Webhook 也触发了但任务状态一直是 pending迟迟没有转成 running。排查思路固定顺序先看 worker 状态。Caveman 的服务进程里调度器和执行器是配合工作的执行器其实就是一组 worker。如果 worker 数量设置为 0或者启动异常任务就会永远排队。可以这样确认caveman status如果显示没有活跃 worker基本可以确定问题方向。此时检查服务日志看 worker 初始化时是否报错。比如某些环境缺少必要的执行依赖或者工作目录没有写权限都会导致 worker 起不来。另外如果任务本身设置了并发限制例如同一时间只允许一个实例运行那么上一个任务如果因为某些原因没有正常结束后续任务就会一直排队。解决办法是手动清理旧的运行状态或者调整并发参数。4.2 构建日志为空但任务失败另一个常见困惑是任务明明失败了可点开日志却没看到任何有效内容。这个问题大多出在命令输出没有被正确捕获上而不是任务真的没有执行。我遇到过两次这种情况。第一次是因为脚本里有一条命令调用了nohup把子进程放到后台导致 caveman 认为主命令已经结束但实际工作还在继续。第二次是脚本中使用了交互式命令进程等待标准输入直到超时被系统杀掉日志自然只有一行超时记录。给个排查建议在任何复杂脚本开头加一行echo start at $(date)在结尾加一行echo end at $(date)就能快速定位是没执行、没输出还是卡住超时。这个习惯成本极低但排查效率提升非常明显。4.3 在容器环境中有权限问题如果你希望任务跑在 Docker 容器里比如在一个固定镜像中编译项目那么权限问题是绕不开的。最常见的是 socket 权限错误Got permission denied while trying to connect to the Docker daemon socket常规解法是把运行 caveman 的用户加入 docker 用户组sudo usermod -aG docker deploy改完用户组以后必须重新登录会话才能生效。如果你通过 systemd 启动服务在添加到 docker 组后要再执行sudo systemctl restart caveman让服务进程重新获得新的权限组。但这引出一个安全方面的思考如果任务会执行任意代码那么这些代码其实是以运行用户权限跑的。给用户 docker 权限等同于给了最高系统权限。所以我后来更推荐在配置层面明确区分哪些任务需要容器环境哪些任务直接本机执行容器命令只放在独立 pipeline 中并且严格限制可访问的仓库。4.4 Webhook 常见三连坑Webhook 不触发、重复触发、触发时机不对这三个问题几乎每家都会踩到。不触发先确认端点和 token 是否正确再确认 Git 平台是否有防火墙规则拦截内网或自定义端口请求。某些托管平台默认禁止 Webhook 发送到非标准端口这时候需要在平台侧放行。重复触发很多 Git 平台在推送 tag、编辑 PR、同步 mirror 等不同场景下都会发不同事件。配置里应该按需声明事件类型并通过分支名过滤避免一个简单 push 变成四五次构建。触发时机不对比如计划在 merge request 通过后再构建而不是每次 push 都构建。这类需求请在配置里使用对应的事件类型和判断条件不要指望“回调之后过滤”。Caveman 的触发配置能完成大部分常见场景但如果你需要的是一门极其精细的判断逻辑直接在任务里写一段 Shell 判断往往更直接可控。4.5 环境变量管理不当导致泄露最后提醒一个很少人提但非常重要的问题YAML 配置本身可能被项目成员查看所以不要在里面写死 Token、密码、密钥。哪怕仓库是私有的也要当它迟早会泄露来管理。一个稳妥的习惯是配置里只写${VAR_NAME}变量值统一放在运行服务的机器的环境文件里。配合 systemd 的EnvironmentFile使用既方便在不同环境部署也避免密钥进入版本历史。如果你需要为不同项目提供不同环境变量我建议不要共用一个环境文件而是以项目名为前缀隔离例如MYAPP_DB_PASSWORD、MYAPP_NOTIFY_TOKEN。这样一个脚本出问题不会影响其他项目排查范围也小得多。最后分享一点个人体会回头看我为什么会坚持用 caveman 这类极简 CI 方案而不是一直躺在 Jenkins 的舒适区里本质原因其实是心智负担。重工具的每一个插件、每一条流水线配置、每一次版本升级都在不断消耗你的注意力而最终换来的能力在小团队里绝大部分根本用不上。我在实际操作中的体会是把 CI 做到最简反而更能逼着团队把构建脚本本身写好。没有图形界面帮你隐藏过程没有神秘动作帮你收拾烂摊子一切都要靠 Shell 脚本和清晰的依赖关系兜底这恰好是工程能力最能沉淀的地方。如果你也想尝试我建议先从一个小项目开始一个仓库、一个 YAML、三个任务。把构建、测试、通知跑通运转一两周以后再慢慢加定时任务和容器步骤。等有一天你会发现你已经完全不关心 CI 平台本身用的是什么因为配置跟着仓库走、命令就是终端命令换哪台服务器都能立刻跑起来这种感觉确实比想象中的还要舒服。