Files

155 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工具栏「发布」按钮 — 设计
## 背景
发布流程(`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 超时。
```ts
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`(新)
```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`。新增:
```ts
// 扩展 → 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「<sha> <subject> 冲突,请手动处理」,远端不变 |
| 推送被拒(远端分支保护 / 并发推送) | 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、提交。