Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b73ae3f927 | |||
| b605b892ac | |||
| 233316f7e8 | |||
| 1b90ffbb8c | |||
| b45ca4e146 | |||
| 25d15578f7 | |||
| fe865a0f49 | |||
| 0ed93a748e | |||
| e02320f65d | |||
| 85a543d8ec | |||
| 10ceb85a92 | |||
| c405ae7117 | |||
| 3687fcb61e | |||
| d4ccdf7105 | |||
| e08b4569e5 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Add an e2e suite to test compiling with v3 CLI.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
v3: Usage tracking
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
v3: Remove aggressive otel flush timeouts in dev/prod
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Make deduplicationKey required when creating/updating a schedule
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
fix: allow command login to read api url from cli args
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Added timezone support to schedules
|
||||
@@ -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
|
||||
|
||||
@@ -29,4 +29,6 @@ jobs:
|
||||
|
||||
# e2e:
|
||||
# uses: ./.github/workflows/e2e.yml
|
||||
# with:
|
||||
# package: webapp
|
||||
# secrets: inherit
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+9
-5
@@ -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>
|
||||
|
||||
+2
-1
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-1
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+82
-22
@@ -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>
|
||||
|
||||
+53
-3
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
+53
-15
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+165
-135
@@ -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());
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare namespace Intl {
|
||||
type Key = "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit";
|
||||
|
||||
function supportedValuesOf(input: Key): string[];
|
||||
}
|
||||
@@ -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,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"],
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
---
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user