Skip to content

Commit f0b4276

Browse files
authored
Merge branch 'main' into feat(webapp)-models-ui
2 parents e9c6275 + f75d4d6 commit f0b4276

35 files changed

+2261
-447
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Large run outputs can use the new API which allows switching object storage providers.

.env.example

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,28 @@ POSTHOG_PROJECT_KEY=
7777
# DEPOT_TOKEN=<Depot org token>
7878
# DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318"
7979
# These are needed for the object store (for handling large payloads/outputs)
80-
# OBJECT_STORE_BASE_URL="https://{bucket}.{accountId}.r2.cloudflarestorage.com"
81-
# OBJECT_STORE_ACCESS_KEY_ID=
82-
# OBJECT_STORE_SECRET_ACCESS_KEY=
80+
#
81+
# Default provider
82+
# OBJECT_STORE_BASE_URL=http://localhost:9005
83+
# OBJECT_STORE_BUCKET=packets
84+
# OBJECT_STORE_ACCESS_KEY_ID=minioadmin
85+
# OBJECT_STORE_SECRET_ACCESS_KEY=minioadmin
86+
# OBJECT_STORE_REGION=us-east-1
87+
# OBJECT_STORE_SERVICE=s3
88+
#
89+
# OBJECT_STORE_DEFAULT_PROTOCOL=s3 # Only specify this if you're going to migrate object storage and set protocol values below
90+
# Named providers (protocol-prefixed data) - optional for multi-provider support
91+
# OBJECT_STORE_S3_BASE_URL=https://s3.amazonaws.com
92+
# OBJECT_STORE_S3_ACCESS_KEY_ID=
93+
# OBJECT_STORE_S3_SECRET_ACCESS_KEY=
94+
# OBJECT_STORE_S3_REGION=us-east-1
95+
# OBJECT_STORE_S3_SERVICE=s3
96+
#
97+
# OBJECT_STORE_R2_BASE_URL=https://{bucket}.{accountId}.r2.cloudflarestorage.com
98+
# OBJECT_STORE_R2_ACCESS_KEY_ID=
99+
# OBJECT_STORE_R2_SECRET_ACCESS_KEY=
100+
# OBJECT_STORE_R2_REGION=auto
101+
# OBJECT_STORE_R2_SERVICE=s3
83102
# CHECKPOINT_THRESHOLD_IN_MS=10000
84103

85104
# These control the server-side internal telemetry
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Multi-provider object storage with protocol-based routing for zero-downtime migration
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Add IAM role-based auth support for object stores (no access keys required).

apps/webapp/app/env.server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,11 +349,18 @@ const EnvironmentSchema = z
349349
.default(60 * 1000 * 15), // 15 minutes
350350

351351
OBJECT_STORE_BASE_URL: z.string().optional(),
352+
OBJECT_STORE_BUCKET: z.string().optional(),
352353
OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(),
353354
OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(),
354355
OBJECT_STORE_REGION: z.string().optional(),
355356
OBJECT_STORE_SERVICE: z.string().default("s3"),
356357

358+
// Protocol to use for new uploads (e.g., "s3", "r2"). Data without protocol uses default provider above.
359+
// If specified, you must configure the corresponding provider using OBJECT_STORE_{PROTOCOL}_* env vars.
360+
// Example: OBJECT_STORE_DEFAULT_PROTOCOL=s3 requires OBJECT_STORE_S3_BASE_URL, OBJECT_STORE_S3_ACCESS_KEY_ID, etc.
361+
// Enables zero-downtime migration between providers (old data keeps working, new data uses new provider).
362+
OBJECT_STORE_DEFAULT_PROTOCOL: z.string().regex(/^[a-z0-9]+$/).optional(),
363+
357364
ARTIFACTS_OBJECT_STORE_BUCKET: z.string().optional(),
358365
ARTIFACTS_OBJECT_STORE_BASE_URL: z.string().optional(),
359366
ARTIFACTS_OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import assertNever from "assert-never";
1515
import { API_VERSIONS, CURRENT_API_VERSION, RunStatusUnspecifiedApiVersion } from "~/api/versions";
1616
import { $replica, prisma } from "~/db.server";
1717
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
18-
import { generatePresignedUrl } from "~/v3/r2.server";
18+
import { generatePresignedUrl } from "~/v3/objectStore.server";
1919
import { tracer } from "~/v3/tracer.server";
2020
import { startSpanWithEnv } from "~/v3/tracing.server";
2121

apps/webapp/app/routes/api.v1.packets.$.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
33
import { z } from "zod";
44
import { authenticateApiRequest } from "~/services/apiAuth.server";
55
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
6-
import { generatePresignedUrl } from "~/v3/r2.server";
6+
import { generatePresignedUrl } from "~/v3/objectStore.server";
77

