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
+341
View File
@@ -0,0 +1,341 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvConfig } from '../config/env.schema';
import { PrismaService } from '../prisma/prisma.service';
import { seedDeployRuns } from './deploy-run.seed';
import {
CreateDeployRunInput,
DeployRunStepStatus,
DeployRunSummary,
DeployRunStatus,
} from './deploy-run.types';
type DeployRunMemoryPatch = {
status?: DeployRunStatus;
jenkinsQueueId?: string;
jenkinsBuildNumber?: number;
jenkinsBuildUrl?: string;
failureSummary?: string;
startedAt?: string;
finishedAt?: string;
};
type DeployRunStepMemoryPatch = {
status?: DeployRunStepStatus;
startedAt?: string;
finishedAt?: string;
message?: string;
errorSummary?: string;
logExcerpt?: string;
};
const terminalRunStatuses = new Set<DeployRunStatus>([
'success',
'failed',
'canceled',
]);
const terminalStepStatuses = new Set<DeployRunStepStatus>([
'success',
'failed',
'canceled',
'skipped',
]);
@Injectable()
export class DeployRunRepository {
private readonly memoryRuns = [...seedDeployRuns];
constructor(
private readonly config: ConfigService<EnvConfig, true>,
private readonly prisma: PrismaService,
) {}
async findMany(): Promise<DeployRunSummary[]> {
if (!this.useDatabaseReads()) {
return [...this.memoryRuns].sort((left, right) =>
right.createdAt.localeCompare(left.createdAt),
);
}
const runs = await this.prisma.deployRun.findMany({
include: {
project: true,
environment: true,
steps: {
orderBy: {
order: 'asc',
},
},
},
orderBy: {
createdAt: 'desc',
},
take: 50,
});
return runs.map((run) => ({
id: run.id,
projectKey: run.project.key,
environment: run.environment.name === 'TEST' ? 'test' : 'production',
ref: run.ref,
status: run.status.toLowerCase() as DeployRunStatus,
trigger: run.trigger.toLowerCase() as DeployRunSummary['trigger'],
operator: run.operator,
jenkinsQueueId: run.jenkinsQueueId ?? undefined,
jenkinsBuildNumber: run.jenkinsBuildNumber ?? undefined,
failureSummary: run.failureSummary ?? undefined,
startedAt: run.startedAt?.toISOString(),
finishedAt: run.finishedAt?.toISOString(),
createdAt: run.createdAt.toISOString(),
steps: run.steps.map((step) => ({
id: step.id,
bpmnNodeId: step.bpmnNodeId,
name: step.name,
status: step.status.toLowerCase() as DeployRunStepStatus,
order: step.order,
startedAt: step.startedAt?.toISOString(),
finishedAt: step.finishedAt?.toISOString(),
errorSummary: step.errorSummary ?? undefined,
})),
}));
}
async findById(id: string): Promise<DeployRunSummary | null> {
if (!this.useDatabaseReads()) {
return this.memoryRuns.find((run) => run.id === id) ?? null;
}
const run = await this.prisma.deployRun.findUnique({
where: { id },
include: {
project: true,
environment: true,
steps: {
orderBy: {
order: 'asc',
},
},
},
});
if (!run) {
return null;
}
return {
id: run.id,
projectKey: run.project.key,
environment: run.environment.name === 'TEST' ? 'test' : 'production',
ref: run.ref,
status: run.status.toLowerCase() as DeployRunStatus,
trigger: run.trigger.toLowerCase() as DeployRunSummary['trigger'],
operator: run.operator,
jenkinsQueueId: run.jenkinsQueueId ?? undefined,
jenkinsBuildNumber: run.jenkinsBuildNumber ?? undefined,
failureSummary: run.failureSummary ?? undefined,
startedAt: run.startedAt?.toISOString(),
finishedAt: run.finishedAt?.toISOString(),
createdAt: run.createdAt.toISOString(),
steps: run.steps.map((step) => ({
id: step.id,
bpmnNodeId: step.bpmnNodeId,
name: step.name,
status: step.status.toLowerCase() as DeployRunStepStatus,
order: step.order,
startedAt: step.startedAt?.toISOString(),
finishedAt: step.finishedAt?.toISOString(),
errorSummary: step.errorSummary ?? undefined,
})),
};
}
create(input: CreateDeployRunInput): DeployRunSummary {
const idempotencyKey =
input.idempotencyKey ??
[
input.projectKey,
input.environment,
input.ref,
input.operator,
new Date().toISOString(),
].join(':');
const existing = this.memoryRuns.find((run) => run.id === idempotencyKey);
if (existing) {
return existing;
}
const now = new Date().toISOString();
const run: DeployRunSummary = {
id: idempotencyKey,
projectKey: input.projectKey,
environment: input.environment,
ref: input.ref,
status: 'pending',
trigger: input.trigger ?? 'manual',
operator: input.operator,
createdAt: now,
steps: [
{
id: `${idempotencyKey}:start-release`,
bpmnNodeId: 'start-release',
name: '提交发布单',
status: 'success',
order: 10,
},
{
id: `${idempotencyKey}:gitea-check`,
bpmnNodeId: 'gitea-check',
name: '校验 Gitea ref',
status: 'pending',
order: 20,
},
{
id: `${idempotencyKey}:jenkins-build`,
bpmnNodeId: 'jenkins-build',
name: '触发 Jenkins 构建',
status: 'pending',
order: 30,
},
{
id: `${idempotencyKey}:health-check`,
bpmnNodeId: 'health-check',
name: '环境健康检查',
status: 'pending',
order: 40,
},
{
id: `${idempotencyKey}:notify-wecom`,
bpmnNodeId: 'notify-wecom',
name: '发送通知',
status: 'pending',
order: 90,
},
{
id: `${idempotencyKey}:finish-release`,
bpmnNodeId: 'finish-release',
name: '归档审计',
status: 'pending',
order: 100,
},
],
};
this.memoryRuns.unshift(run);
return run;
}
updateMemoryStatus(
id: string,
status: DeployRunStatus,
): DeployRunSummary | null {
return this.updateMemoryRun(id, { status });
}
updateMemoryRun(
id: string,
patch: DeployRunMemoryPatch,
): DeployRunSummary | null {
const run = this.memoryRuns.find((item) => item.id === id);
if (!run) {
return null;
}
const now = new Date().toISOString();
if (patch.status) {
run.status = patch.status;
if (patch.status !== 'pending' && !run.startedAt) {
run.startedAt = patch.startedAt ?? now;
}
if (terminalRunStatuses.has(patch.status)) {
run.finishedAt = patch.finishedAt ?? now;
}
}
if ('jenkinsQueueId' in patch) {
run.jenkinsQueueId = patch.jenkinsQueueId;
}
if ('jenkinsBuildNumber' in patch) {
run.jenkinsBuildNumber = patch.jenkinsBuildNumber;
}
if ('jenkinsBuildUrl' in patch) {
run.jenkinsBuildUrl = patch.jenkinsBuildUrl;
}
if ('failureSummary' in patch) {
run.failureSummary = patch.failureSummary;
}
if ('startedAt' in patch) {
run.startedAt = patch.startedAt;
}
if ('finishedAt' in patch) {
run.finishedAt = patch.finishedAt;
}
return run;
}
updateMemoryStep(
runId: string,
bpmnNodeId: string,
patch: DeployRunStepMemoryPatch,
): DeployRunSummary | null {
const run = this.memoryRuns.find((item) => item.id === runId);
const step = run?.steps.find((item) => item.bpmnNodeId === bpmnNodeId);
if (!run || !step) {
return null;
}
const now = new Date().toISOString();
if (patch.status) {
step.status = patch.status;
if (patch.status !== 'pending' && !step.startedAt) {
step.startedAt = patch.startedAt ?? now;
}
if (terminalStepStatuses.has(patch.status)) {
step.finishedAt = patch.finishedAt ?? now;
}
}
if ('startedAt' in patch) {
step.startedAt = patch.startedAt;
}
if ('finishedAt' in patch) {
step.finishedAt = patch.finishedAt;
}
if ('message' in patch) {
step.message = patch.message;
}
if ('errorSummary' in patch) {
step.errorSummary = patch.errorSummary;
}
if ('logExcerpt' in patch) {
step.logExcerpt = patch.logExcerpt;
}
return run;
}
private useDatabaseReads(): boolean {
return (
this.config.get('USE_DATABASE_READS', { infer: true }) &&
Boolean(this.config.get('DATABASE_URL', { infer: true }))
);
}
}