Skip to content

Commit 2f03e2a

Browse files
committed
feat(vercel): Update Vercel SDK and enhance environment variable handling with improved filtering and error management
1 parent 7585145 commit 2f03e2a

File tree

6 files changed

+145
-97
lines changed

6 files changed

+145
-97
lines changed

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

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
import type {
77
GetV9ProjectsIdOrNameCustomEnvironmentsEnvironments,
88
} from "@vercel/sdk/models/getv9projectsidornamecustomenvironmentsop";
9-
import type { GetProjectsProjects } from "@vercel/sdk/models/getprojectsop";
9+
import type { ResponseBodyProjects } from "@vercel/sdk/models/getprojectsop";
1010
import {
1111
Organization,
1212
OrganizationIntegration,
@@ -192,7 +192,7 @@ export type VercelEnvironmentVariableValue = {
192192
};
193193

194194
/** Narrowed Vercel project type – only id and name. */
195-
export type VercelProject = Pick<GetProjectsProjects, "id" | "name">;
195+
export type VercelProject = Pick<ResponseBodyProjects, "id" | "name">;
196196

197197
// ---------------------------------------------------------------------------
198198
// Mapper functions – narrow wide SDK responses into our domain types.
@@ -429,29 +429,81 @@ export class VercelIntegrationRepository {
429429
client: Vercel,
430430
projectId: string,
431431
teamId?: string | null,
432-
target?: string
432+
target?: string,
433+
/** If provided, only include keys that pass this filter */
434+
shouldIncludeKey?: (key: string) => boolean
433435
): ResultAsync<VercelEnvironmentVariableValue[], VercelApiError> {
434436
return wrapVercelCall(
435437
client.projects.filterProjectEnvs({
436438
idOrName: projectId,
437439
...(teamId && { teamId }),
438-
decrypt: "true",
439440
}),
440441
"Failed to fetch Vercel environment variable values",
441442
{ projectId, teamId, target }
442-
).map((response) => {
443-
const envs = extractVercelEnvs(response);
444-
return envs
445-
.filter((env) => {
446-
if (!env.value) return false;
447-
if (target) return normalizeTarget(env.target).includes(target);
448-
return true;
449-
})
450-
.map(toVercelEnvironmentVariableValue)
451-
.filter((v): v is VercelEnvironmentVariableValue => v !== null);
443+
).andThen((response) => {
444+
// Apply all filters BEFORE decryption to avoid unnecessary API calls
445+
const filteredEnvs = extractVercelEnvs(response).filter((env) => {
446+
if (target && !normalizeTarget(env.target).includes(target)) return false;
447+
if (shouldIncludeKey && !shouldIncludeKey(env.key)) return false;
448+
if (isVercelSecretType(env.type)) return false;
449+
return true;
450+
});
451+
452+
// Fetch decrypted values for encrypted vars, use list values for others
453+
return ResultAsync.fromPromise(
454+
Promise.all(
455+
filteredEnvs.map((env) => this.#resolveEnvVarValue(client, projectId, teamId, env))
456+
),
457+
(error) => toVercelApiError(error)
458+
).map((results) => results.filter((v): v is VercelEnvironmentVariableValue => v !== null));
452459
});
453460
}
454461

462+
static async #resolveEnvVarValue(
463+
client: Vercel,
464+
projectId: string,
465+
teamId: string | null | undefined,
466+
env: ResponseBodyEnvs
467+
): Promise<VercelEnvironmentVariableValue | null> {
468+
// Non-encrypted vars: use value from list response if present
469+
if (env.type !== "encrypted" || !env.id) {
470+
if (env.value === undefined || env.value === null) return null;
471+
return toVercelEnvironmentVariableValue(env);
472+
}
473+
474+
// Encrypted vars: fetch decrypted value via individual endpoint
475+
// (list endpoint's decrypt param is deprecated)
476+
const result = await ResultAsync.fromPromise(
477+
client.projects.getProjectEnv({
478+
idOrName: projectId,
479+
id: env.id,
480+
...(teamId && { teamId }),
481+
}),
482+
(error) => error
483+
);
484+
485+
if (result.isErr()) {
486+
logger.warn("Failed to decrypt Vercel env var", {
487+
projectId,
488+
envVarKey: env.key,
489+
error: result.error instanceof Error ? result.error.message : String(result.error),
490+
});
491+
return null;
492+
}
493+
494+
// API returns union: ResponseBody1 has no value, ResponseBody2/3 have value
495+
const decryptedValue = (result.value as { value?: string }).value;
496+
if (typeof decryptedValue !== "string") return null;
497+
498+
return {
499+
key: env.key,
500+
value: decryptedValue,
501+
target: normalizeTarget(env.target),
502+
type: env.type,
503+
isSecret: false,
504+
};
505+
}
506+
455507
static getVercelSharedEnvironmentVariables(
456508
client: Vercel,
457509
teamId: string,
@@ -632,7 +684,12 @@ export class VercelIntegrationRepository {
632684
"Failed to fetch Vercel projects",
633685
{ teamId }
634686
).map((response) => {
635-
const projects = response.projects || [];
687+
// GetProjectsResponseBody is a union: objects with `projects` array, or direct array
688+
const projects = Array.isArray(response)
689+
? response
690+
: "projects" in response
691+
? response.projects
692+
: [];
636693
return projects.map(({ id, name }): VercelProject => ({ id, name }));
637694
});
638695
}
@@ -970,6 +1027,14 @@ export class VercelIntegrationRepository {
9701027
syncEnvVarsMapping: SyncEnvVarsMapping;
9711028
orgIntegration: OrganizationIntegration & { tokenReference: SecretReference };
9721029
}): ResultAsync<{ syncedCount: number; errors: string[] }, VercelApiError> {
1030+
logger.info("pullEnvVarsFromVercel: Starting", {
1031+
projectId: params.projectId,
1032+
vercelProjectId: params.vercelProjectId,
1033+
teamId: params.teamId,
1034+
vercelStagingEnvironment: params.vercelStagingEnvironment,
1035+
syncEnvVarsMappingKeys: Object.keys(params.syncEnvVarsMapping),
1036+
});
1037+
9731038
return this.getVercelClient(params.orgIntegration).andThen((client) =>
9741039
ResultAsync.fromPromise(
9751040
(async () => {
@@ -1046,20 +1111,31 @@ export class VercelIntegrationRepository {
10461111
for (const mapping of envMapping) {
10471112
const iterResult = await ResultAsync.fromPromise(
10481113
(async () => {
1114+
// Build filter to avoid decrypting vars that will be filtered out anyway
1115+
const excludeKeys = new Set(["TRIGGER_SECRET_KEY", "TRIGGER_VERSION"]);
1116+
const shouldIncludeKey = (key: string) =>
1117+
!excludeKeys.has(key) &&
1118+
shouldSyncEnvVar(params.syncEnvVarsMapping, key, mapping.triggerEnvType as TriggerEnvironmentType);
1119+
10491120
const envVarsResult = await this.getVercelEnvironmentVariableValues(
10501121
client,
10511122
params.vercelProjectId,
10521123
params.teamId,
1053-
mapping.vercelTarget
1124+
mapping.vercelTarget,
1125+
shouldIncludeKey
10541126
);
10551127

10561128
if (envVarsResult.isErr()) {
1129+
logger.error("pullEnvVarsFromVercel: Failed to get env vars", {
1130+
triggerEnvType: mapping.triggerEnvType,
1131+
vercelTarget: mapping.vercelTarget,
1132+
error: envVarsResult.error.message,
1133+
});
10571134
errors.push(`Failed to get env vars for ${mapping.triggerEnvType}: ${envVarsResult.error.message}`);
10581135
return;
10591136
}
10601137

10611138
const projectEnvVars = envVarsResult.value;
1062-
10631139
const standardTargets = ["production", "preview", "development"];
10641140
const isCustomEnvironment = !standardTargets.includes(mapping.vercelTarget);
10651141

@@ -1084,7 +1160,7 @@ export class VercelIntegrationRepository {
10841160
if (envVar.isSecret) {
10851161
return false;
10861162
}
1087-
if (envVar.key === "TRIGGER_SECRET_KEY") {
1163+
if (envVar.key === "TRIGGER_SECRET_KEY" || envVar.key === "TRIGGER_VERSION") {
10881164
return false;
10891165
}
10901166
return shouldSyncEnvVar(

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -489,11 +489,12 @@ export class VercelSettingsPresenter extends BasePresenter {
489489
const projectEnvVars = projectEnvVarsResult.isOk() ? projectEnvVarsResult.value : [];
490490
const sharedEnvVars = sharedEnvVarsResult.isOk() ? sharedEnvVarsResult.value : [];
491491

492-
// Filter out TRIGGER_SECRET_KEY (managed by Trigger.dev) and merge project + shared env vars
492+
// Filter out TRIGGER_SECRET_KEY and TRIGGER_VERSION (managed by Trigger.dev) and merge project + shared env vars
493+
const excludedKeys = new Set(["TRIGGER_SECRET_KEY", "TRIGGER_VERSION"]);
493494
const projectEnvVarKeys = new Set(projectEnvVars.map((v) => v.key));
494495
const mergedEnvVars: VercelEnvironmentVariable[] = [
495496
...projectEnvVars
496-
.filter((v) => v.key !== "TRIGGER_SECRET_KEY")
497+
.filter((v) => !excludedKeys.has(v.key))
497498
.map((v) => {
498499
const envVar = { ...v };
499500
if (vercelEnvironmentId && (v as any).customEnvironmentIds?.includes(vercelEnvironmentId)) {
@@ -502,7 +503,7 @@ export class VercelSettingsPresenter extends BasePresenter {
502503
return envVar;
503504
}),
504505
...sharedEnvVars
505-
.filter((v) => !projectEnvVarKeys.has(v.key) && v.key !== "TRIGGER_SECRET_KEY")
506+
.filter((v) => !projectEnvVarKeys.has(v.key) && !excludedKeys.has(v.key))
506507
.map((v) => {
507508
const envVar = {
508509
id: v.id,

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -276,24 +276,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
276276
skipRedirect,
277277
} = submission.value;
278278

279-
let parsedMapping: SyncEnvVarsMapping = {};
280-
if (syncEnvVarsMapping) {
281-
const parseResult = safeJsonParse(syncEnvVarsMapping);
282-
if (parseResult.isOk()) {
283-
parsedMapping = parseResult.value as SyncEnvVarsMapping;
284-
} else {
285-
logger.error("Failed to parse syncEnvVarsMapping", { error: parseResult.error });
286-
}
287-
}
288-
289279
const parsedStagingEnv = parseVercelStagingEnvironment(vercelStagingEnvironment);
280+
const parsedSyncEnvVarsMapping = syncEnvVarsMapping
281+
? safeJsonParse(syncEnvVarsMapping).unwrapOr(undefined) as SyncEnvVarsMapping | undefined
282+
: undefined;
290283

291284
const result = await vercelService.completeOnboarding(project.id, {
292285
vercelStagingEnvironment: parsedStagingEnv,
293286
pullEnvVarsBeforeBuild: pullEnvVarsBeforeBuild as EnvSlug[] | null,
294287
atomicBuilds: atomicBuilds as EnvSlug[] | null,
295288
discoverEnvVars: discoverEnvVars as EnvSlug[] | null,
296-
syncEnvVarsMapping: parsedMapping,
289+
syncEnvVarsMapping: parsedSyncEnvVarsMapping,
297290
});
298291

299292
if (result) {

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

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -398,14 +398,15 @@ export class VercelIntegrationService {
398398
pullEnvVarsBeforeBuild?: EnvSlug[] | null;
399399
atomicBuilds?: EnvSlug[] | null;
400400
discoverEnvVars?: EnvSlug[] | null;
401-
syncEnvVarsMapping: SyncEnvVarsMapping;
401+
syncEnvVarsMapping?: SyncEnvVarsMapping;
402402
}
403403
): Promise<VercelProjectIntegrationWithParsedData | null> {
404404
const existing = await this.getVercelProjectIntegration(projectId);
405405
if (!existing) {
406406
return null;
407407
}
408408

409+
const syncEnvVarsMapping = params.syncEnvVarsMapping ?? { "dev":{}, "stg":{}, "prod":{}, "preview":{} };
409410
const updatedData: VercelProjectIntegrationData = {
410411
...existing.parsedIntegrationData,
411412
config: {
@@ -415,7 +416,7 @@ export class VercelIntegrationService {
415416
discoverEnvVars: params.discoverEnvVars ?? null,
416417
vercelStagingEnvironment: params.vercelStagingEnvironment ?? null,
417418
},
418-
syncEnvVarsMapping: params.syncEnvVarsMapping ?? existing.parsedIntegrationData.syncEnvVarsMapping,
419+
syncEnvVarsMapping: existing.parsedIntegrationData.syncEnvVarsMapping,
419420
onboardingCompleted: true,
420421
};
421422

@@ -433,41 +434,24 @@ export class VercelIntegrationService {
433434
if (orgIntegration) {
434435
const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration);
435436

436-
logger.info("Vercel onboarding: pulling env vars from Vercel", {
437-
projectId,
438-
vercelProjectId: updatedData.vercelProjectId,
439-
teamId,
440-
vercelStagingEnvironment: params.vercelStagingEnvironment,
441-
syncEnvVarsMappingKeys: Object.keys(params.syncEnvVarsMapping),
442-
});
443-
444437
const pullResult = await VercelIntegrationRepository.pullEnvVarsFromVercel({
445438
projectId,
446439
vercelProjectId: updatedData.vercelProjectId,
447440
teamId,
448441
vercelStagingEnvironment: params.vercelStagingEnvironment,
449-
syncEnvVarsMapping: params.syncEnvVarsMapping,
442+
syncEnvVarsMapping,
450443
orgIntegration,
451444
});
452445

453446
if (pullResult.isErr()) {
454447
logger.error("Failed to pull env vars from Vercel during onboarding", {
455448
projectId,
456-
vercelProjectId: updatedData.vercelProjectId,
457449
error: pullResult.error.message,
458450
});
459451
} else if (pullResult.value.errors.length > 0) {
460-
logger.warn("Some errors occurred while pulling env vars from Vercel", {
452+
logger.warn("Errors pulling env vars from Vercel during onboarding", {
461453
projectId,
462-
vercelProjectId: updatedData.vercelProjectId,
463454
errors: pullResult.value.errors,
464-
syncedCount: pullResult.value.syncedCount,
465-
});
466-
} else {
467-
logger.info("Successfully pulled env vars from Vercel", {
468-
projectId,
469-
vercelProjectId: updatedData.vercelProjectId,
470-
syncedCount: pullResult.value.syncedCount,
471455
});
472456
}
473457

apps/webapp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
"@unkey/cache": "^1.5.0",
130130
"@unkey/error": "^0.2.0",
131131
"@upstash/ratelimit": "^1.1.3",
132-
"@vercel/sdk": "^1.18.5",
132+
"@vercel/sdk": "^1.18.10",
133133
"@whatwg-node/fetch": "^0.9.14",
134134
"ai": "^4.3.19",
135135
"assert-never": "^1.2.1",

0 commit comments

Comments
 (0)