Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b27091b
Support native build server for deployments with the cli
myftija Nov 19, 2025
83bd51d
Add changeset
myftija Nov 21, 2025
d645d71
changelog
myftija Nov 21, 2025
bf56f5a
Avoid doing a db migration for the deployment table for now
myftija Nov 21, 2025
7191e0d
Get rid of clack's `taskLog`, not working reliably
myftija Nov 24, 2025
e2d5e83
Show a separate spinner during deployment init
myftija Nov 24, 2025
b9330ed
Return on process exit
myftija Nov 24, 2025
81f63ba
Pass in config file path
myftija Nov 24, 2025
6337613
Make ARTIFACTS_OBJECT_STORE_BUCKET optional
myftija Nov 24, 2025
85fd901
Revert @clack/prompts to old version, latest version causes spinner i…
myftija Nov 24, 2025
d3cb09f
Add --plain option for simpler build server logs
myftija Nov 24, 2025
ec518ec
Remove snipper custom cancel message, not supported in old ver
myftija Nov 24, 2025
ba49783
Remove custom spacing, also not supported n old ver
myftija Nov 24, 2025
fc2b733
Switch logs printing to a simple console log in favor of tighter line…
myftija Nov 24, 2025
91ab549
Ignore SecretsUsedInArgOrEnv docker build warnings, often misleading …
myftija Nov 24, 2025
164a178
Add hint about deployment promotion
myftija Nov 24, 2025
f0cd177
Consolidate failed log messages
myftija Nov 24, 2025
be69de3
Drop the `force` from `--force-local-build`
myftija Nov 25, 2025
5228299
Add changeset
myftija Nov 25, 2025
4365889
Update the original changeset to a minor release
myftija Nov 25, 2025
973a04d
Revert prerelease changeset config change
myftija Nov 25, 2025
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
7 changes: 1 addition & 6 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": [
"@remix-run/changelog-github",
{
"repo": "triggerdotdev/trigger.dev"
}
],
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [["@trigger.dev/*", "trigger.dev"]],
"linked": [],
Expand Down
6 changes: 6 additions & 0 deletions .changeset/proud-birds-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---

Added support for native build server builds in the deploy command
6 changes: 6 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,12 @@ const EnvironmentSchema = z
OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(),
OBJECT_STORE_REGION: z.string().optional(),
OBJECT_STORE_SERVICE: z.string().default("s3"),

ARTIFACTS_OBJECT_STORE_BUCKET: z.string().optional(),
ARTIFACTS_OBJECT_STORE_BASE_URL: z.string().optional(),
ARTIFACTS_OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(),
ARTIFACTS_OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(),
ARTIFACTS_OBJECT_STORE_REGION: z.string().optional(),
EVENTS_BATCH_SIZE: z.coerce.number().int().default(100),
EVENTS_BATCH_INTERVAL: z.coerce.number().int().default(1000),
EVENTS_DEFAULT_LOG_RETENTION: z.coerce.number().int().default(7),
Expand Down
78 changes: 78 additions & 0 deletions apps/webapp/app/routes/api.v1.artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
import {
type CreateArtifactResponseBody,
CreateArtifactRequestBody,
tryCatch,
} from "@trigger.dev/core/v3";
import { authenticateRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ArtifactsService } from "~/v3/services/artifacts.server";

export async function action({ request }: ActionFunctionArgs) {
if (request.method.toUpperCase() !== "POST") {
return json({ error: "Method Not Allowed" }, { status: 405 });
}

const authenticationResult = await authenticateRequest(request, {
apiKey: true,
organizationAccessToken: false,
personalAccessToken: false,
});

if (!authenticationResult || !authenticationResult.result.ok) {
logger.info("Invalid or missing api key", { url: request.url });
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}

const [, rawBody] = await tryCatch(request.json());
const body = CreateArtifactRequestBody.safeParse(rawBody ?? {});

if (!body.success) {
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
}

const { environment: authenticatedEnv } = authenticationResult.result;

const service = new ArtifactsService();
return await service
.createArtifact(body.data.type, authenticatedEnv, body.data.contentLength)
.match(
(result) => {
return json(
{
artifactKey: result.artifactKey,
uploadUrl: result.uploadUrl,
uploadFields: result.uploadFields,
expiresAt: result.expiresAt.toISOString(),
} satisfies CreateArtifactResponseBody,
{ status: 201 }
);
},
(error) => {
switch (error.type) {
case "artifact_size_exceeds_limit": {
logger.warn("Artifact size exceeds limit", { error });
return json(
{
error: `Artifact size (${error.contentLength} bytes) exceeds the allowed limit of ${error.sizeLimit} bytes`,
},
{ status: 400 }
);
}
case "failed_to_create_presigned_post": {
logger.error("Failed to create presigned POST", { error });
return json({ error: "Failed to generate artifact upload URL" }, { status: 500 });
}
case "artifacts_bucket_not_configured": {
logger.error("Artifacts bucket not configured", { error });
return json({ error: "Internal server error" }, { status: 500 });
}
default: {
error satisfies never;
logger.error("Failed creating artifact", { error });
return json({ error: "Internal server error" }, { status: 500 });
}
}
}
);
}
3 changes: 2 additions & 1 deletion apps/webapp/app/routes/api.v1.deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
const service = new InitializeDeploymentService();

