diff --git a/src/integrations/jenkins/jenkins.client.ts b/src/integrations/jenkins/jenkins.client.ts index 30b851f..469df7a 100644 --- a/src/integrations/jenkins/jenkins.client.ts +++ b/src/integrations/jenkins/jenkins.client.ts @@ -1,3 +1,6 @@ +/** + * 封装 Jenkins HTTP API 调用,屏蔽鉴权、队列解析和非参数化 job 兼容策略。 + */ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { IntegrationRequestError, IntegrationUnavailableError } from '../../common/errors/integration-error'; @@ -25,15 +28,50 @@ export class JenkinsClient { parameters: Record, ): Promise { this.assertConfigured(); + const path = this.jobPath(jobPath, 'buildWithParameters'); const body = new URLSearchParams(parameters); + const response = await this.request(path, { + method: 'POST', + body, + }, { + allowFailure: true, + }); + + if (!response.ok) { + const responseText = await response.text(); + + if ( + response.status === 400 && + /not parameterized/i.test(responseText) + ) { + return this.buildWithoutParameters(jobPath); + } + + throw this.requestError(path, { + status: response.status, + request: redactSensitive({ method: 'POST', body: parameters }), + responseExcerpt: redactSensitive({ + responseExcerpt: responseText.slice(0, 500), + }).responseExcerpt, + }); + } + + return this.toQueueItem(response); + } + + private async buildWithoutParameters(jobPath: string): Promise { + const path = this.jobPath(jobPath, 'build'); const response = await this.request( - `/job/${jobPath.split('/').map(encodeURIComponent).join('/job/')}/buildWithParameters`, + path, { method: 'POST', - body, }, ); + return this.toQueueItem(response); + } + + private toQueueItem(response: Response): JenkinsQueueItem { const location = response.headers.get('location') ?? undefined; return { queueId: location?.split('/').filter(Boolean).pop() ?? 'unknown', @@ -62,7 +100,7 @@ export class JenkinsClient { ): Promise { this.assertConfigured(); const response = await this.request( - `/job/${jobPath.split('/').map(encodeURIComponent).join('/job/')}/${buildNumber}/api/json`, + `${this.jobPath(jobPath)}/${buildNumber}/api/json`, ); const payload = (await response.json()) as { building?: boolean; @@ -86,7 +124,7 @@ export class JenkinsClient { ): Promise { this.assertConfigured(); const response = await this.request( - `/job/${jobPath.split('/').map(encodeURIComponent).join('/job/')}/${buildNumber}/logText/progressiveText?start=${start}`, + `${this.jobPath(jobPath)}/${buildNumber}/logText/progressiveText?start=${start}`, ); return { @@ -96,7 +134,11 @@ export class JenkinsClient { }; } - private async request(path: string, init: RequestInit = {}): Promise { + private async request( + path: string, + init: RequestInit = {}, + options: { allowFailure?: boolean } = {}, + ): Promise { const baseUrl = this.config.get('JENKINS_BASE_URL', { infer: true }) ?? ''; const username = this.config.get('JENKINS_USERNAME', { infer: true }) ?? ''; const apiToken = this.config.get('JENKINS_API_TOKEN', { infer: true }) ?? ''; @@ -108,10 +150,9 @@ export class JenkinsClient { }, }); - if (!response.ok) { - throw new IntegrationRequestError('jenkins', 'Jenkins request failed', { + if (!response.ok && !options.allowFailure) { + throw this.requestError(path, { status: response.status, - path, request: redactSensitive(init), }); } @@ -119,6 +160,22 @@ export class JenkinsClient { return response; } + private jobPath(jobPath: string, action?: string): string { + const encoded = `/job/${jobPath.split('/').map(encodeURIComponent).join('/job/')}`; + + return action ? `${encoded}/${action}` : encoded; + } + + private requestError( + path: string, + details?: Record, + ): IntegrationRequestError { + return new IntegrationRequestError('jenkins', 'Jenkins request failed', { + path, + ...details, + }); + } + private assertConfigured(): void { const missing = this.missingConfig();