feat: 支持取消 Jenkins 发布执行
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
DeployNotificationEvent,
|
||||
DeployNotificationService,
|
||||
} from '../notifications/wecom/deploy-notification.service';
|
||||
import { JenkinsCancelResult } from '../integrations/jenkins/jenkins.types';
|
||||
import { JenkinsClient } from '../integrations/jenkins/jenkins.client';
|
||||
import {
|
||||
ProjectEnvironmentSummary,
|
||||
ProjectSummary,
|
||||
@@ -27,6 +29,7 @@ export class DeployRunsService {
|
||||
private readonly auditService: AuditService,
|
||||
private readonly deployNotificationService: DeployNotificationService,
|
||||
private readonly deployExecutionService: DeployExecutionService,
|
||||
private readonly jenkinsClient: JenkinsClient,
|
||||
) {}
|
||||
|
||||
async listRuns(): Promise<DeployRunSummary[]> {
|
||||
@@ -81,25 +84,44 @@ export class DeployRunsService {
|
||||
}
|
||||
|
||||
async cancelRun(id: string): Promise<DeployRunSummary> {
|
||||
const run = await this.deployRunRepository.updateStatus(id, 'canceled');
|
||||
const source = await this.getRun(id);
|
||||
|
||||
if (!run) {
|
||||
if (['success', 'failed', 'canceled'].includes(source.status)) {
|
||||
throw new AppError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Deploy run ${id} not found`,
|
||||
404,
|
||||
'VALIDATION_FAILED',
|
||||
`Deploy run ${id} is already ${source.status} and cannot be canceled`,
|
||||
400,
|
||||
{
|
||||
id,
|
||||
status: source.status,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const project = await this.projectsService.getProject(source.projectKey);
|
||||
const jenkinsCancel = await this.cancelJenkinsExecution(source, project);
|
||||
let run = this.requireRun(
|
||||
id,
|
||||
await this.deployRunRepository.updateRun(id, {
|
||||
status: 'canceled',
|
||||
}),
|
||||
);
|
||||
run = this.requireRun(
|
||||
id,
|
||||
await this.deployRunRepository.updateStep(id, 'jenkins-build', {
|
||||
status: 'canceled',
|
||||
message: undefined,
|
||||
errorSummary: this.cancelSummary(jenkinsCancel),
|
||||
}),
|
||||
);
|
||||
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_CANCELED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
after: {
|
||||
status: run.status,
|
||||
jenkinsCancel,
|
||||
},
|
||||
});
|
||||
await this.recordNotificationResult(run, 'canceled');
|
||||
@@ -194,6 +216,71 @@ export class DeployRunsService {
|
||||
);
|
||||
}
|
||||
|
||||
private async cancelJenkinsExecution(
|
||||
run: DeployRunSummary,
|
||||
project: ProjectSummary,
|
||||
): Promise<JenkinsCancelResult | { requested: false; reason: string }> {
|
||||
const environment = this.findEnvironment(project, run);
|
||||
const jenkinsHealth = this.jenkinsClient.healthSummary();
|
||||
|
||||
if (jenkinsHealth.status === 'not_configured') {
|
||||
return {
|
||||
requested: false,
|
||||
reason: jenkinsHealth.message ?? 'Jenkins is not configured',
|
||||
};
|
||||
}
|
||||
|
||||
if (run.jenkinsBuildNumber) {
|
||||
return this.jenkinsClient.stopBuild(
|
||||
environment.jenkinsJobPath,
|
||||
run.jenkinsBuildNumber,
|
||||
);
|
||||
}
|
||||
|
||||
if (run.jenkinsQueueId) {
|
||||
return this.jenkinsClient.cancelQueueItem(run.jenkinsQueueId);
|
||||
}
|
||||
|
||||
return {
|
||||
requested: false,
|
||||
reason: 'Deploy run has no Jenkins queue id or build number',
|
||||
};
|
||||
}
|
||||
|
||||
private findEnvironment(
|
||||
project: ProjectSummary,
|
||||
run: DeployRunSummary,
|
||||
): ProjectEnvironmentSummary {
|
||||
const environment = project.environments.find(
|
||||
(item) => item.name === run.environment,
|
||||
);
|
||||
|
||||
if (!environment) {
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
`Project ${project.key} does not expose ${run.environment} environment`,
|
||||
400,
|
||||
{ projectKey: project.key, environment: run.environment },
|
||||
);
|
||||
}
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
private cancelSummary(
|
||||
result: JenkinsCancelResult | { requested: false; reason: string },
|
||||
): string {
|
||||
if (!result.requested) {
|
||||
return `Cancellation recorded locally: ${result.reason}`;
|
||||
}
|
||||
|
||||
if (result.target === 'build') {
|
||||
return `Jenkins build #${result.buildNumber ?? '-'} stop requested.`;
|
||||
}
|
||||
|
||||
return `Jenkins queue ${result.queueId ?? '-'} cancel requested.`;
|
||||
}
|
||||
|
||||
private async validateReleaseRequest(
|
||||
input: CreateDeployRunInput,
|
||||
project: ProjectSummary,
|
||||
@@ -267,4 +354,22 @@ export class DeployRunsService {
|
||||
|
||||
return redactSensitive({ message }).message;
|
||||
}
|
||||
|
||||
private requireRun(
|
||||
id: string,
|
||||
run: DeployRunSummary | null,
|
||||
): DeployRunSummary {
|
||||
if (!run) {
|
||||
throw new AppError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Deploy run ${id} not found`,
|
||||
404,
|
||||
{
|
||||
id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return run;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user