diff --git a/vscode/src/jenkins/trigger.test.ts b/vscode/src/jenkins/trigger.test.ts new file mode 100644 index 0000000..532b37e --- /dev/null +++ b/vscode/src/jenkins/trigger.test.ts @@ -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']]) + }) +}) diff --git a/vscode/src/jenkins/trigger.ts b/vscode/src/jenkins/trigger.ts new file mode 100644 index 0000000..4622f08 --- /dev/null +++ b/vscode/src/jenkins/trigger.ts @@ -0,0 +1,52 @@ +/** + * 通过 opencli 触发 Jenkins 构建:`opencli jks build -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[] { + 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, exec?: ExecFn }): Promise { + 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)}`) +}