@hotsuitor/jenkins-cli
v3.0.0
Published
Jenkins 参数化构建 CLI + MCP 服务器: 选分支、触发构建、跟踪、下载产物、SSH 部署与回滚,并可供 Claude Code / Codex 调用
Maintainers
Readme
@hotsuitor/jenkins-cli
交互式 Jenkins 参数化构建 CLI(命令名 rbuild)。在终端里模糊搜索选分支、自动提取版本号、保存预设、一条命令触发构建并全程跟踪,不用再去网页上逐个填参数。
$ rbuild my-preset # 用预设直接触发,全程跟踪到出包
$ rbuild # 交互模式: 模糊搜索选分支 -> 确认 -> 触发/存预设
$ rbuild my-preset --set portal=origin/hotfix # 用预设,但临时换掉一个分支安装
前置要求: Node.js >= 20。
npm install -g @hotsuitor/jenkins-cli团队协作用法(推荐): 把 jobs.json、presets.json 和 .env.example 放进团队自己的 git 仓库,在该目录下运行 rbuild——job 定义和预设随 git pull 全组同步。也可以直接 clone 团队仓库后 pnpm install(会自动编译 TypeScript)再 npm link 使用。
配置凭据
每个开发者用自己的 Jenkins 账号。在数据目录创建 .env:
JENKINS_URL=http://your-jenkins-host
JENKINS_USER=你的Jenkins用户名
JENKINS_TOKEN=你的API Token- API Token 生成: 浏览器登录 Jenkins → 右上角用户名 → 设置(Configure) → API Token → 添加新 Token
- 也可以设置同名系统环境变量(优先级高于
.env) .env含个人凭据,记得加入.gitignore
数据文件
rbuild 按以下顺序查找数据目录(以先找到 jobs.json 的为准):
- 工具安装目录(git clone +
npm link的团队仓库模式) - 当前工作目录
~/.rbuild/
jobs.json(job 登记表)、presets.json(预设)、.env(凭据)、.rbuild-state.json(本地"上次使用"记录)都存放在数据目录。
使用
| 命令 | 说明 |
| --- | --- |
| rbuild | 交互模式: 选 job → 模糊搜索选分支 → 确认 → 触发 / 保存预设 |
| rbuild <job\|预设名> | 预设名 = 直接触发(触发前自动校验分支是否仍存在);job id = 走交互填参 |
| rbuild dl [job] | 下载产物到产物目录,缺省取最近一次成功构建 |
| rbuild changes [job] | 查看构建的 git 变更记录(相对上一次构建的新提交),缺省最近一次完成的构建 |
| rbuild deploy [服务器] | SSH 部署 .bin/.run/.tar.gz/.tgz 产物(见下文「远程部署」) |
| rbuild rollback [服务器] | 回滚到远端备份 |
| rbuild front deploy [机器...] | 把本地前端产物推到前端机(见下文「前端部署」),多选批量 |
| rbuild front rollback [机器...] | 回滚前端到最近一份备份 |
| rbuild front ls | 列出前端机清单 |
| rbuild ls [jobs\|presets\|servers\|front] | 列出 job / 预设 / 后端服务器,缺省前三者 |
| rbuild rm <预设名> | 删除预设 |
位置参数只表示"哪个目标"(job / 服务器 / 前端机)。"哪一版"——构建号、产物文件、备份名——一律走选项,所以解析器不需要靠值的形状去猜:
| 选项 | 说明 |
| --- | --- |
| --set 参数名=值 | 预填/覆盖构建参数,可重复。有 tty 时缺的照常问,无 tty 时缺就报错 |
| --build <号> | dl / changes 指定构建号 |
| --file <路径> | deploy 指定产物文件 |
| --backup <名字> | rollback 指定备份 |
| --dist <目录> | front deploy 指定前端产物目录(默认 ./dist) |
| -o, --download | 构建成功后下载产物到产物目录 |
| --out <目录> | 下载到指定目录(隐含 --download) |
| --deploy[=服务器] | 构建成功后自动下载并部署(省略名字则弹列表选) |
| -d, --detach | 拿到构建号就退出,不跟踪进度 |
| --dry-run | 预览不执行:构建时打印将发送的参数,部署/回滚时打印执行计划(且不连远端) |
| -y, --yes | 跳过确认提示 |
选项按命令白名单校验:不适用于当前命令的选项会直接报错,不会被静默忽略。rbuild <命令> -h 看某条命令自己的选项和示例。
产物目录
rbuild dl 默认把产物下载到 ./output/,rbuild deploy 不带 --file 时也从这里挑候选——一个目录两处生效,不会出现"刚下完的包 deploy 找不到"。
用 --out <目录> 单次改写,或在 .env 里设 RBUILD_ARTIFACT_DIR 长期改写。产物目录为空或不存在时,deploy 会回落到当前目录找。
Shell 补全
rbuild completion install # 写入 ~/.rbuild/completion.*,确认后接入 ~/.bashrc 和 $PROFILE
rbuild completion bash # 只打印脚本,自己决定放哪支持 Git Bash 和 PowerShell(zsh 可用 bashcompinit 加载 bash 版)。补的东西:命令、当前命令允许的选项(用过的不再出现)、job id、预设名、后端服务器名、前端机名、--set 后的参数名、--file 后只留可部署产物。前缀无命中时退化为子串匹配,所以 rbuild front deploy 193<Tab> 直接给出 192.0.2.193。
Tab 时不启动 node。 这台机器上 node 冷启动就要半秒,走 nvs shim 要 1.5 秒,任何"每次 Tab 调一次 rbuild"的方案都不可用。所以命令和选项在生成时写进脚本,预设/服务器名则由 shell 在 Tab 时直接读 JSON(PowerShell 用 ConvertFrom-Json,bash 用 grep——这几个文件里 name/id 键只在实体一层出现)。git pull 拿到新预设立刻可补;但 rbuild 升级后命令表可能变了,rbuild ls 会在末尾提醒你重跑一次 install。
completion install 会另外问一句要不要加 alias rbuild='node …/dist/src/cli/main.js':npm/nvs 给 Git Bash 的 sh shim 每次要起 uname/sed/cygpath 三个子进程,在 Windows 上是将近 1 秒;绕过它每次调用都省这一秒。PowerShell 的 shim 很轻,不需要。
产物下载自动携带你的 Jenkins 凭据(浏览器/wget 直接访问产物 URL 会因未认证被 403 拒绝),显示实时进度,先写 .part 临时文件、完成后原子改名,不会留半截文件。
交互模式的省事设计:
- 分支列表支持输入关键字模糊过滤(如输
5.3.3) - 选完版本来源分支后,联动参数会把同版本分支排到最前
- 版本号参数自动从分支名提取(
origin/5.3.3/dev→5.3.3),可手改 - 时间戳参数每次触发时自动生成,无需关心
- 上次用过的分支会标记
[上次使用]并排在最前
触发后默认全程跟踪: 排队 → 构建中(实时耗时/预计时长) → 结束自动列出本次构建包含的 git 提交(哈希/作者/message) → 成功列产物下载链接,失败打印日志尾部。Ctrl+C 只断开跟踪,不影响 Jenkins 上的构建。
变更记录说明: 提交列表由 Jenkins 记录,含义是"相对该 job 上一次构建新增的提交";如果两次构建切换了分支组合,列表可能跨度较大。
预设共享
预设保存在数据目录的 presets.json。团队模式下提交推送后全组可用:
git add presets.json && git commit -m "preset: xxx" && git push远程部署
在数据目录创建 servers.json 登记部署目标:
{
"servers": [
{
"name": "prod-1",
"host": "192.0.2.10",
"port": 22,
"username": "deploy",
"password": "your-password",
"remoteDir": "/opt/myapp/",
"deploy": { "args": ["--noprogress"], "inheritEnvFromProcess": "chrome" },
"keep": ["backups", "logs", "data"],
"restart": { "containers": ["myapp", "myapp-config"] },
"healthCheck": { "url": "http://localhost:8080/", "timeoutSec": 120 },
"sudo": true
}
]
}| 字段 | 说明 |
| --- | --- |
| remoteDir | 部署目标目录,也用于识别安装器创建的备份 |
| deploy | 可选部署策略覆盖。type 可设 installer 或 archive;args 是传给可执行安装包的参数;timeoutSec 是安装超时(默认 900 秒);inheritEnvFromProcess 按精确进程名从现有进程继承 GUI 环境白名单。通常依扩展名自动识别,无需配置 |
| keep | 现场文件保护:不在构建产物里、但必须跨版本保留的相对路径(日志、运行时数据、历史备份、机器专属配置)。目录用移动、文件用复制 |
| restart | 部署/回滚后重启服务。{"containers": [...]} 走 docker restart;或用 {"command": "..."} 自定义(systemctl、启停脚本皆可) |
| healthCheck | 重启后探活。url 用 curl 检查(默认接受 2xx/3xx,可用 expectStatus: [200] 指定);timeoutSec 默认 120 |
| sudo | 允许在需要时提权(默认 false)。容器以 root 运行时,它写出的日志目录登录用户无权移动——移动目录需要对该目录本身有写权限(内核要更新其中的 .. 项),此时需要 sudo。密码取 sudoPassword,未配则复用 password |
keep 路径的搬迁有三级回退:mv(同文件系统改名,最快)→ sudo mv(保住原归属)→ cp(保底,归属变为登录用户)。三者都失败才报错并提示开启 sudo。
没配 restart 的话,替换目录不会让服务用上新代码——进程仍持有旧文件句柄。工具会在这种情况下打印警告。
rbuild deploy # 弹列表选服务器和产物(可输入关键字过滤)
rbuild deploy prod-1 --file myapp-1.2.3.build.20260902.bin
rbuild deploy prod-1 --dry-run # 只打印执行计划,不连远端、不改任何东西
rbuild my-preset --deploy # 触发构建 -> 跟踪 -> 下载 -> 部署,一条命令服务器、产物、备份、job 都可以省略不写:多个候选时弹出可输入过滤的列表,上次用过的服务器排在最前;只有一个候选时直接用。非交互环境(脚本/CI)必须显式指定,不会卡在提示上。
部署会按产物类型选择策略:
- Linux 安装器
.bin/.run(推荐):SFTP 上传 →chmod +x→ 预热 sudo 凭据 → 执行安装脚本并实时透传输出 → 合并历史日志 → rbuild 再做一次健康检查。安装器生成的.backup.*备份可直接由rbuild rollback使用。 - 旧目录包
.tar.gz/.tgz(兼容):沿用解压、保留现场、原子换目录、重启与探活流程。
安装器默认参数是 --noprogress。以后安装器参数改变,只改 servers.json:
"deploy": { "type": "installer", "args": ["--quiet", "--accept"], "timeoutSec": 1200, "inheritEnvFromProcess": "chrome" }如果将来升级成其他产物格式,只需在 src/core/artifacts.ts 登记扩展名并实现对应策略;CLI 选文件、Jenkins 下载后选包、部署确认和回滚不需要跟着修改。
安装脚本若会重启桌面程序(例如 Chrome),SSH 会话自身没有 DISPLAY。配置 inheritEnvFromProcess 后,rbuild 会在安装前从匹配的现有进程读取 DISPLAY/XAUTHORITY/DBUS_SESSION_BUS_ADDRESS/XDG_RUNTIME_DIR,脚本杀掉旧进程后仍能在正确桌面会话中启动新进程。
注入 DISPLAY 会踩到 makeself 自解压包的一个陷阱:它在"stdout 不是终端 + DISPLAY 有效"时,会 exec 一个图形终端重新执行自己。SSH 端由此立刻拿到退出码 0,安装却跑到了目标机的屏幕上——表现为部署报成功但版本没更新。rbuild 因此总会给安装包补上 --nox11(只关掉 makeself 这层自我重拉,不影响传给安装脚本的 DISPLAY),并在安装后核对目标目录确实发生了变化,退出码为 0 但目录没动时按失败处理。
旧目录包部署流程(设计为失败不伤旧环境):
- 产物上传到远端
/tmp/rbuild-deploy-<时间戳>/(SFTP,带进度) - 在临时目录解压;包内单一顶层目录时用它作为新目录
- 保留现场:把
keep里的路径从当前目录搬进新目录 - 旧目录改名备份为
<remoteDir>.bak-<时间戳>(解压成功后才动旧目录),新目录换入,只保留最近 3 份备份 - 重启服务(
restart配置) - 健康检查(
healthCheck):容器状态 healthy + HTTP 探活;不通过则自动回滚到部署前版本并再次重启(--no-rollback可关闭,留现场排查)
keep 的目录采用移动语义,所以运行时数据(日志、历史备份)始终跟随当前版本,不会被塞进版本快照、也不会被备份轮转的 rm -rf 删掉。
回滚
rbuild rollback # 列出远端备份,交互选一个换回
rbuild rollback prod-1 --backup myapp.bak-20260814-160050
rbuild rollback prod-1 --backup myapp.backup.20260902_170129
rbuild rollback prod-1 --dry-run
rbuild rollback -y # 跳过确认(脚本里用)回滚同样是原子换名:当前版本先另存为新的 .bak-<时间戳>(所以滚错了可以再滚回来),再把选定备份改名换入,然后按 restart/healthCheck 重启并探活。keep 里的现场数据不跟着版本回退,继续留在当前目录。命令只接受该目录自己的 .bak-* 或 .backup.* 备份,拒绝任意路径。
注意 servers.json 含登录口令,只应存放在内网私有仓库;更稳妥的做法是留空 password 字段并改用异构凭据下发(后续版本可加)。
前端部署
rbuild front deploy 把本地构建的前端产物(默认当前目录的 dist/)推到一批前端机,与整包部署是两套独立的清单和语义。
rbuild front deploy # 弹多选(空格勾选),上次推过的那批默认已勾选
rbuild front deploy 193 197 # 关键字直接命中机器名/IP
rbuild front deploy --dist ./build 193 # 产物不在 ./dist 时
rbuild front deploy 193 --dry-run # 打印计划并做产物预检,不连远端
rbuild front rollback 193 # 回到最近一份备份;--backup 指定某一份前端机登记在数据目录的 front-servers.json:
{
"defaults": {
"cleanList": ["assets", "locales", "favicon.ico", "index.html"],
"healthCheck": { "url": "http://localhost:8090/", "timeoutSec": 30 }
},
"servers": [
{ "name": "web-1", "host": "192.0.2.21", "port": 22, "username": "deploy", "password": "your-password",
"remoteDir": "/srv/app/front" }
]
}替换语义是"合并覆盖",不是整目录替换:部署时先删掉 cleanList 列出的路径,再把产物解到 remoteDir 里。产物里有的同名文件会覆盖远端,cleanList 之外、产物里也没有的东西(其他子应用、现场配置)原样保留。defaults 对所有机器生效,单台可覆盖。
每台机器的流程:
- 前置校验:
cleanList里每一项在产物里必须存在且非空——要删远端的 X 就必须提供新的 X。这条规则从cleanList自动推导,专门防"构建没跑完就上传,把车端语言包清空"这类事故 - 本地
tar打包(排除*.map,保留.gz/.br预压缩件),上传到远端/tmp - 把
cleanList涉及的路径备份到同级的<remoteDir>.bak-<时间戳>/(只备这几项,保留 3 份;不放remoteDir里面——那会被 nginx 当成可访问的 URL) - 删
cleanList→ 解包覆盖 - 探活
healthCheck.url(能抓到解包文件权限不对导致的 403)
多台机器串行执行,某台失败(离线/密码错/探活不过)跳过继续,末尾汇总成败并以非零退出码结束。非交互环境必须显式给机器名。
回滚只还原 cleanList 涉及的路径,当前版本会先另存为新备份。
给 AI 工具用(MCP)
rbuild 自带一个 MCP 服务器,可以把触发构建、查状态/变更、下载产物、部署、回滚这些能力交给 Claude Code、Codex 等 AI 编码工具直接调用:
claude mcp add rbuild --scope user -- node /绝对路径/jenkins-web/dist/src/mcp/main.js仓库根目录已带项目级 .mcp.json,在该目录启动 Claude Code 即自动加载。Codex 的 TOML 配置、11 个工具的清单、长任务与安全注意事项见 docs/mcp.md。
想把自己的工具也包装成 MCP 服务,见 MCP 包装教程。
新增 Jenkins job
编辑数据目录的 jobs.json:
{
"jobs": [
{
"id": "my-job",
"displayName": "我的打包任务",
"jobPath": "/view/some-view/job/some-job",
"params": [
{ "name": "AppVersion", "type": "version", "deriveFrom": ["main_repo"] },
{ "name": "BuildDate", "type": "timestamp" },
{ "name": "main_repo", "type": "gitBranch", "versionSource": true },
{ "name": "other_repo", "type": "gitBranch", "syncVersionWith": "main_repo" }
]
}
]
}| 字段 | 说明 |
| --- | --- |
| id | 短名,交互列表里展示 |
| jobPath | Jenkins job 的 URL 路径(域名后、/build 前的部分) |
| params[].type | gitBranch 分支选择(Git Parameter 插件) / version 版本号(自动提取) / timestamp 自动时间戳(yyyyMMdd-HHmmss) / text 自由文本 |
| versionSource | 标在哪个分支参数上,表示从它提取版本号 |
| syncVersionWith | 让该参数的候选列表按某参数的版本号置顶排序 |
| deriveFrom | version 参数从哪些分支参数提取版本号(按顺序尝试) |
| default | text 参数的默认值,交互时作为预填(上次用过的值优先级更高) |
| optional | text 参数是否允许留空。留空时仍会把该参数发给 Jenkins,值为空串 |
文件上传类参数不需要登记,触发时留空。
从 1.x 升级
2.0 重排了参数语法。核心规则:位置参数只表示"哪个目标",不再表示"哪一版"。
1.x 靠值的形状(是不是纯数字、文件存不存在、含不含 .bak-)来猜第二个位置参数的角色,
而这些形状由输入和当前工作目录决定——同一条命令换个目录就可能做别的事。2.0 把这类角色全部改成显式选项。
| 1.x | 2.0 |
| --- | --- |
| rbuild dl 5x 490 | rbuild dl 5x --build 490 |
| rbuild changes 5x 490 | rbuild changes 5x --build 490 |
| rbuild deploy prod-1 app.bin | rbuild deploy prod-1 --file app.bin |
| rbuild rollback prod-1 app.bak-2026 | rbuild rollback prod-1 --backup app.bak-2026 |
| rbuild front 193 | rbuild front deploy 193 |
| rbuild front ./build 193 | rbuild front deploy --dist ./build 193 |
写旧语法不会被误执行:多给一个位置参数会直接报错,并打印对应的新写法。
另外两处行为变化:
- 产物默认下载到
./output/而不是当前目录(见「产物目录」)。旧习惯不会失效——deploy在产物目录为空时仍会回落到当前目录找包。 - 预设名不能和 job id 重名了。保存预设时会校验并拒绝,因为
rbuild <名字>这个位置同时接受 job id 和预设名。
代码结构
3.0 用 TypeScript 重写并分了三层。分层不是为了好看,是为了让同一份业务逻辑能同时服务两个完全不同的调用方:
src/core/ 领域层。不含 console、不含 inquirer、不碰 process.exit。
config/(四个 JSON 的加载与 zod 校验) jenkins/(REST 客户端)
build/(参数装配与构建跟踪) deploy/(后端部署) front/(前端部署)
src/cli/ 终端外壳。参数解析、交互提示、彩色输出、退出码、补全脚本生成。
src/mcp/ MCP 外壳。同一套 core,换一个 Reporter 实现。Reporter 是两层之间唯一的"说话"通道。 core 的函数都接一个 reporter 参数,只调 step/info/success/warn/error/detail/progress/stream 这几个方法。CLI 传一个往终端画状态行的实现,MCP 传一个收进数组的实现——这样 MCP 不必再劫持全局 console 来防止日志污染 stdio 协议流(2.x 为此在 5 处包了 capture())。测试传空实现。
几条配套约束,由工具而不是自觉来保证:
biome的noConsole规则对全仓库生效,core 里写console.log会直接构建失败。tsconfig开了strict+noUncheckedIndexedAccess+exactOptionalPropertyTypes,数组下标和可选字段都必须显式处理。- 四个 JSON 配置在读取时用 zod 校验,字段拼错会直接报出路径(
servers.0.keep: 应为数组),而不是等到部署到一半才以"remoteDir 不合法"的形式冒出来。 test/golden/存着 2.2.0 的 help 与 dry-run 输出,npm test会逐字节比对——重构可以随便改内部结构,但对外的每一个字都不许动。
开发
pnpm install # 装依赖,prepare 钩子自动编译到 dist/
npm run dev -- ls # 用 tsx 直接跑 TypeScript,不必等编译
npm run build # tsc 编译
npm run typecheck # 只查类型
npm run lint # biome
npm test # 编译 + 全部测试(含黄金比对)常见问题
| 现象 | 处理 |
| --- | --- |
| 缺少 Jenkins 配置 | 按上文配置 .env |
| 未找到 jobs.json | 在数据目录创建 job 登记表,见上节 |
| 认证失败(401) | 用户名或 Token 错误,重新生成 Token |
| 拒绝访问(403) | 账号没有该 job 权限,找 Jenkins 管理员开通 |
| 返回 404 | jobs.json 里 jobPath 写错 |
| 无法连接 Jenkins | 检查内网/VPN |
| 预设中的分支已不在...列表里 | 分支被删除/改名了,运行 rbuild 重选分支,存同名预设覆盖 |
| Parameter xx provided value ... is invalid | 提交值不在该参数的实时分支列表(Jenkins 侧校验),同上处理 |
License
MIT
