feat: 开放 Jenkins 构建日志接口
This commit is contained in:
@@ -68,3 +68,14 @@ export type DeployRunJenkinsSyncSummary = {
|
|||||||
syncedRuns: DeployRunSummary[];
|
syncedRuns: DeployRunSummary[];
|
||||||
failedRuns: DeployRunJenkinsSyncError[];
|
failedRuns: DeployRunJenkinsSyncError[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeployRunJenkinsLog = {
|
||||||
|
runId: string;
|
||||||
|
jobPath: string;
|
||||||
|
buildNumber: number;
|
||||||
|
start: number;
|
||||||
|
nextStart: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
text: string;
|
||||||
|
fetchedAt: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
ApiBody,
|
ApiBody,
|
||||||
ApiCreatedResponse,
|
ApiCreatedResponse,
|
||||||
@@ -18,6 +26,7 @@ import { CurrentUser } from '../auth/current-user.decorator';
|
|||||||
import { PasswordChangeGuard } from '../auth/password-change.guard';
|
import { PasswordChangeGuard } from '../auth/password-change.guard';
|
||||||
import { SuperAdminGuard } from '../auth/super-admin.guard';
|
import { SuperAdminGuard } from '../auth/super-admin.guard';
|
||||||
import {
|
import {
|
||||||
|
DeployRunJenkinsLog,
|
||||||
DeployRunJenkinsSyncSummary,
|
DeployRunJenkinsSyncSummary,
|
||||||
DeployRunSummary,
|
DeployRunSummary,
|
||||||
} from './deploy-run.types';
|
} from './deploy-run.types';
|
||||||
@@ -46,6 +55,16 @@ export class DeployRunsController {
|
|||||||
return this.deployRunsService.getRun(id, user);
|
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()
|
@Post()
|
||||||
@ApiBody({ type: CreateDeployRunDto })
|
@ApiBody({ type: CreateDeployRunDto })
|
||||||
@ApiCreatedResponse({ description: '创建发布单并触发构建。' })
|
@ApiCreatedResponse({ description: '创建发布单并触发构建。' })
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { DeployExecutionService } from './deploy-execution.service';
|
|||||||
import { DeployRunRepository } from './deploy-run.repository';
|
import { DeployRunRepository } from './deploy-run.repository';
|
||||||
import {
|
import {
|
||||||
CreateDeployRunInput,
|
CreateDeployRunInput,
|
||||||
|
DeployRunJenkinsLog,
|
||||||
DeployRunJenkinsSyncSummary,
|
DeployRunJenkinsSyncSummary,
|
||||||
DeployRunSummary,
|
DeployRunSummary,
|
||||||
} from './deploy-run.types';
|
} from './deploy-run.types';
|
||||||
@@ -237,6 +238,63 @@ export class DeployRunsService {
|
|||||||
return this.deployExecutionService.syncJenkinsRun(run, project);
|
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(
|
async syncJenkinsRuns(
|
||||||
user?: AuthenticatedUser,
|
user?: AuthenticatedUser,
|
||||||
): Promise<DeployRunJenkinsSyncSummary> {
|
): 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(
|
private async cancelJenkinsExecution(
|
||||||
run: DeployRunSummary,
|
run: DeployRunSummary,
|
||||||
project: ProjectSummary,
|
project: ProjectSummary,
|
||||||
|
|||||||
Reference in New Issue
Block a user