try {
const { deployment, imageRef } = await service.call(authenticatedEnv, body.data);
const { deployment, imageRef, eventStream } = await service.call(authenticatedEnv, body.data);

const responseBody: InitializeDeploymentResponseBody = {
id: deployment.friendlyId,
Expand All @@ -48,6 +48,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"],
imageTag: imageRef,
imagePlatform: deployment.imagePlatform,
eventStream,
};

return json(responseBody, { status: 200 });
Expand Down
25 changes: 25 additions & 0 deletions apps/webapp/app/services/platform.v3.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,31 @@ export async function generateRegistryCredentials(
return result;
}

export async function enqueueBuild(
projectId: string,
deploymentId: string,
artifactKey: string,
options: {
skipPromotion?: boolean;
configFilePath?: string;
}
) {
if (!client) return undefined;
const result = await client.enqueueBuild(projectId, { deploymentId, artifactKey, options });
if (!result.success) {
logger.error("Error enqueuing build", {
error: result.error,
projectId,
deploymentId,
artifactKey,
options,
});
throw new Error("Failed to enqueue build");
}

return result;
}

function isCloud(): boolean {
const acceptableHosts = [
"https://cloud.trigger.dev",
Expand Down
92 changes: 92 additions & 0 deletions apps/webapp/app/v3/services/artifacts.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { BaseService } from "./baseService.server";
import { env } from "~/env.server";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { S3Client } from "@aws-sdk/client-s3";
import { customAlphabet } from "nanoid";
import { errAsync, fromPromise } from "neverthrow";

const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 24);
const objectStoreClient =
env.ARTIFACTS_OBJECT_STORE_ACCESS_KEY_ID &&
env.ARTIFACTS_OBJECT_STORE_SECRET_ACCESS_KEY &&
env.ARTIFACTS_OBJECT_STORE_BASE_URL
? new S3Client({
credentials: {
accessKeyId: env.ARTIFACTS_OBJECT_STORE_ACCESS_KEY_ID,
secretAccessKey: env.ARTIFACTS_OBJECT_STORE_SECRET_ACCESS_KEY,
},
region: env.ARTIFACTS_OBJECT_STORE_REGION,
endpoint: env.ARTIFACTS_OBJECT_STORE_BASE_URL,
forcePathStyle: true,
})
: new S3Client();

const artifactKeyPrefixByType = {
deployment_context: "deployments",
} as const;
const artifactBytesSizeLimitByType = {
deployment_context: 100 * 1024 * 1024, // 100MB
} as const;

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

public createArtifact(
type: "deployment_context",
authenticatedEnv: AuthenticatedEnvironment,
contentLength?: number
) {
const limit = artifactBytesSizeLimitByType[type];

// this is just a validation using client-side data
// the actual limit will be enforced by S3
if (contentLength && contentLength > limit) {
return errAsync({
type: "artifact_size_exceeds_limit" as const,
contentLength,
sizeLimit: limit,
});
}

const uniqueId = nanoid();
const key = `${artifactKeyPrefixByType[type]}/${authenticatedEnv.project.externalRef}/${authenticatedEnv.slug}/${uniqueId}.tar.gz`;

return this.createPresignedPost(key, limit, contentLength).map((result) => ({
artifactKey: key,
uploadUrl: result.url,
uploadFields: result.fields,
expiresAt: result.expiresAt,
}));
}

private createPresignedPost(key: string, sizeLimit: number, contentLength?: number) {
if (!this.bucket) {
return errAsync({
type: "artifacts_bucket_not_configured" as const,
});
}

const ttlSeconds = 300; // 5 minutes
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);

return fromPromise(
createPresignedPost(objectStoreClient, {
Bucket: this.bucket,
Key: key,
Conditions: [["content-length-range", 0, sizeLimit]],
Fields: {
"Content-Type": "application/gzip",
},
Expires: ttlSeconds,
}),
(error) => ({
type: "failed_to_create_presigned_post" as const,
cause: error,
})
).map((result) => ({
...result,
expiresAt,
}));
}
}
Loading