feat: 初始化DevOps平台后端
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AppError } from '../common/errors/app-error';
|
||||
import { redactSensitive } from '../common/security/redact-sensitive';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import {
|
||||
DeployNotificationEvent,
|
||||
DeployNotificationService,
|
||||
} from '../notifications/wecom/deploy-notification.service';
|
||||
import { ProjectsService } from '../projects/projects.service';
|
||||
import { DeployExecutionService } from './deploy-execution.service';
|
||||
import { DeployRunRepository } from './deploy-run.repository';
|
||||
import {
|
||||
CreateDeployRunInput,
|
||||
DeployRunJenkinsSyncSummary,
|
||||
DeployRunSummary,
|
||||
} from './deploy-run.types';
|
||||
|
||||
@Injectable()
|
||||
export class DeployRunsService {
|
||||
constructor(
|
||||
private readonly deployRunRepository: DeployRunRepository,
|
||||
private readonly projectsService: ProjectsService,
|
||||
private readonly auditService: AuditService,
|
||||
private readonly deployNotificationService: DeployNotificationService,
|
||||
private readonly deployExecutionService: DeployExecutionService,
|
||||
) {}
|
||||
|
||||
async listRuns(): Promise<DeployRunSummary[]> {
|
||||
return this.deployRunRepository.findMany();
|
||||
}
|
||||
|
||||
async getRun(id: string): Promise<DeployRunSummary> {
|
||||
const run = await this.deployRunRepository.findById(id);
|
||||
|
||||
if (!run) {
|
||||
throw new AppError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Deploy run ${id} not found`,
|
||||
404,
|
||||
{
|
||||
id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return run;
|
||||
}
|
||||
|
||||
async createRun(input: CreateDeployRunInput): Promise<DeployRunSummary> {
|
||||
if (!['test', 'production'].includes(input.environment)) {
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
'environment must be test or production',
|
||||
400,
|
||||
{ environment: input.environment },
|
||||
);
|
||||
}
|
||||
|
||||
const project = await this.projectsService.getProject(input.projectKey);
|
||||
const run = this.deployRunRepository.create(input);
|
||||
|
||||
this.auditService.record({
|
||||
action: 'DEPLOY_REQUESTED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
actorName: input.operator,
|
||||
after: {
|
||||
projectKey: input.projectKey,
|
||||
environment: input.environment,
|
||||
ref: input.ref,
|
||||
remark: input.remark,
|
||||
},
|
||||
});
|
||||
|
||||
return this.deployExecutionService.execute(run, project);
|
||||
}
|
||||
|
||||
async cancelRun(id: string): Promise<DeployRunSummary> {
|
||||
const run = this.deployRunRepository.updateMemoryStatus(id, 'canceled');
|
||||
|
||||
if (!run) {
|
||||
throw new AppError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Deploy run ${id} not found`,
|
||||
404,
|
||||
{
|
||||
id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.auditService.record({
|
||||
action: 'DEPLOY_CANCELED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
after: {
|
||||
status: run.status,
|
||||
},
|
||||
});
|
||||
await this.recordNotificationResult(run, 'canceled');
|
||||
|
||||
return run;
|
||||
}
|
||||
|
||||
async retryRun(id: string): Promise<DeployRunSummary> {
|
||||
const source = await this.getRun(id);
|
||||
return this.createRun({
|
||||
projectKey: source.projectKey,
|
||||
environment: source.environment,
|
||||
ref: source.ref,
|
||||
operator: source.operator,
|
||||
remark: `Retry of ${source.id}`,
|
||||
idempotencyKey: `${source.id}:retry:${new Date().toISOString()}`,
|
||||
trigger: 'retry',
|
||||
});
|
||||
}
|
||||
|
||||
async syncJenkinsRun(id: string): Promise<DeployRunSummary> {
|
||||
const run = await this.getRun(id);
|
||||
const project = await this.projectsService.getProject(run.projectKey);
|
||||
|
||||
return this.deployExecutionService.syncJenkinsRun(run, project);
|
||||
}
|
||||
|
||||
async syncJenkinsRuns(): Promise<DeployRunJenkinsSyncSummary> {
|
||||
const runs = await this.listRuns();
|
||||
const candidates = runs.filter((run) => this.isJenkinsSyncCandidate(run));
|
||||
const syncedRuns: DeployRunSummary[] = [];
|
||||
const failedRuns: DeployRunJenkinsSyncSummary['failedRuns'] = [];
|
||||
|
||||
for (const run of candidates) {
|
||||
try {
|
||||
const project = await this.projectsService.getProject(run.projectKey);
|
||||
const syncedRun = await this.deployExecutionService.syncJenkinsRun(
|
||||
run,
|
||||
project,
|
||||
);
|
||||
syncedRuns.push(syncedRun);
|
||||
} catch (error) {
|
||||
failedRuns.push({
|
||||
id: run.id,
|
||||
projectKey: run.projectKey,
|
||||
message: this.toSafeSyncError(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.auditService.record({
|
||||
action: 'JENKINS_SYNC_SWEEP_COMPLETED',
|
||||
resourceType: 'deploy_run',
|
||||
after: {
|
||||
totalCandidates: candidates.length,
|
||||
syncedCount: syncedRuns.length,
|
||||
failedCount: failedRuns.length,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
checkedAt: new Date().toISOString(),
|
||||
totalCandidates: candidates.length,
|
||||
syncedRuns,
|
||||
failedRuns,
|
||||
};
|
||||
}
|
||||
|
||||
private async recordNotificationResult(
|
||||
run: DeployRunSummary,
|
||||
event: DeployNotificationEvent,
|
||||
): Promise<void> {
|
||||
const notification =
|
||||
await this.deployNotificationService.sendDeployRunEvent(run, event);
|
||||
|
||||
this.auditService.record({
|
||||
action: `WECOM_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
after: {
|
||||
event,
|
||||
channel: notification.channel,
|
||||
reason: notification.reason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private isJenkinsSyncCandidate(run: DeployRunSummary): boolean {
|
||||
return (
|
||||
['queued', 'running'].includes(run.status) &&
|
||||
Boolean(run.jenkinsQueueId || run.jenkinsBuildNumber)
|
||||
);
|
||||
}
|
||||
|
||||
private toSafeSyncError(error: unknown): string {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Jenkins sync failed';
|
||||
|
||||
return redactSensitive(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user