Compare commits

...

26 Commits

Author SHA1 Message Date
Eric Allam 8eb68dd852 Fix pnpm lock file 2024-04-29 14:50:28 +01:00
github-actions[bot] a42037da03 chore: Update version for release (beta) (#1070)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-29 14:48:01 +01:00
Eric Allam 43bc7ed94e Hoist uncaughtException handler to the top of workers to better report error messages 2024-04-29 14:22:55 +01:00
Matt Aitken 4fdb7f8288 Bulk insert environment variables (#1069)
* Fix for #1066. Correct environment username if dev

* WIP changing the form

* WIP on the form

* WIP on repository

* Adding environment variables en masse is working

* WIP on pasting

* useList hook with reducer

* Got bulk insert working with pasting… it was a pain

* Allow overwriting of values

* Set a max height on the new env var form
2024-04-28 18:31:31 +01:00
Matt Aitken 37b9b056c4 Upload payload packets outside of the db transaction 2024-04-28 18:18:33 +01:00
Matt Aitken 801c86bf73 Added search to the test tasks list 2024-04-28 16:10:46 +01:00
Matt Aitken a1de11a001 Default the test page to the “DEV” tab 2024-04-28 15:59:45 +01:00
Praveen Pendyala 29e9e372ee Added example of github workflow for deploy to staging (#1068) 2024-04-26 18:39:34 +01:00
Eric Allam affc128161 Try to fix prisma generate errors 2024-04-26 18:28:17 +01:00
Eric Allam 40ba8ad0ee Fix the test page when a scheduled task has recent run payloads that aren’t scheduled payloads 2024-04-26 18:20:04 +01:00
Matt Aitken 96168eb383 Fixes for test page problems 2024-04-26 17:45:06 +01:00
Eric Allam 2c68473cc8 Fix pnpm lock file 2024-04-26 17:14:11 +01:00
github-actions[bot] 469c8a2532 chore: Update version for release (beta) (#1067)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-26 17:13:20 +01:00
Eric Allam ebeb790522 Add typescript as a dependency so the esbuild-decorator will work even when running in npx 2024-04-26 17:00:14 +01:00
Matt Aitken e765240370 Latest lockfile 2024-04-26 14:37:46 +01:00
github-actions[bot] 38770a91c7 chore: Update version for release (beta) (#1061)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-26 14:35:30 +01:00
Matt Aitken de188a22c5 Option for users to opt-in to a staging environment (#1064) 2024-04-26 11:29:20 +01:00
Matt Aitken fae93ac747 New tasks page (with activity) and multi-user fixes (#1063)
* Fix for “overridden” spelling mistake

* Make the task function names grey instead of yellow

* The task page now shows each task once

* Blankline

* WIP on activity bars

* Correct size of the graphs

* Add data for all days, even if there are no runs

* Activity graph is working and has a tooltip

* No activity label

* Got the bar bg working

* Activity chart now has a bg

* Added a compound id to make the activity graphs appear faster

* Improvements to the activity graph

* Tried to disable the bg from animating

* Added read replica support to the app, using Nick’s code from his batching PR

* Added the read replica to BasePresenter

* Added the queued and running columns to the Tasks list

* Added avg duration column

* Included completed but failed runs in the average duration

* Get rid of the old useDevEnvironment hook and remove API keys from the projects query

* Unified getting environment info to using a function and doing it in presenters

* Removed the path from the tasks table

* Onboarding for dev for 2nd+ user. Improved environment sorting.

* Removed log

* Improved the spacing

* Enable staging for new v3 projects
2024-04-26 10:44:39 +01:00
Eric Allam c3f6557eb6 Create publish preprelease script 2024-04-25 11:26:14 +01:00
Eric Allam 9491a1649c v3: Support decorators (#1060)
* Adds support for `emitDecoratorMetadata: true` and `experimentalDecorators: true` in your tsconfig

* Implement task.onSuccess/onFailure and config.onSuccess/onFailure

* Added onStart and more docs for lifecycle functions

* Use onStart instead of init for TypeORM
2024-04-25 11:17:13 +01:00
Eric Allam 0a5aa2dc15 Fix slow job retries by optimizing how errored tasks are deleted 2024-04-24 22:07:12 +01:00
Eric Allam 4a1a5b2e4a fix pnpm lock file 2024-04-24 15:07:22 +01:00
github-actions[bot] 503882762c chore: Update version for release (beta) (#1059)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-24 15:06:25 +01:00
Eric Allam e3db257397 Fix error stack traces 2024-04-24 14:18:53 +01:00
Eric Allam de1cc868e3 Fix dev CLI output when not printing update messages 2024-04-24 14:06:49 +01:00
Eric Allam ff7fa9e19a Clear out errored tasks when re-attempting a job run (instead of using the cached error) 2024-04-24 13:00:02 +01:00
139 changed files with 3821 additions and 736 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Hoist uncaughtException handler to the top of workers to better report error messages
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Fix error stack traces
+7
View File
@@ -0,0 +1,7 @@
---
"@trigger.dev/sdk": patch
"trigger.dev": patch
"@trigger.dev/core": patch
---
Implement task.onSuccess/onFailure and config.onSuccess/onFailure
+83
View File
@@ -0,0 +1,83 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---
Adds support for `emitDecoratorMetadata: true` and `experimentalDecorators: true` in your tsconfig using the [`@anatine/esbuild-decorators`](https://github.com/anatine/esbuildnx/tree/main/packages/esbuild-decorators) package. This allows you to use libraries like TypeORM:
```ts orm/index.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity()
export class Photo {
@PrimaryColumn()
id!: number;
@Column()
name!: string;
@Column()
description!: string;
@Column()
filename!: string;
@Column()
views!: number;
@Column()
isPublished!: boolean;
}
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "postgres",
database: "v3-catalog",
entities: [Photo],
synchronize: true,
logging: false,
});
```
And then in your trigger.config.ts file you can initialize the datasource using the new `init` option:
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
import { AppDataSource } from "@/trigger/orm";
export const config: TriggerConfig = {
// ... other options here
init: async (payload, { ctx }) => {
await AppDataSource.initialize();
},
};
```
Now you are ready to use this in your tasks:
```ts
import { task } from "@trigger.dev/sdk/v3";
import { AppDataSource, Photo } from "./orm";
export const taskThatUsesDecorators = task({
id: "taskThatUsesDecorators",
run: async (payload: { message: string }) => {
console.log("Creating a photo...");
const photo = new Photo();
photo.id = 2;
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.views = 1;
photo.isPublished = true;
await AppDataSource.manager.save(photo);
},
});
```
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Fix dev CLI output when not printing update messages
+6
View File
@@ -46,9 +46,12 @@
"changesets": [
"angry-eagles-trade",
"beige-pens-dance",
"big-tomatoes-deliver",
"breezy-gorillas-mate",
"chilled-hornets-move",
"clean-pianos-listen",
"clever-apes-collect",
"clever-carrots-travel",
"cool-glasses-bake",
"cuddly-feet-approve",
"dry-walls-check",
@@ -67,12 +70,15 @@
"loud-actors-remember",
"many-ligers-pump",
"mighty-camels-joke",
"mighty-flowers-train",
"nasty-jars-pump",
"new-rivers-tell",
"ninety-pets-travel",
"odd-poets-own",
"polite-ducks-switch",
"polite-rockets-matter",
"poor-flowers-cross",
"purple-garlics-shop",
"rare-roses-float",
"real-planets-stare",
"rich-kangaroos-unite",
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Add typescript as a dependency so the esbuild-decorator will work even when running in npx
+3 -1
View File
@@ -4,13 +4,15 @@ on:
jobs:
publish:
runs-on: ubuntu-latest
env:
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: 1
outputs:
version: ${{ steps.get_version.outputs.version }}
short_sha: ${{ steps.get_commit.outputs.sha_short }}
steps:
- name: Setup Depot CLI
uses: depot/setup-action@v1
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
@@ -206,3 +206,39 @@ export function TriggerDevStepV3() {
</ClientTabs>
);
}
export function TriggerLoginStepV3() {
return (
<ClientTabs defaultValue="npm">
<ClientTabsList>
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
</ClientTabsList>
<ClientTabsContent value={"npm"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`npx trigger.dev@${v3PackageTag} login`}
/>
</ClientTabsContent>
<ClientTabsContent value={"pnpm"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`pnpm dlx trigger.dev@${v3PackageTag} login`}
/>
</ClientTabsContent>
<ClientTabsContent value={"yarn"}>
<ClipboardField
variant="primary/medium"
iconButton
className="mb-4"
value={`yarn dlx trigger.dev@${v3PackageTag} login`}
/>
</ClientTabsContent>
</ClientTabs>
);
}
@@ -1,5 +1,9 @@
import { Paragraph } from "./Paragraph";
export function Hint({ children }: { children: React.ReactNode }) {
return <Paragraph variant="extra-small">{children}</Paragraph>;
export function Hint({ children, className }: { children: React.ReactNode; className?: string }) {
return (
<Paragraph variant="extra-small" className={className}>
{children}
</Paragraph>
);
}
@@ -73,14 +73,7 @@ export function SpanCodePathAccessory({
>
{accessory.items.map((item, index) => (
<Fragment key={index}>
<span
className={cn(
"truncate",
index === accessory.items.length - 1 ? "text-sun-100" : "text-text-dimmed"
)}
>
{item.text}
</span>
<span className={cn("truncate", "text-text-dimmed")}>{item.text}</span>
{index < accessory.items.length - 1 && (
<span className="text-text-dimmed">
<ChevronRightIcon className="h-4 w-4" />
@@ -27,7 +27,7 @@ type TaskFunctionNameProps = {
export function TaskFunctionName({ variant, functionName, className }: TaskFunctionNameProps) {
return (
<InlineCode variant={variant} className={cn("text-sun-100", className)}>
<InlineCode variant={variant} className={cn("text-text-dimmed", className)}>
{`${functionName}()`}
</InlineCode>
);
@@ -30,6 +30,14 @@ const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
CRASHED: "Task has crashed and won't be retried",
};
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY"];
export const RUNNING_STATUSES: TaskRunStatus[] = [
"EXECUTING",
"RETRYING_AFTER_FAILURE",
"WAITING_TO_RESUME",
];
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
return taskRunStatusDescriptions[status];
}
@@ -36,7 +36,6 @@ type RunsTableProps = {
showJob?: boolean;
runs: RunListItem[];
isLoading?: boolean;
currentUser: User;
};
export function TaskRunsTable({
@@ -45,7 +44,6 @@ export function TaskRunsTable({
filters,
runs,
isLoading = false,
currentUser,
}: RunsTableProps) {
const organization = useOrganization();
const project = useProject();
@@ -78,15 +76,16 @@ export function TaskRunsTable({
) : (
runs.map((run) => {
const path = v3RunSpanPath(organization, project, run, { spanId: run.spanId });
const usernameForEnv =
currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
return (
<TableRow key={run.id}>
<TableCell to={path}>#{run.number}</TableCell>
<TableCell to={path}>{run.taskIdentifier}</TableCell>
<TableCell to={path}>{run.version ?? ""}</TableCell>
<TableCell to={path}>
<EnvironmentLabel environment={run.environment} userName={usernameForEnv} />
<EnvironmentLabel
environment={run.environment}
userName={run.environment.userName}
/>
</TableCell>
<TableCell to={path}>
<TaskRunStatusCombo status={run.status} />
@@ -196,7 +195,12 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
{environment ? (
<>
{" "}
in <EnvironmentLabel environment={environment} size="large" />
in{" "}
<EnvironmentLabel
environment={environment}
userName={environment.userName}
size="large"
/>
</>
) : null}
</Paragraph>
+72 -12
View File
@@ -70,23 +70,21 @@ export { Prisma };
export const prisma = singleton("prisma", getClient);
export const $replica: Omit<PrismaClient, "$transaction"> = singleton(
"replica",
() => getReplicaClient() ?? prisma
);
function getClient() {
const { DATABASE_URL } = process.env;
invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set");
const databaseUrl = new URL(DATABASE_URL);
const databaseUrl = extendQueryParams(DATABASE_URL, {
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
});
// We need to add the connection_limit and pool_timeout query params to the url, in a way that works if the DATABASE_URL already has query params
const query = databaseUrl.searchParams;
query.set("connection_limit", env.DATABASE_CONNECTION_LIMIT.toString());
query.set("pool_timeout", env.DATABASE_POOL_TIMEOUT.toString());
databaseUrl.search = query.toString();
// Remove the username:password in the url and print that to the console
const urlWithoutCredentials = new URL(databaseUrl.href);
urlWithoutCredentials.password = "";
console.log(`🔌 setting up prisma client to ${urlWithoutCredentials.toString()}`);
console.log(`🔌 setting up prisma client to ${redactUrlSecrets(databaseUrl)}`);
const client = new PrismaClient({
datasources: {
@@ -134,6 +132,68 @@ function getClient() {
return client;
}
function getReplicaClient() {
if (!env.DATABASE_READ_REPLICA_URL) {
console.log(`🔌 No database replica, using the regular client`);
return;
}
const replicaUrl = extendQueryParams(env.DATABASE_READ_REPLICA_URL, {
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
});
console.log(`🔌 setting up read replica connection to ${redactUrlSecrets(replicaUrl)}`);
const replicaClient = new PrismaClient({
datasources: {
db: {
url: replicaUrl.href,
},
},
log: [
{
emit: "stdout",
level: "error",
},
{
emit: "stdout",
level: "info",
},
{
emit: "stdout",
level: "warn",
},
],
});
// connect eagerly
replicaClient.$connect();
console.log(`🔌 read replica connected`);
return replicaClient;
}
function extendQueryParams(hrefOrUrl: string | URL, queryParams: Record<string, string>) {
const url = new URL(hrefOrUrl);
const query = url.searchParams;
for (const [key, val] of Object.entries(queryParams)) {
query.set(key, val);
}
url.search = query.toString();
return url;
}
function redactUrlSecrets(hrefOrUrl: string | URL) {
const url = new URL(hrefOrUrl);
url.password = "";
return url.href;
}
export type { PrismaClient } from "@trigger.dev/database";
export const PrismaErrorSchema = z.object({
+1
View File
@@ -19,6 +19,7 @@ const EnvironmentSchema = z.object({
isValidDatabaseUrl,
"DIRECT_URL is invalid, for details please check the additional output above this message."
),
DATABASE_READ_REPLICA_URL: z.string().optional(),
SESSION_SECRET: z.string(),
MAGIC_LINK_SECRET: z.string(),
ENCRYPTION_KEY: z.string(),
-17
View File
@@ -10,20 +10,3 @@ export function useEnvironments(matches?: UIMatch[]) {
return project.environments;
}
export function useDevEnvironment(matches?: UIMatch[]) {
const user = useUser();
const environments = useEnvironments(matches);
if (!environments) return;
return environments.find(
(environment) => environment.type === "DEVELOPMENT" && environment.userId === user.id
);
}
export function useProdEnvironment(matches?: UIMatch[]) {
const environments = useEnvironments(matches);
if (!environments) return;
return environments.find((environment) => environment.type === "PRODUCTION");
}
+72
View File
@@ -0,0 +1,72 @@
import { Reducer, useReducer } from "react";
export type ListState<T> = {
items: T[];
};
type AppendAction<T> = {
type: "append";
items: T[];
};
type UpdateAction<T> = {
type: "update";
index: number;
item: T;
};
type DeleteAction<T> = {
type: "delete";
index: number;
};
type InsertAfter<T> = {
type: "insertAfter";
index: number;
items: T[];
};
type Action<T> = AppendAction<T> | UpdateAction<T> | DeleteAction<T> | InsertAfter<T>;
function reducer<T>(state: ListState<T>, action: Action<T>): ListState<T> {
switch (action.type) {
case "append":
return { items: [...state.items, ...action.items] };
case "update":
return {
items: state.items.map((v, i) => (i === action.index ? action.item : v)),
};
case "delete":
return { items: state.items.filter((_, i) => i !== action.index) };
case "insertAfter":
return {
items: [
...state.items.slice(0, action.index + 1),
...action.items,
...state.items.slice(action.index + 1),
],
};
}
}
type HookReturn<T> = {
items: T[];
append: (items: T[]) => void;
update: (index: number, item: T) => void;
delete: (index: number) => void;
insertAfter: (index: number, items: T[]) => void;
};
export function useList<T>(initialItems: T[]): HookReturn<T> {
const [state, dispatch] = useReducer<Reducer<ListState<T>, Action<T>>>(reducer, {
items: initialItems,
});
return {
items: state.items,
append: (items: T[]) => dispatch({ type: "append", items }),
update: (index: number, item: T) => dispatch({ type: "update", index, item }),
delete: (index: number) => dispatch({ type: "delete", index }),
insertAfter: (index: number, items: T[]) => dispatch({ type: "insertAfter", index, items }),
};
}
@@ -87,8 +87,8 @@ export async function createOrganization(
}
export async function createEnvironment(
organization: Organization,
project: Project,
organization: Pick<Organization, "id">,
project: Pick<Project, "id">,
type: RuntimeEnvironment["type"],
member?: OrgMember,
prismaClient: PrismaClientOrTransaction = prisma
+1 -1
View File
@@ -87,7 +87,7 @@ export async function createProject(
// Create the dev and prod environments
await createEnvironment(organization, project, "PRODUCTION");
if (project.version === "V2") {
if (version === "v2") {
await createEnvironment(organization, project, "STAGING");
}
@@ -1,5 +1,6 @@
import type { RuntimeEnvironment } from "@trigger.dev/database";
import type { Prisma, RuntimeEnvironment } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { getUsername } from "~/utils/username";
export type { RuntimeEnvironment };
@@ -118,3 +119,36 @@ export async function disconnectSession(environmentId: string) {
return session;
});
}
type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
select: {
id: true;
type: true;
orgMember: {
select: {
user: {
select: {
id: true;
name: true;
displayName: true;
};
};
};
};
};
}>;
export function displayableEnvironments(
environment: DisplayableInputEnvironment,
userId: string | undefined
) {
return {
id: environment.id,
type: environment.type,
userName: environment.orgMember
? environment.orgMember.user.id === userId
? undefined
: getUsername(environment.orgMember.user)
: undefined,
};
}
@@ -1,6 +1,8 @@
import { PrismaClient, prisma } from "~/db.server";
import { Project } from "~/models/project.server";
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
import { User } from "~/models/user.server";
import { sortEnvironments } from "~/services/environmentSort.server";
export class ProjectPresenter {
#prismaClient: PrismaClient;
@@ -49,7 +51,13 @@ export class ProjectPresenter {
type: true,
orgMember: {
select: {
userId: true,
user: {
select: {
id: true,
name: true,
displayName: true,
},
},
},
},
apiKey: true,
@@ -76,13 +84,12 @@ export class ProjectPresenter {
hasInactiveExternalTriggers: project._count.sources > 0,
jobCount: project._count.jobs,
httpEndpointCount: project._count.httpEndpoints,
environments: project.environments.map((environment) => ({
id: environment.id,
slug: environment.slug,
type: environment.type,
apiKey: environment.apiKey,
userId: environment.orgMember?.userId,
})),
environments: sortEnvironments(
project.environments.map((environment) => ({
...displayableEnvironments(environment, userId),
userId: environment.orgMember?.user.id,
}))
),
};
}
}
@@ -74,6 +74,7 @@ export class ApiKeysPresenter {
environmentVariableCount: environment._count.environmentVariableValues,
}))
),
hasStaging: environments.some((environment) => environment.type === "STAGING"),
};
}
}
@@ -1,10 +1,12 @@
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
import { Direction } from "~/components/runs/RunStatuses";
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
import { getUsername } from "~/utils/username";
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
type RunListOptions = {
userId?: string;
projectSlug: string;
//filters
tasks?: string[];
@@ -34,6 +36,7 @@ export class RunListPresenter {
}
public async call({
userId,
projectSlug,
tasks,
versions,
@@ -231,12 +234,7 @@ export class RunListPresenter {
attempts: Number(run.attempts),
isReplayable: true,
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
environment: {
type: environment.type,
slug: environment.slug,
userId: environment.orgMember?.user.id,
userName: getUsername(environment.orgMember?.user),
},
environment: displayableEnvironments(environment, userId),
};
}),
pagination: {
@@ -1,19 +1,42 @@
import { Prisma, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
import {
Prisma,
RuntimeEnvironmentType,
TaskRunStatus,
TaskTriggerSource,
} from "@trigger.dev/database";
import { PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
import { sortEnvironments } from "~/services/environmentSort.server";
import { logger } from "~/services/logger.server";
import { getUsername } from "~/utils/username";
import { BasePresenter } from "./basePresenter.server";
import { QUEUED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
export type Task = Awaited<ReturnType<TaskListPresenter["call"]>>[0];
export type Task = {
slug: string;
exportName: string;
filePath: string;
createdAt: Date;
triggerSource: TaskTriggerSource;
environments: {
id: string;
type: RuntimeEnvironmentType;
userName?: string;
}[];
latestRun?: {
createdAt: Date;
status: TaskRunStatus;
};
};
export class TaskListPresenter {
#prismaClient: PrismaClient;
type Return = Awaited<ReturnType<TaskListPresenter["call"]>>;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
export type TaskActivity = Awaited<Return["activity"]>[string];
export class TaskListPresenter extends BasePresenter {
public async call({
userId,
projectSlug,
@@ -23,7 +46,7 @@ export class TaskListPresenter {
projectSlug: Project["slug"];
organizationSlug: Organization["slug"];
}) {
const project = await this.#prismaClient.project.findFirstOrThrow({
const project = await this._replica.project.findFirstOrThrow({
select: {
id: true,
environments: {
@@ -53,7 +76,7 @@ export class TaskListPresenter {
},
});
const tasks = await this.#prismaClient.$queryRaw<
const tasks = await this._replica.$queryRaw<
{
id: string;
slug: string;
@@ -64,73 +87,243 @@ export class TaskListPresenter {
triggerSource: TaskTriggerSource;
}[]
>`
SELECT DISTINCT ON(bwt.slug, bwt."runtimeEnvironmentId")
bwt.slug,
bwt.id,
bwt."exportName",
bwt."filePath",
bwt."runtimeEnvironmentId",
bwt."createdAt",
bwt."triggerSource"
FROM
${sqlDatabaseSchema}."BackgroundWorkerTask" as bwt
WHERE bwt."projectId" = ${project.id}
ORDER BY
bwt.slug,
bwt."runtimeEnvironmentId",
bwt."createdAt" DESC;`;
WITH workers AS (
SELECT DISTINCT ON ("runtimeEnvironmentId") id, "runtimeEnvironmentId", version
FROM ${sqlDatabaseSchema}."BackgroundWorker"
WHERE "runtimeEnvironmentId" IN (${Prisma.join(project.environments.map((e) => e.id))})
ORDER BY "runtimeEnvironmentId", "createdAt" DESC
)
SELECT tasks.id, slug, "filePath", "exportName", "triggerSource", tasks."runtimeEnvironmentId", tasks."createdAt"
FROM workers
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" tasks ON tasks."workerId" = workers.id
ORDER BY slug ASC;`;
let latestRuns = [] as {
createdAt: Date;
status: TaskRunStatus;
lockedById: string;
taskIdentifier: string;
}[];
if (tasks.length > 0) {
latestRuns = await this.#prismaClient.$queryRaw<
const uniqueTaskSlugs = new Set(tasks.map((t) => t.slug));
latestRuns = await this._replica.$queryRaw<
{
createdAt: Date;
status: TaskRunStatus;
lockedById: string;
taskIdentifier: string;
}[]
>`
SELECT * FROM (
SELECT
"createdAt",
"status",
"lockedById",
ROW_NUMBER() OVER (PARTITION BY "lockedById" ORDER BY "updatedAt" DESC) AS rn
"taskIdentifier",
ROW_NUMBER() OVER (PARTITION BY "taskIdentifier" ORDER BY "updatedAt" DESC) AS rn
FROM
${sqlDatabaseSchema}."TaskRun"
WHERE
"lockedById" IN(${Prisma.join(tasks.map((t) => t.id))})
) t
WHERE rn = 1;`;
"taskIdentifier" IN(${Prisma.join(Array.from(uniqueTaskSlugs))})
AND "projectId" = ${project.id}
) t
WHERE rn = 1;`;
}
return tasks.map((task) => {
const latestRun = latestRuns.find((r) => r.lockedById === task.id);
//group by the task identifier (task.slug). Add the latestRun and add all the environments.
const outputTasks = tasks.reduce((acc, task) => {
const latestRun = latestRuns.find((r) => r.taskIdentifier === task.slug);
const environment = project.environments.find((env) => env.id === task.runtimeEnvironmentId);
if (!environment) {
throw new Error(`Environment not found for TaskRun ${task.id}`);
}
return {
...task,
environment: {
id: environment.id,
type: environment.type,
slug: environment.slug,
userId: environment.orgMember?.user.id,
userName: getUsername(environment.orgMember?.user),
},
latestRun: latestRun
? {
createdAt: latestRun.createdAt,
status: latestRun.status,
}
: undefined,
};
});
let existingTask = acc.find((t) => t.slug === task.slug);
if (!existingTask) {
existingTask = {
...task,
environments: [],
};
acc.push(existingTask);
}
existingTask.environments.push(displayableEnvironments(environment, userId));
//order the environments
existingTask.environments = sortEnvironments(existingTask.environments);
existingTask.latestRun = latestRun
? {
createdAt: latestRun.createdAt,
status: latestRun.status,
}
: undefined;
return acc;
}, [] as Task[]);
//then get the activity for each task
const activity = this.#getActivity(
outputTasks.map((t) => t.slug),
project.id
);
const runningStats = this.#getRunningStats(
outputTasks.map((t) => t.slug),
project.id
);
const durations = this.#getAverageDurations(
outputTasks.map((t) => t.slug),
project.id
);
const userEnvironment = project.environments.find((e) => e.orgMember?.user.id === userId);
const userHasTasks = userEnvironment
? outputTasks.some((t) => t.environments.some((e) => e.id === userEnvironment.id))
: false;
return { tasks: outputTasks, userHasTasks, activity, runningStats, durations };
}
async #getActivity(tasks: string[], projectId: string) {
const activity = await this._replica.$queryRaw<
{
taskIdentifier: string;
status: TaskRunStatus;
day: Date;
count: BigInt;
}[]
>`
SELECT
tr."taskIdentifier",
tr."status",
DATE(tr."createdAt") as day,
COUNT(*)
FROM
${sqlDatabaseSchema}."TaskRun" as tr
WHERE
tr."taskIdentifier" IN (${Prisma.join(tasks)})
AND tr."projectId" = ${projectId}
AND tr."createdAt" >= (current_date - interval '6 days')
GROUP BY
tr."taskIdentifier",
tr."status",
day
ORDER BY
tr."taskIdentifier" ASC,
day ASC,
tr."status" ASC;`;
//today with no time
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
return activity.reduce((acc, a) => {
let existingTask = acc[a.taskIdentifier];
if (!existingTask) {
existingTask = [];
//populate the array with the past 7 days
for (let i = 6; i >= 0; i--) {
const day = new Date(today);
day.setUTCDate(today.getDate() - i);
day.setUTCHours(0, 0, 0, 0);
existingTask.push({
day: day.toISOString(),
[TaskRunStatus.COMPLETED_SUCCESSFULLY]: 0,
} as { day: string } & Record<TaskRunStatus, number>);
}
acc[a.taskIdentifier] = existingTask;
}
const dayString = a.day.toISOString();
const day = existingTask.find((d) => d.day === dayString);
if (!day) {
logger.warn(`Day not found for TaskRun`, {
day: dayString,
taskIdentifier: a.taskIdentifier,
existingTask,
});
return acc;
}
day[a.status] = Number(a.count);
return acc;
}, {} as Record<string, ({ day: string } & Record<TaskRunStatus, number>)[]>);
}
async #getRunningStats(tasks: string[], projectId: string) {
const statuses = await this._replica.$queryRaw<
{
taskIdentifier: string;
status: TaskRunStatus;
count: BigInt;
}[]
>`
SELECT
tr."taskIdentifier",
tr."status",
COUNT(*)
FROM
${sqlDatabaseSchema}."TaskRun" as tr
WHERE
tr."taskIdentifier" IN (${Prisma.join(tasks)})
AND tr."projectId" = ${projectId}
AND tr."status" IN ('PENDING', 'WAITING_FOR_DEPLOY', 'EXECUTING', 'RETRYING_AFTER_FAILURE', 'WAITING_TO_RESUME')
GROUP BY
tr."taskIdentifier",
tr."status"
ORDER BY
tr."taskIdentifier" ASC,
tr."status" ASC;`;
return statuses.reduce((acc, a) => {
let existingTask = acc[a.taskIdentifier];
if (!existingTask) {
existingTask = {
queued: 0,
running: 0,
};
acc[a.taskIdentifier] = existingTask;
}
if (QUEUED_STATUSES.includes(a.status)) {
existingTask.queued += Number(a.count);
}
if (RUNNING_STATUSES.includes(a.status)) {
existingTask.running += Number(a.count);
}
return acc;
}, {} as Record<string, { queued: number; running: number }>);
}
async #getAverageDurations(tasks: string[], projectId: string) {
const durations = await this._replica.$queryRaw<
{
taskIdentifier: string;
duration: Number;
}[]
>`
SELECT
tr."taskIdentifier",
AVG(EXTRACT(EPOCH FROM (tr."updatedAt" - tr."lockedAt"))) as duration
FROM
${sqlDatabaseSchema}."TaskRun" as tr
WHERE
tr."taskIdentifier" IN (${Prisma.join(tasks)})
AND tr."projectId" = ${projectId}
AND tr."createdAt" >= (current_date - interval '6 days')
AND tr."status" IN ('COMPLETED_SUCCESSFULLY', 'COMPLETED_WITH_ERRORS')
GROUP BY
tr."taskIdentifier";`;
return Object.fromEntries(durations.map((s) => [s.taskIdentifier, Number(s.duration)]));
}
}
@@ -63,17 +63,18 @@ export class TestPresenter {
const searchParams = createSearchParams(url, TestSearchParams);
//no environmentId
if (!searchParams.success || !searchParams.params.get("environment")) {
if (!searchParams.success) {
return {
hasSelectedEnvironment: false as const,
environments,
};
}
//default to dev environment
const environment = searchParams.params.get("environment") ?? "dev";
//is the environmentId valid?
const matchingEnvironment = project.environments.find(
(env) => env.slug === searchParams.params.get("environment")
);
const matchingEnvironment = project.environments.find((env) => env.slug === environment);
if (!matchingEnvironment) {
return {
hasSelectedEnvironment: false as const,
@@ -101,7 +102,7 @@ export class TestPresenter {
WHERE "runtimeEnvironmentId" = ${matchingEnvironment.id}
),
latest_workers AS (SELECT * FROM workers WHERE rn = 1)
SELECT bwt.id, version, slug as "taskIdentifier", "filePath", "exportName", bwt."friendlyId"
SELECT bwt.id, version, slug as "taskIdentifier", "filePath", "exportName", bwt."friendlyId", bwt."triggerSource"
FROM latest_workers
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" bwt ON bwt."workerId" = latest_workers.id
ORDER BY bwt."exportName" ASC;
@@ -171,17 +171,23 @@ export class TestTaskPresenter {
return {
triggerSource: "SCHEDULED",
task: taskWithEnvironment,
runs: await Promise.all(
latestRuns.map(async (r) => {
const number = Number(r.number);
runs: (
await Promise.all(
latestRuns.map(async (r) => {
const number = Number(r.number);
return {
...r,
number,
payload: await getScheduleTaskRunPayload(r),
};
})
),
const payload = await getScheduleTaskRunPayload(r);
if (payload.success) {
return {
...r,
number,
payload: payload.data,
};
}
})
)
).filter(Boolean),
};
}
}
@@ -189,6 +195,6 @@ export class TestTaskPresenter {
async function getScheduleTaskRunPayload(run: RawRun) {
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
const parsed = ScheduledTaskPayload.parse(payload);
const parsed = ScheduledTaskPayload.safeParse(payload);
return parsed;
}
@@ -1,10 +1,13 @@
import { Span, SpanKind } from "@opentelemetry/api";
import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { $replica, PrismaClientOrTransaction, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { attributesFromAuthenticatedEnv, tracer } from "../../v3/tracer.server";
export abstract class BasePresenter {
constructor(protected readonly _prisma: PrismaClientOrTransaction = prisma) {}
constructor(
protected readonly _prisma: PrismaClientOrTransaction = prisma,
protected readonly _replica: PrismaClientOrTransaction = $replica
) {}
protected async traceWithEnv<T>(
trace: string,
@@ -1,5 +1,4 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { AstroLogo } from "~/assets/logos/AstroLogo";
import { Feedback } from "~/components/Feedback";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
@@ -11,18 +10,17 @@ import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { projectSetupPath } from "~/utils/pathBuilder";
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
export default function SetUpAstro() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const { apiKey } = useV2OnboardingApiKey();
const appOrigin = useAppOrigin();
return (
@@ -67,7 +65,7 @@ export default function SetUpAstro() {
title="Run the CLI 'init' command in an existing Astro project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very simple
@@ -1,5 +1,4 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
import { Feedback } from "~/components/Feedback";
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
@@ -11,20 +10,17 @@ import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { projectSetupPath } from "~/utils/pathBuilder";
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
export default function Page() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const appOrigin = useAppOrigin();
const { apiKey } = useV2OnboardingApiKey();
return (
<div className="mx-auto max-w-3xl pt-16">
@@ -73,7 +69,7 @@ export default function Page() {
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
value={apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
@@ -1,5 +1,4 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { NestjsLogo } from "~/assets/logos/NestjsLogo";
import { Feedback } from "~/components/Feedback";
import { TriggerDevStep } from "~/components/SetupCommands";
@@ -11,12 +10,12 @@ import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { projectSetupPath } from "~/utils/pathBuilder";
import { CodeBlock } from "../../components/code/CodeBlock";
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
const AppModuleCode = `
import { Module } from '@nestjs/common';
@@ -114,11 +113,9 @@ export default function SetupNestJS() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
const { apiKey } = useV2OnboardingApiKey();
const appOrigin = useAppOrigin();
invariant(devEnvironment, "devEnvironment is required");
return (
<div className="mx-auto max-w-3xl pt-16">
<div className="mb-12 grid place-items-center">
@@ -161,7 +158,7 @@ export default function SetupNestJS() {
<CodeBlock
fileName=".env"
showChrome
code={`TRIGGER_API_KEY=${devEnvironment.apiKey}\nTRIGGER_API_URL=${appOrigin}`}
code={`TRIGGER_API_KEY=${apiKey}\nTRIGGER_API_URL=${appOrigin}`}
/>
</StepContentContainer>
<StepNumber stepNumber="3" title="Add the TriggerDevModule" />
@@ -1,6 +1,5 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import { useState } from "react";
import invariant from "tiny-invariant";
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
import { Feedback } from "~/components/Feedback";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
@@ -21,11 +20,11 @@ import { Paragraph } from "~/components/primitives/Paragraph";
import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { projectSetupPath } from "~/utils/pathBuilder";
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
type SelectionChoices = "use-existing-project" | "create-new-next-app";
@@ -33,12 +32,10 @@ export default function SetupNextjs() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
const { apiKey } = useV2OnboardingApiKey();
const appOrigin = useAppOrigin();
const [selectedValue, setSelectedValue] = useState<SelectionChoices | null>(null);
invariant(devEnvironment, "devEnvironment is required");
return (
<div className="mx-auto max-w-3xl pt-16">
<div className="mb-12 grid place-items-center">
@@ -152,7 +149,7 @@ export default function SetupNextjs() {
title="Run the CLI 'init' command in your new Next.js project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
@@ -179,7 +176,7 @@ export default function SetupNextjs() {
title="Run the CLI 'init' command in an existing Next.js project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very
@@ -1,5 +1,4 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { RemixLogo } from "~/assets/logos/RemixLogo";
import { Feedback } from "~/components/Feedback";
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
@@ -11,18 +10,17 @@ import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useAppOrigin } from "~/hooks/useAppOrigin";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { projectSetupPath } from "~/utils/pathBuilder";
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
export default function SetUpRemix() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const { apiKey } = useV2OnboardingApiKey();
const appOrigin = useAppOrigin();
return (
@@ -67,7 +65,7 @@ export default function SetUpRemix() {
title="Run the CLI 'init' command in an existing Remix project"
/>
<StepContentContainer>
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
<Paragraph spacing variant="small">
Youll notice a new folder in your project called 'jobs'. Weve added a very simple
@@ -1,5 +1,4 @@
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
import invariant from "tiny-invariant";
import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo";
import { Feedback } from "~/components/Feedback";
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
@@ -11,18 +10,18 @@ import { ClipboardField } from "~/components/primitives/ClipboardField";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { StepNumber } from "~/components/primitives/StepNumber";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
import { projectSetupPath } from "~/utils/pathBuilder";
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
export default function SetUpSveltekit() {
const organization = useOrganization();
const project = useProject();
useProjectSetupComplete();
const devEnvironment = useDevEnvironment();
invariant(devEnvironment, "Dev environment must be defined");
const { apiKey } = useV2OnboardingApiKey();
return (
<div className="mx-auto max-w-3xl pt-16">
<div className="mb-12 grid place-items-center">
@@ -70,7 +69,7 @@ export default function SetUpSveltekit() {
<ClipboardField
secure
className="w-fit"
value={devEnvironment.apiKey}
value={apiKey}
variant={"secondary/medium"}
icon={<Badge variant="outline">Server</Badge>}
/>
@@ -1,4 +1,48 @@
import { Outlet } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import { prisma } from "~/db.server";
import { useTypedMatchData, useTypedMatchesData } from "~/hooks/useTypedMatchData";
import { requireUserId } from "~/services/session.server";
import { ProjectParamSchema } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
const environment = await prisma.runtimeEnvironment.findFirst({
where: {
organization: {
slug: organizationSlug,
},
project: {
slug: projectParam,
},
orgMember: {
userId,
},
},
});
if (!environment) {
throw new Response("Not Found", { status: 404 });
}
return typedjson({
apiKey: environment.apiKey,
});
};
export function useV2OnboardingApiKey() {
const routeMatch = useTypedMatchesData<typeof loader>({
id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.setup",
});
if (!routeMatch) {
throw new Error("Route match not found");
}
return routeMatch;
}
export default function Page() {
return (
@@ -1,21 +1,24 @@
import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
import { ChatBubbleLeftRightIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid";
import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
import { useEffect } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
import { TaskRunStatus } from "@trigger.dev/database";
import { Fragment, Suspense, useEffect, useState } from "react";
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
import { Feedback } from "~/components/Feedback";
import { InitCommandV3, TriggerDevStepV3 } from "~/components/SetupCommands";
import { InitCommandV3, TriggerDevStepV3, TriggerLoginStepV3 } from "~/components/SetupCommands";
import { StepContentContainer } from "~/components/StepContentContainer";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import { Header1 } from "~/components/primitives/Headers";
import { Callout } from "~/components/primitives/Callout";
import { DateTime, formatDateTime } from "~/components/primitives/DateTime";
import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Spinner } from "~/components/primitives/Spinner";
import { StepNumber } from "~/components/primitives/StepNumber";
import {
Table,
@@ -28,18 +31,22 @@ import {
TableRow,
} from "~/components/primitives/Table";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { TaskFunctionName, TaskPath } from "~/components/runs/v3/TaskPath";
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
import {
TaskRunStatusCombo,
TaskRunStatusIcon,
runStatusClassNameColor,
runStatusTitle,
} from "~/components/runs/v3/TaskRunStatus";
import {
TaskTriggerSourceIcon,
taskTriggerSourceDescription,
} from "~/components/runs/v3/TaskTriggerSource";
import { useDevEnvironment } from "~/hooks/useEnvironments";
import { useEventSource } from "~/hooks/useEventSource";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server";
import { TaskActivity, TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { ProjectParamSchema, v3RunsPath, v3TasksStreamingPath } from "~/utils/pathBuilder";
@@ -50,14 +57,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
try {
const presenter = new TaskListPresenter();
const tasks = await presenter.call({
const { tasks, userHasTasks, activity, runningStats, durations } = await presenter.call({
userId,
organizationSlug,
projectSlug: projectParam,
});
return typedjson({
return typeddefer({
tasks,
userHasTasks,
activity,
runningStats,
durations,
});
} catch (error) {
console.error(error);
@@ -71,8 +82,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
export default function Page() {
const organization = useOrganization();
const project = useProject();
const user = useUser();
const { tasks } = useTypedLoaderData<typeof loader>();
const { tasks, userHasTasks, activity, runningStats, durations } =
useTypedLoaderData<typeof loader>();
const hasTasks = tasks.length > 0;
//live reload the page when the tasks change
@@ -97,35 +108,30 @@ export default function Page() {
<div className={cn("grid h-full grid-cols-1 gap-4")}>
<div className="h-full">
{hasTasks ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 pb-4">
{!userHasTasks && <UserHasNoTasks />}
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Task ID</TableHeaderCell>
<TableHeaderCell>Task</TableHeaderCell>
<TableHeaderCell>Path</TableHeaderCell>
<TableHeaderCell>Environment</TableHeaderCell>
<TableHeaderCell>Running</TableHeaderCell>
<TableHeaderCell>Queued</TableHeaderCell>
<TableHeaderCell>Activity (7d)</TableHeaderCell>
<TableHeaderCell>Avg. duration</TableHeaderCell>
<TableHeaderCell>Environments</TableHeaderCell>
<TableHeaderCell>Last run</TableHeaderCell>
<TableHeaderCell>
<div className="sr-only">Last run status</div>
</TableHeaderCell>
<TableHeaderCell>Created at</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{tasks.length > 0 ? (
tasks.map((task) => {
const usernameForEnv =
user.id !== task.environment.userId
? task.environment.userName
: undefined;
const path = v3RunsPath(organization, project, {
tasks: [task.slug],
environments: [task.environment.id],
});
return (
<TableRow key={task.id} className="group">
<TableRow key={task.slug} className="group">
<TableCell to={path}>
<div className="flex items-center gap-2">
<SimpleTooltip
@@ -135,44 +141,101 @@ export default function Page() {
<span>{task.slug}</span>
</div>
</TableCell>
<TableCell to={path}>
<TableCell to={path} className="py-0" actionClassName="py-0">
<TaskFunctionName
functionName={task.exportName}
variant="extra-small"
variant="extra-extra-small"
/>
</TableCell>
<TableCell to={path}>{task.filePath}</TableCell>
<TableCell to={path} className="p-0">
<Suspense
fallback={
<>
<Spinner color="muted" />
</>
}
>
<TypedAwait resolve={runningStats}>
{(data) => {
const taskData = data[task.slug];
return taskData?.running ?? "0";
}}
</TypedAwait>
</Suspense>
</TableCell>
<TableCell to={path} className="p-0">
<Suspense fallback={<></>}>
<TypedAwait resolve={runningStats}>
{(data) => {
const taskData = data[task.slug];
return taskData?.queued ?? "0";
}}
</TypedAwait>
</Suspense>
</TableCell>
<TableCell to={path} className="p-0" actionClassName="py-0">
<Suspense fallback={<TaskActivityBlankState />}>
<TypedAwait resolve={activity}>
{(data) => {
const taskData = data[task.slug];
return (
<>
{taskData !== undefined ? (
<div className="h-6 w-[5.125rem] rounded-sm">
<TaskActivityGraph activity={taskData} />
</div>
) : (
<TaskActivityBlankState />
)}
</>
);
}}
</TypedAwait>
</Suspense>
</TableCell>
<TableCell to={path} className="p-0">
<Suspense fallback={<></>}>
<TypedAwait resolve={durations}>
{(data) => {
const taskData = data[task.slug];
return taskData
? formatDurationMilliseconds(taskData * 1000, {
style: "short",
})
: "";
}}
</TypedAwait>
</Suspense>
</TableCell>
<TableCell to={path}>
<EnvironmentLabel
environment={task.environment}
userName={usernameForEnv}
/>
<div className="space-x-2">
{task.environments.map((environment) => (
<EnvironmentLabel
key={environment.id}
environment={environment}
userName={environment.userName}
/>
))}
</div>
</TableCell>
<TableCell to={path}>
{task.latestRun ? (
<div
className={cn(
"flex items-center gap-2",
classForTaskRunStatus(task.latestRun.status)
"flex items-center gap-1",
runStatusClassNameColor(task.latestRun.status)
)}
>
<TaskRunStatusIcon
status={task.latestRun.status}
className="h-4 w-4"
/>
<DateTime date={task.latestRun.createdAt} />
</div>
) : (
"Never run"
)}
</TableCell>
<TableCell to={path}>
{task.latestRun ? (
<TaskRunStatusCombo status={task.latestRun.status} />
) : (
""
)}
</TableCell>
<TableCell to={path}>
<DateTime date={task.createdAt} />
</TableCell>
<TableCellChevron to={path} />
</TableRow>
);
@@ -188,7 +251,9 @@ export default function Page() {
</Table>
</div>
) : (
<CreateTaskInstructions />
<MainCenteredContainer className="max-w-prose">
<CreateTaskInstructions />
</MainCenteredContainer>
)}
</div>
</div>
@@ -197,19 +262,9 @@ export default function Page() {
);
}
function classForTaskRunStatus(status: TaskRunStatus) {
switch (status) {
case "SYSTEM_FAILURE":
case "COMPLETED_WITH_ERRORS":
return "text-error";
default:
return "";
}
}
function CreateTaskInstructions() {
return (
<MainCenteredContainer className="max-w-prose">
<div>
<div className="mb-6 flex items-center justify-between border-b">
<Header1 spacing>Get setup in 3 minutes</Header1>
<div className="flex items-center gap-2">
@@ -240,6 +295,150 @@ function CreateTaskInstructions() {
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</MainCenteredContainer>
</div>
);
}
function UserHasNoTasks() {
const [open, setOpen] = useState(false);
return (
<Callout
variant="info"
cta={
<Button
variant="tertiary/small"
TrailingIcon={open ? ChevronUpIcon : ChevronDownIcon}
onClick={() => setOpen((o) => !o)}
>
{open ? "Close" : "Setup your dev environment"}
</Button>
}
>
{open ? (
<div>
<Header2 spacing>Get setup in 3 minutes</Header2>
<StepNumber stepNumber="1" title="Open up your project" className="mt-6" />
<StepContentContainer>
<Paragraph>You'll need to open a terminal at the root of your project.</Paragraph>
</StepContentContainer>
<StepNumber stepNumber="2" title="Run the CLI 'login' command" />
<StepContentContainer>
<TriggerLoginStepV3 />
</StepContentContainer>
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
<StepContentContainer>
<TriggerDevStepV3 />
</StepContentContainer>
<StepNumber stepNumber="4" title="Waiting for tasks" displaySpinner />
<StepContentContainer>
<Paragraph>This page will automatically refresh.</Paragraph>
</StepContentContainer>
</div>
) : (
"Your DEV environment isn't setup yet."
)}
</Callout>
);
}
function TaskActivityGraph({ activity }: { activity: TaskActivity }) {
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={activity}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
width={82}
height={24}
>
<Tooltip
cursor={{ fill: "transparent" }}
content={<CustomTooltip />}
allowEscapeViewBox={{ x: true, y: true }}
wrapperStyle={{ zIndex: 1000 }}
/>
{/* The background */}
<Bar
dataKey="bg"
background={{ fill: "#212327" }}
strokeWidth={0}
stackId="a"
barSize={10}
isAnimationActive={false}
/>
<Bar dataKey="PENDING" fill="#5F6570" stackId="a" strokeWidth={0} barSize={10} />
<Bar dataKey="WAITING_FOR_DEPLOY" fill="#F59E0B" stackId="a" strokeWidth={0} barSize={10} />
<Bar dataKey="EXECUTING" fill="#3B82F6" stackId="a" strokeWidth={0} barSize={10} />
<Bar
dataKey="RETRYING_AFTER_FAILURE"
fill="#3B82F6"
stackId="a"
strokeWidth={0}
barSize={10}
/>
<Bar dataKey="WAITING_TO_RESUME" fill="#3B82F6" stackId="a" strokeWidth={0} barSize={10} />
<Bar
dataKey="COMPLETED_SUCCESSFULLY"
fill="#28BF5C"
stackId="a"
strokeWidth={0}
barSize={10}
/>
<Bar dataKey="CANCELED" fill="#5F6570" stackId="a" strokeWidth={0} barSize={10} />
<Bar
dataKey="COMPLETED_WITH_ERRORS"
fill="#F43F5E"
stackId="a"
strokeWidth={0}
barSize={10}
/>
<Bar dataKey="INTERRUPTED" fill="#F43F5E" stackId="a" strokeWidth={0} barSize={10} />
<Bar dataKey="SYSTEM_FAILURE" fill="#F43F5E" stackId="a" strokeWidth={0} barSize={10} />
<Bar dataKey="PAUSED" fill="#FCD34D" stackId="a" strokeWidth={0} barSize={10} />
<Bar dataKey="CRASHED" fill="#F43F5E" stackId="a" strokeWidth={0} barSize={10} />
</BarChart>
</ResponsiveContainer>
);
}
function TaskActivityBlankState() {
return (
<div className="flex h-6 w-[5.125rem] items-center gap-0.5 rounded-sm">
{[...Array(7)].map((_, i) => (
<div key={i} className="h-full w-2.5 bg-[#212327]" />
))}
</div>
);
}
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
if (active && payload) {
const items = payload.map((p) => ({
status: p.dataKey as TaskRunStatus,
value: p.value,
}));
const title = payload[0].payload.day as string;
const formattedDate = formatDateTime(new Date(title), "UTC", [], false, false);
return (
<div className="rounded-sm border border-grid-bright bg-background-dimmed px-3 py-2">
<Header3 className="border-b-charcoal-650 border-b pb-2">{formattedDate}</Header3>
<div className="mt-2 grid grid-cols-[1fr_auto] gap-2 text-xs text-text-bright">
{items.map((item) => (
<Fragment key={item.status}>
<TaskRunStatusCombo status={item.status} />
<p>{item.value}</p>
</Fragment>
))}
</div>
</div>
);
}
return null;
};
@@ -1,10 +1,12 @@
import { BookOpenIcon } from "@heroicons/react/20/solid";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { Form } from "@remix-run/react";
import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
import { RegenerateApiKeyModal } from "~/components/environments/RegenerateApiKeyModal";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { DateTime } from "~/components/primitives/DateTime";
import { Header3 } from "~/components/primitives/Headers";
@@ -20,11 +22,15 @@ import {
TableRow,
} from "~/components/primitives/Table";
import { TextLink } from "~/components/primitives/TextLink";
import { prisma } from "~/db.server";
import { useFeatures } from "~/hooks/useFeatures";
import { useProject } from "~/hooks/useProject";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { createEnvironment } from "~/models/organization.server";
import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { ProjectParamSchema, docsPath } from "~/utils/pathBuilder";
import { ProjectParamSchema, docsPath, v3ApiKeysPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -32,13 +38,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
try {
const presenter = new ApiKeysPresenter();
const { environments } = await presenter.call({
const { environments, hasStaging } = await presenter.call({
userId,
projectSlug: projectParam,
});
return typedjson({
environments,
hasStaging,
});
} catch (error) {
console.error(error);
@@ -49,9 +56,76 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
}
};
export const action = async ({ request, params }: ActionFunctionArgs) => {
const userId = await requireUserId(request);
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
const project = await prisma.project.findUnique({
where: {
slug: params.projectParam,
organization: {
members: {
some: {
userId,
},
},
},
},
select: {
id: true,
organizationId: true,
environments: {
select: {
type: true,
},
},
},
});
if (!project) {
return redirectWithErrorMessage(
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
request,
"Project not found"
);
}
if (project.environments.some((env) => env.type === "STAGING")) {
return redirectWithErrorMessage(
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
request,
"You already have a staging environment"
);
}
const environment = await createEnvironment(
{ id: project.organizationId },
{ id: project.id },
"STAGING"
);
if (!environment) {
return redirectWithErrorMessage(
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
request,
"Failed to create staging environment"
);
}
return redirectWithSuccessMessage(
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
request,
"Staging environment created"
);
};
export default function Page() {
const { environments } = useTypedLoaderData<typeof loader>();
const project = useProject();
const { environments, hasStaging } = useTypedLoaderData<typeof loader>();
const { isManagedCloud } = useFeatures();
return (
<PageContainer>
@@ -127,6 +201,21 @@ export default function Page() {
))}
</TableBody>
</Table>
{!hasStaging && (
<Callout
variant="info"
cta={
<Form method="post">
<Button variant="tertiary/small">Enable Staging</Button>
</Form>
}
>
{isManagedCloud
? "The Staging environment will be a paid feature when we add billing. In the interim you can enable it for free."
: "You can add a Staging environment to your project."}
</Callout>
)}
</div>
</div>
</PageBody>
@@ -1,35 +1,43 @@
import { Submission, conform, useForm } from "@conform-to/react";
import {
FieldConfig,
list,
requestIntent,
useFieldList,
useFieldset,
useForm,
} from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { Form, useActionData, useLocation, useNavigate, useNavigation } from "@remix-run/react";
import { PlusIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useNavigate, useNavigation } from "@remix-run/react";
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { Fragment, useEffect, useRef, useState } from "react";
import { RefObject, useCallback, useEffect, useRef, useState } from "react";
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import {
environmentTextClassName,
environmentTitle,
} from "~/components/environments/EnvironmentLabel";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { Checkbox } from "~/components/primitives/Checkbox";
import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Switch } from "~/components/primitives/Switch";
import { prisma } from "~/db.server";
import { useList } from "~/hooks/useList";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { EnvironmentVariablesPresenter } from "~/presenters/v3/EnvironmentVariablesPresenter.server";
import { requireUserId } from "~/services/session.server";
import {
ProjectParamSchema,
v3EnvironmentVariablesPath,
v3NewEnvironmentVariablesPath,
} from "~/utils/pathBuilder";
import { cn } from "~/utils/cn";
import { ProjectParamSchema, v3EnvironmentVariablesPath } from "~/utils/pathBuilder";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
import { CreateEnvironmentVariable } from "~/v3/environmentVariables/repository";
import { EnvironmentVariableKey } from "~/v3/environmentVariables/repository";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -47,7 +55,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
environments,
});
} catch (error) {
console.error(error);
throw new Response(undefined, {
status: 400,
statusText: "Something went wrong, if this problem persists please contact support.",
@@ -55,9 +62,39 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
}
};
const Variable = z.object({
key: EnvironmentVariableKey,
value: z.string().nonempty("Value is required"),
});
type Variable = z.infer<typeof Variable>;
const schema = z.object({
action: z.enum(["create", "create-more"]),
...CreateEnvironmentVariable.shape,
overwrite: z.preprocess((i) => {
if (i === "true") return true;
if (i === "false") return false;
return;
}, z.boolean()),
environmentIds: z.preprocess((i) => {
if (typeof i === "string") return [i];
if (Array.isArray(i)) {
const ids = i.filter((v) => typeof v === "string" && v !== "");
if (ids.length === 0) {
return;
}
return ids;
}
return;
}, z.array(z.string(), { required_error: "At least one environment is required" })),
variables: z.preprocess((i) => {
if (!Array.isArray(i)) {
return [];
}
return i;
}, Variable.array().nonempty("At least one variable is required")),
});
export const action = async ({ request, params }: ActionFunctionArgs) => {
@@ -92,22 +129,22 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
const result = await repository.create(project.id, userId, submission.value);
if (!result.success) {
submission.error.key = result.error;
if (result.variableErrors) {
for (const { key, error } of result.variableErrors) {
const index = submission.value.variables.findIndex((v) => v.key === key);
if (index !== -1) {
submission.error[`variables[${index}].key`] = error;
}
}
} else {
submission.error.variables = result.error;
}
return json(submission);
}
switch (submission.value.action) {
case "create":
return redirect(
v3EnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam })
);
case "create-more":
return redirectWithSuccessMessage(
v3NewEnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam }),
request,
`Created ${submission.value.key} environment variable`
);
}
return redirect(v3EnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam }));
};
export default function Page() {
@@ -118,15 +155,11 @@ export default function Page() {
const navigate = useNavigate();
const organization = useOrganization();
const project = useProject();
const keyFieldRef = useRef<HTMLInputElement>(null);
const isLoading =
navigation.state !== "idle" &&
navigation.formMethod === "post" &&
navigation.formData?.get("action") === "create";
const isLoading = navigation.state !== "idle" && navigation.formMethod === "post";
const [form, { key }] = useForm({
id: "create-environment-variable",
const [form, { environmentIds, variables }] = useForm({
id: "create-environment-variables",
// TODO: type this
lastSubmission: lastSubmission as any,
onValidate({ formData }) {
@@ -141,14 +174,6 @@ export default function Page() {
setIsOpen(true);
}, []);
useEffect(() => {
if (navigation.state !== "idle") return;
if (lastSubmission !== undefined) return;
form.ref.current?.reset();
keyFieldRef.current?.focus();
}, [navigation.state, lastSubmission]);
return (
<Dialog
open={isOpen}
@@ -159,60 +184,63 @@ export default function Page() {
}}
>
<DialogContent>
<DialogHeader>New environment variable</DialogHeader>
<Form method="post" {...form.props}>
<DialogHeader>New environment variables</DialogHeader>
<Form
method="post"
{...form.props}
className="max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
>
<Fieldset className="mt-2">
<InputGroup fullWidth>
<Label>Key</Label>
<Input
{...conform.input(key)}
placeholder="e.g. CLIENT_KEY"
autoFocus
ref={keyFieldRef}
/>
</InputGroup>
<InputGroup fullWidth>
<div className="flex items-center justify-between">
<Label>Values</Label>
<Switch
variant="small"
label="Reveal values"
checked={revealAll}
onCheckedChange={(e) => setRevealAll(e.valueOf())}
/>
</div>
<div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-2">
{environments.map((environment, index) => {
return (
<Fragment key={environment.id}>
<input
type="hidden"
name={`values[${index}].environmentId`}
value={environment.id}
/>
<label
className="flex items-center justify-end"
htmlFor={`values[${index}].value`}
<Label>Environments</Label>
<div className="flex flex-wrap items-center gap-2">
{environments.map((environment) => (
<Checkbox
key={environment.id}
id={environment.id}
value={environment.id}
name="environmentIds"
type="radio"
label={
<span
className={cn("text-xs uppercase", environmentTextClassName(environment))}
>
<EnvironmentLabel environment={environment} className="h-5 px-2" />
</label>
<Input
type={revealAll ? "text" : "password"}
name={`values[${index}].value`}
placeholder="Not set"
/>
</Fragment>
);
})}
{environmentTitle(environment)}
</span>
}
variant="button"
/>
))}
</div>
<FormError id={environmentIds.errorId}>{environmentIds.error}</FormError>
<Hint>
Dev environment variables specified here will be overridden by ones in your .env
file when running locally.
</Hint>
</InputGroup>
<Hint>Tip: Paste your .env into this form to populate it:</Hint>
<InputGroup fullWidth>
<FieldLayout>
<Label>Keys</Label>
<div className="flex justify-between gap-1">
<Label>Values</Label>
<Switch
variant="small"
label="Reveal"
checked={revealAll}
onCheckedChange={(e) => setRevealAll(e.valueOf())}
/>
</div>
</FieldLayout>
<VariableFields
revealValues={revealAll}
formId={form.id}
formRef={form.ref}
variablesFields={variables}
/>
<FormError id={variables.errorId}>{variables.error}</FormError>
</InputGroup>
<Callout variant="info" className="inline-flex">
Dev environment variables specified here will be overriden by ones in your{" "}
<InlineCode variant="extra-small">.env</InlineCode> file when running locally.
</Callout>
<FormError id={key.errorId}>{key.error}</FormError>
<FormError>{form.error}</FormError>
<FormButtons
confirmButton={
@@ -221,18 +249,18 @@ export default function Page() {
type="submit"
variant="primary/small"
disabled={isLoading}
name="action"
value="create-more"
name="overwrite"
value="false"
>
{isLoading ? "Saving" : "Save and add another"}
{isLoading ? "Saving" : "Save"}
</Button>
<Button
variant="secondary/small"
disabled={isLoading}
name="action"
value="create"
name="overwrite"
value="true"
>
{isLoading ? "Saving" : "Save"}
{isLoading ? "Overwriting" : "Overwrite"}
</Button>
</div>
}
@@ -251,3 +279,164 @@ export default function Page() {
</Dialog>
);
}
function FieldLayout({ children }: { children: React.ReactNode }) {
return <div className="grid w-full grid-cols-[1fr_1fr_2rem] gap-2">{children}</div>;
}
function VariableFields({
revealValues,
formId,
variablesFields,
formRef,
}: {
revealValues: boolean;
formId?: string;
variablesFields: FieldConfig<any>;
formRef: RefObject<HTMLFormElement>;
}) {
const {
items,
append,
update,
delete: remove,
insertAfter,
} = useList<Variable>([{ key: "", value: "" }]);
const handlePaste = useCallback((index: number, e: React.ClipboardEvent<HTMLInputElement>) => {
const clipboardData = e.clipboardData;
if (!clipboardData) return;
let text = clipboardData.getData("text");
//replace carriage returns
text = text.replace(/\r/g, "");
const lines = text.split("\n");
const keyValuePairs = lines.flatMap((line) => {
if (line.trim().startsWith("#")) return [];
const split = line.split("=");
if (split.length === 2) {
return [{ key: split[0], value: split[1] }];
}
return [];
});
if (keyValuePairs.length === 0) return;
//prevent default pasting
e.preventDefault();
const [firstPair, ...rest] = keyValuePairs;
update(index, firstPair);
for (const pair of rest) {
requestIntent(formRef.current ?? undefined, list.append(variablesFields.name));
}
insertAfter(index, rest);
}, []);
const fields = useFieldList(formRef, variablesFields);
return (
<>
{fields.map((field, index) => {
const item = items[index];
return (
<VariableField
formId={formId}
key={index}
index={index}
value={item}
onChange={(value) => update(index, value)}
onPaste={(e) => handlePaste(index, e)}
onDelete={() => {
requestIntent(
formRef.current ?? undefined,
list.remove(variablesFields.name, { index })
);
remove(index);
}}
showDeleteButton={items.length > 1}
showValue={revealValues}
config={field}
/>
);
})}
<Button
variant="tertiary/medium"
type="button"
onClick={() => {
requestIntent(formRef.current ?? undefined, list.append(variablesFields.name));
append([{ key: "", value: "" }]);
}}
LeadingIcon={PlusIcon}
>
Add another
</Button>
</>
);
}
function VariableField({
formId,
index,
value,
onChange,
onPaste,
onDelete,
showDeleteButton,
showValue,
config,
}: {
formId?: string;
index: number;
value: Variable;
onChange: (value: Variable) => void;
onPaste: (e: React.ClipboardEvent<HTMLInputElement>) => void;
onDelete: () => void;
showDeleteButton: boolean;
showValue: boolean;
config: FieldConfig<Variable>;
}) {
const ref = useRef<HTMLFieldSetElement>(null);
const fields = useFieldset(ref, config);
const baseFieldName = `variables[${index}]`;
return (
<fieldset ref={ref}>
<FieldLayout>
<Input
id={`${formId}-${baseFieldName}.key`}
name={`${baseFieldName}.key`}
placeholder="e.g. CLIENT_KEY"
value={value.key}
onChange={(e) => onChange({ ...value, key: e.currentTarget.value })}
autoFocus={index === 0}
onPaste={onPaste}
/>
<Input
id={`${formId}-${baseFieldName}.value`}
name={`${baseFieldName}.value`}
type={showValue ? "text" : "password"}
placeholder="Not set"
value={value.value}
onChange={(e) => onChange({ ...value, value: e.currentTarget.value })}
/>
{showDeleteButton && (
<Button
variant="minimal/medium"
type="button"
onClick={() => onDelete()}
LeadingIcon={XMarkIcon}
/>
)}
</FieldLayout>
<div className="space-y-2">
<FormError id={fields.key.errorId}>{fields.key.error}</FormError>
<FormError id={fields.value.errorId}>{fields.value.error}</FormError>
</div>
</fieldset>
);
}
@@ -187,8 +187,9 @@ export default function Page() {
to={v3NewEnvironmentVariablesPath(organization, project)}
variant="primary/small"
LeadingIcon={PlusIcon}
shortcut={{ key: "n" }}
>
New environment variable
Add new
</LinkButton>
</div>
<Table>
@@ -247,7 +248,7 @@ export default function Page() {
</Table>
<Callout variant="info" className="mb-4">
Dev environment variables specified here will be overriden by ones in your .env file
Dev environment variables specified here will be overridden by ones in your .env file
when running locally.
</Callout>
</div>
@@ -34,6 +34,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const presenter = new RunListPresenter();
const list = await presenter.call({
userId,
projectSlug: projectParam,
tasks,
versions,
@@ -88,7 +89,6 @@ export default function Page() {
filters={list.filters}
runs={list.runs}
isLoading={isLoading}
currentUser={user}
/>
<ListPagination list={list} className="mt-2 justify-end" />
</div>
@@ -247,7 +247,6 @@ export default function Page() {
}}
runs={schedule.runs}
isLoading={false}
currentUser={user}
/>
</div>
<div className="flex flex-col gap-1">
@@ -9,6 +9,7 @@ import {
} from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Header2 } from "~/components/primitives/Headers";
import { Input } from "~/components/primitives/Input";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RadioButtonCircle } from "~/components/primitives/RadioButton";
@@ -20,6 +21,7 @@ import {
import { Spinner } from "~/components/primitives/Spinner";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableHeader,
@@ -32,6 +34,7 @@ import { useLinkStatus } from "~/hooks/useLinkStatus";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useTextFilter } from "~/hooks/useTextFilter";
import {
SelectedEnvironment,
TaskListItem,
@@ -67,7 +70,7 @@ export default function Page() {
//get optimistic location for the segment control
const optimisticLocation = useOptimisticLocation();
const environment = new URLSearchParams(optimisticLocation.search).get("environment");
const environment = new URLSearchParams(optimisticLocation.search).get("environment") ?? "dev";
const navigation = useNavigation();
@@ -150,11 +153,50 @@ function TaskSelector({
tasks: TaskListItem[];
environmentSlug: string;
}) {
const organization = useOrganization();
const project = useProject();
const { filterText, setFilterText, filteredItems } = useTextFilter<TaskListItem>({
items: tasks,
filter: (task, text) => {
if (task.taskIdentifier.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.exportName.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.filePath.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.id.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.friendlyId.toLowerCase().includes(text.toLowerCase())) {
return true;
}
if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) {
return true;
}
return false;
},
});
return (
<div className="divide-y divide-charcoal-800 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<div className="px-2 pb-2">
<Input
placeholder="Search tasks"
variant="medium"
icon="search"
fullWidth={true}
value={filterText}
autoFocus
onChange={(e) => setFilterText(e.target.value)}
/>
</div>
<Table>
<TableHeader>
<TableRow>
@@ -166,42 +208,17 @@ function TaskSelector({
</TableRow>
</TableHeader>
<TableBody>
{tasks.map((t) => {
const path = v3TestTaskPath(organization, project, t, environmentSlug);
const { isActive, isPending } = useLinkStatus(path);
return (
<TableRow
key={t.taskIdentifier}
className={cn(
(isActive || isPending) &&
"z-20 rounded-sm outline outline-1 outline-offset-[-1px] outline-secondary"
)}
>
<TableCell to={path} actionClassName="pl-2.5 pr-1 py-1">
<RadioButtonCircle checked={isActive || isPending} />
</TableCell>
<TableCell to={path} actionClassName="pl-1 pr-2 py-1">
<div className="flex flex-col gap-0.5">
<TaskFunctionName
variant="extra-small"
functionName={t.exportName}
className="-ml-1 inline-flex"
/>
<div className="flex items-start gap-1">
<TaskTriggerSourceIcon source={t.triggerSource} className="size-3.5" />
<Paragraph variant="extra-small" className="text-text-dimmed">
{t.taskIdentifier}
</Paragraph>
</div>
</div>
</TableCell>
<TableCell to={path} actionClassName="px-2 py-1">
{t.filePath}
</TableCell>
</TableRow>
);
})}
{filteredItems.length > 0 ? (
filteredItems.map((t) => (
<TaskRow key={t.friendlyId} task={t} environmentSlug={environmentSlug} />
))
) : (
<TableBlankRow colSpan={3}>
<Paragraph spacing variant="small">
No tasks match "{filterText}"
</Paragraph>
</TableBlankRow>
)}
</TableBody>
</Table>
</div>
@@ -217,3 +234,43 @@ function NoTaskInstructions({ environment }: { environment?: SelectedEnvironment
</div>
);
}
function TaskRow({ task, environmentSlug }: { task: TaskListItem; environmentSlug: string }) {
const organization = useOrganization();
const project = useProject();
const path = v3TestTaskPath(organization, project, task, environmentSlug);
const { isActive, isPending } = useLinkStatus(path);
return (
<TableRow
key={task.taskIdentifier}
className={cn(
(isActive || isPending) &&
"z-20 rounded-sm outline outline-1 outline-offset-[-1px] outline-secondary"
)}
>
<TableCell to={path} actionClassName="pl-2.5 pr-1 py-1">
<RadioButtonCircle checked={isActive || isPending} />
</TableCell>
<TableCell to={path} actionClassName="pl-1 pr-2 py-1">
<div className="flex flex-col gap-0.5">
<TaskFunctionName
variant="extra-small"
functionName={task.exportName}
className="-ml-1 inline-flex"
/>
<div className="flex items-start gap-1">
<TaskTriggerSourceIcon source={task.triggerSource} className="size-3.5" />
<Paragraph variant="extra-small" className="text-text-dimmed">
{task.taskIdentifier}
</Paragraph>
</div>
</div>
</TableCell>
<TableCell to={path} actionClassName="px-2 py-1">
{task.filePath}
</TableCell>
</TableRow>
);
}
@@ -1,4 +1,5 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { logger } from "./logger.server";
const environmentSortOrder: RuntimeEnvironmentType[] = [
"DEVELOPMENT",
@@ -9,12 +10,23 @@ const environmentSortOrder: RuntimeEnvironmentType[] = [
type SortType = {
type: RuntimeEnvironmentType;
userName?: string | null;
};
export function sortEnvironments<T extends SortType>(environments: T[]): T[] {
return environments.sort((a, b) => {
const aIndex = environmentSortOrder.indexOf(a.type);
const bIndex = environmentSortOrder.indexOf(b.type);
return aIndex - bIndex;
const difference = aIndex - bIndex;
if (difference === 0) {
//same environment so sort by name
const usernameA = a.userName || "";
const usernameB = b.userName || "";
return usernameA.localeCompare(usernameB);
}
return difference;
});
}
@@ -32,6 +32,14 @@ export class ContinueRunService {
throw new Error("Run is not resumable");
}
// Delete any tasks that are errored
const erroredTasks = await tx.task.findMany({
where: {
runId: runId,
status: "ERRORED",
},
});
await tx.jobRun.update({
where: { id: runId },
data: {
@@ -45,9 +53,15 @@ export class ContinueRunService {
},
});
for (const task of erroredTasks) {
await tx.task.delete({
where: { id: task.id },
});
}
await ResumeRunService.enqueue(run, tx);
},
{ timeout: 10000 }
{ timeout: 30_000 }
);
}
}
@@ -1,10 +1,17 @@
import { Prisma, PrismaClient } from "@trigger.dev/database";
import { Prisma, PrismaClient, RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
import { environmentTitle } from "~/components/environments/EnvironmentLabel";
import { $transaction, prisma } from "~/db.server";
import { env } from "~/env.server";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { EnvironmentVariable, ProjectEnvironmentVariable, Repository, Result } from "./repository";
import { env } from "~/env.server";
import {
CreateResult,
EnvironmentVariable,
ProjectEnvironmentVariable,
Repository,
Result,
} from "./repository";
function secretKeyProjectPrefix(projectId: string) {
return `environmentvariable:${projectId}:`;
@@ -35,8 +42,15 @@ export class EnvironmentVariablesRepository implements Repository {
async create(
projectId: string,
userId: string,
options: { key: string; values: { value: string; environmentId: string }[] }
): Promise<Result> {
options: {
overwrite: boolean;
environmentIds: string[];
variables: {
key: string;
value: string;
}[];
}
): Promise<CreateResult> {
const project = await this.prismaClient.project.findUnique({
where: {
id: projectId,
@@ -55,6 +69,18 @@ export class EnvironmentVariablesRepository implements Repository {
id: true,
},
},
environmentVariables: {
select: {
key: true,
values: {
select: {
environment: {
select: { id: true, type: true },
},
},
},
},
},
},
});
@@ -62,58 +88,109 @@ export class EnvironmentVariablesRepository implements Repository {
return { success: false as const, error: "Project not found" };
}
if (options.values.every((v) => !project.environments.some((e) => e.id === v.environmentId))) {
if (options.environmentIds.every((v) => !project.environments.some((e) => e.id === v))) {
return { success: false as const, error: `Environment not found` };
}
//get rid of empty strings
const values = options.values.filter((v) => v.value.trim() !== "");
//get rid of empty variables
const values = options.variables.filter((v) => v.key.trim() !== "" && v.value.trim() !== "");
if (values.length === 0) {
return { success: false as const, error: `You must set at least one value` };
}
//check if any of them exist in an environment we're setting
if (!options.overwrite) {
const existingVariableKeys: { key: string; environments: RuntimeEnvironmentType[] }[] = [];
for (const variable of values) {
const existingVariable = project.environmentVariables.find((v) => v.key === variable.key);
if (
existingVariable &&
existingVariable.values.some((v) => options.environmentIds.includes(v.environment.id))
) {
existingVariableKeys.push({
key: variable.key,
environments: existingVariable.values
.filter((v) => options.environmentIds.includes(v.environment.id))
.map((v) => v.environment.type),
});
}
}
if (existingVariableKeys.length > 0) {
return {
success: false as const,
error: `Some of the variables are already set for these environments`,
variableErrors: existingVariableKeys.map((val) => ({
key: val.key,
error: `Variable already set in ${val.environments
.map((e) => environmentTitle({ type: e }))
.join(", ")}.`,
})),
};
}
}
try {
const result = await $transaction(this.prismaClient, async (tx) => {
const environmentVariable = await tx.environmentVariable.create({
data: {
key: options.key,
friendlyId: generateFriendlyId("envvar"),
project: {
connect: {
id: projectId,
for (const variable of values) {
const environmentVariable = await tx.environmentVariable.upsert({
where: {
projectId_key: {
key: variable.key,
projectId,
},
},
},
});
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
//create the secret values and references
for (const value of values) {
const key = secretKey(projectId, value.environmentId, options.key);
//create the secret reference
const secretReference = await tx.secretReference.create({
data: {
key,
provider: "DATABASE",
create: {
key: variable.key,
friendlyId: generateFriendlyId("envvar"),
project: {
connect: {
id: projectId,
},
},
},
update: {},
});
const variableValue = await tx.environmentVariableValue.create({
data: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
valueReferenceId: secretReference.id,
},
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
await secretStore.setSecret<{ secret: string }>(key, {
secret: value.value,
});
//set the secret values and references
for (const environmentId of options.environmentIds) {
const key = secretKey(projectId, environmentId, variable.key);
//create the secret reference
const secretReference = await tx.secretReference.upsert({
where: {
key,
},
create: {
key,
provider: "DATABASE",
},
update: {},
});
const variableValue = await tx.environmentVariableValue.upsert({
where: {
variableId_environmentId: {
variableId: environmentVariable.id,
environmentId,
},
},
create: {
variableId: environmentVariable.id,
environmentId: environmentId,
valueReferenceId: secretReference.id,
},
update: {},
});
await secretStore.setSecret<{ secret: string }>(key, {
secret: variable.value,
});
}
}
});
@@ -126,7 +203,7 @@ export class EnvironmentVariablesRepository implements Repository {
if (error.code === "P2002") {
return {
success: false as const,
error: `There's already an environment variable called ${options.key}.`,
error: `There was already an existing field`,
};
}
}
@@ -1,22 +1,27 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
const EnvironmentVariable = z
export const EnvironmentVariableKey = z
.string()
.nonempty("Environment variable key is required")
.regex(/^\w+$/, "Environment variables can only contain alphanumeric characters and underscores");
.nonempty("Key is required")
.regex(/^\w+$/, "Keys can only use alphanumeric characters and underscores");
export const CreateEnvironmentVariable = z.object({
key: EnvironmentVariable,
values: z.array(
z.object({
environmentId: z.string(),
value: z.string(),
})
),
export const CreateEnvironmentVariables = z.object({
environmentIds: z.array(z.string()),
variables: z.array(z.object({ key: EnvironmentVariableKey, value: z.string() })),
});
export type CreateEnvironmentVariable = z.infer<typeof CreateEnvironmentVariable>;
export type CreateEnvironmentVariables = z.infer<typeof CreateEnvironmentVariables>;
export type CreateResult =
| {
success: true;
}
| {
success: false;
error: string;
variableErrors?: { key: string; error: string }[];
};
export const EditEnvironmentVariable = z.object({
id: z.string(),
@@ -60,7 +65,11 @@ export type EnvironmentVariable = {
};
export interface Repository {
create(projectId: string, userId: string, options: CreateEnvironmentVariable): Promise<Result>;
create(
projectId: string,
userId: string,
options: CreateEnvironmentVariables
): Promise<CreateResult>;
edit(projectId: string, userId: string, options: EditEnvironmentVariable): Promise<Result>;
getProject(projectId: string, userId: string): Promise<ProjectEnvironmentVariable[]>;
getEnvironment(
@@ -76,6 +76,15 @@ export class TriggerTaskService extends BaseService {
immediate: true,
},
async (event, traceContext) => {
const runFriendlyId = generateFriendlyId("run");
const payloadPacket = await this.#handlePayloadPacket(
body.payload,
body.options?.payloadType ?? "application/json",
runFriendlyId,
environment
);
const lockId = taskIdentifierToLockId(taskId);
const run = await $transaction(this._prisma, async (tx) => {
@@ -105,15 +114,6 @@ export class TriggerTaskService extends BaseService {
event.setAttribute("queueName", queueName);
span.setAttribute("queueName", queueName);
const runFriendlyId = generateFriendlyId("run");
const payloadPacket = await this.#handlePayloadPacket(
body.payload,
body.options?.payloadType ?? "application/json",
runFriendlyId,
environment
);
const taskRun = await tx.taskRun.create({
data: {
status: "PENDING",
+1 -1
View File
@@ -139,7 +139,7 @@
"react-resizable-panels": "^2.0.9",
"react-stately": "^3.29.1",
"react-use": "^17.4.0",
"recharts": "^2.8.0",
"recharts": "^2.12.6",
"remix-auth": "^3.6.0",
"remix-auth-email-link": "2.0.2",
"remix-auth-github": "^1.6.0",
+35 -2
View File
@@ -7,8 +7,10 @@ This simple GitHub action file will deploy you Trigger.dev tasks when new code i
<Warning>The deploy step will fail if any version mismatches are detected. Please see the [version pinning](/v3/github-actions#version-pinning) section for more details.</Warning>
```yaml .github/workflows/release-trigger.yml
name: Deploy to Trigger.dev
<CodeGroup>
```yaml .github/workflows/release-trigger-prod.yml
name: Deploy to Trigger.dev (prod)
on:
push:
@@ -39,6 +41,37 @@ jobs:
npx trigger.dev@beta deploy
```
```yaml .github/workflows/release-trigger-staging.yml
name: Deploy to Trigger.dev (staging)
# Requires manually calling the workflow from a branch / commit to deploy to staging
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm install
- name: 🚀 Deploy Trigger.dev
env:
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
run: |
npx trigger.dev@beta deploy --env staging
```
</CodeGroup>
If you already have a GitHub action file, you can just add the final step "🚀 Deploy Trigger.dev" to your existing file.
You need to add the `TRIGGER_ACCESS_TOKEN` secret to your repository. You can create a new access token by going to your profile page and then clicking on the "Personal Access Tokens" tab.
+126 -10
View File
@@ -79,7 +79,7 @@ export const taskWithRetries = task({
maxTimeoutInMs: 30_000,
randomize: false,
},
run: async ({ payload, ctx }) => {
run: async (payload: any, { ctx }) => {
//...
},
});
@@ -99,7 +99,7 @@ export const oneAtATime = task({
queue: {
concurrencyLimit: 1,
},
run: async ({ payload, ctx }) => {
run: async (payload: any, { ctx }) => {
//...
},
});
@@ -116,7 +116,7 @@ export const heavyTask = task({
cpu: 2,
memory: 4,
},
run: async ({ payload, ctx }) => {
run: async (payload: any, { ctx }) => {
//...
},
});
@@ -124,27 +124,143 @@ export const heavyTask = task({
### `init` function
This function is called before a run attempt.
This function is called before a run attempt:
```ts /trigger/init.ts
export const taskWithInit = task({
id: "task-with-init",
init: async (payload, { ctx }) => {
//...
},
run: async (payload: any, { ctx }) => {
//...
},
});
```
You can also return data from the `init` function that will be available in the params of the `run`, `cleanup`, `onSuccess`, and `onFailure` functions.
```ts /trigger/init-return.ts
export const taskWithInitReturn = task({
id: "task-with-init-return",
init: async (payload, { ctx }) => {
return { someData: "someValue" };
},
run: async (payload: any, { ctx, init }) => {
console.log(init.someData); // "someValue"
},
});
```
### `cleanup` function
This function is called after a run attempt has succeeded or failed.
This function is called after the `run` function is executed, regardless of whether the run was successful or not. It's useful for cleaning up resources, logging, or other side effects.
```ts /trigger/cleanup.ts
export const taskWithCleanup = task({
id: "task-with-cleanup",
cleanup: async (payload, { ctx }) => {
//...
},
run: async (payload: any, { ctx }) => {
//...
},
});
```
### `middleware` function
This function is called before the `run` function, it allows you to wrap the run function with custom code. For more information [read the guide](/v3/middleware).
### `onStart` function
When a task run starts, the `onStart` function is called. It's useful for sending notifications, logging, and other side effects. This function will only be called one per run (not per retry). If you want to run code before each retry, use the `init` function.
```ts /trigger/on-start.ts
export const taskWithOnStart = task({
id: "task-with-on-start",
onStart: async (payload, { ctx }) => {
//...
},
run: async (payload: any, { ctx }) => {
//...
},
});
```
You can also define an `onStart` function in your `trigger.config.ts` file to get notified when any task starts.
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
export const config: TriggerConfig = {
onStart: async (payload, { ctx }) => {
console.log("Task started", ctx.task.id);
},
};
```
### `onSuccess` function
When a task attempt succeeds, the `onSuccess` function is called. It's useful for sending notifications, logging, or other side effects.
When a task run succeeds, the `onSuccess` function is called. It's useful for sending notifications, logging, syncing state to your database, or other side effects.
<Snippet file="coming-soon-slim.mdx" />
```ts /trigger/on-success.ts
export const taskWithOnSuccess = task({
id: "task-with-on-success",
onSuccess: async (payload, output, { ctx }) => {
//...
},
run: async (payload: any, { ctx }) => {
//...
},
});
```
### `onError` function
You can also define an `onSuccess` function in your `trigger.config.ts` file to get notified when any task succeeds.
When a task attempt fails, the `onError` function is called. It's useful for sending notifications, logging, or other side effects.
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
<Snippet file="coming-soon-slim.mdx" />
export const config: TriggerConfig = {
onSuccess: async (payload, output, { ctx }) => {
console.log("Task succeeded", ctx.task.id);
},
};
```
### `onFailure` function
When a task run fails, the `onFailure` function is called. It's useful for sending notifications, logging, or other side effects. It will only be executed once the task run has exhausted all its retries.
```ts /trigger/on-failure.ts
export const taskWithOnFailure = task({
id: "task-with-on-failure",
onFailure: async (payload, error, { ctx }) => {
//...
},
run: async (payload: any, { ctx }) => {
//...
},
});
```
You can also define an `onFailure` function in your `trigger.config.ts` file to get notified when any task fails.
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
export const config: TriggerConfig = {
onFailure: async (payload, error, { ctx }) => {
console.log("Task failed", ctx.task.id);
},
};
```
### `handleError` functions
You can define a function that will be called when an error is thrown in the `run` function, that allows you to control how the error is handled and whether the task should be retried.
Read more about `handleError` in our [Errors and Retrying guide](/v3/errors-retrying).
## Next steps
+121 -1
View File
@@ -31,6 +31,46 @@ export const config: TriggerConfig = {
Most of the time you don't need to change anything in this file, or if you do then we will tell you when you the run the CLI command.
## Global initialization
You can run code before any task is run by adding a `init` function to your `trigger.config.ts` file.
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
export const config: TriggerConfig = {
//..other stuff
init: async (payload, { ctx }) => {
console.log("I run before any task is run");
},
};
```
You'll have access to the run payload and the context object. Currently you cannot return anything from this function.
## Lifecycle functions
You can add lifecycle functions to get notified when any task starts, succeeds, or fails using `onStart`, `onSuccess` and `onFailure`:
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
export const config: TriggerConfig = {
//..other stuff
onSuccess: async (payload, output, { ctx }) => {
console.log("Task succeeded", ctx.task.id);
},
onFailure: async (payload, error, { ctx }) => {
console.log("Task failed", ctx.task.id);
},
onStart: async (payload, { ctx }) => {
console.log("Task started", ctx.task.id);
},
};
```
Read more about task lifecycle functions in the [tasks overview](/v3/tasks-overview).
## Instrumentations
We use OpenTelemetry (OTEL) for our run logs. This means you get a lot of information about your tasks with no effort. But you probably want to add more information to your logs. For example, here's all the Prisma calls automatically logged:
@@ -92,7 +132,6 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
<Step title="package.json postinstall `prisma generate`">
<CodeGroup>
```json default path
@@ -140,6 +179,87 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
</Steps>
## TypeORM support
We support using TypeORM with Trigger. You can use decorators in your entities and then use them in your tasks. Here's an example:
```ts orm/index.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity()
export class Photo {
@PrimaryColumn()
id!: number;
@Column()
name!: string;
@Column()
description!: string;
@Column()
filename!: string;
@Column()
views!: number;
@Column()
isPublished!: boolean;
}
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "postgres",
database: "my-database",
entities: [Photo],
synchronize: true,
logging: false,
});
```
And then in your trigger.config.ts file you can initialize the datasource using the `onStart` lifecycle function option:
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
import { AppDataSource } from "@/trigger/orm";
export const config: TriggerConfig = {
// ... other options here
onStart: async (payload, { ctx }) => {
await AppDataSource.initialize();
},
};
```
Now you are ready to use this in your tasks:
```ts
import { task } from "@trigger.dev/sdk/v3";
import { AppDataSource, Photo } from "./orm";
export const taskThatUsesDecorators = task({
id: "task-that-uses-decorators",
run: async (payload: { message: string }) => {
console.log("Creating a photo...");
const photo = new Photo();
photo.id = 2;
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.views = 1;
photo.isPublished = true;
await AppDataSource.manager.save(photo);
},
});
```
## Troubleshooting
If you have an issue with bundling let us know on [Discord](https://trigger.dev/discord) or [via email](https://trigger.dev/contact).
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/airtable
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"airtable": "^0.12.1",
"zod": "3.22.3"
},
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/github
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"zod": "3.22.3"
},
"engines": {
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/linear
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"zod": "3.22.3"
},
"engines": {
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/slack
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.19"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23"
},
"engines": {
"node": ">=18.0.0"
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/plain
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/replicate
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"replicate": "^0.18.1",
"zod": "3.22.3"
},
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/resend
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"resend": "^2.1.0"
},
"engines": {
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/sendgrid
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.19"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23"
},
"engines": {
"node": ">=16.8.0"
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/shopify
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/shopify",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"zod": "3.22.3"
},
"engines": {
+25
View File
@@ -1,5 +1,30 @@
# @trigger.dev/slack
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"zod": "3.22.3"
},
"engines": {
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/stripe
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"stripe": "^12.14.0",
"zod": "3.22.3"
},
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/supabase
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"supabase-management-js": "^1.0.0",
"zod": "3.22.3"
},
+29
View File
@@ -1,5 +1,34 @@
# @trigger.dev/typeform
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.23
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.22
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
- @trigger.dev/integration-kit@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.20
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.19",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
"@typeform/api-client": "^1.8.0",
"zod": "3.22.3"
},
+25
View File
@@ -1,5 +1,30 @@
# @trigger.dev/astro
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- @trigger.dev/sdk@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@trigger.dev/astro",
"description": "An Astro-native integration for Trigger.dev background jobs platform",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"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.19"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23"
},
"devDependencies": {
"astro": "^3.0.12",
+110
View File
@@ -1,5 +1,115 @@
# trigger.dev
## 3.0.0-beta.23
### Patch Changes
- 43bc7ed94: Hoist uncaughtException handler to the top of workers to better report error messages
- @trigger.dev/core@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- ebeb79052: Add typescript as a dependency so the esbuild-decorator will work even when running in npx
- @trigger.dev/core@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- 9491a1649: Implement task.onSuccess/onFailure and config.onSuccess/onFailure
- 9491a1649: Adds support for `emitDecoratorMetadata: true` and `experimentalDecorators: true` in your tsconfig using the [`@anatine/esbuild-decorators`](https://github.com/anatine/esbuildnx/tree/main/packages/esbuild-decorators) package. This allows you to use libraries like TypeORM:
```ts orm/index.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity()
export class Photo {
@PrimaryColumn()
id!: number;
@Column()
name!: string;
@Column()
description!: string;
@Column()
filename!: string;
@Column()
views!: number;
@Column()
isPublished!: boolean;
}
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "postgres",
database: "v3-catalog",
entities: [Photo],
synchronize: true,
logging: false,
});
```
And then in your trigger.config.ts file you can initialize the datasource using the new `init` option:
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
import { AppDataSource } from "@/trigger/orm";
export const config: TriggerConfig = {
// ... other options here
init: async (payload, { ctx }) => {
await AppDataSource.initialize();
},
};
```
Now you are ready to use this in your tasks:
```ts
import { task } from "@trigger.dev/sdk/v3";
import { AppDataSource, Photo } from "./orm";
export const taskThatUsesDecorators = task({
id: "taskThatUsesDecorators",
run: async (payload: { message: string }) => {
console.log("Creating a photo...");
const photo = new Photo();
photo.id = 2;
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.views = 1;
photo.isPublished = true;
await AppDataSource.manager.save(photo);
},
});
```
- Updated dependencies [9491a1649]
- Updated dependencies [9491a1649]
- @trigger.dev/core@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- de1cc868e: Fix dev CLI output when not printing update messages
- Updated dependencies [e3db25739]
- @trigger.dev/core@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "trigger.dev",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"description": "A Command-Line Interface for Trigger.dev (v3) projects",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -71,6 +71,7 @@
"test": "vitest"
},
"dependencies": {
"@anatine/esbuild-decorators": "^0.2.19",
"@clack/prompts": "^0.7.0",
"@depot/cli": "0.0.1-cli.2.55.0",
"@opentelemetry/api": "^1.8.0",
@@ -85,7 +86,7 @@
"@opentelemetry/sdk-trace-base": "^1.22.0",
"@opentelemetry/sdk-trace-node": "^1.22.0",
"@opentelemetry/semantic-conventions": "^1.22.0",
"@trigger.dev/core": "workspace:3.0.0-beta.19",
"@trigger.dev/core": "workspace:3.0.0-beta.23",
"@types/degit": "^2.8.3",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
@@ -124,7 +125,8 @@
"url": "^0.11.1",
"ws": "^8.12.0",
"zod": "3.22.3",
"zod-validation-error": "^1.5.0"
"zod-validation-error": "^1.5.0",
"typescript": "^5.4.0"
},
"engines": {
"node": ">=18.0.0"
+20 -5
View File
@@ -8,14 +8,13 @@ import {
flattenAttributes,
} from "@trigger.dev/core/v3";
import { recordSpanException } from "@trigger.dev/core/v3/workers";
import chalk from "chalk";
import { Command, Option as CommandOption } from "commander";
import { Metafile, build } from "esbuild";
import { execa } from "execa";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, relative, posix } from "node:path";
import { dirname, join, posix, relative } from "node:path";
import { setTimeout } from "node:timers/promises";
import terminalLink from "terminal-link";
import invariant from "tiny-invariant";
@@ -32,7 +31,7 @@ import {
wrapCommandAction,
} from "../cli/common.js";
import { readConfig } from "../utilities/configFiles.js";
import { createTempDir, readJSONFile, writeJSONFile } from "../utilities/fileSystem";
import { createTempDir, writeJSONFile } from "../utilities/fileSystem";
import { printStandloneInitialBanner } from "../utilities/initialBanner.js";
import {
detectPackageNameFromImportPath,
@@ -43,6 +42,7 @@ import { logger } from "../utilities/logger.js";
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
import { login } from "./login";
import { esbuildDecorators } from "@anatine/esbuild-decorators";
import { Glob, GlobOptions } from "glob";
import type { SetOptional } from "type-fest";
import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../utilities/build";
@@ -53,12 +53,12 @@ import {
parseBuildErrorStack,
parseNpmInstallError,
} from "../utilities/deployErrors";
import { safeJsonParse } from "../utilities/safeJsonParse";
import { JavascriptProject } from "../utilities/javascriptProject";
import { docs, getInTouch } from "../utilities/links";
import { cliRootPath } from "../utilities/resolveInternalFilePath";
import { safeJsonParse } from "../utilities/safeJsonParse";
import { escapeImportPath, spinner } from "../utilities/windows";
import { updateTriggerPackages } from "./update";
import { docs, getInTouch } from "../utilities/links";
const DeployCommandOptions = CommonCommandOptions.extend({
skipTypecheck: z.boolean().default(false),
@@ -202,6 +202,13 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
projectRef: options.projectRef,
});
if (resolvedConfig.status === "error") {
logger.error("Failed to read config:", resolvedConfig.error);
span && recordSpanException(span, resolvedConfig.error);
throw new SkipLoggingError("Failed to read config");
}
logger.debug("Resolved config", { resolvedConfig });
span?.setAttributes({
@@ -1126,6 +1133,9 @@ async function compileProject(
format: "cjs", // This is needed to support opentelemetry instrumentation that uses module patching
target: ["node18", "es2020"],
outdir: "out",
banner: {
js: `process.on("uncaughtException", function(error, origin) { if (error instanceof Error) { process.send && process.send({ type: "EVENT", message: { type: "UNCAUGHT_EXCEPTION", payload: { error: { name: error.name, message: error.message, stack: error.stack }, origin }, version: "v1" } }); } else { process.send && process.send({ type: "EVENT", message: { type: "UNCAUGHT_EXCEPTION", payload: { error: { name: "Error", message: typeof error === "string" ? error : JSON.stringify(error) }, origin }, version: "v1" } }); } });`,
},
define: {
TRIGGER_API_URL: `"${config.triggerUrl}"`,
__PROJECT_CONFIG__: JSON.stringify(config),
@@ -1137,6 +1147,11 @@ async function compileProject(
config.tsconfigPath
),
workerSetupImportConfigPlugin(configPath),
esbuildDecorators({
tsconfig: config.tsconfigPath,
tsx: true,
force: false,
}),
],
});
+23 -8
View File
@@ -53,6 +53,7 @@ import { findUp, pathExists } from "find-up";
import { cliRootPath } from "../utilities/resolveInternalFilePath";
import { escapeImportPath } from "../utilities/windows";
import { updateTriggerPackages } from "./update";
import { esbuildDecorators } from "@anatine/esbuild-decorators";
let apiClient: CliApiClient | undefined;
@@ -135,12 +136,13 @@ async function startDev(
await printStandloneInitialBanner(true);
let displayedUpdateMessage = false;
if (!options.skipUpdateCheck) {
console.log(); // spacing
await updateTriggerPackages(dir, { ...options }, true, true);
displayedUpdateMessage = await updateTriggerPackages(dir, { ...options }, true, true);
}
printDevBanner(!options.skipUpdateCheck);
printDevBanner(displayedUpdateMessage);
logger.debug("Starting dev session", { dir, options, authorization });
@@ -151,6 +153,11 @@ async function startDev(
logger.debug("Initial config", { config });
if (config.status === "error") {
logger.error("Failed to read config", config.error);
process.exit(1);
}
async function getDevReactElement(
configParam: ResolvedConfig,
authorization: { apiUrl: string; accessToken: string },
@@ -162,18 +169,18 @@ async function startDev(
apiClient = new CliApiClient(apiUrl, accessToken);
const devEnv = await apiClient.getProjectEnv({
projectRef: config.config.project,
projectRef: configParam.project,
env: "dev",
});
if (!devEnv.success) {
if (devEnv.error === "Project not found") {
logger.error(
`Project not found: ${config.config.project}. Ensure you are using the correct project ref and CLI profile (use --profile). Currently using the "${options.profile}" profile, which points to ${authorization.apiUrl}`
`Project not found: ${configParam.project}. Ensure you are using the correct project ref and CLI profile (use --profile). Currently using the "${options.profile}" profile, which points to ${authorization.apiUrl}`
);
} else {
logger.error(
`Failed to initialize dev environment: ${devEnv.error}. Using project ref ${config.config.project}`
`Failed to initialize dev environment: ${devEnv.error}. Using project ref ${configParam.project}`
);
}
@@ -386,6 +393,9 @@ function useDev({
resolveDir: process.cwd(),
sourcefile: "__entryPoint.ts",
},
banner: {
js: `process.on("uncaughtException", function(error, origin) { if (error instanceof Error) { process.send && process.send({ type: "UNCAUGHT_EXCEPTION", payload: { error: { name: error.name, message: error.message, stack: error.stack }, origin }, version: "v1" }); } else { process.send && process.send({ type: "UNCAUGHT_EXCEPTION", payload: { error: { name: "Error", message: typeof error === "string" ? error : JSON.stringify(error) }, origin }, version: "v1" }); } });`,
},
bundle: true,
metafile: true,
write: false,
@@ -408,6 +418,11 @@ function useDev({
config.tsconfigPath
),
workerSetupImportConfigPlugin(configPath),
esbuildDecorators({
tsconfig: config.tsconfigPath,
tsx: true,
force: false,
}),
{
name: "trigger.dev v3",
setup(build) {
@@ -595,10 +610,10 @@ function useDev({
} else {
}
if (e.originalError.stack) {
if (e.originalError.message || e.originalError.stack) {
logger.log(
`${chalkError("X Error:")} Worker failed to start`,
e.originalError.stack
e.originalError.stack ?? e.originalError.message
);
}
+11 -4
View File
@@ -50,7 +50,9 @@ export async function updateTriggerPackages(
options: UpdateCommandOptions,
embedded?: boolean,
requireUpdate?: boolean
) {
): Promise<boolean> {
let hasOutput = false;
if (!embedded) {
intro("Updating packages");
}
@@ -61,7 +63,7 @@ export async function updateTriggerPackages(
if (!packageJson) {
log.error("Failed to load package.json. Try to re-run with `-l debug` to see what's going on.");
return;
return false;
}
const cliVersion = getVersion();
@@ -73,6 +75,8 @@ export async function updateTriggerPackages(
`Current: ${cliVersion}\nLatest: ${newCliVersion}`,
"Run latest: npx trigger.dev@beta"
);
hasOutput = true;
}
const triggerDependencies = getTriggerDependencies(packageJson);
@@ -96,8 +100,9 @@ export async function updateTriggerPackages(
if (versionMismatches.length === 0) {
if (!embedded) {
outro(`Nothing to do${newCliVersion ? " ..but you should really update your CLI!" : ""}`);
return hasOutput;
}
return;
return hasOutput;
}
prettyWarning(
@@ -149,7 +154,7 @@ export async function updateTriggerPackages(
outro("You've been warned!");
}
return;
return hasOutput;
}
const installSpinner = spinner();
@@ -213,6 +218,8 @@ export async function updateTriggerPackages(
`Packages updated${newCliVersion ? " ..but you should really update your CLI too!" : ""}`
);
}
return hasOutput;
}
type Dependency = {
+32 -13
View File
@@ -10,6 +10,7 @@ import { createTempDir, readJSONFileSync } from "./fileSystem.js";
import { logger } from "./logger.js";
import { findTriggerDirectories, resolveTriggerDirectories } from "./taskFiles.js";
import { build } from "esbuild";
import { esbuildDecorators } from "@anatine/esbuild-decorators";
function getGlobalConfigFolderPath() {
const configDir = xdgAppPaths("trigger").config();
@@ -125,6 +126,10 @@ export type ReadConfigResult =
| {
status: "in-memory";
config: ResolvedConfig;
}
| {
status: "error";
error: unknown;
};
export async function readConfig(
@@ -172,24 +177,38 @@ export async function readConfig(
target: ["es2018", "node18"],
outfile: builtConfigFilePath,
logLevel: "silent",
plugins: [
esbuildDecorators({
cwd: absoluteDir,
tsx: false,
force: false,
}),
],
});
// import the config file
const userConfigModule = await import(builtConfigFileHref);
try {
// import the config file
const userConfigModule = await import(builtConfigFileHref);
// The --project-ref CLI arg will always override the project specified in the config file
const rawConfig = await normalizeConfig(
userConfigModule?.config,
options?.projectRef ? { project: options?.projectRef } : undefined
);
// The --project-ref CLI arg will always override the project specified in the config file
const rawConfig = await normalizeConfig(
userConfigModule?.config,
options?.projectRef ? { project: options?.projectRef } : undefined
);
const config = Config.parse(rawConfig);
const config = Config.parse(rawConfig);
return {
status: "file",
config: await resolveConfig(absoluteDir, config),
path: configPath,
};
return {
status: "file",
config: await resolveConfig(absoluteDir, config),
path: configPath,
};
} catch (error) {
return {
status: "error",
error,
};
}
}
export async function resolveConfig(path: string, config: Config): Promise<ResolvedConfig> {
@@ -40,15 +40,17 @@ After installation, run Trigger.dev with \`npx trigger.dev\`.`
export async function printStandloneInitialBanner(performUpdateCheck = true) {
const cliVersion = getVersion();
logger.log(`\n${logo()} ${chalkGrey(`(${cliVersion})`)}\n`);
if (performUpdateCheck) {
const maybeNewVersion = await updateCheck();
// Log a slightly more noticeable message if this is a major bump
if (maybeNewVersion !== undefined) {
logger.log(`Update available ${chalk.green(maybeNewVersion)}`);
logger.log(`\n${logo()} ${chalkGrey(`(${cliVersion} -> ${chalk.green(maybeNewVersion)})`)}`);
} else {
logger.log(`\n${logo()} ${chalkGrey(`(${cliVersion})`)}`);
}
} else {
logger.log(`\n${logo()} ${chalkGrey(`(${cliVersion})`)}`);
}
logger.log(`${chalkGrey("-".repeat(54))}`);
@@ -33,19 +33,4 @@ export const sender = new ZodMessageSender({
},
});
process.on("uncaughtException", (error, origin) => {
sender
.send("UNCAUGHT_EXCEPTION", {
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
origin,
})
.catch((err) => {
console.error("Failed to send UNCAUGHT_EXCEPTION message", err);
});
});
taskCatalog.setGlobalTaskCatalog(new StandardTaskCatalog());
@@ -19,22 +19,4 @@ export const tracingSDK = new TracingSDK({
diagLogLevel: (process.env.OTEL_LOG_LEVEL as TracingDiagnosticLogLevel) ?? "none",
});
process.on("uncaughtException", (error, origin) => {
process.send?.({
type: "EVENT",
message: {
type: "UNCAUGHT_EXCEPTION",
payload: {
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
origin,
},
version: "v1",
},
});
});
taskCatalog.setGlobalTaskCatalog(new StandardTaskCatalog());
+31
View File
@@ -1,5 +1,36 @@
# create-trigger
## 3.0.0-beta.23
### Patch Changes
- @trigger.dev/core@3.0.0-beta.23
- @trigger.dev/yalt@3.0.0-beta.23
## 3.0.0-beta.22
### Patch Changes
- @trigger.dev/core@3.0.0-beta.22
- @trigger.dev/yalt@3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- Updated dependencies [9491a1649]
- Updated dependencies [9491a1649]
- @trigger.dev/core@3.0.0-beta.21
- @trigger.dev/yalt@3.0.0-beta.21
## 3.0.0-beta.20
### Patch Changes
- Updated dependencies [e3db25739]
- @trigger.dev/core@3.0.0-beta.20
- @trigger.dev/yalt@3.0.0-beta.20
## 3.0.0-beta.19
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/cli",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"description": "The Trigger.dev CLI",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/core-apps
## 3.0.0-beta.23
## 3.0.0-beta.22
## 3.0.0-beta.21
## 3.0.0-beta.20
## 3.0.0-beta.19
## 3.0.0-beta.18
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@trigger.dev/core-apps",
"description": "Backend core code used across apps",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"private": true,
"license": "MIT",
"main": "./dist/index.js",
+8
View File
@@ -1,5 +1,13 @@
# @trigger.dev/core-backend
## 3.0.0-beta.23
## 3.0.0-beta.22
## 3.0.0-beta.21
## 3.0.0-beta.20
## 3.0.0-beta.19
## 3.0.0-beta.18
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/core-backend",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"description": "Core code used across `@trigger.dev/sdk` and Trigger.dev server",
"license": "MIT",
"main": "./dist/index.js",
+94
View File
@@ -1,5 +1,99 @@
# internal-platform
## 3.0.0-beta.23
## 3.0.0-beta.22
## 3.0.0-beta.21
### Patch Changes
- 9491a1649: Implement task.onSuccess/onFailure and config.onSuccess/onFailure
- 9491a1649: Adds support for `emitDecoratorMetadata: true` and `experimentalDecorators: true` in your tsconfig using the [`@anatine/esbuild-decorators`](https://github.com/anatine/esbuildnx/tree/main/packages/esbuild-decorators) package. This allows you to use libraries like TypeORM:
```ts orm/index.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import { Entity, Column, PrimaryColumn } from "typeorm";
@Entity()
export class Photo {
@PrimaryColumn()
id!: number;
@Column()
name!: string;
@Column()
description!: string;
@Column()
filename!: string;
@Column()
views!: number;
@Column()
isPublished!: boolean;
}
export const AppDataSource = new DataSource({
type: "postgres",
host: "localhost",
port: 5432,
username: "postgres",
password: "postgres",
database: "v3-catalog",
entities: [Photo],
synchronize: true,
logging: false,
});
```
And then in your trigger.config.ts file you can initialize the datasource using the new `init` option:
```ts trigger.config.ts
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
import { AppDataSource } from "@/trigger/orm";
export const config: TriggerConfig = {
// ... other options here
init: async (payload, { ctx }) => {
await AppDataSource.initialize();
},
};
```
Now you are ready to use this in your tasks:
```ts
import { task } from "@trigger.dev/sdk/v3";
import { AppDataSource, Photo } from "./orm";
export const taskThatUsesDecorators = task({
id: "taskThatUsesDecorators",
run: async (payload: { message: string }) => {
console.log("Creating a photo...");
const photo = new Photo();
photo.id = 2;
photo.name = "Me and Bears";
photo.description = "I am near polar bears";
photo.filename = "photo-with-bears.jpg";
photo.views = 1;
photo.isPublished = true;
await AppDataSource.manager.save(photo);
},
});
```
## 3.0.0-beta.20
### Patch Changes
- e3db25739: Fix error stack traces
## 3.0.0-beta.19
## 3.0.0-beta.18
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/core",
"version": "3.0.0-beta.19",
"version": "3.0.0-beta.23",
"description": "Core code used across the Trigger.dev SDK and platform",
"license": "MIT",
"main": "./dist/index.js",
+13 -18
View File
@@ -69,32 +69,27 @@ export function correctErrorStackTrace(
.join("\n");
}
const LINES_TO_IGNORE = [
/ConsoleInterceptor/,
/TriggerTracer/,
/TaskExecutor/,
/EXECUTE_TASK_RUN/,
/@trigger.dev\/core/,
/safeJsonProcess/,
/__entryPoint.ts/,
];
function correctStackTraceLine(line: string, projectDir?: string) {
const regex = /at (.*?) \(?file:\/\/(\/.*?\.ts):(\d+):(\d+)\)?/;
const match = regex.exec(line);
if (!match) {
return;
}
const [_, identifier, path, lineNum, colNum] = match;
if (!path) {
return;
}
// Check to see if the file name is __entryPoint.ts, if it is we can remove it
if (nodePath.basename(path) === "__entryPoint.ts") {
if (LINES_TO_IGNORE.some((regex) => regex.test(line))) {
return;
}
// Check to see if the path is inside the project directory
if (projectDir && !path.includes(projectDir)) {
if (projectDir && !line.includes(projectDir)) {
return;
}
return line;
return line.trim();
}
export function groupTaskMetadataIssuesByTask(tasks: any, issues: z.ZodIssue[]) {
+22 -1
View File
@@ -1,3 +1,4 @@
import { FailureFnParams, InitFnParams, StartFnParams, SuccessFnParams } from ".";
import { LogLevel } from "../logger/taskLogger";
import { RetryOptions } from "../schemas";
import type { InstrumentationOption } from "@opentelemetry/instrumentation";
@@ -16,7 +17,7 @@ export interface ProjectConfig {
* List of additional files to include in your trigger.dev bundle. e.g. ["./prisma/schema.prisma"]
*
* Supports glob patterns.
*
*
* Note: The path separator for glob patterns is `/`, even on Windows!
*/
additionalFiles?: string[];
@@ -48,4 +49,24 @@ export interface ProjectConfig {
* Enable console logging while running the dev CLI. This will print out logs from console.log, console.warn, and console.error. By default all logs are sent to the trigger.dev backend, and not logged to the console.
*/
enableConsoleLogging?: boolean;
/**
* Run before a task is executed, for all tasks. This is useful for setting up any global state that is needed for all tasks.
*/
init?: (payload: unknown, params: InitFnParams) => void | Promise<void>;
/**
* onSuccess is called after the run function has successfully completed.
*/
onSuccess?: (payload: unknown, output: unknown, params: SuccessFnParams<any>) => Promise<void>;
/**
* onFailure is called after a task run has failed (meaning the run function threw an error and won't be retried anymore)
*/
onFailure?: (payload: unknown, error: unknown, params: FailureFnParams<any>) => Promise<void>;
/**
* onStart is called the first time a task is executed in a run (not before every retry)
*/
onStart?: (payload: unknown, params: StartFnParams) => Promise<void>;
}
+10 -4
View File
@@ -22,12 +22,15 @@ export type InitFnParams = Prettify<{
ctx: Context;
}>;
export type StartFnParams = Prettify<{
ctx: Context;
}>;
export type Context = TaskRunContext;
export type SuccessFnParams<TOutput, TInitOutput extends InitOutput> = RunFnParams<TInitOutput> &
Prettify<{
output: TOutput;
}>;
export type SuccessFnParams<TInitOutput extends InitOutput> = RunFnParams<TInitOutput>;
export type FailureFnParams<TInitOutput extends InitOutput> = RunFnParams<TInitOutput>;
export type HandleErrorFnParams<TInitOutput extends InitOutput> = RunFnParams<TInitOutput> &
Prettify<{
@@ -74,5 +77,8 @@ export type TaskMetadataWithFunctions = TaskMetadata & {
error: unknown,
params: HandleErrorFnParams<any>
) => HandleErrorResult;
onSuccess?: (payload: any, output: any, params: SuccessFnParams<any>) => Promise<void>;
onFailure?: (payload: any, error: unknown, params: FailureFnParams<any>) => Promise<void>;
onStart?: (payload: any, params: StartFnParams) => Promise<void>;
};
};
+198 -5
View File
@@ -90,10 +90,16 @@ export class TaskExecutor {
parsedPayload = await parsePacket(payloadPacket);
initOutput = await this.#callTaskInit(parsedPayload, ctx);
if (execution.attempt.number === 1) {
await this.#callOnStartFunctions(parsedPayload, ctx);
}
initOutput = await this.#callInitFunctions(parsedPayload, ctx);
const output = await this.#callRun(parsedPayload, ctx, initOutput);
await this.#callOnSuccessFunctions(parsedPayload, output, ctx, initOutput);
try {
const stringifiedOutput = await stringifyIO(output);
@@ -148,6 +154,15 @@ export class TaskExecutor {
recordSpanException(span, handleErrorResult.error ?? runError);
if (handleErrorResult.status !== "retry") {
await this.#callOnFailureFunctions(
parsedPayload,
handleErrorResult.error ?? runError,
ctx,
initOutput
);
}
return {
id: execution.run.id,
ok: false,
@@ -218,16 +233,194 @@ export class TaskExecutor {
return middlewareFn(payload, { ctx, next: async () => runFn(payload, { ctx, init }) });
}
async #callTaskInit(payload: unknown, ctx: TaskRunContext) {
async #callInitFunctions(payload: unknown, ctx: TaskRunContext) {
await this.#callConfigInit(payload, ctx);
const initFn = this.task.fns.init;
if (!initFn) {
return {};
}
return this._tracer.startActiveSpan("init", async (span) => {
return await initFn(payload, { ctx });
});
return this._tracer.startActiveSpan(
"init",
async (span) => {
return await initFn(payload, { ctx });
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "function",
},
}
);
}
async #callConfigInit(payload: unknown, ctx: TaskRunContext) {
const initFn = this._importedConfig?.init;
if (!initFn) {
return {};
}
return this._tracer.startActiveSpan(
"config.init",
async (span) => {
return await initFn(payload, { ctx });
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "function",
},
}
);
}
async #callOnSuccessFunctions(
payload: unknown,
output: any,
ctx: TaskRunContext,
initOutput: any
) {
await this.#callOnSuccessFunction(
this.task.fns.onSuccess,
"task.onSuccess",
payload,
output,
ctx,
initOutput
);
await this.#callOnSuccessFunction(
this._importedConfig?.onSuccess,
"config.onSuccess",
payload,
output,
ctx,
initOutput
);
}
async #callOnSuccessFunction(
onSuccessFn: TaskMetadataWithFunctions["fns"]["onSuccess"],
name: string,
payload: unknown,
output: any,
ctx: TaskRunContext,
initOutput: any
) {
if (!onSuccessFn) {
return;
}
try {
await this._tracer.startActiveSpan(
name,
async (span) => {
return await onSuccessFn(payload, output, { ctx, init: initOutput });
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "function",
},
}
);
} catch {
// Ignore errors from onSuccess functions
}
}
async #callOnFailureFunctions(
payload: unknown,
error: unknown,
ctx: TaskRunContext,
initOutput: any
) {
await this.#callOnFailureFunction(
this.task.fns.onFailure,
"task.onFailure",
payload,
error,
ctx,
initOutput
);
await this.#callOnFailureFunction(
this._importedConfig?.onFailure,
"config.onFailure",
payload,
error,
ctx,
initOutput
);
}
async #callOnFailureFunction(
onFailureFn: TaskMetadataWithFunctions["fns"]["onFailure"],
name: string,
payload: unknown,
error: unknown,
ctx: TaskRunContext,
initOutput: any
) {
if (!onFailureFn) {
return;
}
try {
return await this._tracer.startActiveSpan(
name,
async (span) => {
return await onFailureFn(payload, error, { ctx, init: initOutput });
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "function",
},
}
);
} catch (e) {
// Ignore errors from onFailure functions
}
}
async #callOnStartFunctions(payload: unknown, ctx: TaskRunContext) {
await this.#callOnStartFunction(
this._importedConfig?.onStart,
"config.onStart",
payload,
ctx,
{}
);
await this.#callOnStartFunction(this.task.fns.onStart, "task.onStart", payload, ctx, {});
}
async #callOnStartFunction(
onStartFn: TaskMetadataWithFunctions["fns"]["onStart"],
name: string,
payload: unknown,
ctx: TaskRunContext,
initOutput: any
) {
if (!onStartFn) {
return;
}
try {
await this._tracer.startActiveSpan(
name,
async (span) => {
return await onStartFn(payload, { ctx });
},
{
attributes: {
[SemanticInternalAttributes.STYLE_ICON]: "function",
},
}
);
} catch {
// Ignore errors from onStart functions
}
}
async #callTaskCleanup(payload: unknown, ctx: TaskRunContext, init: unknown) {

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