Skip to content

Commit db6bd6a

Browse files
committed
fix(webapp): prevent duplicate envs from provision race
1 parent c936c79 commit db6bd6a

4 files changed

Lines changed: 40 additions & 5 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Prevent duplicate Staging and Preview environments when account setup requests overlap

apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.environments.staging.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import {
3+
Prisma,
34
type RuntimeEnvironment,
45
type Organization,
56
type Project,
@@ -61,9 +62,16 @@ async function upsertEnvironment(
6162
type: RuntimeEnvironmentType,
6263
isBranchableEnvironment: boolean
6364
) {
64-
const existingEnvironment = project.environments.find((env) => env.type === type);
65+
const existingEnvironment = project.environments.find(
66+
(env) => env.type === type && env.parentEnvironmentId === null
67+
);
6568

66-
if (!existingEnvironment) {
69+
if (existingEnvironment) {
70+
await updateEnvConcurrencyLimits({ ...existingEnvironment, organization, project });
71+
return { status: "updated", environment: existingEnvironment };
72+
}
73+
74+
try {
6775
const newEnvironment = await createEnvironment({
6876
organization,
6977
project,
@@ -72,8 +80,23 @@ async function upsertEnvironment(
7280
});
7381
await updateEnvConcurrencyLimits({ ...newEnvironment, organization, project });
7482
return { status: "created", environment: newEnvironment };
75-
} else {
76-
await updateEnvConcurrencyLimits({ ...existingEnvironment, organization, project });
77-
return { status: "updated", environment: existingEnvironment };
83+
} catch (error) {
84+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
85+
const existingAfterConflict = await prisma.runtimeEnvironment.findFirst({
86+
where: {
87+
organizationId: organization.id,
88+
projectId: project.id,
89+
type,
90+
parentEnvironmentId: null,
91+
},
92+
});
93+
94+
if (existingAfterConflict) {
95+
await updateEnvConcurrencyLimits({ ...existingAfterConflict, organization, project });
96+
return { status: "updated", environment: existingAfterConflict };
97+
}
98+
}
99+
100+
throw error;
78101
}
79102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "RuntimeEnvironment_projectId_type_staging_preview_root_key"
2+
ON "RuntimeEnvironment" ("projectId", "type")
3+
WHERE "parentEnvironmentId" IS NULL
4+
AND "type" IN ('STAGING', 'PREVIEW');

internal-packages/database/prisma/schema.prisma

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ model RuntimeEnvironment {
394394
taskIdentifiers TaskIdentifier[]
395395
revokedApiKeys RevokedApiKey[]
396396
397+
// A partial unique index also enforces one STAGING/PREVIEW root per project and type.
398+
// It is defined in SQL because Prisma does not support partial indexes in the schema.
397399
@@unique([projectId, slug, orgMemberId])
398400
@@unique([projectId, shortcode])
399401
@@index([parentEnvironmentId])

0 commit comments

Comments
 (0)