feat: 初始化DevOps平台后端
This commit is contained in:
@@ -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 }))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user