fix(webapp): prevent duplicate envs from provision race (#4261)

Fixes TRI-12078

## Summary

Prevents concurrent environment setup requests from creating duplicate
Staging and Preview environments.

## Fix

Adds database-enforced uniqueness for root Staging and Preview
environments.

If two requests race, the losing request loads the environment created
by the winner and continues successfully instead of creating a duplicate
or returning an error.
This commit is contained in:
Chris Arderne
2026-07-21 15:40:48 +01:00
committed by GitHub
parent d05f1a7398
commit 6642c8b785
4 changed files with 40 additions and 5 deletions
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---
Prevent duplicate Staging and Preview environments when account setup requests overlap
@@ -1,5 +1,6 @@
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
import {
Prisma,
type RuntimeEnvironment,
type Organization,
type Project,
@@ -61,9 +62,16 @@ async function upsertEnvironment(
type: RuntimeEnvironmentType,
isBranchableEnvironment: boolean
) {
const existingEnvironment = project.environments.find((env) => env.type === type);
const existingEnvironment = project.environments.find(
(env) => env.type === type && env.parentEnvironmentId === null
);
if (!existingEnvironment) {
if (existingEnvironment) {
await updateEnvConcurrencyLimits({ ...existingEnvironment, organization, project });
return { status: "updated", environment: existingEnvironment };
}
try {
const newEnvironment = await createEnvironment({
organization,
project,
@@ -72,8 +80,23 @@ async function upsertEnvironment(
});
await updateEnvConcurrencyLimits({ ...newEnvironment, organization, project });
return { status: "created", environment: newEnvironment };
} else {
await updateEnvConcurrencyLimits({ ...existingEnvironment, organization, project });
return { status: "updated", environment: existingEnvironment };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
const existingAfterConflict = await prisma.runtimeEnvironment.findFirst({
where: {
organizationId: organization.id,
projectId: project.id,
type,
parentEnvironmentId: null,
},
});
if (existingAfterConflict) {
await updateEnvConcurrencyLimits({ ...existingAfterConflict, organization, project });
return { status: "updated", environment: existingAfterConflict };
}
}
throw error;
}
}
@@ -0,0 +1,4 @@
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "RuntimeEnvironment_projectId_type_staging_preview_root_key"
ON "RuntimeEnvironment" ("projectId", "type")
WHERE "parentEnvironmentId" IS NULL
AND "type" IN ('STAGING', 'PREVIEW');
@@ -394,6 +394,8 @@ model RuntimeEnvironment {
taskIdentifiers TaskIdentifier[]
revokedApiKeys RevokedApiKey[]
// A partial unique index also enforces one STAGING/PREVIEW root per project and type.
// It is defined in SQL because Prisma does not support partial indexes in the schema.
@@unique([projectId, slug, orgMemberId])
@@unique([projectId, shortcode])
@@index([parentEnvironmentId])