Skip to content

Commit 7ecaf2c

Browse files
authored
Sync env vars (parent env vars) (#2120)
* Prefer branch variables, but load preview envs too * WIP with vercel preview env var syncing * Return success if either child or parent env vars are synced * Removed log of env vars * Remove another log
1 parent 7b46371 commit 7ecaf2c

File tree

15 files changed

+163
-50
lines changed

15 files changed

+163
-50
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class ScheduleListPresenter extends BasePresenter {
6969
type: true,
7070
slug: true,
7171
branchName: true,
72+
archivedAt: true,
7273
orgMember: {
7374
select: {
7475
user: {

apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,32 @@ export async function action({ params, request }: ActionFunctionArgs) {
4747
})),
4848
});
4949

50+
if (environment.parentEnvironmentId && body.parentVariables) {
51+
const parentResult = await repository.create(environment.project.id, {
52+
override: typeof body.override === "boolean" ? body.override : false,
53+
environmentIds: [environment.parentEnvironmentId],
54+
variables: Object.entries(body.parentVariables).map(([key, value]) => ({
55+
key,
56+
value,
57+
})),
58+
});
59+
60+
let childFailure = !result.success ? result : undefined;
61+
let parentFailure = !parentResult.success ? parentResult : undefined;
62+
63+
if (result.success || parentResult.success) {
64+
return json({ success: true });
65+
} else {
66+
return json(
67+
{
68+
error: childFailure?.error || parentFailure?.error || "Unknown error",
69+
variableErrors: childFailure?.variableErrors || parentFailure?.variableErrors,
70+
},
71+
{ status: 400 }
72+
);
73+
}
74+
}
75+
5076
if (result.success) {
5177
return json({ success: true });
5278
} else {

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export class UpsertBranchService {
7676
const limits = await checkBranchLimit(
7777
this.#prismaClient,
7878
parentEnvironment.organization.id,
79-
parentEnvironment.project.id
79+
parentEnvironment.project.id,
80+
sanitizedBranchName
8081
);
8182

8283
if (limits.isAtLimit) {
@@ -148,9 +149,10 @@ export class UpsertBranchService {
148149
export async function checkBranchLimit(
149150
prisma: PrismaClientOrTransaction,
150151
organizationId: string,
151-
projectId: string
152+
projectId: string,
153+
newBranchName?: string
152154
) {
153-
const used = await prisma.runtimeEnvironment.count({
155+
const usedEnvs = await prisma.runtimeEnvironment.findMany({
154156
where: {
155157
projectId,
156158
branchName: {
@@ -159,11 +161,15 @@ export async function checkBranchLimit(
159161
archivedAt: null,
160162
},
161163
});
164+
165+
const count = newBranchName
166+
? usedEnvs.filter((env) => env.branchName !== newBranchName).length
167+
: usedEnvs.length;
162168
const limit = await getLimit(organizationId, "branches", 50);
163169

164170
return {
165-
used,
171+
used: count,
166172
limit,
167-
isAtLimit: used >= limit,
173+
isAtLimit: count >= limit,
168174
};
169175
}

apps/webapp/app/v3/services/checkSchedule.server.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@ export class CheckScheduleService extends BaseService {
108108
environments,
109109
}: {
110110
prisma: PrismaClientOrTransaction;
111-
environments: { id: string; type: RuntimeEnvironmentType }[];
111+
environments: { id: string; type: RuntimeEnvironmentType; archivedAt: Date | null }[];
112112
}) {
113-
const deployedEnvironments = environments.filter((env) => env.type !== "DEVELOPMENT");
113+
const deployedEnvironments = environments.filter(
114+
(env) => env.type !== "DEVELOPMENT" && !env.archivedAt
115+
);
114116
const schedulesCount = await prisma.taskScheduleInstance.count({
115117
where: {
116118
environmentId: {

apps/webapp/app/v3/services/createBackgroundWorker.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ export class CreateBackgroundWorkerService extends BaseService {
146146
backgroundWorker,
147147
environment,
148148
});
149+
150+
if (schedulesError instanceof ServiceValidationError) {
151+
throw schedulesError;
152+
}
153+
149154
throw new ServiceValidationError("Error syncing declarative schedules");
150155
}
151156

apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ export class CreateDeploymentBackgroundWorkerService extends BaseService {
114114
error: schedulesError,
115115
});
116116

117-
const serviceError = new ServiceValidationError("Error syncing declarative schedules");
117+
const serviceError =
118+
schedulesError instanceof ServiceValidationError
119+
? schedulesError
120+
: new ServiceValidationError("Error syncing declarative schedules");
118121

119122
await this.#failBackgroundWorkerDeployment(deployment, serviceError);
120123

packages/build/src/extensions/core/syncEnvVars.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build";
22

3-
export type SyncEnvVarsBody = Record<string, string> | Array<{ name: string; value: string }>;
3+
export type SyncEnvVarsBody =
4+
| Record<string, string>
5+
| Array<{ name: string; value: string; isParentEnv?: boolean }>;
46

57
export type SyncEnvVarsResult =
68
| SyncEnvVarsBody
@@ -96,24 +98,11 @@ export function syncEnvVars(fn: SyncEnvVarsFunction, options?: SyncEnvVarsOption
9698
return;
9799
}
98100

99-
const env = Object.entries(result).reduce(
100-
(acc, [key, value]) => {
101-
if (UNSYNCABLE_ENV_VARS.includes(key)) {
102-
return acc;
103-
}
101+
const env = stripUnsyncableEnvVars(result.env);
102+
const parentEnv = result.parentEnv ? stripUnsyncableEnvVars(result.parentEnv) : undefined;
104103

105-
// Strip out any TRIGGER_ prefix env vars
106-
if (UNSYNCABLE_ENV_VARS_PREFIXES.some((prefix) => key.startsWith(prefix))) {
107-
return acc;
108-
}
109-
110-
acc[key] = value;
111-
return acc;
112-
},
113-
{} as Record<string, string>
114-
);
115-
116-
const numberOfEnvVars = Object.keys(env).length;
104+
const numberOfEnvVars =
105+
Object.keys(env).length + (parentEnv ? Object.keys(parentEnv).length : 0);
117106

118107
if (numberOfEnvVars === 0) {
119108
$spinner.stop("No env vars detected");
@@ -129,22 +118,44 @@ export function syncEnvVars(fn: SyncEnvVarsFunction, options?: SyncEnvVarsOption
129118
id: "sync-env-vars",
130119
deploy: {
131120
env,
121+
parentEnv,
132122
override: options?.override ?? true,
133123
},
134124
});
135125
},
136126
};
137127
}
138128

129+
function stripUnsyncableEnvVars(env: Record<string, string>): Record<string, string> {
130+
return Object.entries(env).reduce(
131+
(acc, [key, value]) => {
132+
if (UNSYNCABLE_ENV_VARS.includes(key)) {
133+
return acc;
134+
}
135+
136+
// Strip out any TRIGGER_ prefix env vars
137+
if (UNSYNCABLE_ENV_VARS_PREFIXES.some((prefix) => key.startsWith(prefix))) {
138+
return acc;
139+
}
140+
141+
acc[key] = value;
142+
return acc;
143+
},
144+
{} as Record<string, string>
145+
);
146+
}
147+
139148
async function callSyncEnvVarsFn(
140149
syncEnvVarsFn: SyncEnvVarsFunction | undefined,
141150
env: Record<string, string>,
142151
environment: string,
143152
branch: string | undefined,
144153
context: BuildContext
145-
): Promise<Record<string, string> | undefined> {
154+
): Promise<{ env: Record<string, string>; parentEnv?: Record<string, string> } | undefined> {
146155
if (syncEnvVarsFn && typeof syncEnvVarsFn === "function") {
147-
let resolvedEnvVars: Record<string, string> = {};
156+
let resolvedEnvVars: { env: Record<string, string>; parentEnv?: Record<string, string> } = {
157+
env: {},
158+
};
148159
let result;
149160

150161
try {
@@ -172,11 +183,18 @@ async function callSyncEnvVarsFn(
172183
typeof item.name === "string" &&
173184
typeof item.value === "string"
174185
) {
175-
resolvedEnvVars[item.name] = item.value;
186+
if (item.isParentEnv) {
187+
if (!resolvedEnvVars.parentEnv) {
188+
resolvedEnvVars.parentEnv = {};
189+
}
190+
resolvedEnvVars.parentEnv[item.name] = item.value;
191+
} else {
192+
resolvedEnvVars.env[item.name] = item.value;
193+
}
176194
}
177195
}
178196
} else if (result) {
179-
resolvedEnvVars = result;
197+
resolvedEnvVars.env = result;
180198
}
181199

182200
return resolvedEnvVars;

packages/build/src/extensions/core/vercelSyncEnvVars.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BuildExtension } from "@trigger.dev/core/v3/build";
22
import { syncEnvVars } from "../core.js";
33

4+
type EnvVar = { name: string; value: string; isParentEnv?: boolean };
5+
46
export function syncVercelEnvVars(options?: {
57
projectId?: string;
68
vercelAccessToken?: string;
@@ -57,7 +59,7 @@ export function syncVercelEnvVars(options?: {
5759
}
5860
const params = new URLSearchParams({ decrypt: "true" });
5961
if (vercelTeamId) params.set("teamId", vercelTeamId);
60-
if (branch) params.set("gitBranch", branch);
62+
params.set("target", vercelEnvironment);
6163
const vercelApiUrl = `https://api.vercel.com/v8/projects/${projectId}/env?${params}`;
6264

6365
try {
@@ -73,15 +75,20 @@ export function syncVercelEnvVars(options?: {
7375

7476
const data = await response.json();
7577

76-
const filteredEnvs = data.envs
78+
const isBranchable = ctx.environment === "preview";
79+
80+
const filteredEnvs: EnvVar[] = data.envs
7781
.filter(
7882
(env: { type: string; value: string; target: string[] }) =>
7983
env.value && env.target.includes(vercelEnvironment)
8084
)
81-
.map((env: { key: string; value: string }) => ({
82-
name: env.key,
83-
value: env.value,
84-
}));
85+
.map((env: { key: string; value: string; gitBranch?: string }) => {
86+
return {
87+
name: env.key,
88+
value: env.value,
89+
isParentEnv: isBranchable && !env.gitBranch,
90+
};
91+
});
8592

8693
return filteredEnvs;
8794
} catch (error) {

packages/cli-v3/src/build/extensions.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ function applyLayerToManifest(layer: BuildLayer, manifest: BuildManifest): Build
143143
$manifest.deploy.env ??= {};
144144
$manifest.deploy.sync ??= {};
145145
$manifest.deploy.sync.env ??= {};
146+
$manifest.deploy.sync.parentEnv ??= {};
146147

147148
for (const [key, value] of Object.entries(layer.deploy.env)) {
148149
if (!value) {
@@ -159,6 +160,26 @@ function applyLayerToManifest(layer: BuildLayer, manifest: BuildManifest): Build
159160
}
160161
}
161162

163+
if (layer.deploy?.parentEnv) {
164+
$manifest.deploy.env ??= {};
165+
$manifest.deploy.sync ??= {};
166+
$manifest.deploy.sync.parentEnv ??= {};
167+
168+
for (const [key, value] of Object.entries(layer.deploy.parentEnv)) {
169+
if (!value) {
170+
continue;
171+
}
172+
173+
if (layer.deploy.override || $manifest.deploy.env[key] === undefined) {
174+
const existingValue = $manifest.deploy.env[key];
175+
176+
if (existingValue !== value) {
177+
$manifest.deploy.sync.parentEnv[key] = value;
178+
}
179+
}
180+
}
181+
}
182+
162183
if (layer.dependencies) {
163184
const externals = $manifest.externals ?? [];
164185

0 commit comments

Comments
 (0)