feat: 支持取消 Jenkins 发布执行

This commit is contained in:
湛兮
2026-06-11 23:22:17 +08:00
parent 588caa4209
commit 6ce1543a0a
4 changed files with 269 additions and 35 deletions
+110 -5
View File
@@ -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;
}
}