Files
superwork/docs/superpowers/specs/release-control/spec.md
T

11 KiB
Raw Blame History

工具栏「发布」按钮 — 设计

背景

发布流程(luffy-agent-platform/docs/release-process.md)已改为:main 只往前走;线上跑的是某个 release/YYYY-MM-DD 发布分支;发什么由 Jenkins 的 REF 参数决定;线上紧急修复是「先进 main,再 cherry-pick 到当期发布分支」;回滚是拿旧发布分支再发一次。

扩展现有的「分支同步」按钮建模的是「把远端 autoBuildBranch(jenkins 分支)快进到 devBranch」。jenkins 分支即将删除,这个模型已经无法表达任何东西:按钮永远显示「开发分支与自动化构建分支相同」。

本设计用「发布点管理」替换它。不保留旧功能、不做兼容。

目标

工具栏一个「发布」按钮,做到三件事,与发布文档的三个步骤一一对应:

  1. 切新发布:从 origin/<devBranch> 切当天的 release/YYYY-MM-DD 并推远端。
  2. 摘修复到当期发布:把 main 上还没进发布分支的提交挑选后 cherry-pick 到当期发布分支并推远端。
  3. 部署到正式服:选一个发布分支(默认当期;选旧的即回滚)触发 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,默认 正式服;即 Jenkins REMOTE_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、提交。