diff --git a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts index bd210351ca7..929e1e40ba9 100644 --- a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts @@ -69,6 +69,7 @@ export class ScheduleListPresenter extends BasePresenter { type: true, slug: true, branchName: true, + archivedAt: true, orgMember: { select: { user: { diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts index a44b036abfc..89ce7ed886d 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts @@ -47,6 +47,32 @@ export async function action({ params, request }: ActionFunctionArgs) { })), }); + if (environment.parentEnvironmentId && body.parentVariables) { + const parentResult = await repository.create(environment.project.id, { + override: typeof body.override === "boolean" ? body.override : false, + environmentIds: [environment.parentEnvironmentId], + variables: Object.entries(body.parentVariables).map(([key, value]) => ({ + key, + value, + })), + }); + + let childFailure = !result.success ? result : undefined; + let parentFailure = !parentResult.success ? parentResult : undefined; + + if (result.success || parentResult.success) { + return json({ success: true }); + } else { + return json( + { + error: childFailure?.error || parentFailure?.error || "Unknown error", + variableErrors: childFailure?.variableErrors || parentFailure?.variableErrors, + }, + { status: 400 } + ); + } + } + if (result.success) { return json({ success: true }); } else { diff --git a/apps/webapp/app/services/upsertBranch.server.ts b/apps/webapp/app/services/upsertBranch.server.ts index 4aaeb663497..fe5db6f4c74 100644 --- a/apps/webapp/app/services/upsertBranch.server.ts +++ b/apps/webapp/app/services/upsertBranch.server.ts @@ -76,7 +76,8 @@ export class UpsertBranchService { const limits = await checkBranchLimit( this.#prismaClient, parentEnvironment.organization.id, - parentEnvironment.project.id + parentEnvironment.project.id, + sanitizedBranchName ); if (limits.isAtLimit) { @@ -148,9 +149,10 @@ export class UpsertBranchService { export async function checkBranchLimit( prisma: PrismaClientOrTransaction, organizationId: string, - projectId: string + projectId: string, + newBranchName?: string ) { - const used = await prisma.runtimeEnvironment.count({ + const usedEnvs = await prisma.runtimeEnvironment.findMany({ where: { projectId, branchName: { @@ -159,11 +161,15 @@ export async function checkBranchLimit( archivedAt: null, }, }); + + const count = newBranchName + ? usedEnvs.filter((env) => env.branchName !== newBranchName).length + : usedEnvs.length; const limit = await getLimit(organizationId, "branches", 50); return { - used, + used: count, limit, - isAtLimit: used >= limit, + isAtLimit: count >= limit, }; } diff --git a/apps/webapp/app/v3/services/checkSchedule.server.ts b/apps/webapp/app/v3/services/checkSchedule.server.ts index d5249f6b582..25a0944dc6e 100644 --- a/apps/webapp/app/v3/services/checkSchedule.server.ts +++ b/apps/webapp/app/v3/services/checkSchedule.server.ts @@ -108,9 +108,11 @@ export class CheckScheduleService extends BaseService { environments, }: { prisma: PrismaClientOrTransaction; - environments: { id: string; type: RuntimeEnvironmentType }[]; + environments: { id: string; type: RuntimeEnvironmentType; archivedAt: Date | null }[]; }) { - const deployedEnvironments = environments.filter((env) => env.type !== "DEVELOPMENT"); + const deployedEnvironments = environments.filter( + (env) => env.type !== "DEVELOPMENT" && !env.archivedAt + ); const schedulesCount = await prisma.taskScheduleInstance.count({ where: { environmentId: { diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts index 3fa78a6c225..435b6f0f321 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -146,6 +146,11 @@ export class CreateBackgroundWorkerService extends BaseService { backgroundWorker, environment, }); + + if (schedulesError instanceof ServiceValidationError) { + throw schedulesError; + } + throw new ServiceValidationError("Error syncing declarative schedules"); } diff --git a/apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts index e10d448a9b0..bcd7b1fe021 100644 --- a/apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createDeploymentBackgroundWorker.server.ts @@ -114,7 +114,10 @@ export class CreateDeploymentBackgroundWorkerService extends BaseService { error: schedulesError, }); - const serviceError = new ServiceValidationError("Error syncing declarative schedules"); + const serviceError = + schedulesError instanceof ServiceValidationError + ? schedulesError + : new ServiceValidationError("Error syncing declarative schedules"); await this.#failBackgroundWorkerDeployment(deployment, serviceError); diff --git a/packages/build/src/extensions/core/syncEnvVars.ts b/packages/build/src/extensions/core/syncEnvVars.ts index 107ea153595..4b437835615 100644 --- a/packages/build/src/extensions/core/syncEnvVars.ts +++ b/packages/build/src/extensions/core/syncEnvVars.ts @@ -1,6 +1,8 @@ import { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build"; -export type SyncEnvVarsBody = Record | Array<{ name: string; value: string }>; +export type SyncEnvVarsBody = + | Record + | Array<{ name: string; value: string; isParentEnv?: boolean }>; export type SyncEnvVarsResult = | SyncEnvVarsBody @@ -96,24 +98,11 @@ export function syncEnvVars(fn: SyncEnvVarsFunction, options?: SyncEnvVarsOption return; } - const env = Object.entries(result).reduce( - (acc, [key, value]) => { - if (UNSYNCABLE_ENV_VARS.includes(key)) { - return acc; - } + const env = stripUnsyncableEnvVars(result.env); + const parentEnv = result.parentEnv ? stripUnsyncableEnvVars(result.parentEnv) : undefined; - // Strip out any TRIGGER_ prefix env vars - if (UNSYNCABLE_ENV_VARS_PREFIXES.some((prefix) => key.startsWith(prefix))) { - return acc; - } - - acc[key] = value; - return acc; - }, - {} as Record - ); - - const numberOfEnvVars = Object.keys(env).length; + const numberOfEnvVars = + Object.keys(env).length + (parentEnv ? Object.keys(parentEnv).length : 0); if (numberOfEnvVars === 0) { $spinner.stop("No env vars detected"); @@ -129,6 +118,7 @@ export function syncEnvVars(fn: SyncEnvVarsFunction, options?: SyncEnvVarsOption id: "sync-env-vars", deploy: { env, + parentEnv, override: options?.override ?? true, }, }); @@ -136,15 +126,36 @@ export function syncEnvVars(fn: SyncEnvVarsFunction, options?: SyncEnvVarsOption }; } +function stripUnsyncableEnvVars(env: Record): Record { + return Object.entries(env).reduce( + (acc, [key, value]) => { + if (UNSYNCABLE_ENV_VARS.includes(key)) { + return acc; + } + + // Strip out any TRIGGER_ prefix env vars + if (UNSYNCABLE_ENV_VARS_PREFIXES.some((prefix) => key.startsWith(prefix))) { + return acc; + } + + acc[key] = value; + return acc; + }, + {} as Record + ); +} + async function callSyncEnvVarsFn( syncEnvVarsFn: SyncEnvVarsFunction | undefined, env: Record, environment: string, branch: string | undefined, context: BuildContext -): Promise | undefined> { +): Promise<{ env: Record; parentEnv?: Record } | undefined> { if (syncEnvVarsFn && typeof syncEnvVarsFn === "function") { - let resolvedEnvVars: Record = {}; + let resolvedEnvVars: { env: Record; parentEnv?: Record } = { + env: {}, + }; let result; try { @@ -172,11 +183,18 @@ async function callSyncEnvVarsFn( typeof item.name === "string" && typeof item.value === "string" ) { - resolvedEnvVars[item.name] = item.value; + if (item.isParentEnv) { + if (!resolvedEnvVars.parentEnv) { + resolvedEnvVars.parentEnv = {}; + } + resolvedEnvVars.parentEnv[item.name] = item.value; + } else { + resolvedEnvVars.env[item.name] = item.value; + } } } } else if (result) { - resolvedEnvVars = result; + resolvedEnvVars.env = result; } return resolvedEnvVars; diff --git a/packages/build/src/extensions/core/vercelSyncEnvVars.ts b/packages/build/src/extensions/core/vercelSyncEnvVars.ts index 74a832d8c0d..0dc8280fa87 100644 --- a/packages/build/src/extensions/core/vercelSyncEnvVars.ts +++ b/packages/build/src/extensions/core/vercelSyncEnvVars.ts @@ -1,6 +1,8 @@ import { BuildExtension } from "@trigger.dev/core/v3/build"; import { syncEnvVars } from "../core.js"; +type EnvVar = { name: string; value: string; isParentEnv?: boolean }; + export function syncVercelEnvVars(options?: { projectId?: string; vercelAccessToken?: string; @@ -57,7 +59,7 @@ export function syncVercelEnvVars(options?: { } const params = new URLSearchParams({ decrypt: "true" }); if (vercelTeamId) params.set("teamId", vercelTeamId); - if (branch) params.set("gitBranch", branch); + params.set("target", vercelEnvironment); const vercelApiUrl = `https://api.vercel.com/v8/projects/${projectId}/env?${params}`; try { @@ -73,15 +75,20 @@ export function syncVercelEnvVars(options?: { const data = await response.json(); - const filteredEnvs = data.envs + const isBranchable = ctx.environment === "preview"; + + const filteredEnvs: EnvVar[] = data.envs .filter( (env: { type: string; value: string; target: string[] }) => env.value && env.target.includes(vercelEnvironment) ) - .map((env: { key: string; value: string }) => ({ - name: env.key, - value: env.value, - })); + .map((env: { key: string; value: string; gitBranch?: string }) => { + return { + name: env.key, + value: env.value, + isParentEnv: isBranchable && !env.gitBranch, + }; + }); return filteredEnvs; } catch (error) { diff --git a/packages/cli-v3/src/build/extensions.ts b/packages/cli-v3/src/build/extensions.ts index 8e0f9f0a4e2..7114da03e37 100644 --- a/packages/cli-v3/src/build/extensions.ts +++ b/packages/cli-v3/src/build/extensions.ts @@ -143,6 +143,7 @@ function applyLayerToManifest(layer: BuildLayer, manifest: BuildManifest): Build $manifest.deploy.env ??= {}; $manifest.deploy.sync ??= {}; $manifest.deploy.sync.env ??= {}; + $manifest.deploy.sync.parentEnv ??= {}; for (const [key, value] of Object.entries(layer.deploy.env)) { if (!value) { @@ -159,6 +160,26 @@ function applyLayerToManifest(layer: BuildLayer, manifest: BuildManifest): Build } } + if (layer.deploy?.parentEnv) { + $manifest.deploy.env ??= {}; + $manifest.deploy.sync ??= {}; + $manifest.deploy.sync.parentEnv ??= {}; + + for (const [key, value] of Object.entries(layer.deploy.parentEnv)) { + if (!value) { + continue; + } + + if (layer.deploy.override || $manifest.deploy.env[key] === undefined) { + const existingValue = $manifest.deploy.env[key]; + + if (existingValue !== value) { + $manifest.deploy.sync.parentEnv[key] = value; + } + } + } + } + if (layer.dependencies) { const externals = $manifest.externals ?? []; diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 63f9d87a49f..aa80f7f8e23 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -351,12 +351,17 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { } } - if ( + const hasVarsToSync = buildManifest.deploy.sync && - buildManifest.deploy.sync.env && - Object.keys(buildManifest.deploy.sync.env).length > 0 - ) { - const numberOfEnvVars = Object.keys(buildManifest.deploy.sync.env).length; + ((buildManifest.deploy.sync.env && Object.keys(buildManifest.deploy.sync.env).length > 0) || + (buildManifest.deploy.sync.parentEnv && + Object.keys(buildManifest.deploy.sync.parentEnv).length > 0)); + + if (hasVarsToSync) { + const childVars = buildManifest.deploy.sync?.env ?? {}; + const parentVars = buildManifest.deploy.sync?.parentEnv ?? {}; + + const numberOfEnvVars = Object.keys(childVars).length + Object.keys(parentVars).length; const vars = numberOfEnvVars === 1 ? "var" : "vars"; if (!options.skipSyncEnvVars) { @@ -366,7 +371,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { projectClient.client, resolvedConfig.project, options.env, - buildManifest.deploy.sync.env + childVars, + parentVars ); if (!success) { @@ -495,21 +501,28 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { $spinner ); - throw new SkipLoggingError("Failed to get deployment with worker"); + throw new SkipLoggingError(getDeploymentResponse.error); } const deploymentWithWorker = getDeploymentResponse.data; if (!deploymentWithWorker.worker) { + const errorData = deploymentWithWorker.errorData + ? prepareDeploymentError(deploymentWithWorker.errorData) + : undefined; + await failDeploy( projectClient.client, deployment, - { name: "DeploymentError", message: "Failed to get deployment with worker" }, + { + name: "DeploymentError", + message: errorData?.message ?? "Failed to get deployment with worker", + }, buildResult.logs, $spinner ); - throw new SkipLoggingError("Failed to get deployment with worker"); + throw new SkipLoggingError(errorData?.message ?? "Failed to get deployment with worker"); } const imageReference = options.selfHosted @@ -610,10 +623,12 @@ export async function syncEnvVarsWithServer( apiClient: CliApiClient, projectRef: string, environmentSlug: string, - envVars: Record + envVars: Record, + parentEnvVars?: Record ) { const uploadResult = await apiClient.importEnvVars(projectRef, environmentSlug, { variables: envVars, + parentVariables: parentEnvVars, override: true, }); @@ -629,6 +644,8 @@ async function failDeploy( warnings?: string[], errors?: string[] ) { + logger.debug("failDeploy", { error, logs, warnings, errors }); + $spinner.stop(`Failed to deploy project`); const doOutputLogs = async (prefix: string = "Error") => { @@ -688,7 +705,7 @@ async function failDeploy( : undefined; if (errorData) { - prettyError(errorData.name, errorData.stack, errorData.stderr); + prettyError(errorData.message, errorData.stack, errorData.stderr); if (logs.trim() !== "") { const logPath = await saveLogs(deployment.shortCode, logs); diff --git a/packages/cli-v3/src/commands/workers/build.ts b/packages/cli-v3/src/commands/workers/build.ts index 9daca0ff3c0..62b820aacbc 100644 --- a/packages/cli-v3/src/commands/workers/build.ts +++ b/packages/cli-v3/src/commands/workers/build.ts @@ -281,7 +281,8 @@ async function _workerBuildCommand(dir: string, options: WorkersBuildCommandOpti projectClient.client, resolvedConfig.project, options.env, - buildManifest.deploy.sync.env + buildManifest.deploy.sync.env, + buildManifest.deploy.sync.parentEnv ); if (!success) { @@ -456,10 +457,12 @@ export async function syncEnvVarsWithServer( apiClient: CliApiClient, projectRef: string, environmentSlug: string, - envVars: Record + envVars: Record, + parentEnvVars?: Record ) { const uploadResult = await apiClient.importEnvVars(projectRef, environmentSlug, { variables: envVars, + parentVariables: parentEnvVars, override: true, }); diff --git a/packages/core/src/v3/build/extensions.ts b/packages/core/src/v3/build/extensions.ts index 68ed3b5cee6..3b809040eaa 100644 --- a/packages/core/src/v3/build/extensions.ts +++ b/packages/core/src/v3/build/extensions.ts @@ -62,6 +62,7 @@ export interface BuildLayer { }; deploy?: { env?: Record; + parentEnv?: Record; override?: boolean; }; dependencies?: Record; diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 2fea3f9b086..b8819347bf9 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -805,6 +805,7 @@ export type UpdateEnvironmentVariableRequestBody = z.infer< export const ImportEnvironmentVariablesRequestBody = z.object({ variables: z.record(z.string()), + parentVariables: z.record(z.string()).optional(), override: z.boolean().optional(), }); diff --git a/packages/core/src/v3/schemas/build.ts b/packages/core/src/v3/schemas/build.ts index fb044e2410e..b116d264d04 100644 --- a/packages/core/src/v3/schemas/build.ts +++ b/packages/core/src/v3/schemas/build.ts @@ -52,6 +52,7 @@ export const BuildManifest = z.object({ sync: z .object({ env: z.record(z.string()).optional(), + parentEnv: z.record(z.string()).optional(), }) .optional(), }), diff --git a/references/hello-world/trigger.config.ts b/references/hello-world/trigger.config.ts index fa6cfa566b8..7977880a309 100644 --- a/references/hello-world/trigger.config.ts +++ b/references/hello-world/trigger.config.ts @@ -24,7 +24,8 @@ export default defineConfig({ console.log(ctx.branch); return [ { name: "SYNC_ENV", value: ctx.environment }, - { name: "BRANCH", value: ctx.branch ?? "–" }, + { name: "BRANCH", value: ctx.branch ?? "NO_BRANCH" }, + { name: "BRANCH", value: "PARENT", isParentEnv: true }, { name: "SECRET_KEY", value: "secret-value" }, { name: "ANOTHER_SECRET", value: "another-secret-value" }, ];