Skip to content

Commit 481bd44

Browse files
committed
Uploading env vars in a variety of formats now works
1 parent 86adbb4 commit 481bd44

File tree

13 files changed

+194
-20
lines changed

13 files changed

+194
-20
lines changed

.vscode/launch.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
"cwd": "${workspaceFolder}/references/v3-catalog",
4646
"sourceMaps": true
4747
},
48+
{
49+
"type": "node-terminal",
50+
"request": "launch",
51+
"name": "Debug V3 Management",
52+
"command": "pnpm run management",
53+
"cwd": "${workspaceFolder}/references/v3-catalog",
54+
"sourceMaps": true
55+
},
4856
{
4957
"type": "node",
5058
"request": "attach",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async function parseImportBody(request: Request): Promise<ImportEnvironmentVaria
5858
if (contentType.includes("multipart/form-data")) {
5959
const formData = await request.formData();
6060

61-
const file = formData.get("file");
61+
const file = formData.get("variables");
6262
const overwrite = formData.get("overwrite") === "true";
6363

6464
if (file instanceof File) {

apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export class EnvironmentVariablesRepository implements Repository {
122122
if (existingVariableKeys.length > 0) {
123123
return {
124124
success: false as const,
125-
error: `Some of the variables are already set for these environments`,
125+
error: `Some of the variables are already set for these environments. Set overwrite to true to overwrite them.`,
126126
variableErrors: existingVariableKeys.map((val) => ({
127127
key: val.key,
128128
error: `Variable already set in ${val.environments

docs/v3-openapi.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ paths:
523523
source: |-
524524
import { envvars, configure } from "@trigger.dev/sdk/v3";
525525
526-
configure({ apiKey: "tr_pat_123456" });
526+
configure({ secretKey: "tr_pat_123456" });
527527
528528
const variables = await envvars.list("proj_yubjwjsfkxnylobaqvqz", "dev");
529529
@@ -596,7 +596,7 @@ paths:
596596
source: |-
597597
import { envvars, configure } from "@trigger.dev/sdk/v3";
598598
599-
configure({ apiKey: "tr_pat_123456" });
599+
configure({ secretKey: "tr_pat_123456" });
600600
601601
await envvars.create("proj_yubjwjsfkxnylobaqvqz", "dev", {
602602
name: "SLACK_API_KEY",
@@ -690,10 +690,10 @@ paths:
690690
source: |-
691691
import { envvars, configure } from "@trigger.dev/sdk/v3";
692692
693-
configure({ apiKey: "tr_pat_123456" });
693+
configure({ secretKey: "tr_pat_123456" });
694694
695695
// Import variables from an array
696-
await envvars.import("proj_yubjwjsfkxnylobaqvqz", "dev", {
696+
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
697697
variables: [
698698
{
699699
name: "SLACK_API_KEY",
@@ -704,7 +704,7 @@ paths:
704704
});
705705
706706
// Import variables from a .env file
707-
await envvars.import("proj_yubjwjsfkxnylobaqvqz", "dev", {
707+
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
708708
file: fs.createReadStream(".env"),
709709
overwrite: false
710710
});

packages/core/src/v3/apiClient/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { taskContext } from "../task-context-api";
2727
import { ZodFetchOptions, isRecordLike, zodfetch, zodupload } from "../zodfetch";
2828
import { ImportEnvironmentVariablesParams } from "./types";
2929

30+
export type { ImportEnvironmentVariablesParams };
31+
3032
export type TriggerOptions = {
3133
spanParentAsLink?: boolean;
3234
};

packages/core/src/v3/schemas/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ export const EnvironmentVariableResponseBody = z.object({
394394
success: z.boolean(),
395395
});
396396

397+
export type EnvironmentVariableResponseBody = z.infer<typeof EnvironmentVariableResponseBody>;
398+
397399
export const EnvironmentVariableValue = z.object({
398400
value: z.string(),
399401
});

packages/core/src/v3/zodfetch.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { APIConnectionError, APIError } from "./apiErrors";
44
import { RetryOptions } from "./schemas";
55
import { calculateNextRetryDelay } from "./utils/retries";
66
import { FormDataEncoder } from "form-data-encoder";
7+
import { Readable } from "stream";
78

89
export const defaultRetryOptions = {
910
maxAttempts: 3,
@@ -26,6 +27,13 @@ export async function zodfetch<TResponseBodySchema extends z.ZodTypeAny>(
2627
return await _doZodFetch(schema, url, requestInit, options);
2728
}
2829

30+
export class MultipartBody {
31+
constructor(public body: any) {}
32+
get [Symbol.toStringTag](): string {
33+
return "MultipartBody";
34+
}
35+
}
36+
2937
export async function zodupload<
3038
TResponseBodySchema extends z.ZodTypeAny,
3139
TBody = Record<string, unknown>,
@@ -38,22 +46,25 @@ export async function zodupload<
3846
): Promise<z.output<TResponseBodySchema>> {
3947
const form = await createForm(body);
4048
const encoder = new FormDataEncoder(form);
41-
const headers = encoder.headers;
4249

4350
const finalHeaders: Record<string, string> = {};
4451

4552
for (const [key, value] of Object.entries(requestInit?.headers || {})) {
4653
finalHeaders[key] = value as string;
4754
}
4855

49-
for (const [key, value] of Object.entries(headers)) {
56+
for (const [key, value] of Object.entries(encoder.headers)) {
5057
finalHeaders[key] = value;
5158
}
5259

60+
finalHeaders["Content-Length"] = String(encoder.contentLength);
61+
5362
const finalRequestInit: RequestInit = {
5463
...requestInit,
5564
headers: finalHeaders,
56-
body: form,
65+
body: Readable.from(encoder) as any,
66+
// @ts-expect-error
67+
duplex: "half",
5768
};
5869

5970
return await _doZodFetch(schema, url, finalRequestInit, options);
@@ -228,7 +239,12 @@ const addFormValue = async (form: FormData, key: string, value: unknown): Promis
228239
// TODO: make nested formats configurable
229240
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
230241
form.append(key, String(value));
231-
} else if (isUploadable(value)) {
242+
} else if (
243+
isUploadable(value) ||
244+
isBlobLike(value) ||
245+
value instanceof Buffer ||
246+
value instanceof ArrayBuffer
247+
) {
232248
const file = await toFile(value);
233249
form.append(key, file as File);
234250
} else if (Array.isArray(value)) {
@@ -336,9 +352,6 @@ function propsForError(value: any): string {
336352
const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator<unknown> =>
337353
value != null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";
338354

339-
import { ReadStream as FsReadStream } from "node:fs";
340-
import { Readable } from "node:stream";
341-
342355
/**
343356
* Intended to match web.Blob, node.Blob, node-fetch.Blob, etc.
344357
*/
@@ -372,7 +385,7 @@ export interface ResponseLike {
372385
blob(): Promise<BlobLike>;
373386
}
374387

375-
export type Uploadable = FileLike | ResponseLike | FsReadStream;
388+
export type Uploadable = FileLike | ResponseLike | Readable;
376389

377390
export const isResponseLike = (value: any): value is ResponseLike =>
378391
value != null &&
@@ -402,7 +415,7 @@ export const isBlobLike = (
402415
typeof value.slice === "function" &&
403416
typeof value.arrayBuffer === "function";
404417

405-
export const isFsReadStream = (value: any): value is FsReadStream => value instanceof FsReadStream;
418+
export const isFsReadStream = (value: any): value is Readable => value instanceof Readable;
406419

407420
export const isUploadable = (value: any): value is Uploadable => {
408421
return isFileLike(value) || isResponseLike(value) || isFsReadStream(value);
@@ -420,4 +433,5 @@ export const isRecordLike = (value: any): value is Record<string, string> =>
420433
value != null &&
421434
typeof value === "object" &&
422435
!Array.isArray(value) &&
436+
Object.keys(value).length > 0 &&
423437
Object.keys(value).every((key) => typeof key === "string" && typeof value[key] === "string");
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type {
2+
ImportEnvironmentVariablesParams,
3+
EnvironmentVariableResponseBody,
4+
} from "@trigger.dev/core/v3";
5+
import { SemanticInternalAttributes, apiClientManager, taskContext } from "@trigger.dev/core/v3";
6+
import { apiClientMissingError } from "./shared";
7+
import { tracer } from "./tracer";
8+
9+
export type { ImportEnvironmentVariablesParams };
10+
11+
export async function upload(
12+
projectRef: string,
13+
slug: string,
14+
params: ImportEnvironmentVariablesParams
15+
): Promise<EnvironmentVariableResponseBody>;
16+
export async function upload(
17+
params: ImportEnvironmentVariablesParams
18+
): Promise<EnvironmentVariableResponseBody>;
19+
export async function upload(
20+
projectRefOrParams: string | ImportEnvironmentVariablesParams,
21+
slug?: string,
22+
params?: ImportEnvironmentVariablesParams
23+
): Promise<EnvironmentVariableResponseBody> {
24+
let $projectRef: string;
25+
let $params: ImportEnvironmentVariablesParams;
26+
let $slug: string;
27+
28+
if (taskContext.ctx) {
29+
if (typeof projectRefOrParams === "string") {
30+
$projectRef = projectRefOrParams;
31+
$slug = slug ?? taskContext.ctx.environment.slug;
32+
33+
if (!params) {
34+
throw new Error("params is required");
35+
}
36+
37+
$params = params;
38+
} else {
39+
$params = projectRefOrParams;
40+
$projectRef = taskContext.ctx.project.ref;
41+
$slug = taskContext.ctx.environment.slug;
42+
}
43+
} else {
44+
if (typeof projectRefOrParams !== "string") {
45+
throw new Error("projectRef is required");
46+
}
47+
48+
if (!slug) {
49+
throw new Error("slug is required");
50+
}
51+
52+
if (!params) {
53+
throw new Error("params is required");
54+
}
55+
56+
$projectRef = projectRefOrParams;
57+
$slug = slug;
58+
$params = params;
59+
}
60+
61+
const apiClient = apiClientManager.client;
62+
63+
if (!apiClient) {
64+
throw apiClientMissingError();
65+
}
66+
67+
return await tracer.startActiveSpan(
68+
"envvars.upload",
69+
async (span) => {
70+
return await apiClient.importEnvVars($projectRef, $slug, $params);
71+
},
72+
{
73+
attributes: {
74+
[SemanticInternalAttributes.STYLE_ICON]: "file-upload",
75+
},
76+
}
77+
);
78+
}

packages/trigger-sdk/src/v3/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ export {
2727
type LogLevel,
2828
} from "@trigger.dev/core/v3";
2929

30-
export { runs } from "./management";
30+
export { runs } from "./runs";
3131
export * as schedules from "./schedules";
32+
export * as envvars from "./envvars";
33+
export type { ImportEnvironmentVariablesParams } from "./envvars";
3234

3335
/**
3436
* Register the global API client configuration. Alternatively, you can set the `TRIGGER_SECRET_KEY` and `TRIGGER_API_URL` environment variables.

references/v3-catalog/.uploadable-env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
THIS_IS_MY_KEY=1234567890
2+
ANOTHER_KEY=0987654321

references/v3-catalog/src/management.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,64 @@
11
import { tracer } from "./tracer";
2-
import { APIError, configure, runs, schedules } from "@trigger.dev/sdk/v3";
2+
import { APIError, configure, runs, schedules, envvars } from "@trigger.dev/sdk/v3";
33
import { simpleChildTask } from "./trigger/subtasks";
44
import dotenv from "dotenv";
55
import { firstScheduledTask } from "./trigger/scheduled";
6+
import { createReadStream } from "node:fs";
67

78
dotenv.config();
89

10+
async function uploadEnvVars() {
11+
configure({
12+
secretKey: process.env.TRIGGER_ACCESS_TOKEN,
13+
});
14+
15+
const response1 = await envvars.upload("yubjwjsfkxnylobaqvqz", "dev", {
16+
variables: {
17+
MY_ENV_VAR: "MY_ENV_VAR_VALUE",
18+
},
19+
overwrite: true,
20+
});
21+
22+
console.log("response1", response1);
23+
24+
const response2 = await envvars.upload("yubjwjsfkxnylobaqvqz", "dev", {
25+
variables: createReadStream(".uploadable-env"),
26+
overwrite: true,
27+
});
28+
29+
console.log("response2", response2);
30+
31+
const response3 = await envvars.upload("yubjwjsfkxnylobaqvqz", "prod", {
32+
variables: createReadStream(".uploadable-env"),
33+
overwrite: true,
34+
});
35+
36+
console.log("response3", response3);
37+
38+
const response4 = await envvars.upload("yubjwjsfkxnylobaqvqz", "prod", {
39+
variables: await fetch(
40+
"https://gist.githubusercontent.com/ericallam/7a1001c6b03986a74d0f8aad4fd890aa/raw/fe2bc4da82f3b17178d47f58ec1458af47af5035/.env"
41+
),
42+
overwrite: true,
43+
});
44+
45+
console.log("response4", response4);
46+
47+
const response5 = await envvars.upload("yubjwjsfkxnylobaqvqz", "prod", {
48+
variables: new File(["IM_A_FILE=GREAT_FOR_YOU"], ".env"),
49+
overwrite: true,
50+
});
51+
52+
console.log("response5", response5);
53+
54+
const response6 = await envvars.upload("yubjwjsfkxnylobaqvqz", "prod", {
55+
variables: Buffer.from("IN_BUFFER=TRUE"),
56+
overwrite: true,
57+
});
58+
59+
console.log("response6", response6);
60+
}
61+
962
export async function run() {
1063
await tracer.startActiveSpan("run", async (span) => {
1164
try {
@@ -88,4 +141,5 @@ export async function run() {
88141
});
89142
}
90143

91-
run();
144+
// run();
145+
uploadEnvVars().catch(console.error);

references/v3-catalog/src/trigger/simple.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import "server-only";
2-
import { logger, task, wait } from "@trigger.dev/sdk/v3";
2+
import { envvars, logger, task, wait } from "@trigger.dev/sdk/v3";
33
import { traceAsync } from "@/telemetry";
44

55
export const simplestTask = task({
@@ -31,6 +31,18 @@ export const taskWithSpecialCharacters = task({
3131
},
3232
});
3333

34+
export const updateEnvVars = task({
35+
id: "update-env-vars",
36+
run: async () => {
37+
return await envvars.upload({
38+
variables: await fetch(
39+
"https://gist.githubusercontent.com/ericallam/7a1001c6b03986a74d0f8aad4fd890aa/raw/fe2bc4da82f3b17178d47f58ec1458af47af5035/.env"
40+
),
41+
overwrite: true,
42+
});
43+
},
44+
});
45+
3446
export const createJsonHeroDoc = task({
3547
id: "create-jsonhero-doc",
3648
run: async (payload: { title: string; content: any }, { ctx }) => {

0 commit comments

Comments
 (0)