Skip to content

Commit aae7957

Browse files
committed
feat(vercel): preserve secret status when syncing env vars&improve setting names
- Query existing environment variable values to detect which keys are currently marked as secrets and build a set of secret keys. - Split variables into secret and non-secret groups so non-secrets can be created without unintentionally overriding secret flags. - Create non-secret variables via envVarRepository.create and update synced counters and error handling accordingly. - Rename the Vercel integration settings to be more meaningful - Iterate on Vercel onboarding flow, add GitHub step
1 parent 1c88cfa commit aae7957

File tree

10 files changed

+1161
-545
lines changed

10 files changed

+1161
-545
lines changed

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

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,15 @@ export class VercelIntegrationRepository {
11871187
}
11881188
}
11891189

1190+
logger.info("Vercel pullEnvVarsFromVercel: environment mapping", {
1191+
projectId: params.projectId,
1192+
vercelProjectId: params.vercelProjectId,
1193+
envMappingCount: envMapping.length,
1194+
envMapping: envMapping.map(m => ({ type: m.triggerEnvType, target: m.vercelTarget })),
1195+
runtimeEnvironmentsCount: runtimeEnvironments.length,
1196+
runtimeEnvironments: runtimeEnvironments.map(e => e.type),
1197+
});
1198+
11901199
if (envMapping.length === 0) {
11911200
logger.warn("No environments to sync for Vercel integration", {
11921201
projectId: params.projectId,
@@ -1252,6 +1261,16 @@ export class VercelIntegrationRepository {
12521261
continue;
12531262
}
12541263

1264+
logger.info("Vercel pullEnvVarsFromVercel: fetched env vars for target", {
1265+
projectId: params.projectId,
1266+
vercelTarget: mapping.vercelTarget,
1267+
triggerEnvType: mapping.triggerEnvType,
1268+
projectEnvVarsCount: projectEnvVars.length,
1269+
sharedEnvVarsCount: filteredSharedEnvVars.length,
1270+
mergedEnvVarsCount: mergedEnvVars.length,
1271+
mergedEnvVarKeys: mergedEnvVars.map(v => v.key),
1272+
});
1273+
12551274
// Filter env vars based on syncEnvVarsMapping and exclude TRIGGER_SECRET_KEY
12561275
const varsToSync = mergedEnvVars.filter((envVar) => {
12571276
// Skip secrets (they don't have values anyway)
@@ -1270,34 +1289,105 @@ export class VercelIntegrationRepository {
12701289
);
12711290
});
12721291

1292+
logger.info("Vercel pullEnvVarsFromVercel: filtered vars to sync", {
1293+
projectId: params.projectId,
1294+
vercelTarget: mapping.vercelTarget,
1295+
triggerEnvType: mapping.triggerEnvType,
1296+
varsToSyncCount: varsToSync.length,
1297+
varsToSyncKeys: varsToSync.map(v => v.key),
1298+
});
1299+
12731300
if (varsToSync.length === 0) {
12741301
continue;
12751302
}
12761303

1277-
// Create env vars in Trigger.dev
1278-
const result = await envVarRepository.create(params.projectId, {
1279-
override: true, // Override existing vars
1280-
environmentIds: [mapping.runtimeEnvironmentId],
1281-
isSecret: false, // Vercel env vars we can read are not secrets in our system
1282-
variables: varsToSync.map((v) => ({
1283-
key: v.key,
1284-
value: v.value,
1285-
})),
1304+
// Query existing env vars to check which ones are already secrets
1305+
// We need to preserve the secret status when overriding
1306+
const existingSecretKeys = new Set<string>();
1307+
const existingVarValues = await prisma.environmentVariableValue.findMany({
1308+
where: {
1309+
environmentId: mapping.runtimeEnvironmentId,
1310+
variable: {
1311+
projectId: params.projectId,
1312+
key: {
1313+
in: varsToSync.map((v) => v.key),
1314+
},
1315+
},
1316+
},
1317+
select: {
1318+
isSecret: true,
1319+
variable: {
1320+
select: {
1321+
key: true,
1322+
},
1323+
},
1324+
},
12861325
});
12871326

1288-
if (result.success) {
1289-
syncedCount += varsToSync.length;
1290-
} else {
1291-
const errorMsg = `Failed to sync env vars for ${mapping.triggerEnvType}: ${result.error}`;
1292-
errors.push(errorMsg);
1293-
logger.error(errorMsg, {
1294-
projectId: params.projectId,
1295-
vercelProjectId: params.vercelProjectId,
1296-
vercelTarget: mapping.vercelTarget,
1297-
error: result.error,
1298-
variableErrors: result.variableErrors,
1299-
attemptedKeys: varsToSync.map((v) => v.key),
1327+
for (const varValue of existingVarValues) {
1328+
if (varValue.isSecret) {
1329+
existingSecretKeys.add(varValue.variable.key);
1330+
}
1331+
}
1332+
1333+
// Split vars into secret and non-secret groups
1334+
const secretVars = varsToSync.filter((v) => existingSecretKeys.has(v.key));
1335+
const nonSecretVars = varsToSync.filter((v) => !existingSecretKeys.has(v.key));
1336+
1337+
// Create non-secret vars
1338+
if (nonSecretVars.length > 0) {
1339+
const result = await envVarRepository.create(params.projectId, {
1340+
override: true,
1341+
environmentIds: [mapping.runtimeEnvironmentId],
1342+
isSecret: false,
1343+
variables: nonSecretVars.map((v) => ({
1344+
key: v.key,
1345+
value: v.value,
1346+
})),
1347+
});
1348+
1349+
if (result.success) {
1350+
syncedCount += nonSecretVars.length;
1351+
} else {
1352+
const errorMsg = `Failed to sync env vars for ${mapping.triggerEnvType}: ${result.error}`;
1353+
errors.push(errorMsg);
1354+
logger.error(errorMsg, {
1355+
projectId: params.projectId,
1356+
vercelProjectId: params.vercelProjectId,
1357+
vercelTarget: mapping.vercelTarget,
1358+
error: result.error,
1359+
variableErrors: result.variableErrors,
1360+
attemptedKeys: nonSecretVars.map((v) => v.key),
1361+
});
1362+
}
1363+
}
1364+
1365+
// Create secret vars (preserve their secret status)
1366+
if (secretVars.length > 0) {
1367+
const result = await envVarRepository.create(params.projectId, {
1368+
override: true,
1369+
environmentIds: [mapping.runtimeEnvironmentId],
1370+
isSecret: true, // Preserve secret status
1371+
variables: secretVars.map((v) => ({
1372+
key: v.key,
1373+
value: v.value,
1374+
})),
13001375
});
1376+
1377+
if (result.success) {
1378+
syncedCount += secretVars.length;
1379+
} else {
1380+
const errorMsg = `Failed to sync secret env vars for ${mapping.triggerEnvType}: ${result.error}`;
1381+
errors.push(errorMsg);
1382+
logger.error(errorMsg, {
1383+
projectId: params.projectId,
1384+
vercelProjectId: params.vercelProjectId,
1385+
vercelTarget: mapping.vercelTarget,
1386+
error: result.error,
1387+
variableErrors: result.variableErrors,
1388+
attemptedKeys: secretVars.map((v) => v.key),
1389+
});
1390+
}
13011391
}
13021392
} catch (envError) {
13031393
const errorMsg = `Failed to process env vars for ${mapping.triggerEnvType}: ${envError instanceof Error ? envError.message : "Unknown error"}`;

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmen
55
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
66
import {
77
SyncEnvVarsMapping,
8+
EnvSlug,
89
} from "~/v3/vercel/vercelProjectIntegrationSchema";
910
import { VercelIntegrationService } from "~/services/vercelIntegration.server";
1011

@@ -102,11 +103,11 @@ export class EnvironmentVariablesPresenter {
102103
const vercelIntegration = await vercelService.getVercelProjectIntegration(project.id, true);
103104

104105
let vercelSyncEnvVarsMapping: SyncEnvVarsMapping = {};
105-
let vercelPullEnvVarsEnabled = false;
106+
let vercelPullEnvVarsBeforeBuild: EnvSlug[] | null = null;
106107

107108
if (vercelIntegration) {
108109
vercelSyncEnvVarsMapping = vercelIntegration.parsedIntegrationData.syncEnvVarsMapping;
109-
vercelPullEnvVarsEnabled = vercelIntegration.parsedIntegrationData.config.pullEnvVarsFromVercel;
110+
vercelPullEnvVarsBeforeBuild = vercelIntegration.parsedIntegrationData.config.pullEnvVarsBeforeBuild ?? null;
110111
}
111112

112113
return {
@@ -146,7 +147,7 @@ export class EnvironmentVariablesPresenter {
146147
vercelIntegration: vercelIntegration
147148
? {
148149
enabled: true,
149-
pullEnvVarsEnabled: vercelPullEnvVarsEnabled,
150+
pullEnvVarsBeforeBuild: vercelPullEnvVarsBeforeBuild,
150151
syncEnvVarsMapping: vercelSyncEnvVarsMapping,
151152
}
152153
: null,

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

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type PrismaClient, type RuntimeEnvironmentType } from "@trigger.dev/database";
1+
import { type PrismaClient } from "@trigger.dev/database";
22
import { fromPromise, ok, ResultAsync } from "neverthrow";
33
import { env } from "~/env.server";
44
import { OrgIntegrationRepository } from "~/models/orgIntegration.server";
@@ -40,13 +40,29 @@ export type VercelAvailableProject = {
4040
name: string;
4141
};
4242

43+
export type GitHubAppInstallationForVercel = {
44+
id: string;
45+
appInstallationId: bigint;
46+
targetType: string;
47+
accountHandle: string;
48+
repositories: Array<{
49+
id: string;
50+
name: string;
51+
fullName: string;
52+
private: boolean;
53+
htmlUrl: string;
54+
}>;
55+
};
56+
4357
export type VercelOnboardingData = {
4458
customEnvironments: VercelCustomEnvironment[];
4559
environmentVariables: VercelEnvironmentVariable[];
4660
availableProjects: VercelAvailableProject[];
4761
hasProjectSelected: boolean;
4862
authInvalid?: boolean;
49-
existingVariables: Record<string, { environments: RuntimeEnvironmentType[] }>;
63+
existingVariables: Record<string, { environments: string[] }>; // Environment slugs (non-archived only)
64+
gitHubAppInstallations: GitHubAppInstallationForVercel[];
65+
isGitHubConnected: boolean;
5066
};
5167

5268
export class VercelSettingsPresenter extends BasePresenter {
@@ -234,11 +250,71 @@ export class VercelSettingsPresenter extends BasePresenter {
234250
* Get data needed for the onboarding modal (custom environments and env vars)
235251
*/
236252
public async getOnboardingData(
237-
projectId: string,
253+
projectId: string,
238254
organizationId: string,
239255
vercelEnvironmentId?: string
240256
): Promise<VercelOnboardingData | null> {
241257
try {
258+
// Fetch GitHub app installations and connected repo in parallel with Vercel data
259+
const [gitHubInstallations, connectedGitHubRepo] = await Promise.all([
260+
(this._replica as PrismaClient).githubAppInstallation.findMany({
261+
where: {
262+
organizationId,
263+
deletedAt: null,
264+
suspendedAt: null,
265+
},
266+
select: {
267+
id: true,
268+
accountHandle: true,
269+
targetType: true,
270+
appInstallationId: true,
271+
repositories: {
272+
select: {
273+
id: true,
274+
name: true,
275+
fullName: true,
276+
htmlUrl: true,
277+
private: true,
278+
},
279+
take: 200,
280+
},
281+
},
282+
take: 20,
283+
orderBy: {
284+
createdAt: "desc",
285+
},
286+
}),
287+
(this._replica as PrismaClient).connectedGithubRepository.findFirst({
288+
where: {
289+
projectId,
290+
repository: {
291+
installation: {
292+
deletedAt: null,
293+
suspendedAt: null,
294+
},
295+
},
296+
},
297+
select: {
298+
id: true,
299+
},
300+
}),
301+
]);
302+
303+
const isGitHubConnected = connectedGitHubRepo !== null;
304+
const gitHubAppInstallations: GitHubAppInstallationForVercel[] = gitHubInstallations.map((installation) => ({
305+
id: installation.id,
306+
appInstallationId: installation.appInstallationId,
307+
targetType: installation.targetType,
308+
accountHandle: installation.accountHandle,
309+
repositories: installation.repositories.map((repo) => ({
310+
id: repo.id,
311+
name: repo.name,
312+
fullName: repo.fullName,
313+
private: repo.private,
314+
htmlUrl: repo.htmlUrl,
315+
})),
316+
}));
317+
242318
const orgIntegration = await (this._replica as PrismaClient).organizationIntegration.findFirst({
243319
where: {
244320
organizationId,
@@ -263,6 +339,8 @@ export class VercelSettingsPresenter extends BasePresenter {
263339
hasProjectSelected: false,
264340
authInvalid: true,
265341
existingVariables: {},
342+
gitHubAppInstallations,
343+
isGitHubConnected,
266344
};
267345
}
268346

@@ -292,6 +370,8 @@ export class VercelSettingsPresenter extends BasePresenter {
292370
hasProjectSelected: false,
293371
authInvalid: availableProjectsResult.authInvalid,
294372
existingVariables: {},
373+
gitHubAppInstallations,
374+
isGitHubConnected,
295375
};
296376
}
297377

@@ -303,6 +383,8 @@ export class VercelSettingsPresenter extends BasePresenter {
303383
availableProjects: availableProjectsResult.data,
304384
hasProjectSelected: false,
305385
existingVariables: {},
386+
gitHubAppInstallations,
387+
isGitHubConnected,
306388
};
307389
}
308390

@@ -341,6 +423,8 @@ export class VercelSettingsPresenter extends BasePresenter {
341423
hasProjectSelected: true,
342424
authInvalid: true,
343425
existingVariables: {},
426+
gitHubAppInstallations,
427+
isGitHubConnected,
344428
};
345429
}
346430

@@ -388,13 +472,34 @@ export class VercelSettingsPresenter extends BasePresenter {
388472
);
389473

390474
// Get existing environment variables in Trigger.dev
475+
// Fetch environments with their slugs and archived status to filter properly
476+
const projectEnvs = await (this._replica as PrismaClient).runtimeEnvironment.findMany({
477+
where: {
478+
projectId,
479+
archivedAt: null, // Filter out archived environments
480+
},
481+
select: {
482+
id: true,
483+
slug: true,
484+
type: true,
485+
},
486+
});
487+
const envIdToSlug = new Map(projectEnvs.map((e) => [e.id, e.slug]));
488+
const activeEnvIds = new Set(projectEnvs.map((e) => e.id));
489+
391490
const envVarRepository = new EnvironmentVariablesRepository(this._replica as PrismaClient);
392491
const existingVariables = await envVarRepository.getProject(projectId);
393-
const existingVariablesRecord: Record<string, { environments: RuntimeEnvironmentType[] }> = {};
492+
const existingVariablesRecord: Record<string, { environments: string[] }> = {};
394493
for (const v of existingVariables) {
395-
existingVariablesRecord[v.key] = {
396-
environments: v.values.map((val) => val.environment.type),
397-
};
494+
// Filter out archived environments and map to slugs
495+
const activeEnvSlugs = v.values
496+
.filter((val) => activeEnvIds.has(val.environment.id))
497+
.map((val) => envIdToSlug.get(val.environment.id) || val.environment.type.toLowerCase());
498+
if (activeEnvSlugs.length > 0) {
499+
existingVariablesRecord[v.key] = {
500+
environments: activeEnvSlugs,
501+
};
502+
}
398503
}
399504

400505
return {
@@ -403,6 +508,8 @@ export class VercelSettingsPresenter extends BasePresenter {
403508
availableProjects: availableProjectsResult.data,
404509
hasProjectSelected: true,
405510
existingVariables: existingVariablesRecord,
511+
gitHubAppInstallations,
512+
isGitHubConnected,
406513
};
407514
} catch (error) {
408515
console.error("Error in getOnboardingData:", error);

0 commit comments

Comments
 (0)