From 3eec258e5e7218612390840b54567b389ac433e2 Mon Sep 17 00:00:00 2001 From: Kristof Siket Date: Thu, 2 Jul 2026 14:00:48 +0200 Subject: [PATCH] fix(env): scope branch-override lookup by branchId `findVariableByNaturalKey` listed environment variables by `(projectId, class, key)` only and filtered by branch client-side, but the list endpoint is paginated (default 100 rows, ordered createdAt asc). Once a project accumulates more than a page of preview overrides for a key, the newest branch's row falls off the first page, so the lookup returns null even though the row exists. `project env update --branch` then reports the variable as not found, and `project env add --branch`'s pre-check also misses it and POSTs, which the server rejects with a 409. The two paths contradict each other and neither can touch an existing branch override. Pass `branchId` to the list query for branch-scoped lookups (the endpoint already supports the filter) so the server returns the single exact row regardless of how many branches exist. Role-scoped lookups are unchanged. Co-Authored-By: Claude Opus 4.8 --- packages/cli/src/controllers/app-env-api.ts | 4 ++ packages/cli/tests/app-env.test.ts | 61 +++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/cli/src/controllers/app-env-api.ts b/packages/cli/src/controllers/app-env-api.ts index 954fe322..e229ad9a 100644 --- a/packages/cli/src/controllers/app-env-api.ts +++ b/packages/cli/src/controllers/app-env-api.ts @@ -33,6 +33,7 @@ export async function findVariableByNaturalKey( resolved: ResolvedEnvApiScope, signal: AbortSignal, ): Promise { + // Filter by branchId server-side — the list pages at 100, so client-side-only filtering drops branch rows past page 1. const { data, error, response } = await client.GET( "/v1/environment-variables", { @@ -41,6 +42,9 @@ export async function findVariableByNaturalKey( projectId, class: resolved.apiTarget.class, key, + ...(resolved.apiTarget.branchId !== null + ? { branchId: resolved.apiTarget.branchId } + : {}), }, }, signal, diff --git a/packages/cli/tests/app-env.test.ts b/packages/cli/tests/app-env.test.ts index da3e5770..f2b7a3e4 100644 --- a/packages/cli/tests/app-env.test.ts +++ b/packages/cli/tests/app-env.test.ts @@ -944,6 +944,67 @@ describe("env update", () => { ); }); + it("scopes the branch-override lookup by branchId so it survives pagination", async () => { + const client = createMockClient(); + client.envGET + .mockResolvedValueOnce({ + data: { + data: [makeBranchRow()], + pagination: { hasMore: false, nextCursor: null }, + }, + response: { status: 200 }, + }) + .mockResolvedValueOnce({ + data: { + data: [ + makeVariableRow({ + id: "envvar_branch", + key: "DATABASE_URL", + class: "preview", + branchId: "br_feature", + }), + ], + pagination: { hasMore: false, nextCursor: null }, + }, + response: { status: 200 }, + }); + client.PATCH.mockResolvedValueOnce({ + data: { + data: makeVariableRow({ + id: "envvar_branch", + key: "DATABASE_URL", + class: "preview", + branchId: "br_feature", + }), + }, + response: { status: 200 }, + }); + + const { controllers, createTempCwd, createTestCommandContext } = + await loadControllers(client, "proj_123"); + const cwd = await createTempCwd(); + await writeLocalPin(cwd); + const { context } = await createTestCommandContext({ cwd }); + + await controllers.runEnvUpdate(context, "DATABASE_URL=postgresql://new", { + branchName: "feature/foo", + }); + + expect(client.GET).toHaveBeenCalledWith( + "/v1/environment-variables", + expect.objectContaining({ + params: { + query: expect.objectContaining({ + projectId: "proj_123", + class: "preview", + key: "DATABASE_URL", + branchId: "br_feature", + }), + }, + }), + ); + }); + it("updates variables from a dotenv file via PATCH", async () => { const client = createMockClient(); client.envGET