Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.
11 changes: 11 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,9 @@ 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;
/** Hide the section-level master toggles for "Pull env vars" and "Discover new env vars". */
hideSectionToggles?: boolean;
};
Expand All @@ -39,6 +42,7 @@ export function BuildSettingsFields({
disabledEnvSlugs,
autoPromote,
onAutoPromoteChange,
currentTriggerVersion,
hideSectionToggles,
}: BuildSettingsFieldsProps) {
const isSlugDisabled = (slug: EnvSlug) => !!disabledEnvSlugs?.[slug];
Expand Down Expand Up @@ -208,6 +212,13 @@ 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>
)}
</div>

{/* Auto promotion — only visible when atomic deployments are on */}
Expand Down
8 changes: 6 additions & 2 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 @@ -1054,14 +1054,18 @@ export class VercelIntegrationRepository {
return;
}

// TODO: Vercel rejects type changes on existing env vars (encrypted -> sensitive),
// so for projects whose TRIGGER_SECRET_KEY was created before this change, the
// editProjectEnv call will keep the previous type. Recreate via delete-then-create
// to force the upgrade once we're ready to do it project-wide.
await this.upsertVercelEnvVar({
client,
vercelProjectId: projectIntegration.externalEntityId,
teamId,
key: "TRIGGER_SECRET_KEY",
value: params.apiKey,
target: vercelTarget,
type: "encrypted",
type: "sensitive",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

logger.info("Synced regenerated API key to Vercel", {
Expand Down
38 changes: 33 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,9 @@ 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;
};

export type VercelAvailableProject = {
Expand Down Expand Up @@ -248,13 +251,14 @@ export class VercelSettingsPresenter extends BasePresenter {
customEnvironments: VercelCustomEnvironment[];
autoAssignCustomDomains: boolean | null;
vercelManageAccessUrl?: string;
currentTriggerVersion: string | null;
}> => {
if (!orgIntegration) {
return { customEnvironments: [], autoAssignCustomDomains: null };
return { customEnvironments: [], autoAssignCustomDomains: null, currentTriggerVersion: null };
}
const clientResult = await VercelIntegrationRepository.getVercelClient(orgIntegration);
if (clientResult.isErr()) {
return { customEnvironments: [], autoAssignCustomDomains: null };
return { customEnvironments: [], autoAssignCustomDomains: null, currentTriggerVersion: null };
}
const client = clientResult.value;
const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);
Expand All @@ -275,10 +279,10 @@ export class VercelSettingsPresenter extends BasePresenter {
}

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

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

let currentTriggerVersion: string | null = null;
if (triggerVersionResult.isOk()) {
const match = triggerVersionResult.value.find(
(envVar) => envVar.key === "TRIGGER_VERSION" && envVar.target.includes("production")
);
currentTriggerVersion = match?.value ?? null;
} else {
logger.warn("Failed to fetch current TRIGGER_VERSION from Vercel — continuing without it", {
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,
};
};

return fromPromise(
fetchVercelData(),
(error) => ({ type: "other" as const, cause: error })
).map(({ customEnvironments, autoAssignCustomDomains, vercelManageAccessUrl }) => ({
).map(({ customEnvironments, autoAssignCustomDomains, vercelManageAccessUrl, currentTriggerVersion }) => ({
enabled: true,
hasOrgIntegration,
authInvalid: false,
Expand All @@ -311,6 +338,7 @@ export class VercelSettingsPresenter extends BasePresenter {
customEnvironments,
autoAssignCustomDomains,
vercelManageAccessUrl,
currentTriggerVersion,
} as VercelSettingsResult));
}).mapErr((error) => {
// Log the error and return a safe fallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import {
getAvailableEnvSlugsForBuildSettings,
} from "~/v3/vercel/vercelProjectIntegrationSchema";
import { Result, fromPromise } from "neverthrow";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";

export type ConnectedVercelProject = {
id: string;
Expand Down Expand Up @@ -99,6 +99,7 @@ const UpdateVercelConfigFormSchema = z.object({
discoverEnvVars: envSlugArrayField,
vercelStagingEnvironment: z.string().nullable().optional(),
autoPromote: z.string().optional().transform((val) => val !== "false"),
clearTriggerVersion: z.string().optional().transform((val) => val === "true"),
});

const DisconnectVercelFormSchema = z.object({
Expand Down Expand Up @@ -243,6 +244,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
discoverEnvVars,
vercelStagingEnvironment,
autoPromote,
clearTriggerVersion,
} = submission.value;

const parsedStagingEnv = parseVercelStagingEnvironment(vercelStagingEnvironment);
Expand Down Expand Up @@ -271,6 +273,12 @@ export async function action({ request, params }: ActionFunctionArgs) {
);
}

// When atomic deployments are being disabled and the user confirmed clearing the pin,
// remove TRIGGER_VERSION from Vercel production so future deploys don't stay pinned.
if (clearTriggerVersion && !atomicBuilds?.includes("prod")) {
await vercelService.clearTriggerVersionFromVercelProduction(project.id);
}

return redirectWithSuccessMessage(settingsPath, request, "Vercel settings updated successfully");
}

Expand Down Expand Up @@ -573,6 +581,7 @@ function ConnectedVercelProjectForm({
hasPreviewEnvironment,
customEnvironments,
autoAssignCustomDomains,
currentTriggerVersion,
organizationSlug,
projectSlug,
environmentSlug,
Expand All @@ -582,6 +591,7 @@ function ConnectedVercelProjectForm({
hasPreviewEnvironment: boolean;
customEnvironments: Array<{ id: string; slug: string }>;
autoAssignCustomDomains: boolean | null;
currentTriggerVersion: string | null;
organizationSlug: string;
projectSlug: string;
environmentSlug: string;
Expand Down Expand Up @@ -645,6 +655,28 @@ function ConnectedVercelProjectForm({
},
});

const saveButtonRef = useRef<HTMLButtonElement>(null);
const clearTriggerVersionInputRef = useRef<HTMLInputElement>(null);
const [showClearDialog, setShowClearDialog] = useState(false);

// Modal trigger uses the page-load state of atomicBuilds, not whatever changed in-session,
// because clearing TRIGGER_VERSION only makes sense when atomic was actually on at load time.
const wasAtomicEnabledAtLoad = originalAtomicBuilds.includes("prod");
const isAtomicNowDisabled = !configValues.atomicBuilds.includes("prod");
const shouldPromptClearOnSave =
wasAtomicEnabledAtLoad && isAtomicNowDisabled && Boolean(currentTriggerVersion);

const submitWithClearChoice = (clear: boolean) => {
if (clearTriggerVersionInputRef.current) {
clearTriggerVersionInputRef.current.value = clear ? "true" : "false";
}
setShowClearDialog(false);
// Conform owns the form's React ref via {...configForm.props}, so look it up by id
// (set via useForm({ id: "update-vercel-config" })) rather than fighting for the ref.
const form = document.getElementById("update-vercel-config") as HTMLFormElement | null;
form?.requestSubmit(saveButtonRef.current ?? undefined);
};

const isConfigLoading =
navigation.formData?.get("action") === "update-config" &&
(navigation.state === "submitting" || navigation.state === "loading");
Expand Down Expand Up @@ -742,6 +774,13 @@ function ConnectedVercelProjectForm({
name="autoPromote"
value={String(configValues.autoPromote)}
/>
{/* Toggled to "true" by the clear-pinned-version modal; defaults to "false". */}
<input
type="hidden"
name="clearTriggerVersion"
defaultValue="false"
ref={clearTriggerVersionInputRef}
/>

<Fieldset>
<InputGroup fullWidth>
Expand Down Expand Up @@ -819,6 +858,7 @@ function ConnectedVercelProjectForm({
onAutoPromoteChange={(value) =>
setConfigValues((prev) => ({ ...prev, autoPromote: value }))
}
currentTriggerVersion={currentTriggerVersion}
hideSectionToggles
/>

Expand Down Expand Up @@ -862,19 +902,68 @@ function ConnectedVercelProjectForm({
<FormButtons
confirmButton={
<Button
ref={saveButtonRef}
type="submit"
name="action"
value="update-config"
variant="secondary/small"
disabled={isConfigLoading || !hasConfigChanges}
LeadingIcon={isConfigLoading ? SpinnerWhite : undefined}
onClick={(event) => {
if (shouldPromptClearOnSave) {
event.preventDefault();
setShowClearDialog(true);
}
}}
>
Save
</Button>
}
/>
</Fieldset>
</Form>

<Dialog open={showClearDialog} onOpenChange={setShowClearDialog}>
<DialogContent className="max-w-md">
<DialogHeader>Clear TRIGGER_VERSION from Vercel?</DialogHeader>
<div className="flex flex-col gap-3 pt-3">
<Paragraph className="mb-1">
Atomic deployments are being turned off. The{" "}
<span className="font-mono text-text-bright">TRIGGER_VERSION</span> env var on your
Vercel production environment is currently set to{" "}
<span className="font-mono text-text-bright">{currentTriggerVersion}</span>.
</Paragraph>
<Paragraph className="mb-1">
If you leave it, your Vercel project will stay pinned to this version. Since atomic
deployments will be off, Trigger.dev will no longer update this variable, and future
Vercel deploys will continue using this pinned version. We recommend clearing it.
</Paragraph>
<FormButtons
confirmButton={
<div className="flex gap-2">
<Button
variant="secondary/medium"
onClick={() => submitWithClearChoice(false)}
>
Keep pinned
</Button>
<Button
variant="primary/medium"
onClick={() => submitWithClearChoice(true)}
>
Clear and disable
</Button>
</div>
}
cancelButton={
<DialogClose asChild>
<Button variant="tertiary/medium">Cancel</Button>
</DialogClose>
}
/>
</div>
</DialogContent>
</Dialog>
</>
);
}
Expand Down Expand Up @@ -948,6 +1037,7 @@ function VercelSettingsPanel({
hasPreviewEnvironment={data.hasPreviewEnvironment}
customEnvironments={data.customEnvironments}
autoAssignCustomDomains={data.autoAssignCustomDomains ?? null}
currentTriggerVersion={data.currentTriggerVersion ?? null}
organizationSlug={organizationSlug}
projectSlug={projectSlug}
environmentSlug={environmentSlug}
Expand Down
Loading