Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
81c5d4e
feat(core): add deployment_bundle artifact type and fromBundle deploy…
myftija Jul 21, 2026
6610101
feat(cli): add --local-bundle and --from-bundle deploy modes
myftija Jul 21, 2026
645a4a8
feat(webapp): accept deployment_bundle artifacts and thread fromBundl…
myftija Jul 21, 2026
18c44a5
fix(cli): review fixes for the local-bundle paths
myftija Jul 21, 2026
d7fb9ba
fix(cli): round-2 review refinements for local-bundle
myftija Jul 21, 2026
978bbba
fix(webapp): allow host.docker.internal on the Vite dev server
myftija Jul 21, 2026
45f2f6f
fix(cli): warn when build-tuning flags are ignored with --local-bundle
myftija Jul 21, 2026
c564578
test(cli): unit tests for the bundle archiver
myftija Jul 21, 2026
332549f
fix(cli): review feedback for --from-bundle error handling
myftija Jul 22, 2026
5c373ad
fix(webapp): treat preview environments as cloud installs
myftija Jul 22, 2026
08ce247
fix(cli): stop the bundle archiver dropping the indexer entry points
myftija Jul 22, 2026
1e226e1
feat(deploy): store local-bundle build env vars encrypted on the depl…
myftija Jul 22, 2026
4a0eb68
fix(deploy): drop undefined build env var values before sending
myftija Jul 22, 2026
f3594dc
fix(deploy): fail loud when stored build env vars cannot be read
myftija Jul 22, 2026
8abbe05
fix(webapp): tolerate missing LOGIN_ORIGIN in isCloud
myftija Jul 24, 2026
bf9b894
fix(deploy): adapt build env vars flow to reused deployments and scop…
myftija Aug 21, 2026
03f3cf0
fix(cli): honor --dry-run with --from-bundle and guard bundle artifac…
myftija Aug 21, 2026
c1d03d6
chore: trim inline comments to essential constraints
myftija Aug 21, 2026
a21baf0
chore(cli): reword the local-bundle changeset
myftija Aug 21, 2026
9ca53a0
fix(webapp): let build env var decrypt failures fall through to the g…
myftija Aug 21, 2026
140e4b3
feat(webapp): make deployment artifact and build env var limits confi…
myftija Aug 21, 2026
4a1ebb1
refactor(core,cli,webapp): drop the build env vars stored ack
myftija Aug 21, 2026
bbeea39
chore(cli): tighten the local-bundle flag descriptions
myftija Aug 21, 2026
c16bbcb
refactor(cli): isolate local-bundle and from-bundle from the classic …
myftija Aug 21, 2026
0706c79
fix(cli): review nits for the isolated bundle paths
myftija Aug 21, 2026
b334fcd
fix(cli): correct the artifact guard comment and skip-sync notice con…
myftija Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/local-bundle-deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@trigger.dev/core": patch
"trigger.dev": patch
---

Add an experimental `--local-bundle` deploy flag that runs the install and bundling steps on your machine and uploads only the build output; the image is still built remotely. Useful when your project's install step needs tooling or credentials that only exist locally.
13 changes: 13 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,19 @@ const EnvironmentSchema = z
.number()
.int()
.default(60 * 1000 * 15), // 15 minutes
DEPLOYMENT_CONTEXT_ARTIFACT_SIZE_LIMIT_BYTES: z.coerce
.number()
.int()
.default(100 * 1024 * 1024), // 100MB
DEPLOYMENT_BUNDLE_ARTIFACT_SIZE_LIMIT_BYTES: z.coerce
.number()
.int()
.default(100 * 1024 * 1024), // 100MB
DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES: z.coerce
.number()
.int()
.default(128 * 1024), // 128KB
DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS: z.coerce.number().int().default(400),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Key-limit default differs from stated design

DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS defaults to 400, while the PR description states a 200-key limit. Behavior is otherwise correct; worth confirming the intended default.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// When enabled, reject deploys made by v3 CLI versions (i.e. payloads that
// omit the `type` field). v4 CLI versions always send `type` ("MANAGED" or "V1"),
Expand Down
3 changes: 3 additions & 0 deletions apps/webapp/app/routes/api.v1.artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export async function action({ request }: ActionFunctionArgs) {
case "deployment_context":
errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Make sure you are in the correct directory of your Trigger.dev project. Reach out to us if you are seeing this error consistently.`;
break;
case "deployment_bundle":
errorMessage = `Bundle size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Reach out to us if you are seeing this error consistently.`;
break;
default:
body.data.type satisfies never;
errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { type GetDeploymentBuildEnvVarsResponseBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { authenticateApiKeyWithScope } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { decryptSecret, EncryptedSecretValueSchema } from "~/services/secrets/secretStore.server";
import { FINAL_DEPLOYMENT_STATUSES } from "~/v3/services/failDeployment.server";

const ParamsSchema = z.object({
deploymentId: z.string(),
});

// Secret material, deliberately separate from the main GET deployment endpoint.
export async function loader({ request, params }: LoaderFunctionArgs) {
const parsedParams = ParamsSchema.safeParse(params);

if (!parsedParams.success) {
return json({ error: "Invalid params" }, { status: 400 });
}

try {
const authResult = await authenticateApiKeyWithScope(request, {
action: "read",
resource: { type: "deployments" },
});

if (!authResult.ok) {
logger.info("Invalid or missing api key", { url: request.url });
return json({ error: authResult.error }, { status: authResult.status });
}

const authenticatedEnv = authResult.authentication.environment;

const { deploymentId } = parsedParams.data;

const deployment = await prisma.workerDeployment.findFirst({
where: {
friendlyId: deploymentId,
environmentId: authenticatedEnv.id,
},
select: {
id: true,
status: true,
buildEnvVars: true,
},
});

if (!deployment) {
return json({ error: "Deployment not found" }, { status: 404 });
}

logger.info("Build env vars read", {
deploymentId,
environmentId: authenticatedEnv.id,
projectId: authenticatedEnv.projectId,
status: deployment.status,
hasVars: deployment.buildEnvVars !== null,
});

// Never serve secrets for a build that is no longer active, even if a clear is still in flight
if (FINAL_DEPLOYMENT_STATUSES.includes(deployment.status)) {
return json({ variables: {} } satisfies GetDeploymentBuildEnvVarsResponseBody, {
status: 200,
});
}

if (!deployment.buildEnvVars) {
return json({ variables: {} } satisfies GetDeploymentBuildEnvVarsResponseBody, {
status: 200,
});
}

// Present-but-unreadable must fail loud: an empty record would let the build run without its secrets
const envelope = EncryptedSecretValueSchema.safeParse(deployment.buildEnvVars);

if (!envelope.success) {
logger.error("Stored build env vars are not a valid encrypted envelope", {
deploymentId,
environmentId: authenticatedEnv.id,
});
return json(
{ error: "The stored build environment variables could not be read. Retry the deploy." },
{ status: 500 }
);
}

const decrypted = await decryptSecret(env.ENCRYPTION_KEY, envelope.data);
const variables = z.record(z.string()).parse(JSON.parse(decrypted));

return json({ variables } satisfies GetDeploymentBuildEnvVarsResponseBody, { status: 200 });
} catch (error) {
if (error instanceof Response) throw error;
logger.error("Failed to load deployment build env vars", { error });
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
5 changes: 5 additions & 0 deletions apps/webapp/app/services/platform.v3.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,7 @@ export async function enqueueBuild(
options: {
skipPromotion?: boolean;
configFilePath?: string;
fromBundle?: boolean;
}
) {
if (!client) return undefined;
Expand Down Expand Up @@ -1235,6 +1236,10 @@ export function isCloud(): boolean {
return true;
}

if (env.LOGIN_ORIGIN?.endsWith(".triggerlabs.dev")) {
return true;
}

if (process.env.CLOUD_ENV === "development" && process.env.NODE_ENV === "development") {
return true;
}
Expand Down
7 changes: 5 additions & 2 deletions apps/webapp/app/v3/services/artifacts.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ const objectStoreClient =

const artifactKeyPrefixByType = {
deployment_context: "deployments",
// The key prefix is the one bundle signal that survives schema skew
deployment_bundle: "bundles",
} as const;
const artifactBytesSizeLimitByType = {
deployment_context: 100 * 1024 * 1024, // 100MB
deployment_context: env.DEPLOYMENT_CONTEXT_ARTIFACT_SIZE_LIMIT_BYTES,
deployment_bundle: env.DEPLOYMENT_BUNDLE_ARTIFACT_SIZE_LIMIT_BYTES,
} as const;

export class ArtifactsService extends BaseService {
private readonly bucket = env.ARTIFACTS_OBJECT_STORE_BUCKET;

public createArtifact(
type: "deployment_context",
type: "deployment_context" | "deployment_bundle",
authenticatedEnv: AuthenticatedEnvironment,
contentLength?: number
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { CreateBackgroundWorkerRequestBody } from "@trigger.dev/core/v3";
import { logger, tryCatch } from "@trigger.dev/core/v3";
import type {
BackgroundWorker,
PrismaClientOrTransaction,
WorkerDeployment,
import {
Prisma,
type BackgroundWorker,
type PrismaClientOrTransaction,
type WorkerDeployment,
} from "@trigger.dev/database";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { type TaskMetadataCache } from "~/services/taskMetadataCache.server";
Expand Down Expand Up @@ -313,6 +314,7 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
name: error.name,
message: error.message,
},
buildEnvVars: Prisma.DbNull,
},
});

Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/v3/services/deployment.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { BaseService } from "./baseService.server";
import { errAsync, fromPromise, okAsync, type ResultAsync } from "neverthrow";
import { type WorkerDeployment, type Project, boundedIn } from "@trigger.dev/database";
import { Prisma, type WorkerDeployment, type Project, boundedIn } from "@trigger.dev/database";
import {
BuildServerMetadata,
logger,
Expand Down Expand Up @@ -227,6 +227,7 @@ export class DeploymentService extends BaseService {
status: "CANCELED",
canceledAt: new Date(),
canceledReason: data?.canceledReason,
buildEnvVars: Prisma.DbNull,
},
}),
(error) => ({
Expand Down Expand Up @@ -339,6 +340,7 @@ export class DeploymentService extends BaseService {
options: {
skipPromotion?: boolean;
configFilePath?: string;
fromBundle?: boolean;
}
) {
return fromPromise(
Expand Down
3 changes: 2 additions & 1 deletion apps/webapp/app/v3/services/failDeployment.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PerformDeploymentAlertsService } from "./alerts/performDeploymentAlerts.server";
import { BaseService } from "./baseService.server";
import { logger } from "~/services/logger.server";
import { type WorkerDeploymentStatus } from "@trigger.dev/database";
import { Prisma, type WorkerDeploymentStatus } from "@trigger.dev/database";
import { type FailDeploymentRequestBody } from "@trigger.dev/core/v3/schemas";
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { DeploymentService } from "./deployment.server";
Expand Down Expand Up @@ -49,6 +49,7 @@ export class FailDeploymentService extends BaseService {
status: "FAILED",
failedAt: new Date(),
errorData: params.error,
buildEnvVars: Prisma.DbNull,
},
});

Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/app/v3/services/finalizeDeployment.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FinalizeDeploymentRequestBody } from "@trigger.dev/core/v3/schemas";
import { Prisma } from "@trigger.dev/database";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { updateEnvConcurrencyLimits } from "../runQueue.server";
Expand Down Expand Up @@ -76,6 +77,7 @@ export class FinalizeDeploymentService extends BaseService {
deployedAt: new Date(),
// Only add the digest, if any
imageReference: imageDigest ? `${deployment.imageReference}@${imageDigest}` : undefined,
buildEnvVars: Prisma.DbNull,
},
});

Expand Down
36 changes: 36 additions & 0 deletions apps/webapp/app/v3/services/initializeDeployment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { customAlphabet } from "nanoid";
import { env } from "~/env.server";
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { encryptSecret } from "~/services/secrets/secretStore.server";
import { logger } from "~/services/logger.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { createRemoteImageBuild, remoteBuildsEnabled } from "../remoteImageBuilder.server";
Expand Down Expand Up @@ -268,6 +269,38 @@ export class InitializeDeploymentService extends BaseService {
}
: undefined;

let encryptedBuildEnvVars: Awaited<ReturnType<typeof encryptSecret>> | undefined;

if (
payload.isNativeBuild &&
payload.fromBundle &&
payload.buildEnvVars &&
Object.keys(payload.buildEnvVars).length > 0
) {
const buildEnvVars = payload.buildEnvVars;

const keyCount = Object.keys(buildEnvVars).length;
if (keyCount > env.DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS) {
throw new ServiceValidationError(
`Build environment variable count (${keyCount}) exceeds the allowed limit of ${env.DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS}. Reach out to us if you are seeing this error consistently.`
);
}

const serialized = JSON.stringify(buildEnvVars);
const serializedBytes = Buffer.byteLength(serialized, "utf8");
if (serializedBytes > env.DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES) {
const sizeKB = parseFloat((serializedBytes / 1024).toFixed(1));
const limitKB = parseFloat(
(env.DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES / 1024).toFixed(1)
);
throw new ServiceValidationError(
`Build environment variables size (${sizeKB} KB) exceeds the allowed limit of ${limitKB} KB. Reach out to us if you are seeing this error consistently.`
);
}

encryptedBuildEnvVars = await encryptSecret(env.ENCRYPTION_KEY, serialized);
}

const buildServerMetadata: BuildServerMetadata | undefined =
payload.isNativeBuild || payload.buildId
? {
Expand All @@ -279,6 +312,7 @@ export class InitializeDeploymentService extends BaseService {
skipPromotion: payload.skipPromotion,
configFilePath: payload.configFilePath,
skipEnqueue: payload.skipEnqueue,
fromBundle: payload.fromBundle,
}
: {}),
}
Expand Down Expand Up @@ -343,6 +377,7 @@ export class InitializeDeploymentService extends BaseService {
projectId: environment.projectId,
externalBuildData,
buildServerMetadata,
buildEnvVars: encryptedBuildEnvVars,
triggeredById: triggeredBy?.id,
type: payload.type,
imageReference: imageRef,
Expand Down Expand Up @@ -373,6 +408,7 @@ export class InitializeDeploymentService extends BaseService {
.enqueueBuild(environment, deployment, payload.artifactKey, {
skipPromotion: payload.skipPromotion,
configFilePath: payload.configFilePath,
fromBundle: payload.fromBundle,
})
.orElse((error) => {
logger.error("Failed to enqueue build", {
Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/app/v3/services/timeoutDeployment.server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Prisma } from "@trigger.dev/database";
import { logger } from "~/services/logger.server";
import { BaseService } from "./baseService.server";
import { commonWorker } from "../commonWorker.server";
Expand Down Expand Up @@ -45,6 +46,7 @@ export class TimeoutDeploymentService extends BaseService {
status: "TIMED_OUT",
failedAt: new Date(),
errorData: { message: errorMessage, name: "TimeoutError" },
buildEnvVars: Prisma.DbNull,
},
});

Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export default defineConfig({
clientFiles: ["./app/entry.client.tsx", "./app/root.tsx", "./app/components/**/*.tsx"],
ssrFiles: ["./app/entry.server.tsx", "./app/root.tsx"],
},
// In-build calls from local docker (e.g. the indexer) reach the dev webapp via this host
allowedHosts: ["host.docker.internal"],
},
build: {
sourcemap: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."WorkerDeployment" ADD COLUMN IF NOT EXISTS "buildEnvVars" JSONB;
3 changes: 3 additions & 0 deletions internal-packages/database/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2271,6 +2271,9 @@ model WorkerDeployment {

externalBuildData Json?
buildServerMetadata Json?
/// Encrypted build-time env vars for pre-bundled (fromBundle) deploys, as an
/// EncryptedSecretValue envelope. Cleared when the deployment reaches a terminal status.
buildEnvVars Json?

status WorkerDeploymentStatus @default(PENDING)
type WorkerDeploymentType @default(V1)
Expand Down
15 changes: 15 additions & 0 deletions packages/cli-v3/src/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
DevDisconnectResponseBody,
EnvironmentVariableResponseBody,
FailDeploymentResponseBody,
GetDeploymentBuildEnvVarsResponseBody,
GetDeploymentResponseBody,
GetEnvironmentVariablesResponseBody,
GetLatestDeploymentResponseBody,
Expand Down Expand Up @@ -689,6 +690,20 @@ export class CliApiClient {
);
}

async getDeploymentBuildEnvVars(deploymentId: string) {
if (!this.accessToken) {
throw new Error("getDeploymentBuildEnvVars: No access token");
}

return wrapZodFetch(
GetDeploymentBuildEnvVarsResponseBody,
`${this.apiURL}/api/v1/deployments/${deploymentId}/build-env-vars`,
{
headers: this.getHeaders(),
}
);
}

async getCliPlatformNotification(projectRef?: string, signal?: AbortSignal) {
if (!this.accessToken) {
return { success: true as const, data: { notification: null } };
Expand Down
Loading
Loading