Compare commits

...

15 Commits

Author SHA1 Message Date
Eric Allam b73ae3f927 Release 3.0.0-beta.38 2024-06-19 10:40:29 +01:00
github-actions[bot] b605b892ac chore: Update version for release (beta) (#1166)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-19 10:39:24 +01:00
Eric Allam 233316f7e8 TaskRun.startedAt will now be lazily migrated 2024-06-18 15:49:14 +01:00
Eric Allam 1b90ffbb8c Add a changeset for the usage tracking PR (I forgot) 2024-06-18 14:56:25 +01:00
Matt Aitken b45ca4e146 If the timezone has changed then we need to reschedule the next scheduled run 2024-06-18 14:49:16 +01:00
Eric Allam 25d15578f7 Remove the update migration 2024-06-18 13:51:29 +01:00
Eric Allam fe865a0f49 Make startedAt backwards compat by giving it a default 2024-06-18 13:16:36 +01:00
Eric Allam 0ed93a748e v3: Remove aggressive otel flush timeouts in dev/prod 2024-06-18 11:48:35 +01:00
Michael Li e02320f65d fix: allow command login to read api url from cli args (#1168)
* fix: allow login to read api url from args

* Create strong-phones-smoke.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2024-06-18 09:43:42 +01:00
Eric Allam 85a543d8ec v3: usage tracking (#1163)
* Starting to measure wall time and cpu time in the workers, and reporting that via otel and to completed task run attempts

* Move usage tracking outside of the executor

* WIP prod usage tracking

* WIP

* WIP custom fetch to openmeter

* Create a usage client

* WIP

* WIP

* Implement new machine preset stuff and send usage reports to OpenMeter from webapp

* WIP

* Expose usage info to the client

* Add usage and cost to TaskEvent

* Add ability to globally configure the task machine preset

* Report start run usage

* Change the machine docs to use presets

* setExpirationTime to 24h

* Removed logs

* Update machines.mdx

* Removed console.logs

* Handle revalidating JWT tokens

* Couple tweaks

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-06-18 09:40:23 +01:00
Ryan Lee 10ceb85a92 fix: cloudfront timeouts not being caught (#1162) 2024-06-14 15:11:39 +01:00
Matt Aitken c405ae7117 Schedule limits and timezone support (#1165)
* Added maximumScheduleInstancesLimit column to Org, default to 20

* Docs on the schedule limits and improved soft-limit communication

* Added limit info to the schedules list page

* Created a task that creates schedules, useful for testing

* Make deduplicationKey required when creating/updating a schedule using the SDK

* New schedule button shows an alert if you’re over the limit

* Added timezone to the form and db

* WIP on the timezone dropdown for the create/edit schedule form

* Use the new filter search for timezones

* Made the timezone dropdown faster by fixing the virtualization

* The preview table is working and added a nice message about daylight savings

* Created a page where you can view the full list of timezones

The URL is included in the error message if you send an invalid time using the SDK

* Creating tasks with the timezone

* Added timezone support the the scheduler and the schedules list

* Added timezone support to more of the schedules UI

* The timezone comes through to scheduled runs with nice JSDocs

* Allow setting the timezone from the SDK

* Always have a timezone on a schedule

* Updated jsdocs

* Updated catalog example

* Changed the column to be a string, not null. Added the timezone across the SDK

* API endpoint for getting the timezones

* Added an SDK function to get the list of timezones

* Added timezones to the docs

* Changeset: Added timezone support to schedules

* Added support for testing timezone

* Tidied up imports

* Imports

* Imports

* Update limits.mdx

* Fixed a couple type issues and use the already exported zodfetch

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2024-06-14 13:31:27 +01:00
nicktrn 3687fcb61e Make pod cleaner interval configurable 2024-06-14 12:12:42 +01:00
Émile Ré d4ccdf7105 v3 CLI compiling E2E test suite (#1135)
* Boilerplate server-only use case

* wip: integration suite instrumentation setup

* Working poc testing compileProject

* Add pnpm script to run e2e tests only

* Use vitest globals

* Remove commented line

* Remove useless export

* Add modifier to test only one fixture project

* Handle package manager and log level choice

* Update server-only example

* Setup / teardown + split compile for package manager capabilities

* Ignore yarn files

* Fix issue with corepack, store version in engines field

* Rename test file

* Fix npm updates yarn.lock

* Move typecheking in a dedicated test

* Stop bundling the compile command to allow for more granular testing

* Put config resolving in separate test

* Add no-config test case and add test case expected errors configuration

* Add wantCompilationError option

* Add dependencies handling

* Use packageManager passed as option to resolve required deps

* Remove unused guard clauses

* Add postinstall & hash handling step

* Add worker start test

* Handle yarn.lock copy renaming on sigterm and sigkill

* Update vitest and use concurrent option

* Add a readme file

* Add CI workflow

* Fix handle cli deps

* Run cli v3 e2e tests on publish action

* Increase timeout on deps resolving step

* Add changeset

* Remove .pnp.cjs as we use yarn with nodeLinker node-modules

* Add missing .yarnrc.yml file

* No need to build CLI to run E2E tests

* Remove bun.lockb files

* Update beige-pears-explode.md

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
2024-06-14 11:28:36 +01:00
nicktrn e08b4569e5 Improve checkpoint restore logging 2024-06-13 10:18:41 +01:00
228 changed files with 14862 additions and 576 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Add an e2e suite to test compiling with v3 CLI.
+7
View File
@@ -0,0 +1,7 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
---
v3: Usage tracking
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
v3: Remove aggressive otel flush timeouts in dev/prod
+6
View File
@@ -46,6 +46,7 @@
"changesets": [
"afraid-sheep-joke",
"angry-eagles-trade",
"beige-pears-explode",
"beige-pens-dance",
"big-tomatoes-deliver",
"blue-pumas-whisper",
@@ -71,6 +72,7 @@
"green-bags-wink",
"hot-buckets-behave",
"hot-fishes-retire",
"hot-wasps-sin",
"itchy-chairs-itch",
"khaki-apricots-design",
"khaki-poems-lay",
@@ -87,6 +89,7 @@
"many-ligers-pump",
"mighty-camels-joke",
"mighty-flowers-train",
"mighty-parrots-sin",
"nasty-jars-pump",
"new-pants-beg",
"new-rivers-tell",
@@ -112,6 +115,7 @@
"sharp-zebras-serve",
"shiny-coats-cry",
"silly-suits-switch",
"silver-doors-juggle",
"six-ligers-exist",
"sixty-insects-watch",
"slow-buses-own",
@@ -124,6 +128,7 @@
"strange-sheep-pull",
"strong-lemons-add",
"strong-owls-know",
"strong-phones-smoke",
"stupid-adults-sniff",
"stupid-bulldogs-applaud",
"sweet-lizards-press",
@@ -132,6 +137,7 @@
"tame-guests-know",
"tender-moose-tell",
"tender-oranges-rhyme",
"tender-turkeys-compete",
"thin-parents-heal",
"thirty-islands-kiss",
"tidy-balloons-suffer",
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Make deduplicationKey required when creating/updating a schedule
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
fix: allow command login to read api url from cli args
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Added timezone support to schedules
+47 -3
View File
@@ -1,9 +1,53 @@
name: "🧪 E2E Tests"
name: "E2E"
on:
workflow_call:
inputs:
package:
description: The identifier of the job to run
default: webapp
required: false
type: string
jobs:
e2e:
name: "🧪 E2E Tests"
cli-v3:
name: "🧪 CLI v3 tests"
if: inputs.package == 'cli-v3' || inputs.package == ''
runs-on: buildjet-8vcpu-ubuntu-2204
strategy:
fail-fast: false
matrix:
package-manager: ["npm", "pnpm", "yarn"]
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⎔ Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 8.15.5
- name: ⎔ Setup node
uses: buildjet/setup-node@v3
with:
node-version: 20.11.1
cache: "pnpm"
- name: 📥 Download deps
run: pnpm install --frozen-lockfile --filter trigger.dev...
- name: 🔧 Build v3 cli monorepo dependencies
run: pnpm run build --filter trigger.dev^...
- name: 🔧 Build worker template files
run: pnpm --filter trigger.dev run build:workers
- name: Run E2E Tests
run: |
PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
webapp:
name: "🧪 Webapp tests"
if: inputs.package == 'webapp' || inputs.package == ''
runs-on: buildjet-16vcpu-ubuntu-2204
steps:
- name: 🐳 Login to Docker Hub
+2
View File
@@ -29,4 +29,6 @@ jobs:
# e2e:
# uses: ./.github/workflows/e2e.yml
# with:
# package: webapp
# secrets: inherit
+5 -3
View File
@@ -49,9 +49,11 @@ jobs:
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
# e2e:
# uses: ./.github/workflows/e2e.yml
# secrets: inherit
e2e:
uses: ./.github/workflows/e2e.yml
with:
package: cli-v3
secrets: inherit
publish:
needs: [typecheck, units]
+11 -5
View File
@@ -7,7 +7,12 @@ import {
TaskOperationsIndexOptions,
TaskOperationsRestoreOptions,
} from "@trigger.dev/core-apps";
import { Machine, PostStartCauses, PreStopCauses, EnvironmentType } from "@trigger.dev/core/v3";
import {
MachinePreset,
PostStartCauses,
PreStopCauses,
EnvironmentType,
} from "@trigger.dev/core/v3";
import { randomUUID } from "crypto";
import { TaskMonitor } from "./taskMonitor";
import { PodCleaner } from "./podCleaner";
@@ -16,6 +21,7 @@ const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local";
const NODE_NAME = process.env.NODE_NAME || "local";
const OTEL_EXPORTER_OTLP_ENDPOINT =
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318";
const POD_CLEANER_INTERVAL_SECONDS = Number(process.env.POD_CLEANER_INTERVAL_SECONDS || "300");
const logger = new SimpleLogger(`[${NODE_NAME}]`);
logger.log(`running in ${RUNTIME_ENV} mode`);
@@ -398,10 +404,10 @@ class KubernetesTaskOperations implements TaskOperations {
};
}
#getResourcesFromMachineConfig(config: Machine): ComputeResources {
#getResourcesFromMachineConfig(preset: MachinePreset): ComputeResources {
return {
cpu: `${config.cpu}`,
memory: `${config.memory}G`,
cpu: `${preset.cpu}`,
memory: `${preset.memory}G`,
};
}
@@ -555,7 +561,7 @@ taskMonitor.start();
const podCleaner = new PodCleaner({
runtimeEnv: RUNTIME_ENV,
namespace: "default",
intervalInSeconds: 300,
intervalInSeconds: POD_CLEANER_INTERVAL_SECONDS,
});
podCleaner.start();
@@ -6,6 +6,7 @@ type DateTimeProps = {
timeZone?: string;
includeSeconds?: boolean;
includeTime?: boolean;
showTimezone?: boolean;
};
export const DateTime = ({
@@ -13,6 +14,7 @@ export const DateTime = ({
timeZone,
includeSeconds = true,
includeTime = true,
showTimezone = false,
}: DateTimeProps) => {
const locales = useLocales();
@@ -42,7 +44,12 @@ export const DateTime = ({
);
}, [locales, includeSeconds, realDate]);
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
return (
<Fragment>
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
{showTimezone ? ` (${timeZone ?? "UTC"})` : null}
</Fragment>
);
};
export function formatDateTime(
@@ -8,6 +8,7 @@ import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { ShortcutKey } from "./ShortcutKey";
import { ChevronDown } from "lucide-react";
import { MatchSorterOptions, matchSorter } from "match-sorter";
const sizes = {
small: {
@@ -75,7 +76,10 @@ export interface SelectProps<TValue extends string | string[], TItem>
showHeading?: boolean;
items?: TItem[] | Section<TItem>[];
empty?: React.ReactNode;
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean;
filter?:
| boolean
| MatchSorterOptions<TItem>
| ((item: ItemFromSection<TItem>, search: string, title?: string) => boolean);
children:
| React.ReactNode
| ((
@@ -129,18 +133,44 @@ export function Select<TValue extends string | string[], TItem>({
if (!items) return [];
if (!searchValue || !filter) return items;
if (typeof filter === "function") {
if (isSection(items)) {
return items
.map((section) => ({
...section,
items: section.items.filter((item) =>
filter(item as ItemFromSection<TItem>, searchValue, section.title)
),
}))
.filter((section) => section.items.length > 0);
}
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
}
if (typeof filter === "boolean" && filter) {
if (isSection(items)) {
return items
.map((section) => ({
...section,
items: matchSorter(section.items, searchValue),
}))
.filter((section) => section.items.length > 0);
}
return matchSorter(items, searchValue);
}
if (isSection(items)) {
return items
.map((section) => ({
...section,
items: section.items.filter((item) =>
filter(item as ItemFromSection<TItem>, searchValue, section.title)
),
items: matchSorter(section.items, searchValue, filter),
}))
.filter((section) => section.items.length > 0);
}
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
return matchSorter(items, searchValue, filter);
}, [searchValue, items]);
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
@@ -0,0 +1,63 @@
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
import { SelectItem } from "../primitives/Select";
export function TimezoneList({ timezones }: { timezones: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: timezones.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 28,
});
return (
<div
ref={parentRef}
className="max-h-[calc(min(480px,var(--popover-available-height))-2.35rem)] overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<TimezoneCell
key={virtualItem.key}
size={virtualItem.size}
start={virtualItem.start}
timezone={timezones[virtualItem.index]}
/>
))}
</div>
</div>
);
}
function TimezoneCell({
timezone,
size,
start,
}: {
timezone: string;
size: number;
start: number;
}) {
return (
<SelectItem
value={timezone}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${size}px`,
transform: `translateY(${start}px)`,
}}
>
{timezone}
</SelectItem>
);
}
+16 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server";
import { z } from "zod";
import { isValidRegex } from "./utils/regex";
import { isValidDatabaseUrl } from "./utils/db";
@@ -189,6 +189,21 @@ const EnvironmentSchema = z.object({
V2_MARQS_VERBOSE: z.string().default("0"),
V3_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"),
V2_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"),
/* Usage settings */
USAGE_EVENT_URL: z.string().optional(),
PROD_USAGE_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(),
CENTS_PER_HOUR_MICRO: z.coerce.number().default(0),
CENTS_PER_HOUR_SMALL_1X: z.coerce.number().default(0),
CENTS_PER_HOUR_SMALL_2X: z.coerce.number().default(0),
CENTS_PER_HOUR_MEDIUM_1X: z.coerce.number().default(0),
CENTS_PER_HOUR_MEDIUM_2X: z.coerce.number().default(0),
CENTS_PER_HOUR_LARGE_1X: z.coerce.number().default(0),
CENTS_PER_HOUR_LARGE_2X: z.coerce.number().default(0),
BASE_RUN_COST_IN_CENTS: z.coerce.number().default(0),
USAGE_OPEN_METER_API_KEY: z.string().optional(),
USAGE_OPEN_METER_BASE_URL: z.string().optional(),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
@@ -27,6 +27,7 @@ export function detectResponseIsTimeout(rawBody: string, response?: Response) {
return (
isResponseVercelTimeout(response) ||
isResponseCloudfrontTimeout(response) ||
isResponseDenoDeployTimeout(rawBody, response) ||
isResponseCloudflareTimeout(rawBody, response)
);
@@ -50,3 +51,7 @@ function isResponseVercelTimeout(response: Response) {
function isResponseDenoDeployTimeout(rawBody: string, response: Response) {
return response.status === 502 && rawBody.includes("TIME_LIMIT");
}
function isResponseCloudfrontTimeout(response: Response) {
return response.status === 504 && typeof response.headers.get("x-amz-cf-id") === "string";
}
@@ -80,7 +80,7 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
version: taskRun.lockedToVersion ? taskRun.lockedToVersion.version : undefined,
createdAt: taskRun.createdAt ?? undefined,
updatedAt: taskRun.updatedAt ?? undefined,
startedAt: taskRun.lockedAt ?? undefined,
startedAt: taskRun.startedAt ?? taskRun.lockedAt ?? undefined,
finishedAt: ApiRetrieveRunPresenter.isStatusFinished(apiStatus)
? taskRun.updatedAt
: undefined,
@@ -1,6 +1,7 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
import { getTimezones } from "~/utils/timezones.server";
type EditScheduleOptions = {
userId: string;
@@ -74,6 +75,7 @@ export class EditSchedulePresenter {
return {
possibleTasks: possibleTasks.map((task) => task.slug),
possibleEnvironments,
possibleTimezones: getTimezones(),
schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments),
};
}
@@ -91,6 +93,7 @@ export class EditSchedulePresenter {
externalId: true,
deduplicationKey: true,
userProvidedDeduplicationKey: true,
timezone: true,
taskIdentifier: true,
instances: {
select: {
@@ -156,6 +156,7 @@ export class RunListPresenter extends BasePresenter {
runtimeEnvironmentId: string;
status: TaskRunStatus;
createdAt: Date;
startedAt: Date | null;
lockedAt: Date | null;
updatedAt: Date;
isTest: boolean;
@@ -172,6 +173,7 @@ export class RunListPresenter extends BasePresenter {
tr."runtimeEnvironmentId" AS "runtimeEnvironmentId",
tr.status AS status,
tr."createdAt" AS "createdAt",
tr."startedAt" AS "startedAt",
tr."lockedAt" AS "lockedAt",
tr."updatedAt" AS "updatedAt",
tr."isTest" AS "isTest",
@@ -272,13 +274,15 @@ export class RunListPresenter extends BasePresenter {
const hasFinished = FINISHED_STATUSES.includes(run.status);
const startedAt = run.startedAt ?? run.lockedAt;
return {
id: run.id,
friendlyId: run.runFriendlyId,
number: Number(run.number),
createdAt: run.createdAt.toISOString(),
updatedAt: run.updatedAt.toISOString(),
startedAt: run.lockedAt ? run.lockedAt.toISOString() : undefined,
startedAt: startedAt ? startedAt.toISOString() : undefined,
hasFinished,
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
isTest: run.isTest,
@@ -22,6 +22,7 @@ export type ScheduleListItem = {
userProvidedDeduplicationKey: boolean;
cron: string;
cronDescription: string;
timezone: string;
externalId: string | null;
nextRun: Date;
lastRun: Date | undefined;
@@ -36,7 +37,6 @@ export type ScheduleList = Awaited<ReturnType<ScheduleListPresenter["call"]>>;
export type ScheduleListAppliedFilters = ScheduleList["filters"];
export class ScheduleListPresenter extends BasePresenter {
public async call({
userId,
projectId,
@@ -71,12 +71,23 @@ export class ScheduleListPresenter extends BasePresenter {
},
},
},
organization: {
select: {
maximumSchedulesLimit: true,
},
},
},
where: {
id: projectId,
},
});
const schedulesCount = await this._prisma.taskSchedule.count({
where: {
projectId,
},
});
//get all possible scheduled tasks
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
distinct: ["slug"],
@@ -140,6 +151,7 @@ export class ScheduleListPresenter extends BasePresenter {
userProvidedDeduplicationKey: true,
generatorExpression: true,
generatorDescription: true,
timezone: true,
externalId: true,
instances: {
select: {
@@ -218,10 +230,11 @@ export class ScheduleListPresenter extends BasePresenter {
userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
cron: schedule.generatorExpression,
cronDescription: schedule.generatorDescription,
timezone: schedule.timezone,
active: schedule.active,
externalId: schedule.externalId,
lastRun: latestRun?.createdAt,
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression),
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression, schedule.timezone),
environments: schedule.instances.map((instance) => {
const environment = project.environments.find((env) => env.id === instance.environmentId);
if (!environment) {
@@ -245,6 +258,10 @@ export class ScheduleListPresenter extends BasePresenter {
return displayableEnvironment(environment, userId);
}),
hasFilters,
limits: {
used: schedulesCount,
limit: project.organization.maximumSchedulesLimit,
},
filters: {
tasks,
environments,
@@ -311,7 +311,7 @@ export class TaskListPresenter extends BasePresenter {
>`
SELECT
tr."taskIdentifier",
AVG(EXTRACT(EPOCH FROM (tr."updatedAt" - tr."lockedAt"))) as duration
AVG(EXTRACT(EPOCH FROM (tr."updatedAt" - COALESCE(tr."startedAt", tr."lockedAt")))) as duration
FROM
${sqlDatabaseSchema}."TaskRun" as tr
WHERE
@@ -6,6 +6,7 @@ import {
TaskTriggerSource,
} from "@trigger.dev/database";
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
import { getTimezones } from "~/utils/timezones.server";
import { getUsername } from "~/utils/username";
type TestTaskOptions = {
@@ -37,6 +38,7 @@ export type TestTask =
| {
triggerSource: "SCHEDULED";
task: Task;
possibleTimezones: string[];
runs: ScheduledRun[];
};
@@ -61,6 +63,7 @@ export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
timestamp: Date;
lastTimestamp?: Date;
externalId?: string;
timezone: string;
};
};
@@ -168,9 +171,11 @@ export class TestTaskPresenter {
),
};
case "SCHEDULED":
const possibleTimezones = getTimezones();
return {
triggerSource: "SCHEDULED",
task: taskWithEnvironment,
possibleTimezones,
runs: (
await Promise.all(
latestRuns.map(async (r) => {
@@ -195,6 +200,9 @@ export class TestTaskPresenter {
async function getScheduleTaskRunPayload(run: RawRun) {
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
if (!payload.timezone) {
payload.timezone = "UTC";
}
const parsed = ScheduledTaskPayload.safeParse(payload);
return parsed;
}
@@ -1,8 +1,8 @@
import { ScheduleObject } from "@trigger.dev/core/v3";
import { PrismaClient, prisma } from "~/db.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
import { RunListPresenter } from "./RunListPresenter.server";
import { ScheduleObject } from "@trigger.dev/core/v3";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
type ViewScheduleOptions = {
userId?: string;
@@ -24,6 +24,7 @@ export class ViewSchedulePresenter {
friendlyId: true,
generatorExpression: true,
generatorDescription: true,
timezone: true,
externalId: true,
deduplicationKey: true,
userProvidedDeduplicationKey: true,
@@ -68,7 +69,7 @@ export class ViewSchedulePresenter {
}
const nextRuns = schedule.active
? nextScheduledTimestamps(schedule.generatorExpression, new Date(), 5)
? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
: [];
const runPresenter = new RunListPresenter(this.#prismaClient);
@@ -82,6 +83,7 @@ export class ViewSchedulePresenter {
return {
schedule: {
...schedule,
timezone: schedule.timezone,
cron: schedule.generatorExpression,
cronDescription: schedule.generatorDescription,
nextRuns,
@@ -105,6 +107,7 @@ export class ViewSchedulePresenter {
expression: result.schedule.cron,
description: result.schedule.cronDescription,
},
timezone: result.schedule.timezone,
externalId: result.schedule.externalId ?? undefined,
deduplicationKey: result.schedule.userProvidedDeduplicationKey
? result.schedule.deduplicationKey ?? undefined
@@ -185,7 +185,8 @@ export default function Page() {
const location = useLocation();
const organization = useOrganization();
const project = useProject();
const user = useUser();
const isUtc = schedule.timezone === "UTC";
return (
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
@@ -210,6 +211,7 @@ export default function Page() {
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
</div>
</Property>
<Property label="Timezone">{schedule.timezone}</Property>
<Property label="Environments">
<EnvironmentLabels size="small" environments={schedule.environments} />
</Property>
@@ -245,19 +247,21 @@ export default function Page() {
<Table>
<TableHeader>
<TableRow>
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
<TableHeaderCell>UTC</TableHeaderCell>
<TableHeaderCell>Local time</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{schedule.nextRuns.map((run, index) => (
<TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={schedule.timezone} />
</TableCell>
)}
<TableCell>
<DateTime date={run} timeZone="UTC" />
</TableCell>
<TableCell>
<DateTime date={run} />
</TableCell>
</TableRow>
))}
</TableBody>
@@ -21,7 +21,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
};
export default function Page() {
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
useTypedLoaderData<typeof loader>();
return (
@@ -29,6 +29,7 @@ export default function Page() {
schedule={schedule}
possibleTasks={possibleTasks}
possibleEnvironments={possibleEnvironments}
possibleTimezones={possibleTimezones}
showGenerateField={showGenerateField}
/>
);
@@ -20,7 +20,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
};
export default function Page() {
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
useTypedLoaderData<typeof loader>();
return (
@@ -29,6 +29,7 @@ export default function Page() {
possibleTasks={possibleTasks}
possibleEnvironments={possibleEnvironments}
showGenerateField={showGenerateField}
possibleTimezones={possibleTimezones}
/>
);
}
@@ -4,12 +4,21 @@ import { Outlet, useLocation, useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
import { Feedback } from "~/components/Feedback";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel, EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "~/components/primitives/Dialog";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph";
@@ -78,6 +87,7 @@ export default function Page() {
possibleEnvironments,
hasFilters,
filters,
limits,
currentPage,
totalPages,
} = useTypedLoaderData<typeof loader>();
@@ -107,15 +117,43 @@ export default function Page() {
</PropertyTable>
</AdminDebugTooltip>
<LinkButton
LeadingIcon={PlusIcon}
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
variant="primary/small"
shortcut={{ key: "n" }}
disabled={possibleTasks.length === 0 || isShowingNewPane}
>
New schedule
</LinkButton>
{limits.used >= limits.limit ? (
<Dialog>
<DialogTrigger asChild>
<Button
LeadingIcon={PlusIcon}
variant="primary/small"
shortcut={{ key: "n" }}
disabled={possibleTasks.length === 0 || isShowingNewPane}
>
New schedule
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>You've exceeded your limit</DialogHeader>
<DialogDescription>
You've used {limits.used}/{limits.limit} of your schedules. You can request more
schedules.
</DialogDescription>
<DialogFooter>
<Feedback
button={<Button variant="primary/medium">Request more</Button>}
defaultValue="help"
/>
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<LinkButton
LeadingIcon={PlusIcon}
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
variant="primary/small"
shortcut={{ key: "n" }}
disabled={possibleTasks.length === 0 || isShowingNewPane}
>
New schedule
</LinkButton>
)}
</PageAccessories>
</NavBar>
<PageBody scrollable={false}>
@@ -142,7 +180,21 @@ export default function Page() {
</div>
<SchedulesTable schedules={schedules} hasFilters={hasFilters} />
<div className="mt-2 justify-end">
<div className="mt-2 justify-between">
<Paragraph variant="extra-small" className="mt-3">
<span className={limits.used >= limits.limit ? "text-warning" : ""}>
You've used {limits.used}/{limits.limit} of your schedules.
</span>{" "}
<Feedback
button={
<button className=" text-secondary transition hover:text-indigo-400">
Request more
</button>
}
defaultValue="help"
/>
.
</Paragraph>
<PaginationControls currentPage={currentPage} totalPages={totalPages} />
</div>
</div>
@@ -236,12 +288,13 @@ function SchedulesTable({
<TableRow>
<TableHeaderCell>ID</TableHeaderCell>
<TableHeaderCell>Task ID</TableHeaderCell>
<TableHeaderCell>External ID</TableHeaderCell>
<TableHeaderCell>CRON</TableHeaderCell>
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
<TableHeaderCell>External ID</TableHeaderCell>
<TableHeaderCell>Timezone</TableHeaderCell>
<TableHeaderCell>Next run</TableHeaderCell>
<TableHeaderCell>Last run</TableHeaderCell>
<TableHeaderCell>Deduplication key</TableHeaderCell>
<TableHeaderCell>Next run (UTC)</TableHeaderCell>
<TableHeaderCell>Last run (UTC)</TableHeaderCell>
<TableHeaderCell>Environments</TableHeaderCell>
<TableHeaderCell>Enabled</TableHeaderCell>
</TableRow>
@@ -262,6 +315,9 @@ function SchedulesTable({
<TableCell to={path} className={cellClass}>
{schedule.taskIdentifier}
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.externalId ? schedule.externalId : ""}
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.cron}
</TableCell>
@@ -269,17 +325,21 @@ function SchedulesTable({
{schedule.cronDescription}
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.externalId ? schedule.externalId : ""}
{schedule.timezone}
</TableCell>
<TableCell to={path} className={cellClass}>
<DateTime date={schedule.nextRun} timeZone={schedule.timezone} />
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.lastRun ? (
<DateTime date={schedule.lastRun} timeZone={schedule.timezone} />
) : (
""
)}
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : ""}
</TableCell>
<TableCell to={path} className={cellClass}>
<DateTime date={schedule.nextRun} timeZone="utc" />
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.lastRun ? <DateTime date={schedule.lastRun} timeZone="utc" /> : ""}
</TableCell>
<TableCell to={path} className={cellClass}>
<EnvironmentLabels environments={schedule.environments} size="small" />
</TableCell>
@@ -26,8 +26,10 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "~/components/primitives/Resizable";
import { Select } from "~/components/primitives/Select";
import { TextLink } from "~/components/primitives/TextLink";
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
import { TimezoneList } from "~/components/scheduled/timezones";
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import {
ScheduledRun,
@@ -95,7 +97,13 @@ export default function Page() {
return <StandardTaskForm task={result.task} runs={result.runs} />;
}
case "SCHEDULED": {
return <ScheduledTaskForm task={result.task} runs={result.runs} />;
return (
<ScheduledTaskForm
task={result.task}
runs={result.runs}
possibleTimezones={result.possibleTimezones}
/>
);
}
}
}
@@ -215,12 +223,21 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa
);
}
function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: ScheduledRun[] }) {
function ScheduledTaskForm({
task,
runs,
possibleTimezones,
}: {
task: TestTask["task"];
runs: ScheduledRun[];
possibleTimezones: string[];
}) {
const lastSubmission = useActionData();
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id);
const [timestampValue, setTimestampValue] = useState<Date | undefined>();
const [lastTimestampValue, setLastTimestampValue] = useState<Date | undefined>();
const [externalIdValue, setExternalIdValue] = useState<string | undefined>();
const [timezoneValue, setTimezoneValue] = useState<string>("UTC");
//set initial values
useEffect(() => {
@@ -233,11 +250,20 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
setTimestampValue(initialRun.payload.timestamp);
setLastTimestampValue(initialRun.payload.lastTimestamp);
setExternalIdValue(initialRun.payload.externalId);
setTimezoneValue(initialRun.payload.timezone);
}, [selectedCodeSampleId]);
const [
form,
{ timestamp, lastTimestamp, externalId, triggerSource, taskIdentifier, environmentId },
{
timestamp,
lastTimestamp,
externalId,
triggerSource,
taskIdentifier,
environmentId,
timezone,
},
] = useForm({
id: "test-task-scheduled",
// TODO: type this
@@ -314,6 +340,30 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
</Hint>
<FormError id={lastTimestamp.errorId}>{lastTimestamp.error}</FormError>
</InputGroup>
<InputGroup>
<Label htmlFor={timezone.id}>Timezone</Label>
<Select
{...conform.select(timezone)}
placeholder="Select a timezone"
defaultValue={timezoneValue}
value={timezoneValue}
setValue={(e) => {
if (Array.isArray(e)) return;
setTimezoneValue(e);
}}
items={possibleTimezones}
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
dropdownIcon
variant="tertiary/medium"
>
{(matches) => <TimezoneList timezones={matches} />}
</Select>
<Hint>
The Timestamp and Last timestamp are in UTC so this just changes the timezone
string that comes through in the payload.
</Hint>
<FormError id={timezone.errorId}>{timezone.error}</FormError>
</InputGroup>
<InputGroup>
<Label required={false} htmlFor={externalId.id}>
External ID
@@ -0,0 +1,35 @@
import { Link } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { LogoIcon } from "~/components/LogoIcon";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { getTimezones } from "~/utils/timezones.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
return typedjson({
timezones: getTimezones(),
});
};
export default function Page() {
const { timezones } = useTypedLoaderData<typeof loader>();
return (
<div className="grid grid-rows-[2.5rem,1fr]">
<div className="flex items-center border-b border-b-grid-dimmed px-3">
<Link to="/">
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
</Link>
</div>
<div className="overflow-y-auto p-8 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<Header1 spacing>Supported timezones</Header1>
<Paragraph spacing>We support these timezones when creating a schedule.</Paragraph>
<ul className="">
{timezones.map((timezone) => (
<li key={timezone}>{timezone}</li>
))}
</ul>
</div>
</div>
);
}
+9 -8
View File
@@ -1,5 +1,8 @@
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { InitializeDeploymentRequestBody, InitializeDeploymentResponseBody } from "@trigger.dev/core/v3";
import {
InitializeDeploymentRequestBody,
InitializeDeploymentResponseBody,
} from "@trigger.dev/core/v3";
import { env } from "~/env.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
@@ -37,13 +40,11 @@ export async function action({ request, params }: ActionFunctionArgs) {
contentHash: deployment.contentHash,
shortCode: deployment.shortCode,
version: deployment.version,
externalBuildData: deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"],
externalBuildData:
deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"],
imageTag,
registryHost: env.DEPLOY_REGISTRY_HOST
}
registryHost: env.DEPLOY_REGISTRY_HOST,
};
return json(
responseBody,
{ status: 200 }
);
return json(responseBody, { status: 200 });
}
@@ -123,7 +123,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
const repository = new EnvironmentVariablesRepository();
const variables = await repository.getEnvironment(environment.project.id, environment.id, true);
const variables = await repository.getEnvironment(environment.project.id, environment.id);
const environmentVariable = variables.find((v) => v.key === parsedParams.data.name);
@@ -80,7 +80,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
const repository = new EnvironmentVariablesRepository();
const variables = await repository.getEnvironment(environment.project.id, environment.id, true);
const variables = await repository.getEnvironment(environment.project.id, environment.id);
return json(variables.map((variable) => ({ name: variable.key, value: variable.value })));
}
@@ -2,7 +2,7 @@ import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
import { resolveVariablesForEnvironment } from "~/v3/environmentVariables/environmentVariablesRepository.server";
const ParamsSchema = z.object({
projectRef: z.string(),
@@ -41,9 +41,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
return json({ error: "Project not found" }, { status: 404 });
}
const repository = new EnvironmentVariablesRepository();
const variables = await repository.getEnvironmentVariables(project.id, authenticatedEnv.id);
const variables = await resolveVariablesForEnvironment(authenticatedEnv);
return json({
variables: variables.reduce((acc: Record<string, string>, variable) => {
@@ -79,9 +79,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
friendlyId: parsedParams.data.scheduleId,
taskIdentifier: body.data.task,
cron: body.data.cron,
timezone: body.data.timezone,
environments: [authenticationResult.environment.id],
externalId: body.data.externalId,
deduplicationKey: body.data.deduplicationKey,
};
const schedule = await service.call(authenticationResult.environment.projectId, options);
@@ -95,6 +95,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
expression: schedule.cron,
description: schedule.cronDescription,
},
timezone: schedule.timezone,
externalId: schedule.externalId ?? undefined,
deduplicationKey: schedule.deduplicationKey,
environments: schedule.environments,
@@ -43,6 +43,7 @@ export async function action({ request }: ActionFunctionArgs) {
environments: [authenticationResult.environment.id],
externalId: body.data.externalId,
deduplicationKey: body.data.deduplicationKey,
timezone: body.data.timezone,
};
const schedule = await service.call(authenticationResult.environment.projectId, options);
@@ -56,6 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
expression: schedule.cron,
description: schedule.cronDescription,
},
timezone: schedule.timezone,
externalId: schedule.externalId ?? undefined,
deduplicationKey: schedule.deduplicationKey,
environments: schedule.environments,
@@ -111,6 +113,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
expression: schedule.cron,
description: schedule.cronDescription,
},
timezone: schedule.timezone,
deduplicationKey: schedule.userProvidedDeduplicationKey
? schedule.deduplicationKey
: undefined,
@@ -0,0 +1,28 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { apiCors } from "~/utils/apiCors";
import { getTimezones } from "~/utils/timezones.server";
const SearchParamsSchema = z.object({
excludeUtc: z.preprocess((value) => value === "true", z.boolean()).default(false),
});
export async function loader({ request }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
const rawSearchParams = new URL(request.url).searchParams;
const params = SearchParamsSchema.safeParse(Object.fromEntries(rawSearchParams.entries()));
if (!params.success) {
return apiCors(
request,
json({ error: "Invalid request parameters", issues: params.error.issues }, { status: 400 })
);
}
const timezones = getTimezones(!params.data.excludeUtc);
return apiCors(request, json({ timezones }));
}
@@ -0,0 +1,97 @@
import { ActionFunctionArgs } from "@remix-run/server-runtime";
import { MachinePresetName } from "@trigger.dev/core/v3";
import { z } from "zod";
import { prisma } from "~/db.server";
import { validateJWTTokenAndRenew } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { workerQueue } from "~/services/worker.server";
import { machinePresetFromName } from "~/v3/machinePresets.server";
import { reportUsageEvent } from "~/v3/openMeter.server";
const JWTPayloadSchema = z.object({
environment_id: z.string(),
org_id: z.string(),
project_id: z.string(),
run_id: z.string(),
machine_preset: z.string(),
});
const BodySchema = z.object({
durationMs: z.number(),
});
export async function action({ request }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
const jwtResult = await validateJWTTokenAndRenew(request, JWTPayloadSchema);
if (!jwtResult) {
return { status: 401, body: "Unauthorized" };
}
const rawJson = await request.json();
const json = BodySchema.safeParse(rawJson);
if (!json.success) {
logger.error("Failed to parse request body", { rawJson });
return { status: 400, body: "Bad Request" };
}
const preset = machinePresetFromName(jwtResult.payload.machine_preset as MachinePresetName);
logger.debug("[/api/v1/usage/ingest] Reporting usage", { jwtResult, json: json.data, preset });
if (json.data.durationMs > 0) {
const costInCents = json.data.durationMs * preset.centsPerMs;
await prisma.taskRun.update({
where: {
id: jwtResult.payload.run_id,
},
data: {
usageDurationMs: {
increment: json.data.durationMs,
},
costInCents: {
increment: json.data.durationMs * preset.centsPerMs,
},
},
});
try {
await reportUsageEvent({
source: "webapp",
type: "usage",
subject: jwtResult.payload.org_id,
data: {
durationMs: json.data.durationMs,
costInCents: String(costInCents),
},
});
} catch (e) {
logger.error("Failed to report usage event, enqueing v3.reportUsage", { error: e });
await workerQueue.enqueue("v3.reportUsage", {
orgId: jwtResult.payload.org_id,
data: {
costInCents: String(costInCents),
},
additionalData: {
durationMs: json.data.durationMs,
},
});
}
}
return new Response(null, {
status: 200,
headers: {
"x-trigger-jwt": jwtResult.jwt,
},
});
}
@@ -3,9 +3,10 @@ import { parse } from "@conform-to/zod";
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { useVirtualizer } from "@tanstack/react-virtual";
import { parseExpression } from "cron-parser";
import cronstrue from "cronstrue";
import { useState } from "react";
import { useRef, useState } from "react";
import {
environmentTextClassName,
environmentTitle,
@@ -42,6 +43,7 @@ import { ProjectParamSchema, docsPath, v3SchedulesPath } from "~/utils/pathBuild
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
import { TimezoneList } from "~/components/scheduled/timezones";
const cronFormat = `* * * * *
┬ ┬ ┬ ┬ ┬
@@ -117,9 +119,12 @@ export function UpsertScheduleForm({
schedule,
possibleTasks,
possibleEnvironments,
possibleTimezones,
showGenerateField,
}: EditableScheduleElements & { showGenerateField: boolean }) {
const lastSubmission = useActionData();
const [selectedTimezone, setSelectedTimezone] = useState<string>(schedule?.timezone ?? "UTC");
const isUtc = selectedTimezone === "UTC";
const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? "");
const navigation = useNavigation();
const isLoading = navigation.state !== "idle";
@@ -127,18 +132,20 @@ export function UpsertScheduleForm({
const project = useProject();
const location = useLocation();
const [form, { taskIdentifier, cron, externalId, environments, deduplicationKey }] = useForm({
id: "create-schedule",
// TODO: type this
lastSubmission: lastSubmission as any,
shouldRevalidate: "onSubmit",
onValidate({ formData }) {
return parse(formData, { schema: UpsertSchedule });
},
});
const [form, { taskIdentifier, cron, timezone, externalId, environments, deduplicationKey }] =
useForm({
id: "create-schedule",
// TODO: type this
lastSubmission: lastSubmission as any,
shouldRevalidate: "onSubmit",
onValidate({ formData }) {
return parse(formData, { schema: UpsertSchedule });
},
});
let cronPatternResult: CronPatternResult | undefined = undefined;
let nextRuns: Date[] | undefined = undefined;
if (cronPattern !== "") {
const result = CronPattern.safeParse(cronPattern);
@@ -149,7 +156,10 @@ export function UpsertScheduleForm({
};
} else {
try {
const expression = parseExpression(cronPattern, { utc: true });
const expression = parseExpression(
cronPattern,
isUtc ? { utc: true } : { tz: selectedTimezone }
);
cronPatternResult = {
isValid: true,
description: cronstrue.toString(cronPattern),
@@ -195,6 +205,7 @@ export function UpsertScheduleForm({
items={possibleTasks}
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
dropdownIcon
variant="tertiary/medium"
>
{(matches) => (
<>
@@ -241,25 +252,52 @@ export function UpsertScheduleForm({
<ValidCronMessage isValid={false} message={cronPatternResult.error} />
)}
</InputGroup>
<InputGroup>
<Label htmlFor={timezone.id}>Timezone</Label>
<Select
{...conform.select(timezone)}
placeholder="Select a timezone"
defaultValue={selectedTimezone}
value={selectedTimezone}
setValue={(e) => {
if (Array.isArray(e)) return;
setSelectedTimezone(e);
}}
items={possibleTimezones}
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
dropdownIcon
variant="tertiary/medium"
>
{(matches) => <TimezoneList timezones={matches} />}
</Select>
<Hint>
{isUtc
? "UTC will not change with daylight savings time."
: "This will automatically adjust for daylight savings time."}
</Hint>
<FormError id={timezone.errorId}>{timezone.error}</FormError>
</InputGroup>
{nextRuns !== undefined && (
<div className="flex flex-col gap-1">
<Header3>Next 5 runs</Header3>
<Table>
<TableHeader>
<TableRow>
{!isUtc && <TableHeaderCell>{selectedTimezone}</TableHeaderCell>}
<TableHeaderCell>UTC</TableHeaderCell>
<TableHeaderCell>Local time</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{nextRuns.map((run, index) => (
<TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={selectedTimezone} />
</TableCell>
)}
<TableCell>
<DateTime date={run} timeZone="UTC" />
</TableCell>
<TableCell>
<DateTime date={run} />
</TableCell>
</TableRow>
))}
</TableBody>
+128
View File
@@ -1,6 +1,7 @@
import { Prettify } from "@trigger.dev/core";
import { z } from "zod";
import {
RuntimeEnvironment,
findEnvironmentByApiKey,
findEnvironmentByPublicApiKey,
} from "~/models/runtimeEnvironment.server";
@@ -12,6 +13,9 @@ import {
import { prisma } from "~/db.server";
import { json } from "@remix-run/server-runtime";
import { findProjectByRef } from "~/models/project.server";
import { SignJWT, jwtVerify, errors } from "jose";
import { env } from "~/env.server";
import { logger } from "./logger.server";
type Optional<T, K extends keyof T> = Prettify<Omit<T, K> & Partial<Pick<T, K>>>;
@@ -209,3 +213,127 @@ export async function authenticatedEnvironmentForAuthentication(
}
}
}
const JWT_SECRET = new TextEncoder().encode(env.SESSION_SECRET);
const JWT_ALGORITHM = "HS256";
const DEFAULT_JWT_EXPIRATION_IN_MS = 1000 * 60 * 60; // 1 hour
export async function generateJWTTokenForEnvironment(
environment: RuntimeEnvironment,
payload: Record<string, string>
) {
const jwt = await new SignJWT({
environment_id: environment.id,
org_id: environment.organizationId,
project_id: environment.projectId,
...payload,
})
.setProtectedHeader({ alg: JWT_ALGORITHM })
.setIssuedAt()
.setIssuer("https://id.trigger.dev")
.setAudience("https://api.trigger.dev")
.setExpirationTime(calculateJWTExpiration())
.sign(JWT_SECRET);
return jwt;
}
export async function validateJWTTokenAndRenew<T extends z.ZodTypeAny>(
request: Request,
payloadSchema: T
): Promise<{ payload: z.infer<T>; jwt: string } | undefined> {
try {
const jwt = request.headers.get("x-trigger-jwt");
if (!jwt) {
logger.debug("Missing JWT token in request", {
headers: Object.fromEntries(request.headers),
});
return;
}
const { payload: rawPayload } = await jwtVerify(jwt, JWT_SECRET, {
issuer: "https://id.trigger.dev",
audience: "https://api.trigger.dev",
});
const payload = payloadSchema.safeParse(rawPayload);
if (!payload.success) {
logger.error("Failed to validate JWT", { payload: rawPayload, issues: payload.error.issues });
return;
}
const renewedJwt = await renewJWTToken(payload.data);
return {
payload: payload.data,
jwt: renewedJwt,
};
} catch (error) {
if (error instanceof errors.JWTExpired) {
// Now we need to try and renew the token using the API key auth
const authenticatedEnv = await authenticateApiRequest(request);
if (!authenticatedEnv) {
logger.error("Failed to renew JWT token, missing or invalid Authorization header", {
error: error.message,
});
return;
}
const payload = payloadSchema.safeParse(error.payload);
if (!payload.success) {
logger.error("Failed to parse jwt payload after expired", {
payload: error.payload,
issues: payload.error.issues,
});
return;
}
const renewedJwt = await generateJWTTokenForEnvironment(authenticatedEnv.environment, {
...payload.data,
});
logger.debug("Renewed JWT token from Authorization header API Key", {
environment: authenticatedEnv.environment,
payload: payload.data,
});
return {
payload: payload.data,
jwt: renewedJwt,
};
}
logger.error("Failed to validate JWT token", { error });
}
}
async function renewJWTToken(payload: Record<string, string>) {
const jwt = await new SignJWT(payload)
.setProtectedHeader({ alg: JWT_ALGORITHM })
.setIssuedAt()
.setIssuer("https://id.trigger.dev")
.setAudience("https://api.trigger.dev")
.setExpirationTime(calculateJWTExpiration())
.sign(JWT_SECRET);
return jwt;
}
function calculateJWTExpiration() {
if (env.PROD_USAGE_HEARTBEAT_INTERVAL_MS) {
return (
(Date.now() + Math.max(DEFAULT_JWT_EXPIRATION_IN_MS, env.PROD_USAGE_HEARTBEAT_INTERVAL_MS)) /
1000
);
}
return (Date.now() + DEFAULT_JWT_EXPIRATION_IN_MS) / 1000;
}
@@ -145,11 +145,13 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
"/api/internal/stripe_webhooks",
"/api/v1/authorization-code",
"/api/v1/token",
"/api/v1/usage/ingest",
/^\/api\/v1\/tasks\/[^\/]+\/callback\/[^\/]+$/, // /api/v1/tasks/$id/callback/$secret
/^\/api\/v1\/runs\/[^\/]+\/tasks\/[^\/]+\/callback\/[^\/]+$/, // /api/v1/runs/$runId/tasks/$id/callback/$secret
/^\/api\/v1\/http-endpoints\/[^\/]+\/env\/[^\/]+\/[^\/]+$/, // /api/v1/http-endpoints/$httpEndpointId/env/$envType/$shortcode
/^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id
/^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier
"/api/v1/timezones",
],
log: {
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
+23
View File
@@ -46,6 +46,7 @@ import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout.se
import { ResumeTaskService } from "./tasks/resumeTask.server";
import { RequeueV2Message } from "~/v3/marqs/requeueV2Message.server";
import { MarqsConcurrencyMonitor } from "~/v3/marqs/concurrencyMonitor.server";
import { reportUsageEvent } from "~/v3/openMeter.server";
const workerCatalog = {
indexEndpoint: z.object({
@@ -169,6 +170,13 @@ const workerCatalog = {
"v2.requeueMessage": z.object({
runId: z.string(),
}),
"v3.reportUsage": z.object({
orgId: z.string(),
data: z.object({
costInCents: z.string(),
}),
additionalData: z.record(z.any()).optional(),
}),
};
const executionWorkerCatalog = {
@@ -649,6 +657,21 @@ function getWorkerQueue() {
await service.call(payload.runId);
},
},
"v3.reportUsage": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
await reportUsageEvent({
source: "webapp",
type: "usage",
subject: payload.orgId,
data: {
costInCents: payload.data.costInCents,
...payload.additionalData,
},
});
},
},
},
});
}
@@ -0,0 +1,7 @@
export function getTimezones(includeUtc = true) {
const possibleTimezones = Intl.supportedValuesOf("timeZone").sort();
if (includeUtc) {
possibleTimezones.unshift("UTC");
}
return possibleTimezones;
}
@@ -1,4 +1,9 @@
import { Prisma, PrismaClient, RuntimeEnvironmentType } from "@trigger.dev/database";
import {
Prisma,
PrismaClient,
RuntimeEnvironment,
RuntimeEnvironmentType,
} from "@trigger.dev/database";
import { z } from "zod";
import { environmentTitle } from "~/components/environments/EnvironmentLabel";
import { $transaction, prisma } from "~/db.server";
@@ -427,11 +432,7 @@ export class EnvironmentVariablesRepository implements Repository {
return results;
}
async getEnvironment(
projectId: string,
environmentId: string,
excludeInternalVariables?: boolean
): Promise<EnvironmentVariable[]> {
async getEnvironment(projectId: string, environmentId: string): Promise<EnvironmentVariable[]> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
@@ -453,124 +454,7 @@ export class EnvironmentVariablesRepository implements Repository {
return [];
}
return this.getEnvironmentVariables(projectId, environmentId, excludeInternalVariables);
}
async #getTriggerEnvironmentVariables(environmentId: string): Promise<EnvironmentVariable[]> {
const environment = await this.prismaClient.runtimeEnvironment.findFirst({
where: {
id: environmentId,
},
});
if (!environment) {
return [];
}
if (environment.type === "DEVELOPMENT") {
return [
{
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
value: env.DEV_OTEL_EXPORTER_OTLP_ENDPOINT ?? env.APP_ORIGIN,
},
].concat(
env.DEV_OTEL_BATCH_PROCESSING_ENABLED === "1"
? [
{
key: "OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_LOG_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_LOG_MAX_QUEUE_SIZE,
},
]
: []
);
}
return [
{
key: "TRIGGER_SECRET_KEY",
value: environment.apiKey,
},
{
key: "TRIGGER_API_URL",
value: env.APP_ORIGIN,
},
{
key: "TRIGGER_RUNTIME_WAIT_THRESHOLD_IN_MS",
value: String(env.RUNTIME_WAIT_THRESHOLD_IN_MS),
},
...(env.PROD_OTEL_BATCH_PROCESSING_ENABLED === "1"
? [
{
key: "OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_LOG_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_LOG_MAX_QUEUE_SIZE,
},
]
: []),
];
return this.getEnvironmentVariables(projectId, environmentId);
}
async #getSecretEnvironmentVariables(
@@ -597,18 +481,9 @@ export class EnvironmentVariablesRepository implements Repository {
async getEnvironmentVariables(
projectId: string,
environmentId: string,
excludeInternalVariables?: boolean
environmentId: string
): Promise<EnvironmentVariable[]> {
const secretEnvVars = await this.#getSecretEnvironmentVariables(projectId, environmentId);
if (excludeInternalVariables) {
return secretEnvVars;
}
const triggerEnvVars = await this.#getTriggerEnvironmentVariables(environmentId);
return [...secretEnvVars, ...triggerEnvVars];
return this.#getSecretEnvironmentVariables(projectId, environmentId);
}
async delete(projectId: string, options: DeleteEnvironmentVariable): Promise<Result> {
@@ -782,3 +657,158 @@ export class EnvironmentVariablesRepository implements Repository {
}
}
}
export const environmentVariablesRepository = new EnvironmentVariablesRepository();
export async function resolveVariablesForEnvironment(runtimeEnvironment: RuntimeEnvironment) {
const projectSecrets = await environmentVariablesRepository.getEnvironmentVariables(
runtimeEnvironment.projectId,
runtimeEnvironment.id
);
const builtInVariables =
runtimeEnvironment.type === "DEVELOPMENT"
? await resolveBuiltInDevVariables(runtimeEnvironment)
: await resolveBuiltInProdVariables(runtimeEnvironment);
return [...projectSecrets, ...builtInVariables];
}
async function resolveBuiltInDevVariables(runtimeEnvironment: RuntimeEnvironment) {
let result: Array<EnvironmentVariable> = [
{
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
value: env.DEV_OTEL_EXPORTER_OTLP_ENDPOINT ?? env.APP_ORIGIN,
},
];
if (env.DEV_OTEL_BATCH_PROCESSING_ENABLED === "1") {
result = result.concat([
{
key: "OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_LOG_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_LOG_MAX_QUEUE_SIZE,
},
]);
}
const commonVariables = await resolveCommonBuiltInVariables(runtimeEnvironment);
return [...result, ...commonVariables];
}
async function resolveBuiltInProdVariables(runtimeEnvironment: RuntimeEnvironment) {
let result: Array<EnvironmentVariable> = [
{
key: "TRIGGER_SECRET_KEY",
value: runtimeEnvironment.apiKey,
},
{
key: "TRIGGER_API_URL",
value: env.APP_ORIGIN,
},
{
key: "TRIGGER_RUNTIME_WAIT_THRESHOLD_IN_MS",
value: String(env.RUNTIME_WAIT_THRESHOLD_IN_MS),
},
{
key: "TRIGGER_ORG_ID",
value: runtimeEnvironment.organizationId,
},
];
if (env.PROD_OTEL_BATCH_PROCESSING_ENABLED === "1") {
result = result.concat([
{
key: "OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_LOG_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_LOG_MAX_QUEUE_SIZE,
},
]);
}
if (env.PROD_USAGE_HEARTBEAT_INTERVAL_MS && env.USAGE_EVENT_URL) {
result = result.concat([
{
key: "USAGE_HEARTBEAT_INTERVAL_MS",
value: String(env.PROD_USAGE_HEARTBEAT_INTERVAL_MS),
},
{
key: "USAGE_EVENT_URL",
value: env.USAGE_EVENT_URL,
},
]);
}
const commonVariables = await resolveCommonBuiltInVariables(runtimeEnvironment);
return [...result, ...commonVariables];
}
async function resolveCommonBuiltInVariables(
runtimeEnvironment: RuntimeEnvironment
): Promise<Array<EnvironmentVariable>> {
return [];
}
@@ -76,16 +76,8 @@ export interface Repository {
create(projectId: string, options: CreateEnvironmentVariables): Promise<CreateResult>;
edit(projectId: string, options: EditEnvironmentVariable): Promise<Result>;
getProject(projectId: string): Promise<ProjectEnvironmentVariable[]>;
getEnvironment(
projectId: string,
environmentId: string,
excludeInternalVariables?: boolean
): Promise<EnvironmentVariable[]>;
getEnvironmentVariables(
projectId: string,
environmentId: string,
excludeInternalVariables?: boolean
): Promise<EnvironmentVariable[]>;
getEnvironment(projectId: string, environmentId: string): Promise<EnvironmentVariable[]>;
getEnvironmentVariables(projectId: string, environmentId: string): Promise<EnvironmentVariable[]>;
delete(projectId: string, options: DeleteEnvironmentVariable): Promise<Result>;
deleteValue(projectId: string, options: DeleteEnvironmentVariableValue): Promise<Result>;
}
@@ -0,0 +1,81 @@
import { MachineConfig, MachinePreset, MachinePresetName } from "@trigger.dev/core/v3";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
export const presets = {
micro: {
cpu: 0.25,
memory: 0.25,
centsPerMs: env.CENTS_PER_HOUR_MICRO / 3_600_000,
},
"small-1x": {
cpu: 0.5,
memory: 0.5,
centsPerMs: env.CENTS_PER_HOUR_SMALL_1X / 3_600_000,
},
"small-2x": {
cpu: 1,
memory: 1,
centsPerMs: env.CENTS_PER_HOUR_SMALL_2X / 3_600_000,
},
"medium-1x": {
cpu: 1,
memory: 2,
centsPerMs: env.CENTS_PER_HOUR_MEDIUM_1X / 3_600_000,
},
"medium-2x": {
cpu: 2,
memory: 4,
centsPerMs: env.CENTS_PER_HOUR_MEDIUM_2X / 3_600_000,
},
"large-1x": {
cpu: 4,
memory: 8,
centsPerMs: env.CENTS_PER_HOUR_LARGE_1X / 3_600_000,
},
"large-2x": {
cpu: 8,
memory: 16,
centsPerMs: env.CENTS_PER_HOUR_LARGE_2X / 3_600_000,
},
};
export function machinePresetFromConfig(config: unknown): MachinePreset {
const parsedConfig = MachineConfig.safeParse(config);
if (!parsedConfig.success) {
logger.error("Failed to parse machine config", { config });
return machinePresetFromName("small-1x");
}
if (parsedConfig.data.preset) {
return machinePresetFromName(parsedConfig.data.preset);
}
if (parsedConfig.data.cpu && parsedConfig.data.memory) {
const name = derivePresetNameFromValues(parsedConfig.data.cpu, parsedConfig.data.memory);
return machinePresetFromName(name);
}
return machinePresetFromName("small-1x");
}
export function machinePresetFromName(name: MachinePresetName): MachinePreset {
return {
name,
...presets[name],
};
}
// Finds the smallest machine preset name that satisfies the given CPU and memory requirements
function derivePresetNameFromValues(cpu: number, memory: number): MachinePresetName {
for (const [name, preset] of Object.entries(presets)) {
if (preset.cpu >= cpu && preset.memory >= memory) {
return name as MachinePresetName;
}
}
return "small-1x";
}
@@ -15,7 +15,8 @@ import { createNewSession, disconnectSession } from "~/models/runtimeEnvironment
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
import { EnvironmentVariablesRepository } from "../environmentVariables/environmentVariablesRepository.server";
import { resolveVariablesForEnvironment } from "../environmentVariables/environmentVariablesRepository.server";
import { FailedTaskRunService } from "../failedTaskRun.server";
import { CancelTaskRunService } from "../services/cancelTaskRun.server";
import { CompleteAttemptService } from "../services/completeAttempt.server";
import { CreateTaskRunAttemptService } from "../services/createTaskRunAttempt.server";
@@ -25,7 +26,6 @@ import {
tracer,
} from "../tracer.server";
import { DevSubscriber, devPubSub } from "./devPubSub.server";
import { FailedTaskRunService } from "../failedTaskRun.server";
const MessageBody = z.discriminatedUnion("type", [
z.object({
@@ -415,6 +415,7 @@ export class DevQueueConsumer {
lockedById: backgroundTask.id,
status: "EXECUTING",
lockedToVersionId: backgroundWorker.id,
startedAt: existingTaskRun.startedAt ?? new Date(),
},
include: {
attempts: {
@@ -473,11 +474,7 @@ export class DevQueueConsumer {
return;
}
const environmentRepository = new EnvironmentVariablesRepository();
const variables = await environmentRepository.getEnvironmentVariables(
this.env.project.id,
this.env.id
);
const variables = await resolveVariablesForEnvironment(this.env);
if (backgroundWorker.supportsLazyAttempts) {
const payload: TaskRunExecutionLazyAttemptPayload = {
@@ -524,6 +521,7 @@ export class DevQueueConsumer {
lockedAt: null,
lockedById: null,
status: "PENDING",
startedAt: existingTaskRun.startedAt,
},
}),
]);
@@ -581,6 +579,7 @@ export class DevQueueConsumer {
lockedAt: null,
lockedById: null,
status: "PENDING",
startedAt: existingTaskRun.startedAt,
},
}),
]);
@@ -1,6 +1,6 @@
import { Context, ROOT_CONTEXT, Span, SpanKind, context, trace } from "@opentelemetry/api";
import {
Machine,
MachinePreset,
ProdTaskRunExecution,
ProdTaskRunExecutionPayload,
TaskRunError,
@@ -15,15 +15,19 @@ import { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
import {
BackgroundWorker,
BackgroundWorkerTask,
RuntimeEnvironment,
TaskRun,
TaskRunAttemptStatus,
TaskRunStatus,
} from "@trigger.dev/database";
import { z } from "zod";
import { prisma } from "~/db.server";
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
import { logger } from "~/services/logger.server";
import { singleton } from "~/utils/singleton";
import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
import { EnvironmentVariablesRepository } from "../environmentVariables/environmentVariablesRepository.server";
import { resolveVariablesForEnvironment } from "../environmentVariables/environmentVariablesRepository.server";
import { FailedTaskRunService } from "../failedTaskRun.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { socketIo } from "../handleSocketIo.server";
import {
@@ -31,12 +35,14 @@ import {
getWorkerDeploymentFromWorker,
getWorkerDeploymentFromWorkerTask,
} from "../models/workerDeployment.server";
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
import { SEMINTATTRS_FORCE_RECORDING, tracer } from "../tracer.server";
import { CrashTaskRunService } from "../services/crashTaskRun.server";
import { FailedTaskRunService } from "../failedTaskRun.server";
import { CreateTaskRunAttemptService } from "../services/createTaskRunAttempt.server";
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
import { tracer } from "../tracer.server";
import { generateJWTTokenForEnvironment } from "~/services/apiAuth.server";
import { EnvironmentVariable } from "../environmentVariables/repository";
import { machinePresetFromConfig } from "../machinePresets.server";
import { env } from "~/env.server";
const WithTraceContext = z.object({
traceparent: z.string().optional(),
@@ -405,6 +411,9 @@ export class SharedQueueConsumer {
lockedAt: new Date(),
lockedById: backgroundTask.id,
lockedToVersionId: deployment.worker.id,
startedAt: existingTaskRun.startedAt ?? new Date(),
baseCostInCents: env.BASE_RUN_COST_IN_CENTS,
machinePreset: machinePresetFromConfig(backgroundTask.machineConfig ?? {}).name,
},
include: {
runtimeEnvironment: true,
@@ -505,18 +514,7 @@ export class SharedQueueConsumer {
});
} else {
const machineConfig = lockedTaskRun.lockedBy?.machineConfig;
const machine = Machine.safeParse(machineConfig ?? {});
if (!machine.success) {
logger.error("Failed to parse machine config", {
queueMessage: message.data,
messageId: message.messageId,
machineConfig,
});
await this.#ackAndDoMoreWork(message.messageId);
return;
}
const machine = machinePresetFromConfig(machineConfig ?? {});
await this._sender.send("BACKGROUND_WORKER_MESSAGE", {
backgroundWorkerId: deployment.worker.friendlyId,
@@ -524,7 +522,7 @@ export class SharedQueueConsumer {
type: "SCHEDULE_ATTEMPT",
image: deployment.imageReference,
version: deployment.version,
machine: machine.data,
machine,
// identifiers
id: "placeholder", // TODO: Remove this completely in a future release
envId: lockedTaskRun.runtimeEnvironment.id,
@@ -554,6 +552,7 @@ export class SharedQueueConsumer {
lockedAt: null,
lockedById: null,
status: lockedTaskRun.status,
startedAt: existingTaskRun.startedAt,
},
}),
]);
@@ -1008,6 +1007,8 @@ class SharedQueueTasks {
const { backgroundWorkerTask, taskRun, queue } = attempt;
const machinePreset = machinePresetFromConfig(backgroundWorkerTask.machineConfig ?? {});
const execution: ProdTaskRunExecution = {
task: {
id: backgroundWorkerTask.slug,
@@ -1028,9 +1029,13 @@ class SharedQueueTasks {
payloadType: taskRun.payloadType,
context: taskRun.context,
createdAt: taskRun.createdAt,
startedAt: taskRun.startedAt ?? taskRun.createdAt,
tags: taskRun.tags.map((tag) => tag.name),
isTest: taskRun.isTest,
idempotencyKey: taskRun.idempotencyKey ?? undefined,
durationMs: taskRun.usageDurationMs,
costInCents: taskRun.costInCents,
baseCostInCents: taskRun.baseCostInCents,
},
queue: {
id: queue.friendlyId,
@@ -1061,12 +1066,13 @@ class SharedQueueTasks {
contentHash: attempt.backgroundWorker.contentHash,
version: attempt.backgroundWorker.version,
},
machine: machinePreset,
};
const environmentRepository = new EnvironmentVariablesRepository();
const variables = await environmentRepository.getEnvironmentVariables(
attempt.runtimeEnvironment.projectId,
attempt.runtimeEnvironmentId
const variables = await this.#buildEnvironmentVariables(
attempt.runtimeEnvironment,
taskRun,
machinePreset
);
const payload: ProdTaskRunExecutionPayload = {
@@ -1126,6 +1132,9 @@ class SharedQueueTasks {
id: runId,
runtimeEnvironmentId: environment.id,
},
include: {
lockedBy: true,
},
});
if (!run) {
@@ -1133,11 +1142,9 @@ class SharedQueueTasks {
return;
}
const environmentRepository = new EnvironmentVariablesRepository();
const variables = await environmentRepository.getEnvironmentVariables(
environment.projectId,
environment.id
);
const machinePreset = machinePresetFromConfig(run.lockedBy?.machineConfig ?? {});
const variables = await this.#buildEnvironmentVariables(environment, run, machinePreset);
return {
traceContext: run.traceContext as Record<string, unknown>,
@@ -1178,6 +1185,31 @@ class SharedQueueTasks {
await service.call(completion.id, completion);
}
async #buildEnvironmentVariables(
environment: RuntimeEnvironment,
run: TaskRun,
machinePreset: MachinePreset
): Promise<Array<EnvironmentVariable>> {
const variables = await resolveVariablesForEnvironment(environment);
const jwt = await generateJWTTokenForEnvironment(environment, {
run_id: run.id,
machine_preset: machinePreset.name,
});
return [
...variables,
...[
{ key: "TRIGGER_JWT", value: jwt },
{ key: "TRIGGER_RUN_ID", value: run.id },
{
key: "TRIGGER_MACHINE_PRESET",
value: machinePreset.name,
},
],
];
}
}
export const sharedQueueTasks = singleton("sharedQueueTasks", () => new SharedQueueTasks());
+47
View File
@@ -0,0 +1,47 @@
import { randomUUID } from "node:crypto";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
export type UsageEvent = {
source: string;
subject: string;
type: string;
id?: string;
time?: Date;
data?: Record<string, unknown>;
};
export async function reportUsageEvent(event: UsageEvent) {
if (!env.USAGE_OPEN_METER_BASE_URL || !env.USAGE_OPEN_METER_API_KEY) {
return;
}
const body = {
specversion: "1.0",
id: event.id ?? randomUUID(),
source: event.source,
type: event.type,
time: (event.time ?? new Date()).toISOString(),
subject: event.subject,
datacontenttype: "application/json",
data: event.data,
};
const url = `${env.USAGE_OPEN_METER_BASE_URL}/api/v1/events`;
logger.debug("Reporting usage event to OpenMeter", { url, body });
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/cloudevents+json",
Authorization: `Bearer ${env.USAGE_OPEN_METER_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) {
logger.error(`Failed to report usage event: ${response.status} ${response.statusText}`);
}
}
+41
View File
@@ -307,6 +307,19 @@ function convertSpansToCreateableEvents(resourceSpan: ResourceSpans): Array<Crea
"."
)
) ?? resourceProperties.attemptNumber,
usageDurationMs:
extractDoubleAttribute(
span.attributes ?? [],
SemanticInternalAttributes.USAGE_DURATION_MS
) ??
extractNumberAttribute(
span.attributes ?? [],
SemanticInternalAttributes.USAGE_DURATION_MS
),
usageCostInCents: extractDoubleAttribute(
span.attributes ?? [],
SemanticInternalAttributes.USAGE_COST_IN_CENTS
),
};
})
.filter(Boolean);
@@ -360,6 +373,20 @@ function extractResourceProperties(attributes: KeyValue[]) {
queueName: extractStringAttribute(attributes, SemanticInternalAttributes.QUEUE_NAME),
batchId: extractStringAttribute(attributes, SemanticInternalAttributes.BATCH_ID),
idempotencyKey: extractStringAttribute(attributes, SemanticInternalAttributes.IDEMPOTENCY_KEY),
machinePreset: extractStringAttribute(
attributes,
SemanticInternalAttributes.MACHINE_PRESET_NAME
),
machinePresetCpu:
extractDoubleAttribute(attributes, SemanticInternalAttributes.MACHINE_PRESET_CPU) ??
extractNumberAttribute(attributes, SemanticInternalAttributes.MACHINE_PRESET_CPU),
machinePresetMemory:
extractDoubleAttribute(attributes, SemanticInternalAttributes.MACHINE_PRESET_MEMORY) ??
extractNumberAttribute(attributes, SemanticInternalAttributes.MACHINE_PRESET_MEMORY),
machinePresetCentsPerMs: extractDoubleAttribute(
attributes,
SemanticInternalAttributes.MACHINE_PRESET_CENTS_PER_MS
),
};
}
@@ -611,6 +638,20 @@ function extractNumberAttribute(
return isIntValue(attribute?.value) ? Number(attribute.value.intValue) : fallback;
}
function extractDoubleAttribute(attributes: KeyValue[], name: string): number | undefined;
function extractDoubleAttribute(attributes: KeyValue[], name: string, fallback: number): number;
function extractDoubleAttribute(
attributes: KeyValue[],
name: string,
fallback?: number
): number | undefined {
const attribute = attributes.find((attribute) => attribute.key === name);
if (!attribute) return fallback;
return isDoubleValue(attribute?.value) ? Number(attribute.value.doubleValue) : fallback;
}
function extractBooleanAttribute(attributes: KeyValue[], name: string): boolean | undefined;
function extractBooleanAttribute(attributes: KeyValue[], name: string, fallback: boolean): boolean;
function extractBooleanAttribute(
+1
View File
@@ -55,6 +55,7 @@ export const UpsertSchedule = z.object({
),
externalId: z.string().optional(),
deduplicationKey: z.string().optional(),
timezone: z.string().optional(),
});
export type UpsertSchedule = z.infer<typeof UpsertSchedule>;
@@ -88,6 +88,7 @@ export class CompleteAttemptService extends BaseService {
completedAt: new Date(),
output: completion.output,
outputType: completion.outputType,
usageDurationMs: completion.usage?.durationMs,
taskRun: {
update: {
data: {
@@ -138,6 +139,7 @@ export class CompleteAttemptService extends BaseService {
// We need to cancel the task run instead of fail it
const cancelService = new CancelAttemptService();
// TODO: handle usages
await cancelService.call(
taskRunAttempt.friendlyId,
taskRunAttempt.taskRunId,
@@ -157,6 +159,7 @@ export class CompleteAttemptService extends BaseService {
status: "FAILED",
completedAt: new Date(),
error: completion.error,
usageDurationMs: completion.usage?.durationMs,
},
});
@@ -5,18 +5,21 @@ import { logger } from "~/services/logger.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { TaskRun, TaskRunAttempt } from "@trigger.dev/database";
import { machinePresetFromConfig } from "../machinePresets.server";
import { workerQueue } from "~/services/worker.server";
export class CreateTaskRunAttemptService extends BaseService {
public async call(
runId: string,
env?: AuthenticatedEnvironment,
authenticatedEnv?: AuthenticatedEnvironment,
setToExecuting = true
): Promise<{
execution: TaskRunExecution;
run: TaskRun;
attempt: TaskRunAttempt;
}> {
const environment = env ?? (await getAuthenticatedEnvironmentFromRun(runId, this._prisma));
const environment =
authenticatedEnv ?? (await getAuthenticatedEnvironmentFromRun(runId, this._prisma));
if (!environment) {
throw new ServiceValidationError("Environment not found", 404);
@@ -128,6 +131,20 @@ export class CreateTaskRunAttemptService extends BaseService {
throw new ServiceValidationError("Failed to create task run attempt", 500);
}
if (taskRunAttempt.number === 1 && taskRun.baseCostInCents > 0) {
await workerQueue.enqueue("v3.reportUsage", {
orgId: environment.organizationId,
data: {
costInCents: String(taskRun.baseCostInCents),
},
additionalData: {
runId: taskRun.id,
},
});
}
const machinePreset = machinePresetFromConfig(taskRun.lockedBy.machineConfig ?? {});
const execution: TaskRunExecution = {
task: {
id: taskRun.lockedBy.slug,
@@ -151,6 +168,10 @@ export class CreateTaskRunAttemptService extends BaseService {
tags: taskRun.tags.map((tag) => tag.name),
isTest: taskRun.isTest,
idempotencyKey: taskRun.idempotencyKey ?? undefined,
startedAt: taskRun.startedAt ?? taskRun.createdAt,
durationMs: taskRun.usageDurationMs,
costInCents: taskRun.costInCents,
baseCostInCents: taskRun.baseCostInCents,
},
queue: {
id: queue.friendlyId,
@@ -176,6 +197,7 @@ export class CreateTaskRunAttemptService extends BaseService {
taskRun.batchItems[0] && taskRun.batchItems[0].batchTaskRun
? { id: taskRun.batchItems[0].batchTaskRun.friendlyId }
: undefined,
machine: machinePreset,
};
return {
@@ -20,6 +20,7 @@ export class RegisterNextTaskScheduleInstanceService extends BaseService {
const nextScheduledTimestamp = calculateNextScheduledTimestamp(
instance.taskSchedule.generatorExpression,
instance.taskSchedule.timezone,
instance.lastScheduledTimestamp ?? new Date()
);
@@ -1,9 +1,9 @@
import { TaskRunStatus, type Checkpoint, TaskRunAttemptStatus } from "@trigger.dev/database";
import { TaskRunAttemptStatus, TaskRunStatus, type Checkpoint } from "@trigger.dev/database";
import { logger } from "~/services/logger.server";
import { socketIo } from "../handleSocketIo.server";
import { CreateCheckpointRestoreEventService } from "./createCheckpointRestoreEvent.server";
import { machinePresetFromConfig } from "../machinePresets.server";
import { BaseService } from "./baseService.server";
import { Machine } from "@trigger.dev/core/v3";
import { CreateCheckpointRestoreEventService } from "./createCheckpointRestoreEvent.server";
const RESTORABLE_RUN_STATUSES: TaskRunStatus[] = ["WAITING_TO_RESUME"];
const RESTORABLE_ATTEMPT_STATUSES: TaskRunAttemptStatus[] = ["PAUSED"];
@@ -45,7 +45,7 @@ export class RestoreCheckpointService extends BaseService {
});
if (!checkpointEvent) {
logger.error("Checkpoint event not found", params);
logger.error("Checkpoint event not found", { eventId: params.eventId });
return;
}
@@ -56,30 +56,26 @@ export class RestoreCheckpointService extends BaseService {
if (!runIsRestorable) {
logger.error("Run is unrestorable", {
id: checkpoint.runId,
status: checkpoint.run.status,
eventId: params.eventId,
runId: checkpoint.runId,
runStatus: checkpoint.run.status,
attemptId: checkpoint.attemptId,
});
return;
}
if (!attemptIsRestorable && !params.isRetry) {
logger.error("Attempt is unrestorable", {
id: checkpoint.attemptId,
status: checkpoint.attempt.status,
eventId: params.eventId,
runId: checkpoint.runId,
attemptId: checkpoint.attemptId,
attemptStatus: checkpoint.attempt.status,
});
return;
}
const { machineConfig } = checkpoint.attempt.backgroundWorkerTask;
const machine = Machine.safeParse(machineConfig ?? {});
if (!machine.success) {
logger.error("Failed to parse machine config", {
attemptId: checkpoint.attemptId,
machineConfig: checkpoint.attempt.backgroundWorkerTask.machineConfig,
});
return;
}
const machine = machinePresetFromConfig(machineConfig ?? {});
const restoreEvent = await this._prisma.checkpointRestoreEvent.findFirst({
where: {
@@ -90,6 +86,8 @@ export class RestoreCheckpointService extends BaseService {
if (restoreEvent) {
logger.error("Restore event already exists", {
runId: checkpoint.runId,
attemptId: checkpoint.attemptId,
checkpointId: checkpoint.id,
restoreEventId: restoreEvent.id,
});
@@ -106,7 +104,7 @@ export class RestoreCheckpointService extends BaseService {
location: checkpoint.location,
reason: checkpoint.reason ?? undefined,
imageRef: checkpoint.imageRef,
machine: machine.data,
machine,
// identifiers
checkpointId: checkpoint.id,
envId: checkpoint.runtimeEnvironment.id,
@@ -27,6 +27,7 @@ export class TestTaskService extends BaseService {
scheduleId: "sched_1234",
timestamp: data.timestamp,
lastTimestamp: data.lastTimestamp,
timezone: data.timezone,
externalId: data.externalId,
upcoming: [],
};
@@ -96,8 +96,10 @@ export class TriggerScheduledTaskService extends BaseService {
timestamp: instance.nextScheduledTimestamp,
lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
externalId: instance.taskSchedule.externalId ?? undefined,
timezone: instance.taskSchedule.timezone,
upcoming: nextScheduledTimestamps(
instance.taskSchedule.generatorExpression,
instance.taskSchedule.timezone,
instance.nextScheduledTimestamp!,
10
),
@@ -8,6 +8,8 @@ import { BaseService, ServiceValidationError } from "./baseService.server";
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
import cronstrue from "cronstrue";
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
import { getTimezones } from "~/utils/timezones.server";
import { env } from "~/env.server";
export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
@@ -62,6 +64,48 @@ export class UpsertTaskScheduleService extends BaseService {
);
}
//if creating a schedule, check they're under the limits
if (!schedule.friendlyId) {
//check they're within their limit
const limits = await this._prisma.organization.findFirst({
select: {
maximumSchedulesLimit: true,
},
where: {
projects: {
some: {
id: projectId,
},
},
},
});
if (!limits) {
throw new ServiceValidationError("Organization not found");
}
const schedulesCount = await this._prisma.taskSchedule.count({
where: {
projectId,
},
});
if (schedulesCount >= limits.maximumSchedulesLimit) {
throw new ServiceValidationError(
`You have created ${schedulesCount}/${limits.maximumSchedulesLimit} schedules so you'll need to increase your limits or delete some schedules. Increase your limits by contacting support.`
);
}
}
if (schedule.timezone) {
const possibleTimezones = getTimezones();
if (!possibleTimezones.includes(schedule.timezone)) {
throw new ServiceValidationError(
`Invalid IANA timezone: "${schedule.timezone}". View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
);
}
}
const result = await $transaction(this._prisma, async (tx) => {
const deduplicationKey =
typeof schedule.deduplicationKey === "string" && schedule.deduplicationKey !== ""
@@ -115,6 +159,7 @@ export class UpsertTaskScheduleService extends BaseService {
options.deduplicationKey !== undefined && options.deduplicationKey !== "",
generatorExpression: options.cron,
generatorDescription: cronstrue.toString(options.cron),
timezone: options.timezone ?? "UTC",
externalId: options.externalId ? options.externalId : undefined,
},
});
@@ -164,12 +209,14 @@ export class UpsertTaskScheduleService extends BaseService {
data: {
generatorExpression: options.cron,
generatorDescription: cronstrue.toString(options.cron),
timezone: options.timezone ?? "UTC",
externalId: options.externalId ? options.externalId : null,
},
});
const scheduleHasChanged =
scheduleRecord.generatorExpression !== existingSchedule.generatorExpression;
scheduleRecord.generatorExpression !== existingSchedule.generatorExpression ||
scheduleRecord.timezone !== existingSchedule.timezone;
// find the existing instances
const existingInstances = await tx.taskScheduleInstance.findMany({
@@ -280,7 +327,11 @@ export class UpsertTaskScheduleService extends BaseService {
: undefined,
cron: taskSchedule.generatorExpression,
cronDescription: taskSchedule.generatorDescription,
nextRun: calculateNextScheduledTimestamp(taskSchedule.generatorExpression),
timezone: taskSchedule.timezone,
nextRun: calculateNextScheduledTimestamp(
taskSchedule.generatorExpression,
taskSchedule.timezone
),
environments: instances.map((instance) => ({
id: instance.environment.id,
shortcode: instance.environment.shortcode,
+1
View File
@@ -32,6 +32,7 @@ export const TestTaskData = z
(val) => (val === "" ? undefined : val),
z.coerce.date().optional()
),
timezone: z.string(),
externalId: z.preprocess((val) => (val === "" ? undefined : val), z.string().optional()),
}),
])
@@ -2,21 +2,23 @@ import { parseExpression } from "cron-parser";
export function calculateNextScheduledTimestamp(
schedule: string,
timezone: string | null,
lastScheduledTimestamp: Date = new Date()
) {
let nextStep = calculateNextStep(schedule, lastScheduledTimestamp);
let nextStep = calculateNextStep(schedule, timezone, lastScheduledTimestamp);
while (nextStep.getTime() < Date.now()) {
nextStep = calculateNextStep(schedule, nextStep);
nextStep = calculateNextStep(schedule, timezone, nextStep);
}
return nextStep;
}
function calculateNextStep(schedule: string, currentDate: Date) {
function calculateNextStep(schedule: string, timezone: string | null, currentDate: Date) {
return parseExpression(schedule, {
currentDate,
utc: true,
utc: timezone === null,
tz: timezone ?? undefined,
})
.next()
.toDate();
@@ -24,6 +26,7 @@ function calculateNextStep(schedule: string, currentDate: Date) {
export function nextScheduledTimestamps(
cron: string,
timezone: string | null,
lastScheduledTimestamp: Date,
count: number = 1
) {
@@ -31,7 +34,11 @@ export function nextScheduledTimestamps(
let nextScheduledTimestamp = lastScheduledTimestamp;
for (let i = 0; i < count; i++) {
nextScheduledTimestamp = calculateNextScheduledTimestamp(cron, nextScheduledTimestamp);
nextScheduledTimestamp = calculateNextScheduledTimestamp(
cron,
timezone,
nextScheduledTimestamp
);
result.push(nextScheduledTimestamp);
}
+5
View File
@@ -0,0 +1,5 @@
declare namespace Intl {
type Key = "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit";
function supportedValuesOf(input: Key): string[];
}
+1
View File
@@ -123,6 +123,7 @@
"intl-parse-accept-language": "^1.0.0",
"ioredis": "^5.3.2",
"isbot": "^3.6.5",
"jose": "^5.4.0",
"jsonpointer": "^5.0.1",
"jwt-decode": "^4.0.0",
"lodash.omit": "^4.5.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"exclude": ["./cypress", "./cypress.config.ts"],
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["remix.env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"types": ["vitest/globals"],
"lib": ["DOM", "DOM.Iterable", "ES2019"],
+7
View File
@@ -0,0 +1,7 @@
<Accordion title="How to increase these limits?">
These are soft-limits and can be increased. Before we introduce paid plans in July you can request
more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact).
If you increase these defaults you may have to subscribe to a paid plan when we introduce them.
For more details on the v3 Cloud pricing see the [pricing
details](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
</Accordion>
+2 -1
View File
@@ -190,7 +190,8 @@
"v3/management/schedules/update",
"v3/management/schedules/delete",
"v3/management/schedules/deactivate",
"v3/management/schedules/activate"
"v3/management/schedules/activate",
"v3/management/schedules/timezones"
]
},
{
+506 -16
View File
@@ -5,7 +5,7 @@ info:
description: "The REST API lets you trigger and manage runs on Trigger.dev. You
can trigger a run, get the status of a run, and get the results of a run. "
version: 2024-04
license:
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
@@ -48,6 +48,8 @@ paths:
const schedule = await schedules.create({
task: 'my-task',
cron: '0 0 * * *'
deduplicationKey: 'my-schedule',
timezone: 'America/New_York'
});
get:
@@ -138,7 +140,7 @@ paths:
content:
application/json:
schema:
"$ref": "#/components/schemas/CreateScheduleOptions"
"$ref": "#/components/schemas/UpdateScheduleOptions"
responses:
"200":
description: Schedule updated successfully
@@ -265,6 +267,34 @@ paths:
const schedule = await schedules.activate(scheduleId);
"/api/v1/timezones":
get:
operationId: get_timezones_v1
summary: Get all supported timezones
description: Get all supported timezones that schedule tasks support.
parameters:
- in: query
name: excludeUtc
schema:
type: boolean
required: false
description: Defaults to false. Whether to include UTC in the results or not.
responses:
"200":
description: Successful request
content:
application/json:
schema:
"$ref": "#/components/schemas/GetTimezonesResult"
tags:
- schedules
x-codeSamples:
- lang: typescript
source: |-
import { schedules } from "@trigger.dev/sdk/v3";
const { timezones } = await schedules.timezones();
"/api/v1/runs/{runId}/replay":
parameters:
- $ref: "#/components/parameters/runId"
@@ -401,7 +431,7 @@ paths:
operationId: retrieve_run_v1
summary: Retrieve a run
description: |
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
responses:
"200":
description: Successful request
@@ -619,8 +649,6 @@ paths:
for (const run of response.data) {
console.log(`Run ID: ${run.id}, Status: ${run.status}`);
}
"/api/v1/projects/{projectRef}/envvars/{env}":
parameters:
@@ -792,7 +820,7 @@ paths:
format: binary
override:
type: boolean
required:
required:
- variables
responses:
"200":
@@ -851,7 +879,7 @@ paths:
source: |-
import { envvars } from "@trigger.dev/sdk/v3";
import { createReadStream } from "node:fs";
// Import variables in dotenv format from a file
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: createReadStream(".env"),
@@ -861,7 +889,7 @@ paths:
label: Import variables from a response
source: |-
import { envvars } from "@trigger.dev/sdk/v3";
// Import variables in dotenv format from a response
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: await fetch("https://example.com/.env"),
@@ -871,7 +899,7 @@ paths:
label: Import variables from a Buffer
source: |-
import { envvars } from "@trigger.dev/sdk/v3";
// Import variables in dotenv format from a buffer
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: Buffer.from("SLACK_API_KEY=slack_1234"),
@@ -881,14 +909,13 @@ paths:
label: Import variables from a File
source: |-
import { envvars } from "@trigger.dev/sdk/v3";
// Import variables in dotenv format from a file
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: new File(["SLACK_API_KEY=slack_1234"], ".env"),
override: false
});
"/api/v1/projects/{projectRef}/envvars/{env}/{name}":
parameters:
- $ref: "#/components/parameters/projectRef"
@@ -951,7 +978,7 @@ paths:
console.log(`Value: ${variable.value}`);
}
})
delete:
operationId: delete_project_envvar_v1
summary: Delete environment variable
@@ -1068,7 +1095,6 @@ paths:
});
}
})
components:
parameters:
@@ -1471,11 +1497,11 @@ components:
payload:
type: object
description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key
example: {"foo": "bar"}
example: { "foo": "bar" }
output:
type: object
description: The output of the run. Will be omitted if the request was made with a Public API key
example: {"foo": "bar"}
example: { "foo": "bar" }
idempotencyKey:
type: string
description: The idempotency key used to prevent creating duplicate runs, if provided
@@ -1580,6 +1606,27 @@ components:
type: string
externalId:
type: string
timezone:
type: string
example: "America/New_York"
description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
required:
- task
- cron
- deduplicationKey
UpdateScheduleOptions:
type: object
properties:
task:
type: string
cron:
type: string
externalId:
type: string
timezone:
type: string
example: "America/New_York"
description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
required:
- task
- cron
@@ -1623,6 +1670,10 @@ components:
type: string
description: The description of the generator in plain english
example: Every day at midnight
timezone:
type: string
example: "America/New_York"
description: Defaults to UTC. In IANA format, if set then it will trigger at the CRON frequency in that timezone and respect daylight savings time.
nextRun:
type: string
format: date-time
@@ -1648,6 +1699,445 @@ components:
type: integer
count:
type: integer
GetTimezonesResult:
type: object
properties:
timezones:
type: array
items:
type: string
example:
[
"UTC",
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/La_Rioja",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Fort_Nelson",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Monticello",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Santa_Isabel",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Colombo",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Hebron",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ulaanbaatar",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Sydney",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis",
]
ScheduleEnvironment:
type: object
properties:
@@ -1659,7 +2149,7 @@ components:
type: string
SerializedError:
type: object
required:
required:
- message
properties:
message:
+16 -2
View File
@@ -7,13 +7,13 @@ description: "There are some hard and soft limits in v3 that you might hit."
These are the default limits on a free account.
Before we introduce paid plans in July you can request more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact). If you increase these defaults you may have to subscribe to a paid plan when we introduce them. For more details on the v3 Cloud pricing see the [pricing page](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
| Limit | Details |
| ------------ | ------------------ |
| Organization | 10 concurrent runs |
| Environment | 5 concurrent runs |
<Snippet file="v3/soft-limit.mdx" />
## Rate limits
| Limit | Details |
@@ -23,3 +23,17 @@ Before we introduce paid plans in July you can request more [on Discord](https:/
Generally speaking each SDK call is an API call.
The most common cause of hitting the API rate limit is if you're calling `trigger()` on a task in a loop, instead of doing this use `batchTrigger()` which will trigger multiple tasks in a single API call. You can have up to 100 tasks in a single batch trigger call.
## Schedules
| Limit | Details |
| --------- | ------------- |
| Schedules | 5 per project |
When attaching schedules to tasks we strongly recommend you add them in our dashboard if they're "static". That way you can control them easily per environment.
If you add them dynamically using code make sure you add a `deduplicationKey` so you don't add the same schedule to a task multiple times. If you don't your task will get triggered multiple times, it will cost you more, and you will hit the limit.
If you're creating schedules for your user you will definitely need to request more schedules from us.
<Snippet file="v3/soft-limit.mdx" />
+22 -4
View File
@@ -9,8 +9,7 @@ The `machine` configuration is optional. Using higher spec machines will increas
export const heavyTask = task({
id: "heavy-task",
machine: {
cpu: 2,
memory: 4,
preset: "large-1x",
},
run: async ({ payload, ctx }) => {
//...
@@ -18,6 +17,25 @@ export const heavyTask = task({
});
```
## Possible configurations
The default machine is `small-1x` which has 0.5 vCPU and 0.5 GB of RAM. You can change the default machine in your `trigger.config.ts` file:
<Snippet file="incomplete-docs.mdx" />
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
export const config: TriggerConfig = {
machine: "small-2x",
// ... other config
};
```
## Machine configurations
| Preset | vCPU | Memory |
| --------- | ---- | ------ |
| micro | 0.25 | 0.25 |
| small-1x | 0.5 | 0.5 |
| small-2x | 1 | 1 |
| medium-1x | 1 | 2 |
| medium-2x | 2 | 4 |
| large-1x | 4 | 8 |
| large-2x | 8 | 16 |
@@ -0,0 +1,4 @@
---
title: "Get timezones"
openapi: "v3-openapi GET /api/v1/timezones"
---
+1 -2
View File
@@ -113,8 +113,7 @@ Some tasks require more vCPUs or GBs of RAM. You can specify these requirements
export const heavyTask = task({
id: "heavy-task",
machine: {
cpu: 2,
memory: 4,
preset: "large-1x", // 4 vCPU, 8 GB RAM
},
run: async (payload: any, { ctx }) => {
//...
+42 -11
View File
@@ -29,6 +29,16 @@ export const firstScheduledTask = schedules.task({
//this can be undefined if it's never been run
console.log(payload.lastTimestamp); //is a Date object or undefined
//the timezone the schedule was registered with, defaults to "UTC"
//this is in IANA format, e.g. "America/New_York"
//See the full list here: https://cloud.trigger.dev/timezones
console.log(payload.timezone); //is a string
//If you want to output the time in the user's timezone do this:
const formatted = payload.timestamp.toLocaleString("en-US", {
timeZone: payload.timezone,
});
//the schedule id (you can have many schedules for the same task)
//using this you can remove the schedule, update it, etc
console.log(payload.scheduleId); //is a string
@@ -46,8 +56,9 @@ export const firstScheduledTask = schedules.task({
You can see from the comments that the payload has several useful properties:
- `timestamp` - the time the task was scheduled to run
- `lastTimestamp` - the time the task was last run
- `timestamp` - the time the task was scheduled to run, as a UTC date.
- `lastTimestamp` - the time the task was last run, as a UTC date.
- `timezone` - the timezone the schedule was registered with, defaults to "UTC". In IANA format, e.g. "America/New_York".
- `scheduleId` - the id of the schedule that triggered the task
- `externalId` - the external id you (optionally) provided when creating the schedule
- `upcoming` - the next 5 times the task is scheduled to run
@@ -103,6 +114,7 @@ These are the options when creating a schedule:
| ----------------- | --------------------------------------------------------------------------------------------- |
| Task | The id of the task you want to attach to. |
| CRON pattern | The schedule in CRON format. |
| Timezone | The timezone the schedule will run in. Defaults to "UTC" |
| External id | An optional external id, usually you'd use a userId. |
| Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. |
| Environments | The environments this schedule will run in. |
@@ -121,6 +133,8 @@ const createdSchedule = await schedules.create({
task: firstScheduledTask.id,
//The schedule in CRON format.
cron: "0 0 * * *",
//this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
deduplicationKey: "my-deduplication-key",
});
```
@@ -136,17 +150,21 @@ const createdSchedule = await schedules.create({
task: firstScheduledTask.id,
//The schedule in CRON format.
cron: "0 0 * * *",
// Optional, it defaults to "UTC". In IANA format, e.g. "America/New_York".
// In this case, the task will run at midnight every day in New York time.
// If you specify a timezone it will automatically work with daylight saving time.
timezone: "America/New_York",
//Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
//This allows you to have per-user CRON tasks.
externalId: "user_123456",
//(Optional) You can only create one schedule with this key.
//You can only create one schedule with this key.
//If you use it twice, the second call will update the schedule.
//This is useful if you don't want to create duplicate schedules for a user.
//This is useful because you don't want to create duplicate schedules for a user.
deduplicationKey: "user_123456-todo_reminder",
});
```
See [the SDK reference](/v3/management-create-schedule) for full details.
See [the SDK reference](/v3/management/schedules/create) for full details.
### Dynamic schedules (or multi-tenant schedules)
@@ -189,6 +207,8 @@ export async function POST(request: Request) {
task: reminderTask.id,
//8am every day
cron: "0 8 * * *",
//the user's timezone
timezone: data.timezone,
//the user id
externalId: data.userId,
//this makes it impossible to have two reminder schedules for the same user
@@ -228,7 +248,7 @@ You can test a scheduled task in the dashboard. Note that the `scheduleId` will
const retrievedSchedule = await schedules.retrieve(scheduleId);
```
See [the SDK reference](/v3/management-retrieve-schedule) for full details.
See [the SDK reference](/v3/management/schedules/retrieve) for full details.
### Listing schedules
@@ -236,7 +256,7 @@ See [the SDK reference](/v3/management-retrieve-schedule) for full details.
const allSchedules = await schedules.list();
```
See [the SDK reference](/v3/management-list-schedules) for full details.
See [the SDK reference](/v3/management/schedules/list) for full details.
### Updating a schedule
@@ -245,10 +265,11 @@ const updatedSchedule = await schedules.update(scheduleId, {
task: firstScheduledTask.id,
cron: "0 0 1 * *",
externalId: "ext_1234444",
deduplicationKey: "my-deduplication-key",
});
```
See [the SDK reference](/v3/management-update-schedule) for full details.
See [the SDK reference](/v3/management/schedules/update) for full details.
### Deactivating a schedule
@@ -256,7 +277,7 @@ See [the SDK reference](/v3/management-update-schedule) for full details.
const deactivatedSchedule = await schedules.deactivate(scheduleId);
```
See [the SDK reference](/v3/management-deactivate-schedule) for full details.
See [the SDK reference](/v3/management/schedules/deactivate) for full details.
### Activating a schedule
@@ -264,7 +285,7 @@ See [the SDK reference](/v3/management-deactivate-schedule) for full details.
const activatedSchedule = await schedules.activate(scheduleId);
```
See [the SDK reference](/v3/management-activate-schedule) for full details.
See [the SDK reference](/v3/management/schedules/activate) for full details.
### Deleting a schedule
@@ -272,4 +293,14 @@ See [the SDK reference](/v3/management-activate-schedule) for full details.
const deletedSchedule = await schedules.del(scheduleId);
```
See [the SDK reference](/v3/management-delete-schedule) for full details.
See [the SDK reference](/v3/management/schedules/delete) for full details.
### Getting possible timezones
You might want to show a dropdown menu in your UI so your users can select their timezone. You can get a list of all possible timezones using the SDK:
```ts
const timezones = await schedules.timezones();
```
See [the SDK reference](/v3/management/schedules/timezones) for full details.
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/airtable
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "Trigger.dev integration for airtable",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"airtable": "^0.12.1",
"zod": "3.22.3"
},
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/github
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -30,8 +30,8 @@
"@octokit/request-error": "^5.0.1",
"@octokit/webhooks": "^12.0.10",
"octokit": "^3.1.2",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"zod": "3.22.3"
},
"engines": {
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/linear
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "Trigger.dev integration for @linear/sdk",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@linear/sdk": "^8.0.0",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"zod": "3.22.3"
},
"engines": {
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/slack
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -42,8 +42,8 @@
},
"dependencies": {
"openai": "^4.16.1",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38"
},
"engines": {
"node": ">=18.0.0"
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/plain
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/replicate
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "Trigger.dev integration for replicate",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"replicate": "^0.18.1",
"zod": "3.22.3"
},
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/resend
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"resend": "^2.1.0"
},
"engines": {
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/sendgrid
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38"
},
"engines": {
"node": ">=16.8.0"
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/shopify
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/shopify",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "Trigger.dev integration for @shopify/shopify-api",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@shopify/shopify-api": "^8.0.2",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"zod": "3.22.3"
},
"engines": {
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/slack
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"zod": "3.22.3"
},
"engines": {
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/stripe
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "Trigger.dev integration for stripe",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"stripe": "^12.14.0",
"zod": "3.22.3"
},
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/supabase
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"supabase-management-js": "^1.0.0",
"zod": "3.22.3"
},
+9
View File
@@ -1,5 +1,14 @@
# @trigger.dev/typeform
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
- @trigger.dev/integration-kit@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"description": "The official Typeform integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.37",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.38",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38",
"@typeform/api-client": "^1.8.0",
"zod": "3.22.3"
},
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/astro
## 3.0.0-beta.38
### Patch Changes
- Updated dependencies [1b90ffbb8]
- Updated dependencies [c405ae711]
- @trigger.dev/sdk@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@trigger.dev/astro",
"description": "An Astro-native integration for Trigger.dev background jobs platform",
"version": "3.0.0-beta.37",
"version": "3.0.0-beta.38",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
@@ -20,7 +20,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^3.0.0-beta.37"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.38"
},
"devDependencies": {
"astro": "^3.0.12",
+13
View File
@@ -1,5 +1,18 @@
# trigger.dev
## 3.0.0-beta.38
### Patch Changes
- d4ccdf710: Add an e2e suite to test compiling with v3 CLI.
- 1b90ffbb8: v3: Usage tracking
- e02320f65: fix: allow command login to read api url from cli args
- Updated dependencies [1b90ffbb8]
- Updated dependencies [0ed93a748]
- Updated dependencies [c405ae711]
- Updated dependencies [c405ae711]
- @trigger.dev/core@3.0.0-beta.38
## 3.0.0-beta.37
### Patch Changes
+177
View File
@@ -0,0 +1,177 @@
# Trigger.dev CLI E2E suite
E2E test suite for the Trigger.dev v3 CLI.
Note: this only works with Trigger.dev v3 projects and later. There is no E2E test suite for the [@trigger.dev/cli](https://www.npmjs.com/package/@trigger.dev/cli) package yet.
Trigger.dev is an open source platform that makes it easy to create event-driven background tasks directly in your existing project.
## Description
This suite aims to test the outputs fo the `triggerdev deploy` command.
To do so, it runs the deploy code against fixture projects that are located under `packages/cli-v3/e2e/fixtures/`.
Those fixtures reproduce minimal project structure and contents, in order to reproduce known bugs and run fast.
**Notes**
- The suite uses vitest
- Everything happens locally
- There is no login required
- There is not real project reference needed
- No docker image is created or built, instead, the bundled worker file is started with node directly inside the vitest process
## Usage
If you have not done it yet, build the CLI:
```sh
pnpm run build --filter trigger.dev
```
Then, run the v3 CLI E2E test suite:
```sh
pnpm --filter trigger.dev run test:e2e
```
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------- |
| `MOD=<fixture-name>` | The name of any folder directly nested under `packages/cli-v3/e2e/fixtures/` |
| `PM=<package-manager>` | The package manager to use. One of `npm`, `pnpm`, `yarn`. Defaults to `npm` |
Example:
```sh
MOD=server-only PM=yarn pnpm --filter trigger.dev run test:e2e
```
This will run the test suite for the `server-only` fixture using `yarn` to install and resolve dependencies.
## Debugging
When debugging an issue with the `triggerdev deploy` or `triggerdev dev` command, it is recommended to reproduce it with a minimal project fixture in the e2e suite.
Check [Adding a fixture](#adding-a-fixture) for more information.
Then run:
```sh
MOD=<fixture-name> pnpm run test:e2e
```
This will test your fixture project, and generate outputs in the `packages/cli-v3/e2e/fixtures/<fixture-name>/.trigger` folder, so you can easily debug.
## Adding a fixture
1. Create a new `packages/cli-v3/e2e/fixtures/<fixture-name>` folder.
It will hold the project to test.
2. Add a `package.json` file in your `packages/cli-v3/e2e/fixtures/<fixture-name>` folder.
Use the following template:
```json package.json
{
"name": "<fixture-name>",
"private": true,
"engines": {
"pnpm": "8.15.5",
"yarn": "4.2.2"
},
"packageManager": "pnpm@8.15.5"
}
```
> The `engines` field is used to store the versions of pnpm and yarn to use when running the suite.
3. Add an empty `pnpm-workspace.yaml` in your `packages/cli-v3/e2e/fixtures/<fixture-name>` folder.
This is necessary to prevent the Trigger.dev monorepo from handling this project.
Please check https://github.com/pnpm/pnpm/issues/2412 for more inforation.
4. Add an empty `yarn.lock` in your fixture folder.
This is necessary to allow to use `yarn` without having a warning on the current project being a `pnpm` project.
5. Install the fixture dependencies and generate lockfiles.
Like you would in any project.
E.g. if your fixture contains a trigger task that uses the `jsdom` library:
```sh
cd packages/cli-v3/e2e/fixtures/<fixture-name>
corepack use pnpm@8.15.5
pnpm install jsdom
```
> This will update the `package.json` and generate the `pnpm-lock.yaml` file.
6. To run the test suite against multiple package manager, we need to generate the other lockfiles.
```sh
cd packages/cli-v3/e2e/fixtures/<fixture-name>
rm -rf node_modules
npm install
rm -rf node_modules
corepack use yarn # will update the yarn lockfile
```
> Do it in this order, otherwise `npm install` will update the existing `yarn.lock` file with legacy version 1.
7. Create a new `packages/cli-v3/e2e/fixtures/trigger` folder, and create a trigger task in it.
Here is an example:
```javascript
import { task } from "@trigger.dev/sdk/v3";
export const helloWorldTask = task({
id: "hello-world",
run: async (payload) => {
console.log("Hello, World!", payload);
},
});
```
8. Add a trigger configuration file.
The configuration file is mandatory here, the E2E suite does not execute `trigger.dev` commands.
```javascript
export const config = {
project: "<fixture-name>",
triggerDirectories: ["./trigger"],
};
```
> The project reference can be anything here, as the suite runs locally without connecting to the platform.
9. Commit your changes.
10. Add your fixture test configuration in `testCases.json`.
```json testCases.json
[
...
{
"name": "<fixture-name>",
},
...
]
```
You can configure your test case by adding other properties to the JSON object. Here is the `TestCase` type for reference:
```typescript
type TestCase = {
name: string;
skipTypecheck?: boolean;
wantConfigNotFoundError?: boolean;
wantBadConfigError?: boolean;
wantCompilationError?: boolean;
wantWorkerError?: boolean;
wantDependenciesError?: boolean;
wantInstallationError?: boolean;
};
```
> You might expect a specific error at a specific test, so use those configuration option at your discretion.

Some files were not shown because too many files have changed in this diff Show More