✨ feat(vscode): 通过 opencli 触发 Jenkins 构建
Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
1 parent
ac8d4bc7a7
commit
e10fe93bc6
2 files changed
+87
No files matched your search
@@ -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']])
|
||||
})
|
||||
})
|
||||
@@ -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)}`)
|
||||
}
|
||||
Reference in new issue
Block a user