feat: 支持发布单 MySQL 持久化
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"lint": "eslint \"{src,prisma}/**/*.{ts,js}\"",
|
"lint": "eslint \"{src,prisma}/**/*.{ts,js}\"",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate:dev": "prisma migrate dev",
|
"prisma:migrate:dev": "prisma migrate dev",
|
||||||
|
"prisma:seed": "node dist/prisma/seed.js",
|
||||||
"prisma:studio": "prisma studio",
|
"prisma:studio": "prisma studio",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 将代码内的纳管项目清单同步到数据库,保持项目、环境和 Jenkins Job 可重复初始化。
|
||||||
|
*/
|
||||||
|
import { EnvironmentKind, PrismaClient, ProjectStatus } from '@prisma/client';
|
||||||
|
import { seedProjects } from '../src/projects/project.seed';
|
||||||
|
import type { ProjectEnvironmentSummary } from '../src/projects/project.types';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
for (const project of seedProjects) {
|
||||||
|
const projectRecord = await prisma.project.upsert({
|
||||||
|
where: {
|
||||||
|
key: project.key,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: project.id,
|
||||||
|
key: project.key,
|
||||||
|
name: project.name,
|
||||||
|
repositoryUrl: project.repositoryUrl,
|
||||||
|
defaultBranch: project.defaultBranch,
|
||||||
|
status:
|
||||||
|
project.status === 'active'
|
||||||
|
? ProjectStatus.ACTIVE
|
||||||
|
: ProjectStatus.ARCHIVED,
|
||||||
|
metadata: {
|
||||||
|
source: 'seedProjects',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: project.name,
|
||||||
|
repositoryUrl: project.repositoryUrl,
|
||||||
|
defaultBranch: project.defaultBranch,
|
||||||
|
status:
|
||||||
|
project.status === 'active'
|
||||||
|
? ProjectStatus.ACTIVE
|
||||||
|
: ProjectStatus.ARCHIVED,
|
||||||
|
metadata: {
|
||||||
|
source: 'seedProjects',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const environment of project.environments) {
|
||||||
|
await upsertEnvironment(projectRecord.id, environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertEnvironment(
|
||||||
|
projectId: string,
|
||||||
|
environment: ProjectEnvironmentSummary,
|
||||||
|
): Promise<void> {
|
||||||
|
const environmentName = toEnvironmentKind(environment.name);
|
||||||
|
const environmentRecord = await prisma.environment.upsert({
|
||||||
|
where: {
|
||||||
|
projectId_name: {
|
||||||
|
projectId,
|
||||||
|
name: environmentName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
projectId,
|
||||||
|
name: environmentName,
|
||||||
|
displayName: environment.displayName,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
displayName: environment.displayName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.deployJob.upsert({
|
||||||
|
where: {
|
||||||
|
projectId_environmentId: {
|
||||||
|
projectId,
|
||||||
|
environmentId: environmentRecord.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
projectId,
|
||||||
|
environmentId: environmentRecord.id,
|
||||||
|
jenkinsJobPath: environment.jenkinsJobPath,
|
||||||
|
jenkinsParameters: {
|
||||||
|
releasePolicy: environment.releasePolicy,
|
||||||
|
refPattern: environment.refPattern,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
jenkinsJobPath: environment.jenkinsJobPath,
|
||||||
|
jenkinsParameters: {
|
||||||
|
releasePolicy: environment.releasePolicy,
|
||||||
|
refPattern: environment.refPattern,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEnvironmentKind(
|
||||||
|
environment: ProjectEnvironmentSummary['name'],
|
||||||
|
): EnvironmentKind {
|
||||||
|
return environment === 'test'
|
||||||
|
? EnvironmentKind.TEST
|
||||||
|
: EnvironmentKind.PRODUCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch(async (error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -34,7 +34,7 @@ export class DeployExecutionService {
|
|||||||
|
|
||||||
let currentRun = this.requireRun(
|
let currentRun = this.requireRun(
|
||||||
run.id,
|
run.id,
|
||||||
this.deployRunRepository.updateMemoryRun(run.id, { status: 'running' }),
|
await this.deployRunRepository.updateRun(run.id, { status: 'running' }),
|
||||||
);
|
);
|
||||||
let currentStep: string | undefined;
|
let currentStep: string | undefined;
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export class DeployExecutionService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
currentStep = 'gitea-check';
|
currentStep = 'gitea-check';
|
||||||
currentRun = this.completeStep(currentRun.id, currentStep, 'success');
|
currentRun = await this.completeStep(currentRun.id, currentStep, 'success');
|
||||||
|
|
||||||
currentStep = 'jenkins-build';
|
currentStep = 'jenkins-build';
|
||||||
const jenkinsResult = await this.advanceJenkinsBuild(currentRun, project);
|
const jenkinsResult = await this.advanceJenkinsBuild(currentRun, project);
|
||||||
@@ -65,16 +65,16 @@ export class DeployExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentStep = 'health-check';
|
currentStep = 'health-check';
|
||||||
currentRun = this.completeStep(currentRun.id, currentStep, 'success');
|
currentRun = await this.completeStep(currentRun.id, currentStep, 'success');
|
||||||
|
|
||||||
currentStep = 'notify-wecom';
|
currentStep = 'notify-wecom';
|
||||||
currentRun = await this.advanceNotification(currentRun, 'succeeded');
|
currentRun = await this.advanceNotification(currentRun, 'succeeded');
|
||||||
|
|
||||||
currentStep = 'finish-release';
|
currentStep = 'finish-release';
|
||||||
currentRun = this.completeStep(currentRun.id, currentStep, 'success');
|
currentRun = await this.completeStep(currentRun.id, currentStep, 'success');
|
||||||
currentRun = this.requireRun(
|
currentRun = this.requireRun(
|
||||||
currentRun.id,
|
currentRun.id,
|
||||||
this.deployRunRepository.updateMemoryRun(currentRun.id, {
|
await this.deployRunRepository.updateRun(currentRun.id, {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -97,21 +97,21 @@ export class DeployExecutionService {
|
|||||||
|
|
||||||
if (currentStep) {
|
if (currentStep) {
|
||||||
currentRun =
|
currentRun =
|
||||||
this.deployRunRepository.updateMemoryStep(
|
(await this.deployRunRepository.updateStep(
|
||||||
currentRun.id,
|
currentRun.id,
|
||||||
currentStep,
|
currentStep,
|
||||||
{
|
{
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errorSummary: failureSummary,
|
errorSummary: failureSummary,
|
||||||
},
|
},
|
||||||
) ?? currentRun;
|
)) ?? currentRun;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentRun =
|
currentRun =
|
||||||
this.deployRunRepository.updateMemoryRun(currentRun.id, {
|
(await this.deployRunRepository.updateRun(currentRun.id, {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
failureSummary,
|
failureSummary,
|
||||||
}) ?? currentRun;
|
})) ?? currentRun;
|
||||||
|
|
||||||
currentRun = await this.advanceNotification(currentRun, 'failed');
|
currentRun = await this.advanceNotification(currentRun, 'failed');
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export class DeployExecutionService {
|
|||||||
if (!queueItem.buildNumber) {
|
if (!queueItem.buildNumber) {
|
||||||
return this.requireRun(
|
return this.requireRun(
|
||||||
currentRun.id,
|
currentRun.id,
|
||||||
this.deployRunRepository.updateMemoryRun(currentRun.id, {
|
await this.deployRunRepository.updateRun(currentRun.id, {
|
||||||
status: 'queued',
|
status: 'queued',
|
||||||
jenkinsQueueId: queueItem.queueId,
|
jenkinsQueueId: queueItem.queueId,
|
||||||
jenkinsBuildUrl: queueItem.executableUrl,
|
jenkinsBuildUrl: queueItem.executableUrl,
|
||||||
@@ -166,14 +166,14 @@ export class DeployExecutionService {
|
|||||||
buildNumber = queueItem.buildNumber;
|
buildNumber = queueItem.buildNumber;
|
||||||
currentRun = this.requireRun(
|
currentRun = this.requireRun(
|
||||||
currentRun.id,
|
currentRun.id,
|
||||||
this.deployRunRepository.updateMemoryRun(currentRun.id, {
|
await this.deployRunRepository.updateRun(currentRun.id, {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
jenkinsQueueId: queueItem.queueId,
|
jenkinsQueueId: queueItem.queueId,
|
||||||
jenkinsBuildNumber: queueItem.buildNumber,
|
jenkinsBuildNumber: queueItem.buildNumber,
|
||||||
jenkinsBuildUrl: queueItem.executableUrl,
|
jenkinsBuildUrl: queueItem.executableUrl,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
currentRun = this.markStep(
|
currentRun = await this.markStep(
|
||||||
currentRun.id,
|
currentRun.id,
|
||||||
'jenkins-build',
|
'jenkins-build',
|
||||||
'running',
|
'running',
|
||||||
@@ -204,7 +204,7 @@ export class DeployExecutionService {
|
|||||||
|
|
||||||
currentRun = this.requireRun(
|
currentRun = this.requireRun(
|
||||||
currentRun.id,
|
currentRun.id,
|
||||||
this.deployRunRepository.updateMemoryRun(currentRun.id, {
|
await this.deployRunRepository.updateRun(currentRun.id, {
|
||||||
status: buildStatus.building || !buildStatus.result ? 'running' : currentRun.status,
|
status: buildStatus.building || !buildStatus.result ? 'running' : currentRun.status,
|
||||||
jenkinsBuildNumber: buildStatus.buildNumber,
|
jenkinsBuildNumber: buildStatus.buildNumber,
|
||||||
jenkinsBuildUrl: buildStatus.url,
|
jenkinsBuildUrl: buildStatus.url,
|
||||||
@@ -242,7 +242,7 @@ export class DeployExecutionService {
|
|||||||
const reason =
|
const reason =
|
||||||
health.message ??
|
health.message ??
|
||||||
'Jenkins is not configured; simulated build completed.';
|
'Jenkins is not configured; simulated build completed.';
|
||||||
const currentRun = this.completeStep(run.id, 'jenkins-build', 'success');
|
const currentRun = await this.completeStep(run.id, 'jenkins-build', 'success');
|
||||||
|
|
||||||
this.auditService.record({
|
this.auditService.record({
|
||||||
action: 'JENKINS_DRY_RUN_COMPLETED',
|
action: 'JENKINS_DRY_RUN_COMPLETED',
|
||||||
@@ -268,7 +268,7 @@ export class DeployExecutionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.markStep(run.id, 'jenkins-build', 'running');
|
await this.markStep(run.id, 'jenkins-build', 'running');
|
||||||
const queueItem = await this.jenkinsClient.buildWithParameters(
|
const queueItem = await this.jenkinsClient.buildWithParameters(
|
||||||
environment.jenkinsJobPath,
|
environment.jenkinsJobPath,
|
||||||
this.buildParameters(run),
|
this.buildParameters(run),
|
||||||
@@ -276,14 +276,14 @@ export class DeployExecutionService {
|
|||||||
const nextStatus = queueItem.buildNumber ? 'running' : 'queued';
|
const nextStatus = queueItem.buildNumber ? 'running' : 'queued';
|
||||||
let currentRun = this.requireRun(
|
let currentRun = this.requireRun(
|
||||||
run.id,
|
run.id,
|
||||||
this.deployRunRepository.updateMemoryRun(run.id, {
|
await this.deployRunRepository.updateRun(run.id, {
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
jenkinsQueueId: queueItem.queueId,
|
jenkinsQueueId: queueItem.queueId,
|
||||||
jenkinsBuildNumber: queueItem.buildNumber,
|
jenkinsBuildNumber: queueItem.buildNumber,
|
||||||
jenkinsBuildUrl: queueItem.executableUrl,
|
jenkinsBuildUrl: queueItem.executableUrl,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
currentRun = this.markStep(
|
currentRun = await this.markStep(
|
||||||
run.id,
|
run.id,
|
||||||
'jenkins-build',
|
'jenkins-build',
|
||||||
nextStatus,
|
nextStatus,
|
||||||
@@ -312,7 +312,7 @@ export class DeployExecutionService {
|
|||||||
run: DeployRunSummary,
|
run: DeployRunSummary,
|
||||||
event: DeployNotificationEvent,
|
event: DeployNotificationEvent,
|
||||||
): Promise<DeployRunSummary> {
|
): Promise<DeployRunSummary> {
|
||||||
this.markStep(run.id, 'notify-wecom', 'running');
|
await this.markStep(run.id, 'notify-wecom', 'running');
|
||||||
const notification =
|
const notification =
|
||||||
await this.deployNotificationService.sendDeployRunEvent(run, event);
|
await this.deployNotificationService.sendDeployRunEvent(run, event);
|
||||||
const stepStatus =
|
const stepStatus =
|
||||||
@@ -321,7 +321,7 @@ export class DeployExecutionService {
|
|||||||
: notification.status === 'failed'
|
: notification.status === 'failed'
|
||||||
? 'failed'
|
? 'failed'
|
||||||
: 'skipped';
|
: 'skipped';
|
||||||
const currentRun = this.completeStep(
|
const currentRun = await this.completeStep(
|
||||||
run.id,
|
run.id,
|
||||||
'notify-wecom',
|
'notify-wecom',
|
||||||
stepStatus,
|
stepStatus,
|
||||||
@@ -346,13 +346,13 @@ export class DeployExecutionService {
|
|||||||
private async completeSuccessfulJenkinsRun(
|
private async completeSuccessfulJenkinsRun(
|
||||||
run: DeployRunSummary,
|
run: DeployRunSummary,
|
||||||
): Promise<DeployRunSummary> {
|
): Promise<DeployRunSummary> {
|
||||||
let currentRun = this.completeStep(run.id, 'jenkins-build', 'success');
|
let currentRun = await this.completeStep(run.id, 'jenkins-build', 'success');
|
||||||
currentRun = this.completeStep(currentRun.id, 'health-check', 'success');
|
currentRun = await this.completeStep(currentRun.id, 'health-check', 'success');
|
||||||
currentRun = await this.advanceNotification(currentRun, 'succeeded');
|
currentRun = await this.advanceNotification(currentRun, 'succeeded');
|
||||||
currentRun = this.completeStep(currentRun.id, 'finish-release', 'success');
|
currentRun = await this.completeStep(currentRun.id, 'finish-release', 'success');
|
||||||
currentRun = this.requireRun(
|
currentRun = this.requireRun(
|
||||||
currentRun.id,
|
currentRun.id,
|
||||||
this.deployRunRepository.updateMemoryRun(currentRun.id, {
|
await this.deployRunRepository.updateRun(currentRun.id, {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -380,16 +380,16 @@ export class DeployExecutionService {
|
|||||||
const logExcerpt = await this.readFailureLogExcerpt(run, jobPath);
|
const logExcerpt = await this.readFailureLogExcerpt(run, jobPath);
|
||||||
const failureSummary = `Jenkins build #${run.jenkinsBuildNumber ?? '-'} ${result}`;
|
const failureSummary = `Jenkins build #${run.jenkinsBuildNumber ?? '-'} ${result}`;
|
||||||
let currentRun =
|
let currentRun =
|
||||||
this.deployRunRepository.updateMemoryStep(run.id, 'jenkins-build', {
|
(await this.deployRunRepository.updateStep(run.id, 'jenkins-build', {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errorSummary: failureSummary,
|
errorSummary: failureSummary,
|
||||||
logExcerpt,
|
logExcerpt,
|
||||||
}) ?? run;
|
})) ?? run;
|
||||||
currentRun =
|
currentRun =
|
||||||
this.deployRunRepository.updateMemoryRun(currentRun.id, {
|
(await this.deployRunRepository.updateRun(currentRun.id, {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
failureSummary,
|
failureSummary,
|
||||||
}) ?? currentRun;
|
})) ?? currentRun;
|
||||||
currentRun = await this.advanceNotification(currentRun, 'failed');
|
currentRun = await this.advanceNotification(currentRun, 'failed');
|
||||||
|
|
||||||
this.auditService.record({
|
this.auditService.record({
|
||||||
@@ -463,30 +463,30 @@ export class DeployExecutionService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private completeStep(
|
private async completeStep(
|
||||||
runId: string,
|
runId: string,
|
||||||
bpmnNodeId: string,
|
bpmnNodeId: string,
|
||||||
status: Extract<DeployRunStepStatus, 'success' | 'skipped' | 'failed'>,
|
status: Extract<DeployRunStepStatus, 'success' | 'skipped' | 'failed'>,
|
||||||
errorSummary?: string,
|
errorSummary?: string,
|
||||||
): DeployRunSummary {
|
): Promise<DeployRunSummary> {
|
||||||
return this.requireRun(
|
return this.requireRun(
|
||||||
runId,
|
runId,
|
||||||
this.deployRunRepository.updateMemoryStep(runId, bpmnNodeId, {
|
await this.deployRunRepository.updateStep(runId, bpmnNodeId, {
|
||||||
status,
|
status,
|
||||||
errorSummary,
|
errorSummary,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private markStep(
|
private async markStep(
|
||||||
runId: string,
|
runId: string,
|
||||||
bpmnNodeId: string,
|
bpmnNodeId: string,
|
||||||
status: Extract<DeployRunStepStatus, 'queued' | 'running'>,
|
status: Extract<DeployRunStepStatus, 'queued' | 'running'>,
|
||||||
message?: string,
|
message?: string,
|
||||||
): DeployRunSummary {
|
): Promise<DeployRunSummary> {
|
||||||
return this.requireRun(
|
return this.requireRun(
|
||||||
runId,
|
runId,
|
||||||
this.deployRunRepository.updateMemoryStep(runId, bpmnNodeId, {
|
await this.deployRunRepository.updateStep(runId, bpmnNodeId, {
|
||||||
status,
|
status,
|
||||||
message,
|
message,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* 发布单仓储同时支持本地内存模式和 MySQL/Prisma 持久化模式。
|
||||||
|
* 业务层只关心 run/step 状态流转,是否落库由环境配置统一控制。
|
||||||
|
*/
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import {
|
||||||
|
DeployRunStatus as PrismaDeployRunStatus,
|
||||||
|
DeployRunTrigger as PrismaDeployRunTrigger,
|
||||||
|
EnvironmentKind,
|
||||||
|
Prisma,
|
||||||
|
RunStepStatus as PrismaRunStepStatus,
|
||||||
|
} from '@prisma/client';
|
||||||
import { EnvConfig } from '../config/env.schema';
|
import { EnvConfig } from '../config/env.schema';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { seedDeployRuns } from './deploy-run.seed';
|
import { seedDeployRuns } from './deploy-run.seed';
|
||||||
@@ -29,6 +40,24 @@ type DeployRunStepMemoryPatch = {
|
|||||||
logExcerpt?: string;
|
logExcerpt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DeployRunRecord = Prisma.DeployRunGetPayload<{
|
||||||
|
include: {
|
||||||
|
project: true;
|
||||||
|
environment: true;
|
||||||
|
steps: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type DeployRunMetadata = Prisma.InputJsonObject & {
|
||||||
|
jenkinsBuildUrl?: string;
|
||||||
|
remark?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RunStepMetadata = Prisma.InputJsonObject & {
|
||||||
|
message?: string;
|
||||||
|
logExcerpt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const terminalRunStatuses = new Set<DeployRunStatus>([
|
const terminalRunStatuses = new Set<DeployRunStatus>([
|
||||||
'success',
|
'success',
|
||||||
'failed',
|
'failed',
|
||||||
@@ -40,6 +69,31 @@ const terminalStepStatuses = new Set<DeployRunStepStatus>([
|
|||||||
'canceled',
|
'canceled',
|
||||||
'skipped',
|
'skipped',
|
||||||
]);
|
]);
|
||||||
|
const runStatusToPrisma: Record<DeployRunStatus, PrismaDeployRunStatus> = {
|
||||||
|
pending: PrismaDeployRunStatus.PENDING,
|
||||||
|
queued: PrismaDeployRunStatus.QUEUED,
|
||||||
|
running: PrismaDeployRunStatus.RUNNING,
|
||||||
|
success: PrismaDeployRunStatus.SUCCESS,
|
||||||
|
failed: PrismaDeployRunStatus.FAILED,
|
||||||
|
canceled: PrismaDeployRunStatus.CANCELED,
|
||||||
|
};
|
||||||
|
const stepStatusToPrisma: Record<DeployRunStepStatus, PrismaRunStepStatus> = {
|
||||||
|
pending: PrismaRunStepStatus.PENDING,
|
||||||
|
queued: PrismaRunStepStatus.RUNNING,
|
||||||
|
running: PrismaRunStepStatus.RUNNING,
|
||||||
|
success: PrismaRunStepStatus.SUCCESS,
|
||||||
|
failed: PrismaRunStepStatus.FAILED,
|
||||||
|
canceled: PrismaRunStepStatus.CANCELED,
|
||||||
|
skipped: PrismaRunStepStatus.SKIPPED,
|
||||||
|
};
|
||||||
|
const triggerToPrisma: Record<
|
||||||
|
DeployRunSummary['trigger'],
|
||||||
|
PrismaDeployRunTrigger
|
||||||
|
> = {
|
||||||
|
manual: PrismaDeployRunTrigger.MANUAL,
|
||||||
|
gitea_webhook: PrismaDeployRunTrigger.GITEA_WEBHOOK,
|
||||||
|
retry: PrismaDeployRunTrigger.RETRY,
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeployRunRepository {
|
export class DeployRunRepository {
|
||||||
@@ -51,7 +105,7 @@ export class DeployRunRepository {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findMany(): Promise<DeployRunSummary[]> {
|
async findMany(): Promise<DeployRunSummary[]> {
|
||||||
if (!this.useDatabaseReads()) {
|
if (!this.useDatabase()) {
|
||||||
return [...this.memoryRuns].sort((left, right) =>
|
return [...this.memoryRuns].sort((left, right) =>
|
||||||
right.createdAt.localeCompare(left.createdAt),
|
right.createdAt.localeCompare(left.createdAt),
|
||||||
);
|
);
|
||||||
@@ -73,40 +127,28 @@ export class DeployRunRepository {
|
|||||||
take: 50,
|
take: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
return runs.map((run) => ({
|
return runs.map((run) => this.toSummary(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> {
|
async findById(id: string): Promise<DeployRunSummary | null> {
|
||||||
if (!this.useDatabaseReads()) {
|
if (!this.useDatabase()) {
|
||||||
return this.memoryRuns.find((run) => run.id === id) ?? null;
|
return this.memoryRuns.find((run) => run.id === id) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = await this.prisma.deployRun.findUnique({
|
const run = await this.findRecordById(id);
|
||||||
where: { id },
|
|
||||||
|
return run ? this.toSummary(run) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(input: CreateDeployRunInput): Promise<DeployRunSummary> {
|
||||||
|
const idempotencyKey = this.resolveIdempotencyKey(input);
|
||||||
|
|
||||||
|
if (!this.useDatabase()) {
|
||||||
|
return this.createMemoryRun(input, idempotencyKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await this.prisma.deployRun.findUnique({
|
||||||
|
where: { idempotencyKey },
|
||||||
include: {
|
include: {
|
||||||
project: true,
|
project: true,
|
||||||
environment: true,
|
environment: true,
|
||||||
@@ -118,55 +160,284 @@ export class DeployRunRepository {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!run) {
|
if (existing) {
|
||||||
|
return this.toSummary(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = await this.findTargetProjectEnvironment(input);
|
||||||
|
const initialRun = this.buildInitialRun(input, idempotencyKey);
|
||||||
|
const metadata = this.inputMetadata(input);
|
||||||
|
|
||||||
|
const created = await this.prisma.$transaction(async (tx) => {
|
||||||
|
const run = await tx.deployRun.create({
|
||||||
|
data: {
|
||||||
|
projectId: target.project.id,
|
||||||
|
environmentId: target.environment.id,
|
||||||
|
jobId: target.deployJob?.id,
|
||||||
|
status: PrismaDeployRunStatus.PENDING,
|
||||||
|
trigger: triggerToPrisma[input.trigger ?? 'manual'],
|
||||||
|
ref: input.ref,
|
||||||
|
operator: input.operator,
|
||||||
|
idempotencyKey,
|
||||||
|
metadata,
|
||||||
|
steps: {
|
||||||
|
create: initialRun.steps.map((step) => ({
|
||||||
|
bpmnNodeId: step.bpmnNodeId,
|
||||||
|
name: step.name,
|
||||||
|
status: stepStatusToPrisma[step.status],
|
||||||
|
order: step.order,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
project: true,
|
||||||
|
environment: true,
|
||||||
|
steps: {
|
||||||
|
orderBy: {
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return run;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.toSummary(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStatus(
|
||||||
|
id: string,
|
||||||
|
status: DeployRunStatus,
|
||||||
|
): Promise<DeployRunSummary | null> {
|
||||||
|
return this.updateRun(id, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateRun(
|
||||||
|
id: string,
|
||||||
|
patch: DeployRunMemoryPatch,
|
||||||
|
): Promise<DeployRunSummary | null> {
|
||||||
|
if (!this.useDatabase()) {
|
||||||
|
return this.updateMemoryRun(id, patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = await this.prisma.deployRun.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const metadata = this.objectMetadata<DeployRunMetadata>(current.metadata);
|
||||||
|
const data: Prisma.DeployRunUpdateInput = {
|
||||||
|
version: {
|
||||||
|
increment: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (patch.status) {
|
||||||
|
data.status = runStatusToPrisma[patch.status];
|
||||||
|
|
||||||
|
if (patch.status !== 'pending' && !current.startedAt) {
|
||||||
|
data.startedAt = this.toDate(patch.startedAt) ?? now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terminalRunStatuses.has(patch.status)) {
|
||||||
|
data.finishedAt = this.toDate(patch.finishedAt) ?? now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('jenkinsQueueId' in patch) {
|
||||||
|
data.jenkinsQueueId = patch.jenkinsQueueId ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('jenkinsBuildNumber' in patch) {
|
||||||
|
data.jenkinsBuildNumber = patch.jenkinsBuildNumber ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('jenkinsBuildUrl' in patch) {
|
||||||
|
if (patch.jenkinsBuildUrl) {
|
||||||
|
metadata.jenkinsBuildUrl = patch.jenkinsBuildUrl;
|
||||||
|
} else {
|
||||||
|
delete metadata.jenkinsBuildUrl;
|
||||||
|
}
|
||||||
|
data.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('failureSummary' in patch) {
|
||||||
|
data.failureSummary = patch.failureSummary ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('startedAt' in patch) {
|
||||||
|
data.startedAt = this.toDate(patch.startedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('finishedAt' in patch) {
|
||||||
|
data.finishedAt = this.toDate(patch.finishedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.deployRun.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStep(
|
||||||
|
runId: string,
|
||||||
|
bpmnNodeId: string,
|
||||||
|
patch: DeployRunStepMemoryPatch,
|
||||||
|
): Promise<DeployRunSummary | null> {
|
||||||
|
if (!this.useDatabase()) {
|
||||||
|
return this.updateMemoryStep(runId, bpmnNodeId, patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = await this.prisma.runStep.findUnique({
|
||||||
|
where: {
|
||||||
|
deployRunId_bpmnNodeId: {
|
||||||
|
deployRunId: runId,
|
||||||
|
bpmnNodeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const metadata = this.objectMetadata<RunStepMetadata>(current.metadata);
|
||||||
|
const data: Prisma.RunStepUpdateInput = {};
|
||||||
|
|
||||||
|
if (patch.status) {
|
||||||
|
data.status = stepStatusToPrisma[patch.status];
|
||||||
|
|
||||||
|
if (patch.status !== 'pending' && !current.startedAt) {
|
||||||
|
data.startedAt = this.toDate(patch.startedAt) ?? now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terminalStepStatuses.has(patch.status)) {
|
||||||
|
data.finishedAt = this.toDate(patch.finishedAt) ?? now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('startedAt' in patch) {
|
||||||
|
data.startedAt = this.toDate(patch.startedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('finishedAt' in patch) {
|
||||||
|
data.finishedAt = this.toDate(patch.finishedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('message' in patch) {
|
||||||
|
if (patch.message) {
|
||||||
|
metadata.message = patch.message;
|
||||||
|
} else {
|
||||||
|
delete metadata.message;
|
||||||
|
}
|
||||||
|
data.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('errorSummary' in patch) {
|
||||||
|
data.errorSummary = patch.errorSummary ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('logExcerpt' in patch) {
|
||||||
|
if (patch.logExcerpt) {
|
||||||
|
metadata.logExcerpt = patch.logExcerpt;
|
||||||
|
} else {
|
||||||
|
delete metadata.logExcerpt;
|
||||||
|
}
|
||||||
|
data.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.runStep.update({
|
||||||
|
where: {
|
||||||
|
deployRunId_bpmnNodeId: {
|
||||||
|
deployRunId: runId,
|
||||||
|
bpmnNodeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.findById(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findRecordById(id: string): Promise<DeployRunRecord | null> {
|
||||||
|
return this.prisma.deployRun.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
project: true,
|
||||||
|
environment: true,
|
||||||
|
steps: {
|
||||||
|
orderBy: {
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findTargetProjectEnvironment(input: CreateDeployRunInput) {
|
||||||
|
const project = await this.prisma.project.findUnique({
|
||||||
|
where: { key: input.projectKey },
|
||||||
|
include: {
|
||||||
|
environments: {
|
||||||
|
include: {
|
||||||
|
deployJobs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetEnvironmentName =
|
||||||
|
input.environment === 'test'
|
||||||
|
? EnvironmentKind.TEST
|
||||||
|
: EnvironmentKind.PRODUCTION;
|
||||||
|
const environment = project?.environments.find(
|
||||||
|
(item) => item.name === targetEnvironmentName,
|
||||||
|
);
|
||||||
|
const deployJob = environment?.deployJobs[0];
|
||||||
|
|
||||||
|
if (!project || !environment) {
|
||||||
|
throw new Error(
|
||||||
|
`Project ${input.projectKey} ${input.environment} is not seeded in database`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: run.id,
|
project,
|
||||||
projectKey: run.project.key,
|
environment,
|
||||||
environment: run.environment.name === 'TEST' ? 'test' : 'production',
|
deployJob,
|
||||||
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 {
|
private createMemoryRun(
|
||||||
const idempotencyKey =
|
input: CreateDeployRunInput,
|
||||||
input.idempotencyKey ??
|
idempotencyKey: string,
|
||||||
[
|
): DeployRunSummary {
|
||||||
input.projectKey,
|
|
||||||
input.environment,
|
|
||||||
input.ref,
|
|
||||||
input.operator,
|
|
||||||
new Date().toISOString(),
|
|
||||||
].join(':');
|
|
||||||
const existing = this.memoryRuns.find((run) => run.id === idempotencyKey);
|
const existing = this.memoryRuns.find((run) => run.id === idempotencyKey);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const run = this.buildInitialRun(input, idempotencyKey);
|
||||||
|
this.memoryRuns.unshift(run);
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildInitialRun(
|
||||||
|
input: CreateDeployRunInput,
|
||||||
|
idempotencyKey: string,
|
||||||
|
): DeployRunSummary {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const run: DeployRunSummary = {
|
|
||||||
|
return {
|
||||||
id: idempotencyKey,
|
id: idempotencyKey,
|
||||||
projectKey: input.projectKey,
|
projectKey: input.projectKey,
|
||||||
environment: input.environment,
|
environment: input.environment,
|
||||||
@@ -220,19 +491,9 @@ export class DeployRunRepository {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.memoryRuns.unshift(run);
|
|
||||||
return run;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMemoryStatus(
|
private updateMemoryRun(
|
||||||
id: string,
|
|
||||||
status: DeployRunStatus,
|
|
||||||
): DeployRunSummary | null {
|
|
||||||
return this.updateMemoryRun(id, { status });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMemoryRun(
|
|
||||||
id: string,
|
id: string,
|
||||||
patch: DeployRunMemoryPatch,
|
patch: DeployRunMemoryPatch,
|
||||||
): DeployRunSummary | null {
|
): DeployRunSummary | null {
|
||||||
@@ -283,7 +544,7 @@ export class DeployRunRepository {
|
|||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMemoryStep(
|
private updateMemoryStep(
|
||||||
runId: string,
|
runId: string,
|
||||||
bpmnNodeId: string,
|
bpmnNodeId: string,
|
||||||
patch: DeployRunStepMemoryPatch,
|
patch: DeployRunStepMemoryPatch,
|
||||||
@@ -332,7 +593,84 @@ export class DeployRunRepository {
|
|||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
private useDatabaseReads(): boolean {
|
private toSummary(run: DeployRunRecord): DeployRunSummary {
|
||||||
|
const metadata = this.objectMetadata<DeployRunMetadata>(run.metadata);
|
||||||
|
const sortedSteps = [...run.steps].sort((left, right) => left.order - right.order);
|
||||||
|
|
||||||
|
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,
|
||||||
|
jenkinsBuildUrl: metadata.jenkinsBuildUrl,
|
||||||
|
failureSummary: run.failureSummary ?? undefined,
|
||||||
|
startedAt: run.startedAt?.toISOString(),
|
||||||
|
finishedAt: run.finishedAt?.toISOString(),
|
||||||
|
createdAt: run.createdAt.toISOString(),
|
||||||
|
steps: sortedSteps.map((step) => {
|
||||||
|
const stepMetadata = this.objectMetadata<RunStepMetadata>(
|
||||||
|
step.metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
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(),
|
||||||
|
message: stepMetadata.message,
|
||||||
|
errorSummary: step.errorSummary ?? undefined,
|
||||||
|
logExcerpt: stepMetadata.logExcerpt,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private objectMetadata<T extends Record<string, unknown>>(
|
||||||
|
value: Prisma.JsonValue | null | undefined,
|
||||||
|
): T {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
return { ...(value as Record<string, unknown>) } as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private inputMetadata(input: CreateDeployRunInput): Prisma.InputJsonObject {
|
||||||
|
const metadata: DeployRunMetadata = {};
|
||||||
|
|
||||||
|
if (input.remark) {
|
||||||
|
metadata.remark = input.remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveIdempotencyKey(input: CreateDeployRunInput): string {
|
||||||
|
return (
|
||||||
|
input.idempotencyKey ??
|
||||||
|
[
|
||||||
|
input.projectKey,
|
||||||
|
input.environment,
|
||||||
|
input.ref,
|
||||||
|
input.operator,
|
||||||
|
new Date().toISOString(),
|
||||||
|
].join(':')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toDate(value: string | undefined): Date | null | undefined {
|
||||||
|
return value ? new Date(value) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private useDatabase(): boolean {
|
||||||
return (
|
return (
|
||||||
this.config.get('USE_DATABASE_READS', { infer: true }) &&
|
this.config.get('USE_DATABASE_READS', { infer: true }) &&
|
||||||
Boolean(this.config.get('DATABASE_URL', { infer: true }))
|
Boolean(this.config.get('DATABASE_URL', { infer: true }))
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export class DeployRunsService {
|
|||||||
|
|
||||||
const project = await this.projectsService.getProject(input.projectKey);
|
const project = await this.projectsService.getProject(input.projectKey);
|
||||||
this.validateReleaseRequest(input, project);
|
this.validateReleaseRequest(input, project);
|
||||||
const run = this.deployRunRepository.create(input);
|
const run = await this.deployRunRepository.create(input);
|
||||||
|
|
||||||
this.auditService.record({
|
this.auditService.record({
|
||||||
action: 'DEPLOY_REQUESTED',
|
action: 'DEPLOY_REQUESTED',
|
||||||
@@ -81,7 +81,7 @@ export class DeployRunsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async cancelRun(id: string): Promise<DeployRunSummary> {
|
async cancelRun(id: string): Promise<DeployRunSummary> {
|
||||||
const run = this.deployRunRepository.updateMemoryStatus(id, 'canceled');
|
const run = await this.deployRunRepository.updateStatus(id, 'canceled');
|
||||||
|
|
||||||
if (!run) {
|
if (!run) {
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
|
|||||||
Reference in New Issue
Block a user