Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
area: webapp
type: feature
---

Show the currently pinned `TRIGGER_VERSION` under the Atomic deployments toggle on the Vercel
integration settings, and prompt the user to clear it from Vercel production when they disable
atomic deployments. Also mark `TRIGGER_SECRET_KEY` writes to Vercel as `sensitive` so the value
cannot be read back from the Vercel dashboard or API once written.
22 changes: 22 additions & 0 deletions apps/webapp/app/components/integrations/VercelBuildSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ type BuildSettingsFieldsProps = {
disabledEnvSlugs?: Partial<Record<EnvSlug, string>>;
autoPromote?: boolean;
onAutoPromoteChange?: (value: boolean) => void;
/** The currently pinned TRIGGER_VERSION on Vercel production, if any. Shown under the
* Atomic deployments toggle so the user knows what version is set on Vercel right now. */
currentTriggerVersion?: string | null;
/** True when the Vercel lookup for TRIGGER_VERSION failed. We show this so the user knows
* the pin status is unknown — distinct from "not set". */
currentTriggerVersionFetchFailed?: boolean;
/** Hide the section-level master toggles for "Pull env vars" and "Discover new env vars". */
hideSectionToggles?: boolean;
};
Expand All @@ -39,6 +45,8 @@ export function BuildSettingsFields({
disabledEnvSlugs,
autoPromote,
onAutoPromoteChange,
currentTriggerVersion,
currentTriggerVersionFetchFailed,
hideSectionToggles,
}: BuildSettingsFieldsProps) {
const isSlugDisabled = (slug: EnvSlug) => !!disabledEnvSlugs?.[slug];
Expand Down Expand Up @@ -208,6 +216,20 @@ export function BuildSettingsFields({
</TextLink>
.
</Hint>
{currentTriggerVersion && (
<Hint className="pr-6">
Currently pinned to{" "}
<span className="font-mono text-text-bright">{currentTriggerVersion}</span> in Vercel
production.
</Hint>
)}
{!currentTriggerVersion && currentTriggerVersionFetchFailed && (
<Hint className="pr-6 text-warning">
Couldn't read{" "}
<span className="font-mono text-text-bright">TRIGGER_VERSION</span> from Vercel —
check the Vercel dashboard to confirm the production pin.
</Hint>
)}
</div>

{/* Auto promotion — only visible when atomic deployments are on */}
Expand Down
70 changes: 33 additions & 37 deletions apps/webapp/app/models/vercelIntegration.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,7 @@ export class VercelIntegrationRepository {
key: "TRIGGER_SECRET_KEY",
value: runtimeEnv.apiKey,
target: vercelTarget,
type: "encrypted",
type: "sensitive",
environmentType: runtimeEnv.type,
});
}
Expand Down Expand Up @@ -1061,7 +1061,7 @@ export class VercelIntegrationRepository {
key: "TRIGGER_SECRET_KEY",
value: params.apiKey,
target: vercelTarget,
type: "encrypted",
type: "sensitive",
});
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

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

// Always delete-then-create rather than editProjectEnv, because Vercel rejects
// in-place type changes (e.g. encrypted -> sensitive).
if (existingEnv && existingEnv.id) {
await client.projects.editProjectEnv({
idOrName: vercelProjectId,
id: existingEnv.id,
...(teamId && { teamId }),
requestBody: {
value,
type,
},
});
} else {
await client.projects.createProjectEnv({
await client.projects.batchRemoveProjectEnv({
idOrName: vercelProjectId,
...(teamId && { teamId }),
requestBody: {
key,
value,
type,
customEnvironmentIds: [customEnvironmentId],
} as any,
requestBody: { ids: [existingEnv.id] },
});
}

await client.projects.createProjectEnv({
idOrName: vercelProjectId,
...(teamId && { teamId }),
requestBody: {
key,
value,
type,
customEnvironmentIds: [customEnvironmentId],
} as any,
});
})(),
(error) => toVercelApiError(error)
)
Expand Down Expand Up @@ -1709,29 +1707,27 @@ export class VercelIntegrationRepository {
return target.length === envTargets.length && target.every((t) => envTargets.includes(t));
});

