Skip to content

Commit bf82c98

Browse files
committed
fix(webapp): scope development branches to each member
1 parent e2d3b83 commit bf82c98

2 files changed

Lines changed: 89 additions & 9 deletions

File tree

apps/webapp/app/services/upsertBranch.server.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { type PrismaClient, type PrismaClientOrTransaction } from "@trigger.dev/database";
1+
import {
2+
type Prisma,
3+
type PrismaClient,
4+
type PrismaClientOrTransaction,
5+
} from "@trigger.dev/database";
26
import slug from "slug";
37
import { prisma } from "~/db.server";
48
import { createApiKeyForEnv, createPkApiKeyForEnv } from "~/models/api-key.server";
@@ -13,6 +17,7 @@ import { logger } from "./logger.server";
1317
import { getCurrentPlan, getLimit } from "./platform.v3.server";
1418
import { type z } from "zod";
1519
import invariant from "tiny-invariant";
20+
import { nanoid } from "nanoid";
1621
import { type CreateBranchOptions } from "~/utils/branches";
1722
import {
1823
applyBillingLimitPauseAfterEnvCreate,
@@ -141,20 +146,36 @@ export class UpsertBranchService {
141146
const branchSlug = `${slug(`${parentEnvironment.slug}-${sanitizedBranchName}`)}`;
142147
const apiKey = createApiKeyForEnv(parentEnvironment.type);
143148
const pkApiKey = createPkApiKeyForEnv(parentEnvironment.type);
144-
const shortcode = branchSlug;
149+
const isDevelopmentBranch = parentEnvironment.type === "DEVELOPMENT";
150+
// Dev branch slugs are member-scoped, but shortcodes remain project-scoped.
151+
// Keep the readable slug while giving each member's branch a unique shortcode.
152+
const shortcode = isDevelopmentBranch ? `${branchSlug}-${nanoid()}` : branchSlug;
153+
let branchWhere: Prisma.RuntimeEnvironmentWhereUniqueInput;
154+
if (isDevelopmentBranch) {
155+
invariant(parentEnvironment.orgMemberId, "Development branches require an org member");
156+
branchWhere = {
157+
projectId_slug_orgMemberId: {
158+
projectId: parentEnvironment.project.id,
159+
slug: branchSlug,
160+
orgMemberId: parentEnvironment.orgMemberId,
161+
},
162+
};
163+
} else {
164+
branchWhere = {
165+
projectId_shortcode: {
166+
projectId: parentEnvironment.project.id,
167+
shortcode,
168+
},
169+
};
170+
}
145171
const billingPause = await getInitialEnvPauseStateForBillingLimit(
146172
parentEnvironment.organization.id,
147173
parentEnvironment.type
148174
);
149175

150176
const now = new Date();
151177
const branch = await this.#prismaClient.runtimeEnvironment.upsert({
152-
where: {
153-
projectId_shortcode: {
154-
projectId: parentEnvironment.project.id,
155-
shortcode: shortcode,
156-
},
157-
},
178+
where: branchWhere,
158179
create: {
159180
slug: branchSlug,
160181
apiKey,

apps/webapp/test/devBranchServices.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import slug from "slug";
44
import { describe, expect, vi } from "vitest";
55
import { ArchiveBranchService } from "~/services/archiveBranch.server";
66
import { UpsertBranchService } from "~/services/upsertBranch.server";
7-
import { createTestOrgProjectWithMember, uniqueId } from "./fixtures/environmentVariablesFixtures";
7+
import {
8+
createTestOrgProjectWithMember,
9+
createTestUser,
10+
uniqueId,
11+
} from "./fixtures/environmentVariablesFixtures";
812

913
vi.setConfig({ testTimeout: 60_000 });
1014

@@ -77,6 +81,61 @@ describe("UpsertBranchService — DEVELOPMENT parent", () => {
7781
}
7882
);
7983

84+
postgresTest("allows different members to use the same branch name", async ({ prisma }) => {
85+
const {
86+
organization,
87+
project,
88+
user: firstUser,
89+
orgMember: firstMember,
90+
} = await createTestOrgProjectWithMember(prisma);
91+
const secondUser = await createTestUser(prisma);
92+
const secondMember = await prisma.orgMember.create({
93+
data: {
94+
organizationId: organization.id,
95+
userId: secondUser.id,
96+
role: "MEMBER",
97+
},
98+
});
99+
100+
await Promise.all([
101+
createDevRoot(prisma, project.id, organization.id, firstMember.id),
102+
createDevRoot(prisma, project.id, organization.id, secondMember.id),
103+
]);
104+
105+
const options = {
106+
projectId: project.id,
107+
env: "development" as const,
108+
branchName: "shared-name",
109+
};
110+
const [firstResult, secondResult] = await Promise.all([
111+
new UpsertBranchService(prisma).call(
112+
{ type: "userMembership", userId: firstUser.id },
113+
options
114+
),
115+
new UpsertBranchService(prisma).call(
116+
{ type: "userMembership", userId: secondUser.id },
117+
options
118+
),
119+
]);
120+
121+
expect(firstResult.success && secondResult.success).toBe(true);
122+
if (!firstResult.success || !secondResult.success) return;
123+
expect(firstResult.branch.id).not.toBe(secondResult.branch.id);
124+
expect(firstResult.branch.slug).toBe(secondResult.branch.slug);
125+
expect(firstResult.branch.shortcode).not.toBe(secondResult.branch.shortcode);
126+
expect(firstResult.branch.orgMemberId).toBe(firstMember.id);
127+
expect(secondResult.branch.orgMemberId).toBe(secondMember.id);
128+
129+
const firstRetry = await new UpsertBranchService(prisma).call(
130+
{ type: "userMembership", userId: firstUser.id },
131+
options
132+
);
133+
expect(firstRetry.success).toBe(true);
134+
if (!firstRetry.success) return;
135+
expect(firstRetry.alreadyExisted).toBe(true);
136+
expect(firstRetry.branch.id).toBe(firstResult.branch.id);
137+
});
138+
80139
postgresTest(
81140
"rejects an invalid branch name without touching the database",
82141
async ({ prisma }) => {

0 commit comments

Comments
 (0)