✨ feat(vscode): 通过 opencli 触发 Jenkins 构建

Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
cruldra committed 2026-08-31 23:33:34 +08:00
1 parent ac8d4bc7a7
commit e10fe93bc6
2 files changed
+87

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import { buildJenkinsArgs, triggerJenkinsBuild } from './trigger'
describe('buildJenkinsArgs', () => {
it('每个参数一个 -p,不带 -s', () => {
expect(buildJenkinsArgs('luffy', { REF: 'release/2026-08-31', REMOTE_HOST: '正式服' }))
.toEqual(['jks', 'build', 'luffy', '-p', 'REF=release/2026-08-31', '-p', 'REMOTE_HOST=正式服'])
})
})
describe('triggerJenkinsBuild', () => {
it('opencli 缺失 → 明确报错', async () => {
const exec = async () => {
const e = new Error('spawn opencli ENOENT') as NodeJS.ErrnoException
e.code = 'ENOENT'
throw e
}
await expect(triggerJenkinsBuild({ job: 'j', params: {}, exec })).rejects.toThrow(/未找到 opencli/)
})
it('退出码非零 → 带 stderr', async () => {
const exec = async () => ({ code: 1, stderr: 'boom: not a valid parameter' })
await expect(triggerJenkinsBuild({ job: 'j', params: {}, exec })).rejects.toThrow(/not a valid parameter/)
})
it('成功', async () => {
const calls: string[][] = []
const exec = async (_b: string, a: string[]) => {
calls.push(a)
return { code: 0, stderr: '' }
}
await triggerJenkinsBuild({ job: 'j', params: { REF: 'x' }, exec })
expect(calls).toEqual([['jks', 'build', 'j', '-p', 'REF=x']])
})
})
+52
View File
@@ -0,0 +1,52 @@
/**
* 通过 opencli 触发 Jenkins 构建:`opencli jks build <job> -p K=V …`。
*
* 故意不加 `-s`(同步等待):opencli 文档说明同步模式下命令行一断,Jenkins 会中止构建,
* 而扩展进程随时可能被关掉,只做「触发」最稳妥。
*/
import { execFile } from 'node:child_process'
export type ExecFn = (bin: string, args: string[]) => Promise<{ code: number | null, stderr: string }>
const OPENCLI_TIMEOUT_MS = 60_000
export function buildJenkinsArgs(job: string, params: Record<string, string>): string[] {
const args = ['jks', 'build', job]
for (const [key, value] of Object.entries(params))
args.push('-p', `${key}=${value}`)
return args
}
/** 二进制不存在(ENOENT)原样抛出,其它失败都折叠成退出码 + stderr,由调用方统一措辞 */
function execDefault(bin: string, args: string[]): Promise<{ code: number | null, stderr: string }> {
return new Promise((resolve, reject) => {
execFile(bin, args, { timeout: OPENCLI_TIMEOUT_MS, encoding: 'utf8' }, (err, _stdout, stderr) => {
if (!err) {
resolve({ code: 0, stderr: stderr ?? '' })
return
}
const e = err as NodeJS.ErrnoException & { code?: number | string | null }
if (e.code === 'ENOENT') {
reject(e)
return
}
resolve({ code: typeof e.code === 'number' ? e.code : 1, stderr: stderr ?? e.message })
})
})
}
export async function triggerJenkinsBuild(o: { job: string, params: Record<string, string>, exec?: ExecFn }): Promise<void> {
const exec = o.exec ?? execDefault
let result: { code: number | null, stderr: string }
try {
result = await exec('opencli', buildJenkinsArgs(o.job, o.params))
}
catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT')
throw new Error('未找到 opencli,请确认已安装并在 PATH 中')
throw err
}
if (result.code !== 0)
throw new Error(`Jenkins 触发失败(退出码 ${result.code}):${result.stderr.trim().slice(-400)}`)
}