✨ feat(vscode): 发布按钮扩展侧 handler,删除分支同步

Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
cruldra committed 2026-08-31 23:41:03 +08:00
1 parent 6f59b4f3fd
commit b363e08ef9
12 files changed
+292 -345

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
/**
* 工具栏「发布」按钮的扩展侧 handler:切新发布分支、摘修复到当期发布分支、触发正式服部署。
*
* git 逻辑在 `src/git/release.ts`,Jenkins 触发在 `src/jenkins/trigger.ts`;这里只负责
* 读设置、弹 QuickPick、发 toast、维护 `panel.releaseRunning` 互斥。
*/
import type { KanbanWebviewPanel } from '../KanbanPanel'
import { window, workspace } from 'vscode'
import { gitFetch } from '../../git/branches'
import {
cherryPickToRelease,
cutRelease,
getReleaseStatus,
listReleaseBranches,
listUnreleasedCommits,
todayReleaseBranch,
} from '../../git/release'
import { triggerJenkinsBuild } from '../../jenkins/trigger'
import { getSettings } from '../../settings/store'
import { dismiss, toast } from './handoffFlow'
export async function handleReleaseStatus(panel: KanbanWebviewPanel): Promise<void> {
const root = workspace.workspaceFolders?.[0]?.uri.fsPath
const s = getSettings(panel.context)
const base = {
type: 'release/status' as const,
devBranch: s.devBranch,
nextBranch: todayReleaseBranch(s.releaseBranchPrefix),
running: panel.releaseRunning,
}
if (!root) {
panel.postMessage({ ...base, unreleased: 0, unavailable: '请先打开一个工作区文件夹' })
return
}
const st = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
panel.postMessage({ ...base, current: st.current, unreleased: st.unreleased, unavailable: st.unavailable })
}
function requireRoot(): string {
const root = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!root)
throw new Error('请先打开一个工作区文件夹')
return root
}
/** 三个写动作共用:互斥 + 前后刷新状态 + 统一错误 toast */
async function runReleaseAction(panel: KanbanWebviewPanel, fn: () => Promise<void>): Promise<void> {
if (panel.releaseRunning)
return
panel.releaseRunning = true
void handleReleaseStatus(panel)
try {
await fn()
}
catch (err) {
toast(panel, 'error', err instanceof Error ? err.message : String(err), { dismissOnTimer: 10_000 })
}
finally {
panel.releaseRunning = false
void handleReleaseStatus(panel)
}
}
async function deploy(panel: KanbanWebviewPanel, branch: string): Promise<void> {
const s = getSettings(panel.context)
await triggerJenkinsBuild({ job: s.jenkinsJob, params: { REF: branch, REMOTE_HOST: s.jenkinsProdHost } })
toast(panel, 'success', `已触发 Jenkins ${s.jenkinsJob}:REF=${branch} → ${s.jenkinsProdHost}`)
}
export function handleReleaseCut(panel: KanbanWebviewPanel): Promise<void> {
return runReleaseAction(panel, async () => {
const root = requireRoot()
const s = getSettings(panel.context)
const branch = todayReleaseBranch(s.releaseBranchPrefix)
const count = await cutRelease({ cwd: root, devBranch: s.devBranch, branch })
toast(panel, 'success', `已切发布分支 ${branch}(${count} 个提交)`)
if (!s.jenkinsJob)
return
const pick = await window.showInformationMessage(`现在部署 ${branch} 到${s.jenkinsProdHost}?`, '部署')
if (pick === '部署')
await deploy(panel, branch)
})
}
interface CommitPickItem { label: string, description: string, sha: string }
export function handleReleaseCherryPick(panel: KanbanWebviewPanel): Promise<void> {
return runReleaseAction(panel, async () => {
const root = requireRoot()
const s = getSettings(panel.context)
const status = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
if (status.unavailable)
throw new Error(status.unavailable)
const current = status.current
if (!current)
throw new Error('尚无发布分支,先切新发布')
const commits = await listUnreleasedCommits({ cwd: root, devBranch: s.devBranch, current })
if (commits.length === 0) {
toast(panel, 'info', '没有待摘的提交')
return
}
const picked = await pickCommits(current, commits.map(c => ({
label: c.subject,
description: `${c.sha.slice(0, 7)} · ${c.author} · ${c.date}`,
sha: c.sha,
})))
if (!picked || picked.length === 0)
return
// 列表是 git log 顺序(新→旧),cherry-pick 要按时间顺序(旧→新)
const order = new Map(commits.map((c, i) => [c.sha, i]))
const shas = [...picked].sort((a, b) => (order.get(b.sha) ?? 0) - (order.get(a.sha) ?? 0)).map(p => p.sha)
const spinner = toast(panel, 'info', `正在摘 ${shas.length} 个提交到 ${current}…`, { spinner: true, dismissOnTimer: 0 })
try {
await cherryPickToRelease({ cwd: root, branch: current, shas })
}
finally {
dismiss(panel, spinner)
}
toast(panel, 'success', `已摘 ${shas.length} 个提交到 ${current}`)
})
}
/** 多选 QuickPick;用户按 Esc 返回 undefined */
function pickCommits(current: string, items: CommitPickItem[]): Promise<CommitPickItem[] | undefined> {
return new Promise((resolve) => {
const qp = window.createQuickPick<CommitPickItem>()
qp.canSelectMany = true
qp.title = `摘修复到 ${current}`
qp.placeholder = '勾选要摘到发布分支的提交'
qp.items = items
qp.onDidAccept(() => {
resolve([...qp.selectedItems])
qp.hide()
})
qp.onDidHide(() => {
resolve(undefined)
qp.dispose()
})
qp.show()
})
}
export function handleReleaseDeploy(panel: KanbanWebviewPanel): Promise<void> {
return runReleaseAction(panel, async () => {
const root = requireRoot()
const s = getSettings(panel.context)
if (!s.jenkinsJob)
throw new Error('未配置 Jenkins 任务名,请先在设置里填写')
await gitFetch(root)
const branches = await listReleaseBranches({ cwd: root, prefix: s.releaseBranchPrefix })
if (branches.length === 0)
throw new Error('尚无发布分支')
const [current, ...older] = branches
const items = [
{ label: current, description: '当期' },
...older.map(label => ({ label, description: '回滚到此发布点' })),
]
const pick = await window.showQuickPick(items, {
title: `部署到${s.jenkinsProdHost}`,
placeHolder: '选择要部署的发布分支(默认当期,选旧的即回滚)',
})
if (!pick)
return
await deploy(panel, pick.label)
})
}