Skip to content

Commit 1e4ee41

Browse files
authored
Merge branch 'main' into fix/webapp-sanitize-prisma-leaks
2 parents 758e915 + aec7e0a commit 1e4ee41

37 files changed

Lines changed: 1897 additions & 375 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+
Dashboard runs, sessions, batches, and schedule-detail loaders now return 404 (or redirect to the user's home with a toast for missing projects) instead of 500 when a slug doesn't resolve.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Speed up env-var lookups on the projects API by indexing `EnvironmentVariableValue.environmentId`.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Expose `is_warm_start` in the TRQL `runs` schema so warm vs cold start data can be queried and visualized in Dashboards.
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+
Include the S2 access-token scope fingerprint in its cache key so a scope change in code (e.g. adding a new op) auto-invalidates pre-deploy cached tokens instead of returning stale ones for up to 24h.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Show the currently pinned `TRIGGER_VERSION` under the Atomic deployments toggle on the Vercel
7+
integration settings, and prompt the user to clear it from Vercel production when they disable
8+
atomic deployments. Also mark `TRIGGER_SECRET_KEY` writes to Vercel as `sensitive` so the value
9+
cannot be read back from the Vercel dashboard or API once written.

apps/webapp/app/components/integrations/VercelBuildSettings.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ type BuildSettingsFieldsProps = {
2323
disabledEnvSlugs?: Partial<Record<EnvSlug, string>>;
2424
autoPromote?: boolean;
2525
onAutoPromoteChange?: (value: boolean) => void;
26+
/** The currently pinned TRIGGER_VERSION on Vercel production, if any. Shown under the
27+
* Atomic deployments toggle so the user knows what version is set on Vercel right now. */
28+
currentTriggerVersion?: string | null;
29+
/** True when the Vercel lookup for TRIGGER_VERSION failed. We show this so the user knows
30+
* the pin status is unknown — distinct from "not set". */
31+
currentTriggerVersionFetchFailed?: boolean;
2632
/** Hide the section-level master toggles for "Pull env vars" and "Discover new env vars". */
2733
hideSectionToggles?: boolean;
2834
};
@@ -39,6 +45,8 @@ export function BuildSettingsFields({
3945
disabledEnvSlugs,
4046
autoPromote,
4147
onAutoPromoteChange,
48+
currentTriggerVersion,
49+
currentTriggerVersionFetchFailed,
4250
hideSectionToggles,
4351
}: BuildSettingsFieldsProps) {
4452
const isSlugDisabled = (slug: EnvSlug) => !!disabledEnvSlugs?.[slug];
@@ -208,6 +216,20 @@ export function BuildSettingsFields({
208216
</TextLink>
209217
.
210218
</Hint>
219+
{currentTriggerVersion && (
220+
<Hint className="pr-6">
221+
Currently pinned to{" "}
222+
<span className="font-mono text-text-bright">{currentTriggerVersion}</span> in Vercel
223+
production.
224+
</Hint>
225+
)}
226+
{!currentTriggerVersion && currentTriggerVersionFetchFailed && (
227+
<Hint className="pr-6 text-warning">
228+
Couldn't read{" "}
229+
<span className="font-mono text-text-bright">TRIGGER_VERSION</span> from Vercel —
230+
check the Vercel dashboard to confirm the production pin.
231+
</Hint>
232+
)}
211233
</div>
212234

213235
{/* Auto promotion — only visible when atomic deployments are on */}

apps/webapp/app/models/vercelIntegration.server.ts

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,7 @@ export class VercelIntegrationRepository {
960960
key: "TRIGGER_SECRET_KEY",
961961
value: runtimeEnv.apiKey,
962962
target: vercelTarget,
963-
type: "encrypted",
963+
type: "sensitive",
964964
environmentType: runtimeEnv.type,
965965
});
966966
}
@@ -1061,7 +1061,7 @@ export class VercelIntegrationRepository {
10611061
key: "TRIGGER_SECRET_KEY",
10621062
value: params.apiKey,
10631063
target: vercelTarget,
1064-
type: "encrypted",
1064+
type: "sensitive",
10651065
});
10661066

10671067
logger.info("Synced regenerated API key to Vercel", {
@@ -1115,28 +1115,26 @@ export class VercelIntegrationRepository {
11151115
return (env as any).customEnvironmentIds?.includes(customEnvironmentId);
11161116
});
11171117

1118+
// Always delete-then-create rather than editProjectEnv, because Vercel rejects
1119+
// in-place type changes (e.g. encrypted -> sensitive).
11181120
if (existingEnv && existingEnv.id) {
1119-
await client.projects.editProjectEnv({
1120-
idOrName: vercelProjectId,
1121-
id: existingEnv.id,
1122-
...(teamId && { teamId }),
1123-
requestBody: {
1124-
value,
1125-
type,
1126-
},
1127-
});
1128-
} else {
1129-
await client.projects.createProjectEnv({
1121+
await client.projects.batchRemoveProjectEnv({
11301122
idOrName: vercelProjectId,
11311123
...(teamId && { teamId }),
1132-
requestBody: {
1133-
key,
1134-
value,
1135-
type,
1136-
customEnvironmentIds: [customEnvironmentId],
1137-
} as any,
1124+
requestBody: { ids: [existingEnv.id] },
11381125
});
11391126
}
1127+
1128+
await client.projects.createProjectEnv({
1129+
idOrName: vercelProjectId,
1130+
...(teamId && { teamId }),
1131+
requestBody: {
1132+
key,
1133+
value,
1134+
type,
1135+
customEnvironmentIds: [customEnvironmentId],
1136+
} as any,
1137+
});
11401138
})(),
11411139
(error) => toVercelApiError(error)
11421140
)
@@ -1709,29 +1707,27 @@ export class VercelIntegrationRepository {
17091707
return target.length === envTargets.length && target.every((t) => envTargets.includes(t));
17101708
});
17111709

