feat: 初始化DevOps平台后端

This commit is contained in:
湛兮
2026-06-11 20:49:59 +08:00
commit caec5a618d
88 changed files with 11893 additions and 0 deletions
+198
View File
@@ -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);
}
}