Skip to content

Commit 74bde91

Browse files
authored
Merge branch 'main' into feat/expose-is-warm-start-trql
2 parents e931cd4 + 2fbac48 commit 74bde91

6 files changed

Lines changed: 334 additions & 44 deletions

File tree

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

0 commit comments

Comments
 (0)