// Always delete-then-create rather than editProjectEnv, because Vercel rejects
// in-place type changes (e.g. encrypted -> sensitive). Same approach used by
// syncApiKeysToVercel via removeAllVercelEnvVarsByKey.
if (existingEnv && existingEnv.id) {
await client.projects.editProjectEnv({
idOrName: vercelProjectId,
id: existingEnv.id,
...(teamId && { teamId }),
requestBody: {
value,
target: target as any,
type,
},
});
} else {
await client.projects.createProjectEnv({
await client.projects.batchRemoveProjectEnv({
idOrName: vercelProjectId,
...(teamId && { teamId }),
requestBody: {
key,
value,
target: target as any,
type,
},
requestBody: { ids: [existingEnv.id] },
});
}

await client.projects.createProjectEnv({
idOrName: vercelProjectId,
...(teamId && { teamId }),
requestBody: {
key,
value,
target: target as any,
type,
},
});
}

static getAutoAssignCustomDomains(
Expand Down
49 changes: 44 additions & 5 deletions apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export type VercelSettingsResult = {
autoAssignCustomDomains?: boolean | null;
/** URL to manage Vercel integration access (project sharing) on vercel.com */
vercelManageAccessUrl?: string;
/** The currently pinned TRIGGER_VERSION on Vercel production, if set. Used to surface
* the pin in the UI and prompt the user to clear it when atomic deployments are disabled. */
currentTriggerVersion?: string | null;
/** True when the Vercel lookup for TRIGGER_VERSION failed (network/auth/etc). Distinct
* from "no pin set" — the UI uses this to warn the user and still prompt them on disable
* so they can manually verify that production isn't pinned. */
currentTriggerVersionFetchFailed?: boolean;
};

export type VercelAvailableProject = {
Expand Down Expand Up @@ -248,13 +255,17 @@ export class VercelSettingsPresenter extends BasePresenter {
customEnvironments: VercelCustomEnvironment[];
autoAssignCustomDomains: boolean | null;
vercelManageAccessUrl?: string;
currentTriggerVersion: string | null;
currentTriggerVersionFetchFailed: boolean;
}> => {
if (!orgIntegration) {
return { customEnvironments: [], autoAssignCustomDomains: null };
return { customEnvironments: [], autoAssignCustomDomains: null, currentTriggerVersion: null, currentTriggerVersionFetchFailed: false };
}
const clientResult = await VercelIntegrationRepository.getVercelClient(orgIntegration);
if (clientResult.isErr()) {
return { customEnvironments: [], autoAssignCustomDomains: null };
// We couldn't even build a Vercel client — treat as fetch failure so the UI
// still prompts the user when they disable atomic deployments.
return { customEnvironments: [], autoAssignCustomDomains: null, currentTriggerVersion: null, currentTriggerVersionFetchFailed: true };
}
const client = clientResult.value;
const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);
Expand All @@ -275,10 +286,10 @@ export class VercelSettingsPresenter extends BasePresenter {
}

if (!connectedProject) {
return { customEnvironments: [], autoAssignCustomDomains: null, vercelManageAccessUrl };
return { customEnvironments: [], autoAssignCustomDomains: null, vercelManageAccessUrl, currentTriggerVersion: null, currentTriggerVersionFetchFailed: false };
}

const [customEnvsResult, autoAssignResult] = await Promise.all([
const [customEnvsResult, autoAssignResult, triggerVersionResult] = await Promise.all([
VercelIntegrationRepository.getVercelCustomEnvironments(
client,
connectedProject.vercelProjectId,
Expand All @@ -289,18 +300,44 @@ export class VercelSettingsPresenter extends BasePresenter {
connectedProject.vercelProjectId,
teamId
),
VercelIntegrationRepository.getVercelEnvironmentVariableValues(
client,
connectedProject.vercelProjectId,
teamId,
"production",
(key) => key === "TRIGGER_VERSION"
),
]);

let currentTriggerVersion: string | null = null;
let currentTriggerVersionFetchFailed = false;
if (triggerVersionResult.isOk()) {
const match = triggerVersionResult.value.find(
(envVar) => envVar.key === "TRIGGER_VERSION" && envVar.target.includes("production")
);
currentTriggerVersion = match?.value ?? null;
} else {
currentTriggerVersionFetchFailed = true;
logger.warn("Failed to fetch current TRIGGER_VERSION from Vercel — surfacing as unknown", {
projectId,
vercelProjectId: connectedProject.vercelProjectId,
error: triggerVersionResult.error.message,
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
customEnvironments: customEnvsResult.isOk() ? customEnvsResult.value : [],
autoAssignCustomDomains: autoAssignResult.isOk() ? autoAssignResult.value : null,
vercelManageAccessUrl,
currentTriggerVersion,
currentTriggerVersionFetchFailed,
};
};

return fromPromise(
fetchVercelData(),
(error) => ({ type: "other" as const, cause: error })
).map(({ customEnvironments, autoAssignCustomDomains, vercelManageAccessUrl }) => ({
).map(({ customEnvironments, autoAssignCustomDomains, vercelManageAccessUrl, currentTriggerVersion, currentTriggerVersionFetchFailed }) => ({
enabled: true,
hasOrgIntegration,
authInvalid: false,
Expand All @@ -311,6 +348,8 @@ export class VercelSettingsPresenter extends BasePresenter {
customEnvironments,
autoAssignCustomDomains,
vercelManageAccessUrl,
currentTriggerVersion,
currentTriggerVersionFetchFailed,
} as VercelSettingsResult));
}).mapErr((error) => {
// Log the error and return a safe fallback
Expand Down
Loading