88
const ParamsSchema = z.object({
99
"*": z.string(),
@@ -29,7 +29,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
2929
authenticationResult.environment.project.externalRef,
3030
authenticationResult.environment.slug,
3131
filename,
32-
"PUT"
32+
"PUT",
33+
{ forceNoPrefix: true }
3334
);
3435

3536
if (!signed.success) {

apps/webapp/app/routes/api.v1.waitpoints.tokens.$waitpointFriendlyId.callback.$hash.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
2-
import {
3-
type CompleteWaitpointTokenResponseBody,
4-
conditionallyExportPacket,
5-
stringifyIO,
6-
} from "@trigger.dev/core/v3";
2+
import { type CompleteWaitpointTokenResponseBody, stringifyIO } from "@trigger.dev/core/v3";
73
import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
84
import { z } from "zod";
95
import { $replica } from "~/db.server";
106
import { env } from "~/env.server";
7+
import { processWaitpointCompletionPacket } from "~/runEngine/concerns/waitpointCompletionPacket.server";
8+
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
119
import { verifyHttpCallbackHash } from "~/services/httpCallback.server";
1210
import { logger } from "~/services/logger.server";
1311
import { engine } from "~/v3/runEngine.server";
@@ -41,8 +39,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
4139
},
4240
include: {
4341
environment: {
44-
select: {
45-
apiKey: true,
42+
include: {
43+
project: true,
44+
organization: true,
45+
orgMember: true,
4646
parentEnvironment: {
4747
select: {
4848
apiKey: true,
@@ -77,9 +77,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
7777
const body = await request.json().catch(() => ({}));
7878

7979
const stringifiedData = await stringifyIO(body);
80-
const finalData = await conditionallyExportPacket(
80+
const finalData = await processWaitpointCompletionPacket(
8181
stringifiedData,
82-
`${waitpointId}/waitpoint/http-callback`
82+
waitpoint.environment,
83+
`${WaitpointId.toFriendlyId(waitpointId)}/http-callback`
8384
);
8485

8586
const result = await engine.completeWaitpoint({

apps/webapp/app/routes/api.v1.waitpoints.tokens.$waitpointFriendlyId.complete.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import { json } from "@remix-run/server-runtime";
22
import {
33
CompleteWaitpointTokenRequestBody,
44
type CompleteWaitpointTokenResponseBody,
5-
conditionallyExportPacket,
65
stringifyIO,
76
} from "@trigger.dev/core/v3";
87
import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
98
import { z } from "zod";
109
import { $replica } from "~/db.server";
1110
import { env } from "~/env.server";
1211
import { logger } from "~/services/logger.server";
12+
import { processWaitpointCompletionPacket } from "~/runEngine/concerns/waitpointCompletionPacket.server";
1313
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
1414
import { engine } from "~/v3/runEngine.server";
1515

@@ -52,9 +52,10 @@ const { action, loader } = createActionApiRoute(
5252
}
5353

5454
const stringifiedData = await stringifyIO(body.data);
55-
const finalData = await conditionallyExportPacket(
55+
const finalData = await processWaitpointCompletionPacket(
5656
stringifiedData,
57-
`${waitpointId}/waitpoint/token`
57+
authentication.environment,
58+
`${WaitpointId.toFriendlyId(waitpointId)}/token`
5859
);
5960

6061
const result = await engine.completeWaitpoint({
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
2+
import { json } from "@remix-run/server-runtime";
3+
import { z } from "zod";
4+
import { authenticateApiRequest } from "~/services/apiAuth.server";
5+
import { generatePresignedUrl } from "~/v3/objectStore.server";
6+
7+
const ParamsSchema = z.object({
8+
"*": z.string(),
9+
});
10+
11+
/**
12+
* PUT-only presign for packet uploads (SDK offload). Uses OBJECT_STORE_DEFAULT_PROTOCOL for
13+
* unprefixed keys; returns canonical storagePath for IOPacket.data. GET presigns use v1.
14+
*/
15+
export async function action({ request, params }: ActionFunctionArgs) {
16+
if (request.method.toUpperCase() !== "PUT") {
17+
return { status: 405, body: "Method Not Allowed" };
18+
}
19+
20+
const authenticationResult = await authenticateApiRequest(request);
21+
22+
if (!authenticationResult) {
23+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
24+
}
25+
26+
const parsedParams = ParamsSchema.parse(params);
27+
const filename = parsedParams["*"];
28+
29+
const signed = await generatePresignedUrl(
30+
authenticationResult.environment.project.externalRef,
31+
authenticationResult.environment.slug,
32+
filename,
33+
"PUT"
34+
);
35+
36+
if (!signed.success) {
37+
return json({ error: `Failed to generate presigned URL: ${signed.error}` }, { status: 500 });
38+
}
39+
40+
if (signed.storagePath === undefined) {
41+
return json({ error: "Failed to resolve storage path for packet upload" }, { status: 500 });
42+
}
43+
44+
return json({ presignedUrl: signed.url, storagePath: signed.storagePath });
45+
}

0 commit comments

Comments
 (0)