1710+
// Always delete-then-create rather than editProjectEnv, because Vercel rejects
1711+
// in-place type changes (e.g. encrypted -> sensitive). Same approach used by
1712+
// syncApiKeysToVercel via removeAllVercelEnvVarsByKey.
17121713
if (existingEnv && existingEnv.id) {
1713-
await client.projects.editProjectEnv({
1714-
idOrName: vercelProjectId,
1715-
id: existingEnv.id,
1716-
...(teamId && { teamId }),
1717-
requestBody: {
1718-
value,
1719-
target: target as any,
1720-
type,
1721-
},
1722-
});
1723-
} else {
1724-
await client.projects.createProjectEnv({
1714+
await client.projects.batchRemoveProjectEnv({
17251715
idOrName: vercelProjectId,
17261716
...(teamId && { teamId }),
1727-
requestBody: {
1728-
key,
1729-
value,
1730-
target: target as any,
1731-
type,
1732-
},
1717+
requestBody: { ids: [existingEnv.id] },
17331718
});
17341719
}
1720+
1721+
await client.projects.createProjectEnv({
1722+
idOrName: vercelProjectId,
1723+
...(teamId && { teamId }),
1724+
requestBody: {
1725+
key,
1726+
value,
1727+
target: target as any,
1728+
type,
1729+
},
1730+
});
17351731
}
17361732

17371733
static getAutoAssignCustomDomains(

apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ export type VercelSettingsResult = {
4242
autoAssignCustomDomains?: boolean | null;
4343
/** URL to manage Vercel integration access (project sharing) on vercel.com */
4444
vercelManageAccessUrl?: string;
45+
/** The currently pinned TRIGGER_VERSION on Vercel production, if set. Used to surface
46+
* the pin in the UI and prompt the user to clear it when atomic deployments are disabled. */
47+
currentTriggerVersion?: string | null;
48+
/** True when the Vercel lookup for TRIGGER_VERSION failed (network/auth/etc). Distinct
49+
* from "no pin set" — the UI uses this to warn the user and still prompt them on disable
50+
* so they can manually verify that production isn't pinned. */
51+
currentTriggerVersionFetchFailed?: boolean;
4552
};
4653

4754
export type VercelAvailableProject = {
@@ -248,13 +255,17 @@ export class VercelSettingsPresenter extends BasePresenter {
248255
customEnvironments: VercelCustomEnvironment[];
249256
autoAssignCustomDomains: boolean | null;
250257
vercelManageAccessUrl?: string;
258+
currentTriggerVersion: string | null;
259+
currentTriggerVersionFetchFailed: boolean;
251260
}> => {
252261
if (!orgIntegration) {
253-
return { customEnvironments: [], autoAssignCustomDomains: null };
262+
return { customEnvironments: [], autoAssignCustomDomains: null, currentTriggerVersion: null, currentTriggerVersionFetchFailed: false };
254263
}
255264
const clientResult = await VercelIntegrationRepository.getVercelClient(orgIntegration);
256265
if (clientResult.isErr()) {
257-
return { customEnvironments: [], autoAssignCustomDomains: null };
266+
// We couldn't even build a Vercel client — treat as fetch failure so the UI
267+
// still prompts the user when they disable atomic deployments.
268+
return { customEnvironments: [], autoAssignCustomDomains: null, currentTriggerVersion: null, currentTriggerVersionFetchFailed: true };
258269
}
259270
const client = clientResult.value;
260271
const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);
@@ -275,10 +286,10 @@ export class VercelSettingsPresenter extends BasePresenter {
275286
}
276287

277288
if (!connectedProject) {
278-
return { customEnvironments: [], autoAssignCustomDomains: null, vercelManageAccessUrl };
289+
return { customEnvironments: [], autoAssignCustomDomains: null, vercelManageAccessUrl, currentTriggerVersion: null, currentTriggerVersionFetchFailed: false };
279290
}
280291

281-
const [customEnvsResult, autoAssignResult] = await Promise.all([
292+
const [customEnvsResult, autoAssignResult, triggerVersionResult] = await Promise.all([
282293
VercelIntegrationRepository.getVercelCustomEnvironments(
283294
client,
284295
connectedProject.vercelProjectId,
@@ -289,18 +300,44 @@ export class VercelSettingsPresenter extends BasePresenter {
289300
connectedProject.vercelProjectId,
290301
teamId
291302
),
303+
VercelIntegrationRepository.getVercelEnvironmentVariableValues(
304+
client,
305+
connectedProject.vercelProjectId,
306+
teamId,
307+
"production",
308+
(key) => key === "TRIGGER_VERSION"
309+
),
292310
]);
311+
312+
let currentTriggerVersion: string | null = null;
313+
let currentTriggerVersionFetchFailed = false;
314+
if (triggerVersionResult.isOk()) {
315+
const match = triggerVersionResult.value.find(
316+
(envVar) => envVar.key === "TRIGGER_VERSION" && envVar.target.includes("production")
317+
);
318+
currentTriggerVersion = match?.value ?? null;
319+
} else {
320+
currentTriggerVersionFetchFailed = true;
321+
logger.warn("Failed to fetch current TRIGGER_VERSION from Vercel — surfacing as unknown", {
322+
projectId,
323+
vercelProjectId: connectedProject.vercelProjectId,
324+
error: triggerVersionResult.error.message,
325+
});
326+
}
327+
293328
return {
294329
customEnvironments: customEnvsResult.isOk() ? customEnvsResult.value : [],
295330
autoAssignCustomDomains: autoAssignResult.isOk() ? autoAssignResult.value : null,
296331
vercelManageAccessUrl,
332+
currentTriggerVersion,
333+
currentTriggerVersionFetchFailed,
297334
};
298335
};
299336

300337
return fromPromise(
301338
fetchVercelData(),
302339
(error) => ({ type: "other" as const, cause: error })
303-
).map(({ customEnvironments, autoAssignCustomDomains, vercelManageAccessUrl }) => ({
340+
).map(({ customEnvironments, autoAssignCustomDomains, vercelManageAccessUrl, currentTriggerVersion, currentTriggerVersionFetchFailed }) => ({
304341
enabled: true,
305342
hasOrgIntegration,
306343
authInvalid: false,
@@ -311,6 +348,8 @@ export class VercelSettingsPresenter extends BasePresenter {
311348
customEnvironments,
312349
autoAssignCustomDomains,
313350
vercelManageAccessUrl,
351+
currentTriggerVersion,
352+
currentTriggerVersionFetchFailed,
314353
} as VercelSettingsResult));
315354
}).mapErr((error) => {
316355
// Log the error and return a safe fallback

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
v3BatchPath,
5555
v3BatchRunsPath,
5656
} from "~/utils/pathBuilder";
57+
import { throwNotFound } from "~/utils/httpErrors";
5758

5859
export const meta: MetaFunction = () => {
5960
return [
@@ -74,7 +75,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7475

7576
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
7677
if (!environment) {
77-
throw new Error("Environment not found");
78+
throwNotFound("Environment not found");
7879
}
7980

8081
const url = new URL(request.url);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { useOrganization } from "~/hooks/useOrganizations";
4040
import { useProject } from "~/hooks/useProject";
4141
import { useSearchParams } from "~/hooks/useSearchParam";
4242
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
43+
import { redirectWithErrorMessage } from "~/models/message.server";
4344
import { findProjectBySlug } from "~/models/project.server";
4445
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
4546
import { getRunFiltersFromRequest } from "~/presenters/RunFilters.server";
@@ -59,6 +60,7 @@ import {
5960
v3TestPath,
6061
v3TestTaskPath,
6162
} from "~/utils/pathBuilder";
63+
import { throwNotFound } from "~/utils/httpErrors";
6264
import { ListPagination } from "../../components/ListPagination";
6365
import { CreateBulkActionInspector } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction";
6466
import { Callout } from "~/components/primitives/Callout";
@@ -77,12 +79,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7779

7880
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
7981
if (!project) {
80-
throw new Error("Project not found");
82+
return redirectWithErrorMessage("/", request, "Project not found");
8183
}
8284

8385
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
8486
if (!environment) {
85-
throw new Error("Environment not found");
87+
throwNotFound("Environment not found");
8688
}
8789

8890
const filters = await getRunFiltersFromRequest(request);

0 commit comments

Comments
 (0)