fix: 避免幂等发布重复执行

- src/deploy-runs/deploy-run.repository.ts: 返回发布单是否新建,唯一键竞争时回读已有记录
- src/deploy-runs/deploy-runs.service.ts: 重复 idempotencyKey 只返回已有发布单并记录复用审计,不再重复触发 Jenkins
This commit is contained in:
湛兮
2026-06-12 05:31:51 +08:00
parent d0077636c7
commit dea6cfd7fe
2 changed files with 45 additions and 12 deletions
+38 -10
View File
@@ -49,6 +49,14 @@ type DeployRunRecord = Prisma.DeployRunGetPayload<{
};
}>;
type DeployRunDatabaseClient = PrismaService | Prisma.TransactionClient;
type CreateDeployRunResult = {
run: DeployRunSummary;
created: boolean;
};
type CreateDatabaseDeployRunResult = {
record: DeployRunRecord;
created: boolean;
};
type DeployRunMetadata = Prisma.InputJsonObject & {
jenkinsBuildUrl?: string;
@@ -143,16 +151,19 @@ export class DeployRunRepository {
return run ? this.toSummary(run) : null;
}
async create(input: CreateDeployRunInput): Promise<DeployRunSummary> {
async create(input: CreateDeployRunInput): Promise<CreateDeployRunResult> {
const idempotencyKey = this.resolveIdempotencyKey(input);
if (!this.useDatabase()) {
return this.createMemoryRun(input, idempotencyKey);
}
const created = await this.createDatabaseRun(input, idempotencyKey);
const result = await this.createDatabaseRun(input, idempotencyKey);
return this.toSummary(created);
return {
run: this.toSummary(result.record),
created: result.created,
};
}
async updateStatus(
@@ -437,7 +448,7 @@ export class DeployRunRepository {
private async createDatabaseRun(
input: CreateDeployRunInput,
idempotencyKey: string,
): Promise<DeployRunRecord> {
): Promise<CreateDatabaseDeployRunResult> {
try {
return await this.prisma.$transaction(async (tx) => {
const existing = await this.findRecordByIdempotencyKey(
@@ -446,14 +457,17 @@ export class DeployRunRepository {
);
if (existing) {
return existing;
return {
record: existing,
created: false,
};
}
const target = await this.findTargetProjectEnvironment(input, tx);
const initialRun = this.buildInitialRun(input, idempotencyKey);
const metadata = this.inputMetadata(input);
return tx.deployRun.create({
const record = await tx.deployRun.create({
data: {
projectId: target.project.id,
environmentId: target.environment.id,
@@ -483,13 +497,21 @@ export class DeployRunRepository {
},
},
});
return {
record,
created: true,
};
});
} catch (error) {
if (this.isIdempotencyUniqueConflict(error)) {
const existing = await this.findRecordByIdempotencyKey(idempotencyKey);
if (existing) {
return existing;
return {
record: existing,
created: false,
};
}
}
@@ -565,16 +587,22 @@ export class DeployRunRepository {
private createMemoryRun(
input: CreateDeployRunInput,
idempotencyKey: string,
): DeployRunSummary {
): CreateDeployRunResult {
const existing = this.memoryRuns.find((run) => run.id === idempotencyKey);
if (existing) {
return existing;
return {
run: existing,
created: false,
};
}
const run = this.buildInitialRun(input, idempotencyKey);
this.memoryRuns.unshift(run);
return run;
return {
run,
created: true,
};
}
private buildInitialRun(
+7 -2
View File
@@ -114,11 +114,12 @@ export class DeployRunsService {
const project = await this.projectsService.getProject(input.projectKey);
await this.validateReleaseRequest(input, project);
const run = await this.deployRunRepository.create(input);
const creation = await this.deployRunRepository.create(input);
const run = creation.run;
const confirmation = this.productionConfirmationDigest(input);
await this.auditService.record({
action: 'DEPLOY_REQUESTED',
action: creation.created ? 'DEPLOY_REQUESTED' : 'DEPLOY_REQUEST_REUSED',
resourceType: 'deploy_run',
resourceId: run.id,
actorName: input.operator,
@@ -132,6 +133,10 @@ export class DeployRunsService {
},
});
if (!creation.created) {
return run;
}
return this.deployExecutionService.execute(run, project);
}