feat: 开放 Jenkins 构建日志接口

This commit is contained in:
湛兮
2026-06-12 02:37:07 +08:00
parent fb736701ca
commit b41c9c902f
3 changed files with 110 additions and 1 deletions
+11
View File
@@ -68,3 +68,14 @@ export type DeployRunJenkinsSyncSummary = {
syncedRuns: DeployRunSummary[];
failedRuns: DeployRunJenkinsSyncError[];
};
export type DeployRunJenkinsLog = {
runId: string;
jobPath: string;
buildNumber: number;
start: number;
nextStart: number;
hasMore: boolean;
text: string;
fetchedAt: string;
};
+20 -1
View File
@@ -1,4 +1,12 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Get,
Param,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import {
ApiBody,
ApiCreatedResponse,
@@ -18,6 +26,7 @@ import { CurrentUser } from '../auth/current-user.decorator';
import { PasswordChangeGuard } from '../auth/password-change.guard';
import { SuperAdminGuard } from '../auth/super-admin.guard';
import {
DeployRunJenkinsLog,
DeployRunJenkinsSyncSummary,
DeployRunSummary,
} from './deploy-run.types';
@@ -46,6 +55,16 @@ export class DeployRunsController {
return this.deployRunsService.getRun(id, user);
}
@Get(':id/jenkins-log')
@ApiOkResponse({ description: '读取发布单对应 Jenkins build 日志。' })
async getJenkinsLog(
@Param('id') id: string,
@Query('start') start: string | undefined,
@CurrentUser() user: AuthenticatedUser,
): Promise<DeployRunJenkinsLog> {
return this.deployRunsService.getJenkinsLog(id, start, user);
}
@Post()
@ApiBody({ type: CreateDeployRunDto })
@ApiCreatedResponse({ description: '创建发布单并触发构建。' })
+79
View File
@@ -19,6 +19,7 @@ import { DeployExecutionService } from './deploy-execution.service';
import { DeployRunRepository } from './deploy-run.repository';
import {
CreateDeployRunInput,
DeployRunJenkinsLog,
DeployRunJenkinsSyncSummary,
DeployRunSummary,
} from './deploy-run.types';
@@ -237,6 +238,63 @@ export class DeployRunsService {
return this.deployExecutionService.syncJenkinsRun(run, project);
}
async getJenkinsLog(
id: string,
start: string | undefined,
user?: AuthenticatedUser,
): Promise<DeployRunJenkinsLog> {
const run = await this.getRun(id, user);
if (run.jenkinsBuildNumber === undefined || run.jenkinsBuildNumber === null) {
throw new AppError(
'VALIDATION_FAILED',
`发布记录 ${id} 尚未关联 Jenkins build,无法读取日志`,
400,
{
id,
jenkinsQueueId: run.jenkinsQueueId,
},
);
}
const project = await this.projectsService.getProject(run.projectKey);
const environment = this.findEnvironment(project, run);
const normalizedStart = this.normalizeLogStart(start);
const log = await this.jenkinsClient.getProgressiveText(
environment.jenkinsJobPath,
run.jenkinsBuildNumber,
normalizedStart,
);
const text = redactSensitive({ text: log.text }).text;
await this.auditService.record({
action: 'JENKINS_LOG_VIEWED',
resourceType: 'deploy_run',
resourceId: run.id,
actorName: user?.account ?? run.operator,
after: {
projectKey: run.projectKey,
environment: run.environment,
jenkinsBuildNumber: run.jenkinsBuildNumber,
start: normalizedStart,
nextStart: log.nextStart,
hasMore: log.hasMore,
textLength: text.length,
},
});
return {
runId: run.id,
jobPath: environment.jenkinsJobPath,
buildNumber: run.jenkinsBuildNumber,
start: normalizedStart,
nextStart: log.nextStart,
hasMore: log.hasMore,
text,
fetchedAt: new Date().toISOString(),
};
}
async syncJenkinsRuns(
user?: AuthenticatedUser,
): Promise<DeployRunJenkinsSyncSummary> {
@@ -311,6 +369,27 @@ export class DeployRunsService {
);
}
private normalizeLogStart(start: string | undefined): number {
if (start === undefined || start === '') {
return 0;
}
const value = Number(start);
if (!Number.isSafeInteger(value) || value < 0) {
throw new AppError(
'VALIDATION_FAILED',
'Jenkins 日志 start 必须是非负整数',
400,
{
start,
},
);
}
return value;
}
private async cancelJenkinsExecution(
run: DeployRunSummary,
project: ProjectSummary,