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 DeployRunDatabaseClient = PrismaService | Prisma.TransactionClient;
type CreateDeployRunResult = {
run: DeployRunSummary;
created: boolean;
};
type CreateDatabaseDeployRunResult = {
record: DeployRunRecord;
created: boolean;
};
type DeployRunMetadata = Prisma.InputJsonObject & { type DeployRunMetadata = Prisma.InputJsonObject & {
jenkinsBuildUrl?: string; jenkinsBuildUrl?: string;
@@ -143,16 +151,19 @@ export class DeployRunRepository {
return run ? this.toSummary(run) : null; return run ? this.toSummary(run) : null;
} }
async create(input: CreateDeployRunInput): Promise<DeployRunSummary> { async create(input: CreateDeployRunInput): Promise<CreateDeployRunResult> {
const idempotencyKey = this.resolveIdempotencyKey(input); const idempotencyKey = this.resolveIdempotencyKey(input);
if (!this.useDatabase()) { if (!this.useDatabase()) {
return this.createMemoryRun(input, idempotencyKey); 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( async updateStatus(
@@ -437,7 +448,7 @@ export class DeployRunRepository {
private async createDatabaseRun( private async createDatabaseRun(
input: CreateDeployRunInput, input: CreateDeployRunInput,
idempotencyKey: string, idempotencyKey: string,
): Promise<DeployRunRecord> { ): Promise<CreateDatabaseDeployRunResult> {
try { try {
return await this.prisma.$transaction(async (tx) => { return await this.prisma.$transaction(async (tx) => {
const existing = await this.findRecordByIdempotencyKey( const existing = await this.findRecordByIdempotencyKey(
@@ -446,14 +457,17 @@ export class DeployRunRepository {
); );
if (existing) { if (existing) {
return existing; return {
record: existing,
created: false,
};
} }
const target = await this.findTargetProjectEnvironment(input, tx); const target = await this.findTargetProjectEnvironment(input, tx);
const initialRun = this.buildInitialRun(input, idempotencyKey); const initialRun = this.buildInitialRun(input, idempotencyKey);
const metadata = this.inputMetadata(input); const metadata = this.inputMetadata(input);
return tx.deployRun.create({ const record = await tx.deployRun.create({
data: { data: {
projectId: target.project.id, projectId: target.project.id,
environmentId: target.environment.id, environmentId: target.environment.id,
@@ -483,13 +497,21 @@ export class DeployRunRepository {
}, },
}, },
}); });
return {
record,
created: true,
};
}); });
} catch (error) { } catch (error) {
if (this.isIdempotencyUniqueConflict(error)) { if (this.isIdempotencyUniqueConflict(error)) {
const existing = await this.findRecordByIdempotencyKey(idempotencyKey); const existing = await this.findRecordByIdempotencyKey(idempotencyKey);
if (existing) { if (existing) {
return existing; return {
record: existing,
created: false,
};
} }
} }
@@ -565,16 +587,22 @@ export class DeployRunRepository {
private createMemoryRun( private createMemoryRun(
input: CreateDeployRunInput, input: CreateDeployRunInput,
idempotencyKey: string, idempotencyKey: string,
): DeployRunSummary { ): CreateDeployRunResult {
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 {
run: existing,
created: false,
};
} }
const run = this.buildInitialRun(input, idempotencyKey); const run = this.buildInitialRun(input, idempotencyKey);
this.memoryRuns.unshift(run); this.memoryRuns.unshift(run);
return run; return {
run,
created: true,
};
} }
private buildInitialRun( private buildInitialRun(
+7 -2
View File
@@ -114,11 +114,12 @@ export class DeployRunsService {
const project = await this.projectsService.getProject(input.projectKey); const project = await this.projectsService.getProject(input.projectKey);
await this.validateReleaseRequest(input, project); 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); const confirmation = this.productionConfirmationDigest(input);
await this.auditService.record({ await this.auditService.record({
action: 'DEPLOY_REQUESTED', action: creation.created ? 'DEPLOY_REQUESTED' : 'DEPLOY_REQUEST_REUSED',
resourceType: 'deploy_run', resourceType: 'deploy_run',
resourceId: run.id, resourceId: run.id,
actorName: input.operator, actorName: input.operator,
@@ -132,6 +133,10 @@ export class DeployRunsService {
}, },
}); });
if (!creation.created) {
return run;
}
return this.deployExecutionService.execute(run, project); return this.deployExecutionService.execute(run, project);
} }