11 KiB
工具栏「发布」按钮 — 设计
背景
发布流程(luffy-agent-platform/docs/release-process.md)已改为:main 只往前走;线上跑的是某个 release/YYYY-MM-DD 发布分支;发什么由 Jenkins 的 REF 参数决定;线上紧急修复是「先进 main,再 cherry-pick 到当期发布分支」;回滚是拿旧发布分支再发一次。
扩展现有的「分支同步」按钮建模的是「把远端 autoBuildBranch(jenkins 分支)快进到 devBranch」。jenkins 分支即将删除,这个模型已经无法表达任何东西:按钮永远显示「开发分支与自动化构建分支相同」。
本设计用「发布点管理」替换它。不保留旧功能、不做兼容。
目标
工具栏一个「发布」按钮,做到三件事,与发布文档的三个步骤一一对应:
- 切新发布:从
origin/<devBranch>切当天的release/YYYY-MM-DD并推远端。 - 摘修复到当期发布:把 main 上还没进发布分支的提交挑选后 cherry-pick 到当期发布分支并推远端。
- 部署到正式服:选一个发布分支(默认当期;选旧的即回滚)触发 Jenkins 构建。
并随时显示:当期发布点是哪个、devBranch 上还有多少提交没发出去。
非目标
- 打 tag、删旧发布分支、测试服部署(推 main 已自动触发)、查询 Jenkins 构建结果。
- 本地分支操作:所有 git 写操作只碰远端引用和临时 worktree,不改用户当前工作区的分支与文件。
术语
| 词 | 含义 |
|---|---|
devBranch |
日常开发分支,现有设置项,默认 main |
releaseBranchPrefix |
发布分支前缀,新设置项,默认 release/ |
| 当期发布分支 | 远端 <releaseBranchPrefix>* 里名字排序最大的那个(名字是 ISO 日期,字典序即时间序) |
| 未发布提交 | origin/<当期发布分支>..origin/<devBranch> 中、按补丁内容去重后仍在的提交(git rev-list --right-only --cherry-pick --no-merges),被 cherry-pick 过的不算 |
设置
src/settings/store.ts:
- 删除
autoBuildBranch(类型、默认值、读取归一化、设置面板输入框、消息类型、README)。 - 新增
releaseBranchPrefix: string,默认release/;trim 后为空回退默认;不以/结尾时读取时补上。 - 新增
jenkinsJob: string,默认'';空 = 不提供「部署到正式服」动作。 - 新增
jenkinsProdHost: string,默认正式服;即 JenkinsREMOTE_HOST参数的取值。
设置面板「认证」分组里,autoBuildBranch 输入框原位换成这三个字段(releaseBranchPrefix、jenkinsJob、jenkinsProdHost)。
模块划分
src/git/release.ts(新,纯 git,可测)
不依赖 VS Code API。所有命令用 execFile('git', ['-C', cwd, ...]),30s 超时。
export interface ReleaseStatus {
devBranch: string
/** 当期发布分支全名(含前缀);远端没有任何发布分支时为 undefined */
current: string | undefined
/** 未发布提交数;current 为 undefined 时为 0,由 UI 显示「尚无发布点」 */
unreleased: number
/** 结构性不可用(不是 git 仓库 / fetch 失败 / devBranch 远端不存在),带原因 */
unavailable?: string
}
export function todayReleaseBranch(prefix: string, now?: Date): string // 'release/2026-08-31'
export function pickCurrentRelease(prefix: string, remoteBranches: string[]): string | undefined
export async function getReleaseStatus(o: { cwd, devBranch, prefix }): Promise<ReleaseStatus>
export async function listUnreleasedCommits(o: { cwd, devBranch, current }): Promise<Array<{ sha: string, subject: string, author: string, date: string }>>
export async function listReleaseBranches(o: { cwd, prefix }): Promise<string[]> // 新→旧
export async function cutRelease(o: { cwd, devBranch, branch }): Promise<void>
export async function cherryPickToRelease(o: { cwd, branch, shas: string[] }): Promise<void>
细节:
getReleaseStatus先git fetch origin --prune,再git for-each-ref refs/remotes/origin/<prefix>取分支列表,unreleased用git rev-list --count --right-only --cherry-pick --no-merges origin/<current>...origin/<devBranch>。cutRelease:先确认origin/<branch>不存在(存在则抛「今天的发布分支已存在」),然后git push origin origin/<devBranch>:refs/heads/<branch>。纯远端,不建本地分支。cherryPickToRelease:git worktree add --detach <tmp> origin/<branch>→ 在 tmp 里按 shas 给定顺序(旧→新)git cherry-pick -x <sha>...→git push origin HEAD:refs/heads/<branch>→git worktree remove --force <tmp>。任一步失败:cherry-pick 冲突先git cherry-pick --abort,无论如何finally里删 worktree,抛出的错误里带上冲突的 sha 与 subject。tmp 目录在os.tmpdir()下,名字带随机后缀。-x让发布分支上的提交留下「cherry picked from」痕迹,便于追溯。listUnreleasedCommits:git log --right-only --cherry-pick --no-merges --format=<sha>%x00<subject>%x00<an>%x00<ad> origin/<current>...origin/<devBranch>,默认 git log 顺序(新→旧)。
pickCurrentRelease 与 todayReleaseBranch 是纯函数,单测覆盖:前缀过滤、字典序取最大、空列表、非日期名混入(只按字典序,不解析日期)。
src/jenkins/trigger.ts(新)
export async function triggerJenkinsBuild(o: { job: string, params: Record<string, string> }): Promise<void>
执行 opencli jks build <job> -p K=V -p K=V(每个参数一个 -p),不加 -s(文档明确:同步等待时命令行一断 Jenkins 会中止构建)。60s 超时。opencli 不在 PATH → 抛「未找到 opencli」;退出码非零 → 抛并带 stderr 尾部。
src/panel/handlers/release.ts(新,替换 toolbar.ts 里的 branchSync 两个 handler)
handleReleaseStatus(panel):读设置 →getReleaseStatus→postMessage({ type: 'release/status', ... })。触发时机与旧branch-sync/check完全相同(面板就绪、用户点刷新、每个写动作结束后)。handleReleaseCut(panel):cutRelease今天的分支 → 成功 toast已切发布分支 release/…(N 个提交);若jenkinsJob非空,toast 之后紧接window.showInformationMessage('现在部署 release/… 到正式服?', '部署'),点了就走部署。handleReleaseCherryPick(panel):无当期发布分支 → error toast「尚无发布分支,先切新发布」。listUnreleasedCommits为空 → info toast「没有待摘的提交」。否则 VS Code QuickPick 多选(label = subject,description = 短 sha · 作者 · 日期),确认后按 git 时间顺序(旧→新)cherry-pick;进行中 spinner toast,成功 toast已摘 N 个提交到 release/…。失败 error toast 带冲突 sha。handleReleaseDeploy(panel):listReleaseBranches→ QuickPick 单选,默认选中当期,其它项 description 标「回滚到此发布点」→triggerJenkinsBuild({ job, params: { REF: branch, REMOTE_HOST: jenkinsProdHost } })→ 成功 toast已触发 Jenkins ${job}:REF=${branch} → ${jenkinsProdHost}。
每个写动作 finally 都重新 handleReleaseStatus。三个动作共用一个 panel.releaseRunning 互斥标志(与 commitRunning 同模式),运行中按钮禁用。
消息(src/panel/messages.ts / webview-ui/src/lib/messages.ts)
删 branch-sync/status | check | run。新增:
// 扩展 → webview
| { type: 'release/status', devBranch: string, current?: string, unreleased: number, unavailable?: string, running: boolean }
// webview → 扩展
| { type: 'release/check' }
| { type: 'release/cut' }
| { type: 'release/cherry-pick' }
| { type: 'release/deploy' }
release/status 里带 running,webview 不再自己维护 running 标志(旧实现里 webview 自置 running 再等 status 清除,两头各维护一份状态是多余的)。
Webview
-
PanelHeader.tsx:旧同步按钮换成「发布」按钮,图标Rocket(lucide);unreleased > 0时右上角小圆角数字角标(≥100 显示99+);running时转圈;unavailable时禁用。 -
悬停文案:
- unavailable → 原因
- 无 current →
尚无发布点 · 点击切第一个发布分支 - 有 current →
正式服发布点 ${current} · ${devBranch} 领先 ${unreleased} 个提交(0 时写「已全部发布」)
-
点击弹出下拉菜单(与工具栏其它按钮风格一致,
absolute定位,点外面关):切新发布(release/今天)摘修复到 ${current}(无 current 时禁用)部署到正式服…(jenkinsJob为空时不渲染此项)
菜单项点击发对应消息后关闭。所有多选/单选在扩展侧用 VS Code QuickPick 完成,webview 不做列表 UI。
-
useIssues.ts:删branchSync*五个状态与runBranchSync,换成release: ReleaseStatusMsg | undefined与runRelease(action: 'cut' | 'cherry-pick' | 'deploy')。App.tsx三处透传随之更新。
删除
src/git/branchSync.ts里的checkBranchSync、runBranchSync、BranchSyncStatus、CheckBranchSyncOpts、RunBranchSyncOpts及对应测试。留下的runGit、gitFetch、resolveFeatureBranch、deleteLocalBranch、pruneRemoteTrackingRef被 issues / worktree / branchCleanup 使用,整体改名为src/git/branches.ts(连同测试文件),更新 4 处 import。toolbar.ts里handleBranchSyncCheck/handleBranchSyncRun、KanbanPanel.ts的handleBranchSyncCheck方法与消息分发。- README:工具栏一句里「分支同步(…)」换成「发布(切发布分支 · 摘修复 · 触发正式服部署)」;设置表里
autoBuildBranch行换成三个新设置项;工作流章节末尾补一小节「发布」三步。
错误处理
| 情况 | 表现 |
|---|---|
| 不是 git 仓库 / 没有 origin / fetch 失败 | 按钮禁用,悬停显示原因 |
origin/<devBranch> 不存在 |
同上,原因「远端没有 main」 |
| 当天发布分支已存在 | error toast,不推送 |
| cherry-pick 冲突 | abort、删临时 worktree、error toast「 冲突,请手动处理」,远端不变 |
| 推送被拒(远端分支保护 / 并发推送) | error toast 带 git stderr 尾部 |
opencli 缺失或 Jenkins 拒绝 |
error toast 带原因;git 侧已完成的动作不回滚(发布分支切好了就是切好了) |
测试
src/git/release.test.ts:todayReleaseBranch(前缀补/、日期格式)、pickCurrentRelease(过滤、排序、空)纯函数单测;getReleaseStatus/cutRelease/cherryPickToRelease用vitest在临时目录里真建 bare 仓库当 origin 做集成测试(仓库里test/git/已有类似做法):切分支、cherry-pick 干净、cherry-pick 冲突后远端不变且 worktree 被清理、已 cherry-pick 的提交不再计入 unreleased。src/jenkins/trigger.test.ts:参数拼装(每个-p一对、不含-s);用注入的 exec 函数,不真跑 opencli。- 手动验收:在 luffy-agent-platform 工作区看角标数字 =
git rev-list --count --right-only --cherry-pick origin/release/2026-08-31...origin/main。
收尾
按 vscode/CLAUDE.md:版本 patch +1、打 VSIX、装两个 profile、提交。