Skip to content

Commit ca5a494

Browse files
committed
feat(env): skip unchanged env var updates and preserve secrets
Query and read existing environment variable secret values from the secret store and track which vars are marked secret. Compare current values to the incoming Vercel values and skip any variables whose stored secret value matches the incoming value. Log when no changes are detected and when only changed vars are updated. Also adjust the split between secret and non-secret groups to operate on the filtered changed set. Additionally, avoid redundant secret writes when creating/updating secret references in the environment variables repository by checking the secret store first and skipping unchanged values. These changes reduce unnecessary secret writes and API calls, preserve secret status when overriding, and add diagnostic logging for skipped and changed variables.
1 parent aae7957 commit ca5a494

File tree

4 files changed

+65
-9
lines changed

4 files changed

+65
-9
lines changed

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

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,9 +1301,11 @@ export class VercelIntegrationRepository {
13011301
continue;
13021302
}
13031303

1304-
// Query existing env vars to check which ones are already secrets
1305-
// We need to preserve the secret status when overriding
1304+
// Query existing env vars to check which ones are already secrets and get their current values
1305+
// We need to preserve the secret status when overriding and skip unchanged values
13061306
const existingSecretKeys = new Set<string>();
1307+
const existingValues = new Map<string, string>();
1308+
13071309
const existingVarValues = await prisma.environmentVariableValue.findMany({
13081310
where: {
13091311
environmentId: mapping.runtimeEnvironmentId,
@@ -1316,6 +1318,11 @@ export class VercelIntegrationRepository {
13161318
},
13171319
select: {
13181320
isSecret: true,
1321+
valueReference: {
1322+
select: {
1323+
key: true,
1324+
},
1325+
},
13191326
variable: {
13201327
select: {
13211328
key: true,
@@ -1324,15 +1331,59 @@ export class VercelIntegrationRepository {
13241331
},
13251332
});
13261333

1327-
for (const varValue of existingVarValues) {
1328-
if (varValue.isSecret) {
1329-
existingSecretKeys.add(varValue.variable.key);
1334+
// Get existing values from the secret store
1335+
if (existingVarValues.length > 0) {
1336+
const secretStore = getSecretStore("DATABASE", { prismaClient: prisma });
1337+
const SecretValue = z.object({ secret: z.string() });
1338+
1339+
for (const varValue of existingVarValues) {
1340+
if (varValue.isSecret) {
1341+
existingSecretKeys.add(varValue.variable.key);
1342+
}
1343+
1344+
// Fetch the current value from the secret store
1345+
if (varValue.valueReference?.key) {
1346+
try {
1347+
const existingSecret = await secretStore.getSecret(SecretValue, varValue.valueReference.key);
1348+
if (existingSecret) {
1349+
existingValues.set(varValue.variable.key, existingSecret.secret);
1350+
}
1351+
} catch {
1352+
// If we can't read the existing value, we'll update it anyway
1353+
}
1354+
}
13301355
}
13311356
}
13321357

1358+
// Filter out vars that haven't changed
1359+
const changedVars = varsToSync.filter((v) => {
1360+
const existingValue = existingValues.get(v.key);
1361+
// Include if: no existing value, or value is different
1362+
return existingValue === undefined || existingValue !== v.value;
1363+
});
1364+
1365+
if (changedVars.length === 0) {
1366+
logger.info("Vercel pullEnvVarsFromVercel: no changes detected, skipping", {
1367+
projectId: params.projectId,
1368+
vercelTarget: mapping.vercelTarget,
1369+
triggerEnvType: mapping.triggerEnvType,
1370+
skippedCount: varsToSync.length,
1371+
});
1372+
continue;
1373+
}
1374+
1375+
logger.info("Vercel pullEnvVarsFromVercel: updating changed vars", {
1376+
projectId: params.projectId,
1377+
vercelTarget: mapping.vercelTarget,
1378+
triggerEnvType: mapping.triggerEnvType,
1379+
changedCount: changedVars.length,
1380+
unchangedCount: varsToSync.length - changedVars.length,
1381+
changedKeys: changedVars.map((v) => v.key),
1382+
});
1383+
13331384
// 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));
1385+
const secretVars = changedVars.filter((v) => existingSecretKeys.has(v.key));
1386+
const nonSecretVars = changedVars.filter((v) => !existingSecretKeys.has(v.key));
13361387

13371388
// Create non-secret vars
13381389
if (nonSecretVars.length > 0) {

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,6 @@ export default function Page() {
408408
</TableCell>
409409
)}
410410
<TableCellMenu
411-
className={cn(cellClassName, borderedCellClassName)}
412411
isSticky
413412
hiddenButtons={
414413
<>

apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ export class EnvironmentVariablesRepository implements Repository {
190190
for (const environmentId of options.environmentIds) {
191191
const key = secretKey(projectId, environmentId, variable.key);
192192

193+
// Check if value already exists and is the same - skip update if unchanged
194+
const existingSecret = await secretStore.getSecret(SecretValue, key);
195+
if (existingSecret && existingSecret.secret === variable.value) {
196+
// Value is unchanged, skip this variable for this environment
197+
continue;
198+
}
199+
193200
//create the secret reference
194201
const secretReference = await tx.secretReference.upsert({
195202
where: {

packages/core/src/v3/schemas/api.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,6 @@ export const InitializeDeploymentResponseBody = z.object({
577577
version: z.string(),
578578
imageTag: z.string(),
579579
imagePlatform: z.string(),
580-
environmentSlug: z.string(),
581580
externalBuildData: ExternalBuildData.optional().nullable(),
582581
eventStream: z
583582
.object({

0 commit comments

Comments
 (0)