Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e85fc501a6 | |||
| 0bfeb0816f | |||
| b207601732 | |||
| 3913e57ef4 | |||
| 26f310397a | |||
| 0a845767a0 | |||
| ed2a26c865 | |||
| 4a68e71583 | |||
| b657eb6555 | |||
| 62c9a5b712 | |||
| f339b41ef3 | |||
| ae40ce3995 | |||
| 374edef020 | |||
| b82db67b81 | |||
| 26093896d2 |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
- Fix additionalFiles that aren't decendants
|
||||
- Stop swallowing uncaught exceptions in prod
|
||||
- Improve warnings and errors, fail early on critical warnings
|
||||
- New arg to --save-logs even for successful builds
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
v3 CLI update command and package manager detection fix
|
||||
@@ -44,6 +44,8 @@
|
||||
"@trigger.dev/yalt": "2.3.18"
|
||||
},
|
||||
"changesets": [
|
||||
"angry-eagles-trade",
|
||||
"beige-pens-dance",
|
||||
"breezy-gorillas-mate",
|
||||
"chilled-hornets-move",
|
||||
"clean-pianos-listen",
|
||||
@@ -71,7 +73,10 @@
|
||||
"rare-roses-float",
|
||||
"real-planets-stare",
|
||||
"rotten-dryers-exercise",
|
||||
"shaggy-spoons-taste",
|
||||
"sharp-emus-compare",
|
||||
"sharp-zebras-serve",
|
||||
"shiny-coats-cry",
|
||||
"silly-suits-switch",
|
||||
"smart-needles-move",
|
||||
"smart-olives-eat",
|
||||
@@ -85,6 +90,7 @@
|
||||
"tame-guests-know",
|
||||
"tender-oranges-rhyme",
|
||||
"tidy-balloons-suffer",
|
||||
"tidy-dryers-sleep",
|
||||
"tiny-doors-type",
|
||||
"tiny-elephants-scream",
|
||||
"tricky-bulldogs-heal"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Updates the `trigger`, `batchTrigger` and their `*AndWait` variants to use the first parameter for the payload/items, and the second parameter for options.
|
||||
|
||||
Before:
|
||||
|
||||
```ts
|
||||
await yourTask.trigger({ payload: { foo: "bar" }, options: { idempotencyKey: "key_1234" } });
|
||||
await yourTask.triggerAndWait({ payload: { foo: "bar" }, options: { idempotencyKey: "key_1234" } });
|
||||
|
||||
await yourTask.batchTrigger({ items: [{ payload: { foo: "bar" } }, { payload: { foo: "baz" } }] });
|
||||
await yourTask.batchTriggerAndWait({ items: [{ payload: { foo: "bar" } }, { payload: { foo: "baz" } }] });
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```ts
|
||||
await yourTask.trigger({ foo: "bar" }, { idempotencyKey: "key_1234" });
|
||||
await yourTask.triggerAndWait({ foo: "bar" }, { idempotencyKey: "key_1234" });
|
||||
|
||||
await yourTask.batchTrigger([{ payload: { foo: "bar" } }, { payload: { foo: "baz" } }]);
|
||||
await yourTask.batchTriggerAndWait([{ payload: { foo: "bar" } }, { payload: { foo: "baz" } }]);
|
||||
```
|
||||
|
||||
We've also changed the API of the `triggerAndWait` result. Before, if the subtask that was triggered finished with an error, we would automatically "rethrow" the error in the parent task.
|
||||
|
||||
Now instead we're returning a `TaskRunResult` object that allows you to discriminate between successful and failed runs in the subtask:
|
||||
|
||||
Before:
|
||||
|
||||
```ts
|
||||
try {
|
||||
const result = await yourTask.triggerAndWait({ foo: "bar" });
|
||||
|
||||
// result is the output of your task
|
||||
console.log("result", result);
|
||||
|
||||
} catch (error) {
|
||||
// handle subtask errors here
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```ts
|
||||
const result = await yourTask.triggerAndWait({ foo: "bar" });
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`Run ${result.id} succeeded with output`, result.output);
|
||||
} else {
|
||||
console.log(`Run ${result.id} failed with error`, result.error);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
When using idempotency keys, triggerAndWait and batchTriggerAndWait will still work even if the existing runs have already been completed (or even partially completed, in the case of batchTriggerAndWait)
|
||||
|
||||
- TaskRunExecutionResult.id is now the run friendlyId, not the attempt friendlyId
|
||||
- A single TaskRun can now have many batchItems, in the case of batchTriggerAndWait while using idempotency keys
|
||||
- A run’s idempotencyKey is now added to the ctx as well as the TaskEvent and displayed in the span view
|
||||
- When resolving batchTriggerAndWait, the runtimes no longer reject promises, leading to an error in the parent task
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Add additional logging around cleaning up dev workers, and always kill them after 5 seconds if they haven't already exited
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Fixes an issue that caused failed tasks when resuming after calling `triggerAndWait` or `batchTriggerAndWait` in prod/staging (this doesn't effect dev).
|
||||
|
||||
The version of Node.js we use for deployed workers (latest 20) would crash with an out-of-memory error when the checkpoint was restored. This crash does not happen on Node 18x or Node21x, so we've decided to upgrade the worker version to Node.js21x, to mitigate this issue.
|
||||
|
||||
You'll need to re-deploy to production to fix the issue.
|
||||
@@ -2,8 +2,14 @@ import { Fragment } from "react";
|
||||
import { Modifier, ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useOperatingSystem } from "./OperatingSystemProvider";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
|
||||
const variants = {
|
||||
export const variants = {
|
||||
small:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase",
|
||||
medium:
|
||||
@@ -23,7 +29,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
const isMac = platform === "mac";
|
||||
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const modifiers = relevantShortcut.modifiers ?? [];
|
||||
const character = keyString(relevantShortcut.key, isMac);
|
||||
const character = keyString(relevantShortcut.key, isMac, variant);
|
||||
|
||||
return (
|
||||
<span className={cn(variants[variant], className)}>
|
||||
@@ -35,10 +41,22 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
);
|
||||
}
|
||||
|
||||
function keyString(key: String, isMac: boolean) {
|
||||
function keyString(key: String, isMac: boolean, size: "small" | "medium") {
|
||||
key = key.toLowerCase();
|
||||
|
||||
const className = size === "small" ? "w-2.5 h-4" : "w-3 h-5";
|
||||
|
||||
switch (key) {
|
||||
case "enter":
|
||||
return isMac ? "↵" : key;
|
||||
case "arrowdown":
|
||||
return <ChevronDownIcon className={className} />;
|
||||
case "arrowup":
|
||||
return <ChevronUpIcon className={className} />;
|
||||
case "arrowleft":
|
||||
return <ChevronLeftIcon className={className} />;
|
||||
case "arrowright":
|
||||
return <ChevronRightIcon className={className} />;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { VirtualItem, Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { motion } from "framer-motion";
|
||||
import { MutableRefObject, RefObject, useCallback, useEffect, useReducer, useRef } from "react";
|
||||
import { UnmountClosed } from "react-collapse";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { NodeState, NodesState, reducer } from "./reducer";
|
||||
import { applyFilterToState, concreteStateFromInput, selectedIdFromState } from "./utils";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export type TreeViewProps<TData> = {
|
||||
tree: FlatTree<TData>;
|
||||
@@ -165,6 +165,11 @@ export type UseTreeStateOutput = {
|
||||
expandNode: (id: string, scrollToNode?: boolean) => void;
|
||||
collapseNode: (id: string) => void;
|
||||
toggleExpandNode: (id: string, scrollToNode?: boolean) => void;
|
||||
expandAllBelowDepth: (depth: number) => void;
|
||||
collapseAllBelowDepth: (depth: number) => void;
|
||||
expandLevel: (level: number) => void;
|
||||
collapseLevel: (level: number) => void;
|
||||
toggleExpandLevel: (level: number) => void;
|
||||
selectFirstVisibleNode: (scrollToNode?: boolean) => void;
|
||||
selectLastVisibleNode: (scrollToNode?: boolean) => void;
|
||||
selectNextVisibleNode: (scrollToNode?: boolean) => void;
|
||||
@@ -333,6 +338,41 @@ export function useTree<TData>({
|
||||
[state]
|
||||
);
|
||||
|
||||
const expandAllBelowDepth = useCallback(
|
||||
(depth: number) => {
|
||||
dispatch({ type: "EXPAND_ALL_BELOW_DEPTH", payload: { tree, depth } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const collapseAllBelowDepth = useCallback(
|
||||
(depth: number) => {
|
||||
dispatch({ type: "COLLAPSE_ALL_BELOW_DEPTH", payload: { tree, depth } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const expandLevel = useCallback(
|
||||
(level: number) => {
|
||||
dispatch({ type: "EXPAND_LEVEL", payload: { tree, level } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const collapseLevel = useCallback(
|
||||
(level: number) => {
|
||||
dispatch({ type: "COLLAPSE_LEVEL", payload: { tree, level } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const toggleExpandLevel = useCallback(
|
||||
(level: number) => {
|
||||
dispatch({ type: "TOGGLE_EXPAND_LEVEL", payload: { tree, level } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const getTreeProps = useCallback(() => {
|
||||
return {
|
||||
role: "tree",
|
||||
@@ -368,25 +408,48 @@ export function useTree<TData>({
|
||||
}
|
||||
case "Left":
|
||||
case "ArrowLeft": {
|
||||
e.preventDefault();
|
||||
|
||||
const selected = selectedIdFromState(state.nodes);
|
||||
if (selected) {
|
||||
const treeNode = tree.find((node) => node.id === selected);
|
||||
if (treeNode && treeNode.hasChildren && state.nodes[selected].expanded) {
|
||||
|
||||
if (e.altKey) {
|
||||
if (treeNode && treeNode.hasChildren) {
|
||||
collapseLevel(treeNode.level);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const shouldCollapse =
|
||||
treeNode && treeNode.hasChildren && state.nodes[selected].expanded;
|
||||
if (shouldCollapse) {
|
||||
collapseNode(selected);
|
||||
} else {
|
||||
selectParentNode(true);
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case "Right":
|
||||
case "ArrowRight": {
|
||||
e.preventDefault();
|
||||
|
||||
const selected = selectedIdFromState(state.nodes);
|
||||
|
||||
if (selected) {
|
||||
const treeNode = tree.find((node) => node.id === selected);
|
||||
|
||||
if (e.altKey) {
|
||||
if (treeNode && treeNode.hasChildren) {
|
||||
expandLevel(treeNode.level);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
expandNode(selected, true);
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
@@ -427,6 +490,11 @@ export function useTree<TData>({
|
||||
expandNode,
|
||||
collapseNode,
|
||||
toggleExpandNode,
|
||||
expandAllBelowDepth,
|
||||
collapseAllBelowDepth,
|
||||
expandLevel,
|
||||
collapseLevel,
|
||||
toggleExpandLevel,
|
||||
selectFirstVisibleNode,
|
||||
selectLastVisibleNode,
|
||||
selectNextVisibleNode,
|
||||
|
||||
@@ -91,6 +91,46 @@ type ToggleExpandNodeAction = {
|
||||
} & WithScrollToNode;
|
||||
};
|
||||
|
||||
type ExpandAllBelowDepthAction = {
|
||||
type: "EXPAND_ALL_BELOW_DEPTH";
|
||||
payload: {
|
||||
depth: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type CollapseAllBelowDepthAction = {
|
||||
type: "COLLAPSE_ALL_BELOW_DEPTH";
|
||||
payload: {
|
||||
depth: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpandLevelAction = {
|
||||
type: "EXPAND_LEVEL";
|
||||
payload: {
|
||||
level: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type CollapseLevelAction = {
|
||||
type: "COLLAPSE_LEVEL";
|
||||
payload: {
|
||||
level: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type ToggleExpandLevelAction = {
|
||||
type: "TOGGLE_EXPAND_LEVEL";
|
||||
payload: {
|
||||
level: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type SelectFirstVisibleNodeAction = {
|
||||
type: "SELECT_FIRST_VISIBLE_NODE";
|
||||
payload: {
|
||||
@@ -135,6 +175,11 @@ export type Action =
|
||||
| ExpandNodeAction
|
||||
| CollapseNodeAction
|
||||
| ToggleExpandNodeAction
|
||||
| ExpandAllBelowDepthAction
|
||||
| CollapseAllBelowDepthAction
|
||||
| ExpandLevelAction
|
||||
| CollapseLevelAction
|
||||
| ToggleExpandLevelAction
|
||||
| SelectFirstVisibleNodeAction
|
||||
| SelectLastVisibleNodeAction
|
||||
| SelectNextVisibleNodeAction
|
||||
@@ -229,6 +274,109 @@ export function reducer(state: TreeState, action: Action): TreeState {
|
||||
});
|
||||
}
|
||||
}
|
||||
case "EXPAND_ALL_BELOW_DEPTH": {
|
||||
const nodesToExpand = action.payload.tree.filter(
|
||||
(n) => n.level >= action.payload.depth && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "COLLAPSE_ALL_BELOW_DEPTH": {
|
||||
const nodesToCollapse = action.payload.tree.filter(
|
||||
(n) => n.level >= action.payload.depth && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "EXPAND_LEVEL": {
|
||||
const nodesToExpand = action.payload.tree.filter(
|
||||
(n) => n.level <= action.payload.level && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "COLLAPSE_LEVEL": {
|
||||
const nodesToCollapse = action.payload.tree.filter(
|
||||
(n) => n.level === action.payload.level && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "TOGGLE_EXPAND_LEVEL": {
|
||||
//first get the first item at that level in the tree. If it is expanded, collapse all nodes at that level
|
||||
//if it is collapsed, expand all nodes at that level
|
||||
const nodesAtLevel = action.payload.tree.filter(
|
||||
(n) => n.level === action.payload.level && n.hasChildren
|
||||
);
|
||||
const firstNode = nodesAtLevel[0];
|
||||
if (!firstNode) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const currentlyExpanded = state.nodes[firstNode.id]?.expanded ?? true;
|
||||
const currentVisible = state.nodes[firstNode.id]?.visible ?? true;
|
||||
if (currentlyExpanded && currentVisible) {
|
||||
return reducer(state, {
|
||||
type: "COLLAPSE_LEVEL",
|
||||
payload: {
|
||||
level: action.payload.level,
|
||||
tree: action.payload.tree,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return reducer(state, {
|
||||
type: "EXPAND_LEVEL",
|
||||
payload: {
|
||||
level: action.payload.level,
|
||||
tree: action.payload.tree,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
case "SELECT_FIRST_VISIBLE_NODE": {
|
||||
const node = firstVisibleNode(action.payload.tree, state.nodes);
|
||||
if (node) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { TimeFrameFilter } from "./TimeFrameFilter";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { useCallback } from "react";
|
||||
import assertNever from "assert-never";
|
||||
|
||||
export function RunsFilters() {
|
||||
const navigate = useNavigate();
|
||||
@@ -182,8 +183,7 @@ export function FilterStatusIcon({
|
||||
case "FAILED":
|
||||
return <XCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,8 +205,7 @@ export function filterStatusTitle(status: FilterableStatus): string {
|
||||
case "TIMEDOUT":
|
||||
return "Timed out";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,8 +227,7 @@ export function filterStatusClassNameColor(status: FilterableStatus): string {
|
||||
case "TIMEDOUT":
|
||||
return "text-amber-300";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { JobRunStatus } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import { z } from "zod";
|
||||
import assertNever from "assert-never";
|
||||
|
||||
export function RunStatus({ status }: { status: JobRunStatus }) {
|
||||
return (
|
||||
@@ -51,8 +52,7 @@ export function RunStatusIcon({ status, className }: { status: JobRunStatus; cla
|
||||
case "CANCELED":
|
||||
return <NoSymbolIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,8 +89,7 @@ export function runStatusTitle(status: JobRunStatus): string {
|
||||
case "INVALID_PAYLOAD":
|
||||
return "Invalid payload";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,8 +122,7 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
|
||||
case "CANCELED":
|
||||
return "text-charcoal-500";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { WorkerDeploymentStatus } from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
@@ -54,8 +55,7 @@ export function DeploymentStatusIcon({
|
||||
/>
|
||||
);
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,8 +74,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus):
|
||||
case "FAILED":
|
||||
return "text-error";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,8 +96,7 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
|
||||
case "FAILED":
|
||||
return "Failed";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type { TaskRunAttemptStatus as TaskRunAttemptStatusType } from "@trigger.dev/database";
|
||||
import { TaskRunAttemptStatus } from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import { SnowflakeIcon } from "lucide-react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { cn } from "~/utils/cn";
|
||||
@@ -72,8 +73,7 @@ export function TaskRunAttemptStatusIcon({
|
||||
case "COMPLETED":
|
||||
return <CheckCircleIcon className={cn(runAttemptStatusClassNameColor(status), className)} />;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,8 +99,7 @@ export function runAttemptStatusClassNameColor(status: ExtendedTaskAttemptStatus
|
||||
case "COMPLETED":
|
||||
return "text-success";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,8 +125,7 @@ export function runAttemptStatusTitle(status: ExtendedTaskAttemptStatus | null):
|
||||
case "COMPLETED":
|
||||
return "Completed";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { TaskRunStatus } from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import { SnowflakeIcon } from "lucide-react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { cn } from "~/utils/cn";
|
||||
@@ -88,8 +89,7 @@ export function TaskRunStatusIcon({
|
||||
return <FireIcon className={cn(runStatusClassNameColor(status), className)} />;
|
||||
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,7 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
|
||||
case "CRASHED":
|
||||
return "text-error";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,8 +152,7 @@ export function runStatusTitle(status: TaskRunStatus): string {
|
||||
case "CRASHED":
|
||||
return "Crashed";
|
||||
default: {
|
||||
const _exhaustiveCheck: never = status;
|
||||
throw new Error(`Non-exhaustive match for value: ${status}`);
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { StopIcon } from "@heroicons/react/24/outline";
|
||||
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { formatDuration } from "@trigger.dev/core/v3";
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { useEnvironments } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
|
||||
import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
||||
import { DateTime } from "../../primitives/DateTime";
|
||||
import { Paragraph } from "../../primitives/Paragraph";
|
||||
@@ -14,21 +20,14 @@ import {
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellChevron,
|
||||
TableCellMenu,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "../../primitives/Table";
|
||||
import { formatDuration } from "@trigger.dev/core/v3";
|
||||
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
||||
import { useEnvironments } from "~/hooks/useEnvironments";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { CancelRunDialog } from "./CancelRunDialog";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { ReplayRunDialog } from "./ReplayRunDialog";
|
||||
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
||||
|
||||
type RunsTableProps = {
|
||||
total: number;
|
||||
@@ -78,7 +77,7 @@ export function TaskRunsTable({
|
||||
<BlankState isLoading={isLoading} filters={filters} />
|
||||
) : (
|
||||
runs.map((run) => {
|
||||
const path = v3RunPath(organization, project, run);
|
||||
const path = v3RunSpanPath(organization, project, run, { spanId: run.spanId });
|
||||
const usernameForEnv =
|
||||
currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
|
||||
return (
|
||||
|
||||
@@ -76,7 +76,6 @@ const EnvironmentSchema = z.object({
|
||||
REDIS_PASSWORD: z.string().optional(),
|
||||
REDIS_TLS_DISABLED: z.string().optional(),
|
||||
|
||||
DEFAULT_QUEUE_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(5),
|
||||
DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(10),
|
||||
DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(10),
|
||||
DEFAULT_DEV_ENV_EXECUTION_ATTEMPTS: z.coerce.number().int().positive().default(1),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useOptimisticLocation } from "./useOptimisticLocation";
|
||||
import type { Location } from "@remix-run/react";
|
||||
|
||||
export function useReplaceLocation() {
|
||||
const optimisticLocation = useOptimisticLocation();
|
||||
const [location, setLocation] = useState(optimisticLocation);
|
||||
|
||||
const replaceLocation = useCallback((location: Location<any>) => {
|
||||
const fullPath = location.pathname + location.search + location.hash;
|
||||
//replace the URL in the browser
|
||||
history.replaceState(null, "", fullPath);
|
||||
//update the state (new object in case the same location ref was modified)
|
||||
const newLocation = { ...location };
|
||||
setLocation(newLocation);
|
||||
}, []);
|
||||
|
||||
const replaceSearchParam = useCallback(
|
||||
(key: string, value?: string) => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
if (value) {
|
||||
searchParams.set(key, value);
|
||||
} else {
|
||||
searchParams.delete(key);
|
||||
}
|
||||
replaceLocation({ ...optimisticLocation, search: "?" + searchParams.toString() });
|
||||
},
|
||||
[optimisticLocation, replaceLocation]
|
||||
);
|
||||
|
||||
return { location, replaceLocation, replaceSearchParam };
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
TaskRunError,
|
||||
TaskRunExecutionResult,
|
||||
TaskRunFailedExecutionResult,
|
||||
TaskRunSuccessfulExecutionResult,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import {
|
||||
BatchTaskRunItemStatus,
|
||||
TaskRun,
|
||||
TaskRunAttempt,
|
||||
TaskRunAttemptStatus,
|
||||
TaskRunStatus,
|
||||
} from "@trigger.dev/database";
|
||||
import { assertNever } from "assert-never";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const SUCCESSFUL_STATUSES = [TaskRunStatus.COMPLETED_SUCCESSFULLY];
|
||||
const FAILURE_STATUSES = [
|
||||
TaskRunStatus.CANCELED,
|
||||
TaskRunStatus.INTERRUPTED,
|
||||
TaskRunStatus.COMPLETED_WITH_ERRORS,
|
||||
TaskRunStatus.SYSTEM_FAILURE,
|
||||
TaskRunStatus.CRASHED,
|
||||
];
|
||||
|
||||
export type TaskRunWithAttempts = TaskRun & {
|
||||
attempts: TaskRunAttempt[];
|
||||
};
|
||||
|
||||
export function executionResultForTaskRun(
|
||||
taskRun: TaskRunWithAttempts
|
||||
): TaskRunExecutionResult | undefined {
|
||||
if (SUCCESSFUL_STATUSES.includes(taskRun.status)) {
|
||||
// find the last attempt that was successful
|
||||
const attempt = taskRun.attempts.find((a) => a.status === TaskRunAttemptStatus.COMPLETED);
|
||||
|
||||
if (!attempt) {
|
||||
logger.error("Task run is successful but no successful attempt found", {
|
||||
taskRunId: taskRun.id,
|
||||
taskRunStatus: taskRun.status,
|
||||
taskRunAttempts: taskRun.attempts.map((a) => a.status),
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
id: taskRun.friendlyId,
|
||||
output: attempt.output ?? undefined,
|
||||
outputType: attempt.outputType,
|
||||
} satisfies TaskRunSuccessfulExecutionResult;
|
||||
}
|
||||
|
||||
if (FAILURE_STATUSES.includes(taskRun.status)) {
|
||||
if (taskRun.status === TaskRunStatus.CANCELED) {
|
||||
return {
|
||||
ok: false,
|
||||
id: taskRun.friendlyId,
|
||||
error: {
|
||||
type: "INTERNAL_ERROR",
|
||||
code: "TASK_RUN_CANCELLED",
|
||||
},
|
||||
} satisfies TaskRunFailedExecutionResult;
|
||||
}
|
||||
|
||||
const attempt = taskRun.attempts.find((a) => a.status === TaskRunAttemptStatus.FAILED);
|
||||
|
||||
if (!attempt) {
|
||||
logger.error("Task run is failed but no failed attempt found", {
|
||||
taskRunId: taskRun.id,
|
||||
taskRunStatus: taskRun.status,
|
||||
taskRunAttempts: taskRun.attempts.map((a) => a.status),
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const error = TaskRunError.safeParse(attempt.error);
|
||||
|
||||
if (!error.success) {
|
||||
logger.error("Failed to parse error from failed task run attempt", {
|
||||
taskRunId: taskRun.id,
|
||||
taskRunStatus: taskRun.status,
|
||||
taskRunAttempts: taskRun.attempts.map((a) => a.status),
|
||||
error: attempt.error,
|
||||
});
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
id: taskRun.friendlyId,
|
||||
error: {
|
||||
type: "INTERNAL_ERROR",
|
||||
code: "CONFIGURED_INCORRECTLY",
|
||||
},
|
||||
} satisfies TaskRunFailedExecutionResult;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
id: taskRun.friendlyId,
|
||||
error: error.data,
|
||||
} satisfies TaskRunFailedExecutionResult;
|
||||
}
|
||||
}
|
||||
|
||||
export function batchTaskRunItemStatusForRunStatus(status: TaskRunStatus): BatchTaskRunItemStatus {
|
||||
switch (status) {
|
||||
case TaskRunStatus.COMPLETED_SUCCESSFULLY:
|
||||
return BatchTaskRunItemStatus.COMPLETED;
|
||||
case TaskRunStatus.CANCELED:
|
||||
case TaskRunStatus.INTERRUPTED:
|
||||
case TaskRunStatus.COMPLETED_WITH_ERRORS:
|
||||
case TaskRunStatus.SYSTEM_FAILURE:
|
||||
case TaskRunStatus.CRASHED:
|
||||
case TaskRunStatus.COMPLETED_WITH_ERRORS:
|
||||
return BatchTaskRunItemStatus.FAILED;
|
||||
case TaskRunStatus.PENDING:
|
||||
case TaskRunStatus.WAITING_FOR_DEPLOY:
|
||||
case TaskRunStatus.WAITING_TO_RESUME:
|
||||
case TaskRunStatus.RETRYING_AFTER_FAILURE:
|
||||
case TaskRunStatus.EXECUTING:
|
||||
case TaskRunStatus.PAUSED:
|
||||
return BatchTaskRunItemStatus.PENDING;
|
||||
default:
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BatchTaskRunExecutionResult } from "@trigger.dev/core/v3";
|
||||
import { executionResultForTaskRun } from "~/models/taskRun.server";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
export class ApiBatchResultsPresenter extends BasePresenter {
|
||||
public async call(
|
||||
friendlyId: string,
|
||||
env: AuthenticatedEnvironment
|
||||
): Promise<BatchTaskRunExecutionResult | undefined> {
|
||||
return this.traceWithEnv("call", env, async (span) => {
|
||||
const batchRun = await this._prisma.batchTaskRun.findUnique({
|
||||
where: {
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: env.id,
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
taskRun: {
|
||||
include: {
|
||||
attempts: {
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!batchRun) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: batchRun.friendlyId,
|
||||
items: batchRun.items
|
||||
.map((item) => executionResultForTaskRun(item.taskRun))
|
||||
.filter(Boolean),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { TaskRunExecutionResult } from "@trigger.dev/core/v3";
|
||||
import { executionResultForTaskRun } from "~/models/taskRun.server";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
export class ApiRunResultPresenter extends BasePresenter {
|
||||
public async call(
|
||||
friendlyId: string,
|
||||
env: AuthenticatedEnvironment
|
||||
): Promise<TaskRunExecutionResult | undefined> {
|
||||
return this.traceWithEnv("call", env, async (span) => {
|
||||
const taskRun = await this._prisma.taskRun.findUnique({
|
||||
where: {
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: env.id,
|
||||
},
|
||||
include: {
|
||||
attempts: {
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!taskRun) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return executionResultForTaskRun(taskRun);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,7 @@ export class RunListPresenter {
|
||||
lockedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
isTest: boolean;
|
||||
spanId: string;
|
||||
attempts: BigInt;
|
||||
}[]
|
||||
>`
|
||||
@@ -121,6 +122,7 @@ export class RunListPresenter {
|
||||
tr."lockedAt" AS "lockedAt",
|
||||
tra."completedAt" AS "completedAt",
|
||||
tr."isTest" AS "isTest",
|
||||
tr."spanId" AS "spanId",
|
||||
COUNT(tra.id) AS attempts
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" tr
|
||||
@@ -225,6 +227,7 @@ export class RunListPresenter {
|
||||
status: run.status,
|
||||
version: run.version,
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
spanId: run.spanId,
|
||||
attempts: Number(run.attempts),
|
||||
isReplayable: true,
|
||||
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Span, SpanKind } from "@opentelemetry/api";
|
||||
import { 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) {}
|
||||
|
||||
protected async traceWithEnv<T>(
|
||||
trace: string,
|
||||
env: AuthenticatedEnvironment,
|
||||
fn: (span: Span) => Promise<T>
|
||||
): Promise<T> {
|
||||
return tracer.startActiveSpan(
|
||||
`${this.constructor.name}.${trace}`,
|
||||
{ attributes: attributesFromAuthenticatedEnv(env), kind: SpanKind.SERVER },
|
||||
async (span) => {
|
||||
try {
|
||||
return await fn(span);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
span.recordException(e);
|
||||
} else {
|
||||
span.recordException(new Error(String(e)));
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -60,7 +60,7 @@ export default function Page() {
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
{job.hasIntegrationsRequiringAction && (
|
||||
<Callout variant="error" to={organizationIntegrationsPath(organization)} className="mb-2">
|
||||
{simplur`This Job has ${
|
||||
@@ -96,6 +96,6 @@ export default function Page() {
|
||||
</div>
|
||||
)}
|
||||
</Help>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+209
-89
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Outlet, useNavigate, useParams, useRevalidator } from "@remix-run/react";
|
||||
import type { Location } from "@remix-run/react";
|
||||
import { useParams, useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Virtualizer } from "@tanstack/react-virtual";
|
||||
import {
|
||||
@@ -23,7 +26,7 @@ import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody } from "~/components/layout/AppLayout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/primitives/Resizable";
|
||||
import { ShortcutKey, variants } from "~/components/primitives/ShortcutKey";
|
||||
import { Slider } from "~/components/primitives/Slider";
|
||||
import { Switch } from "~/components/primitives/Switch";
|
||||
import * as Timeline from "~/components/primitives/Timeline";
|
||||
@@ -45,8 +49,9 @@ import { useDebounce } from "~/hooks/useDebounce";
|
||||
import { useEventSource } from "~/hooks/useEventSource";
|
||||
import { useInitialDimensions } from "~/hooks/useInitialDimensions";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useReplaceLocation } from "~/hooks/useReplaceLocation";
|
||||
import { Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { RunEvent, RunPresenter } from "~/presenters/v3/RunPresenter.server";
|
||||
import { getResizableRunSettings, setResizableRunSettings } from "~/services/resizablePanel";
|
||||
@@ -60,6 +65,11 @@ import {
|
||||
v3RunStreamingPath,
|
||||
v3RunsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route";
|
||||
import { number } from "zod";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Popover, PopoverArrowTrigger, PopoverContent } from "~/components/primitives/Popover";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -82,19 +92,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
});
|
||||
};
|
||||
|
||||
function getSpanId(path: string): string | undefined {
|
||||
const regex = /spans\/([^\/]*)/;
|
||||
const match = path.match(regex);
|
||||
return match ? match[1] : undefined;
|
||||
function getSpanId(location: Location<any>): string | undefined {
|
||||
const search = new URLSearchParams(location.search);
|
||||
return search.get("span") ?? undefined;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { run, trace, resizeSettings } = useTypedLoaderData<typeof loader>();
|
||||
const navigate = useNavigate();
|
||||
const organization = useOrganization();
|
||||
const pathName = usePathName();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
const { location, replaceSearchParam } = useReplaceLocation();
|
||||
const selectedSpanId = getSpanId(location);
|
||||
|
||||
const usernameForEnv = user.id !== run.environment.userId ? run.environment.userName : undefined;
|
||||
|
||||
@@ -133,10 +142,8 @@ export default function Page() {
|
||||
|
||||
const { events, parentRunFriendlyId, duration, rootSpanStatus, rootStartedAt } = trace;
|
||||
|
||||
const selectedSpanId = getSpanId(pathName);
|
||||
|
||||
const changeToSpan = useDebounce((selectedSpan: string) => {
|
||||
navigate(v3RunSpanPath(organization, project, run, { spanId: selectedSpan }));
|
||||
replaceSearchParam("span", selectedSpan);
|
||||
}, 250);
|
||||
|
||||
const revalidator = useRevalidator();
|
||||
@@ -166,62 +173,47 @@ export default function Page() {
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
<div className={cn("grid h-full max-h-full grid-cols-1 overflow-hidden")}>
|
||||
{selectedSpanId === undefined ? (
|
||||
<TasksTreeView
|
||||
selectedId={selectedSpanId}
|
||||
key={events[0]?.id ?? "-"}
|
||||
events={events}
|
||||
parentRunFriendlyId={parentRunFriendlyId}
|
||||
onSelectedIdChanged={(selectedSpan) => {
|
||||
//instantly close the panel if no span is selected
|
||||
if (!selectedSpan) {
|
||||
navigate(v3RunPath(organization, project, run));
|
||||
return;
|
||||
}
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="h-full max-h-full"
|
||||
onLayout={(layout) => {
|
||||
if (layout.length !== 2) return;
|
||||
if (!selectedSpanId) return;
|
||||
setResizableRunSettings(document, layout);
|
||||
}}
|
||||
>
|
||||
<ResizablePanel order={1} minSize={30} defaultSize={resizeSettings.layout?.[0]}>
|
||||
<TasksTreeView
|
||||
selectedId={selectedSpanId}
|
||||
key={events[0]?.id ?? "-"}
|
||||
events={events}
|
||||
parentRunFriendlyId={parentRunFriendlyId}
|
||||
onSelectedIdChanged={(selectedSpan) => {
|
||||
//instantly close the panel if no span is selected
|
||||
if (!selectedSpan) {
|
||||
replaceSearchParam("span");
|
||||
return;
|
||||
}
|
||||
|
||||
changeToSpan(selectedSpan);
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
environmentType={run.environment.type}
|
||||
/>
|
||||
) : (
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="h-full max-h-full"
|
||||
onLayout={(layout) => {
|
||||
if (layout.length !== 2) return;
|
||||
setResizableRunSettings(document, layout);
|
||||
}}
|
||||
>
|
||||
<ResizablePanel order={1} minSize={30} defaultSize={resizeSettings.layout?.[0]}>
|
||||
<TasksTreeView
|
||||
selectedId={selectedSpanId}
|
||||
key={events[0]?.id ?? "-"}
|
||||
events={events}
|
||||
parentRunFriendlyId={parentRunFriendlyId}
|
||||
onSelectedIdChanged={(selectedSpan) => {
|
||||
//instantly close the panel if no span is selected
|
||||
if (!selectedSpan) {
|
||||
navigate(v3RunPath(organization, project, run));
|
||||
return;
|
||||
}
|
||||
|
||||
changeToSpan(selectedSpan);
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
environmentType={run.environment.type}
|
||||
changeToSpan(selectedSpan);
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
environmentType={run.environment.type}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
{selectedSpanId && (
|
||||
<ResizablePanel order={2} minSize={30} defaultSize={resizeSettings.layout?.[1]}>
|
||||
<SpanView
|
||||
runParam={run.friendlyId}
|
||||
spanId={selectedSpanId}
|
||||
closePanel={() => replaceSearchParam("span")}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel order={2} minSize={30} defaultSize={resizeSettings.layout?.[1]}>
|
||||
<Outlet key={selectedSpanId} />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
)}
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</PageBody>
|
||||
</>
|
||||
@@ -263,6 +255,9 @@ function TasksTreeView({
|
||||
getNodeProps,
|
||||
toggleNodeSelection,
|
||||
toggleExpandNode,
|
||||
expandAllBelowDepth,
|
||||
toggleExpandLevel,
|
||||
collapseAllBelowDepth,
|
||||
selectNode,
|
||||
scrollToNode,
|
||||
virtualizer,
|
||||
@@ -286,7 +281,7 @@ function TasksTreeView({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-rows-[2.5rem_1fr] overflow-hidden">
|
||||
<div className="grid h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden">
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
<Input
|
||||
placeholder="Search log"
|
||||
@@ -297,30 +292,12 @@ function TasksTreeView({
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<LiveReloadingStatus rootSpanCompleted={rootSpanStatus !== "executing"} />
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Errors only"
|
||||
checked={errorsOnly}
|
||||
onCheckedChange={(e) => setErrorsOnly(e.valueOf())}
|
||||
/>
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Show durations"
|
||||
checked={showDurations}
|
||||
onCheckedChange={(e) => setShowDurations(e.valueOf())}
|
||||
/>
|
||||
<Slider
|
||||
variant={"tertiary"}
|
||||
className="w-20"
|
||||
LeadingIcon={MagnifyingGlassMinusIcon}
|
||||
TrailingIcon={MagnifyingGlassPlusIcon}
|
||||
value={[scale]}
|
||||
onValueChange={(value) => setScale(value[0])}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ResizablePanelGroup
|
||||
@@ -333,14 +310,15 @@ function TasksTreeView({
|
||||
{/* Tree list */}
|
||||
<ResizablePanel order={1} minSize={20} defaultSize={50} className="pl-3">
|
||||
<div className="grid h-full grid-rows-[2rem_1fr] overflow-hidden">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center pr-2">
|
||||
{parentRunFriendlyId ? (
|
||||
<ShowParentLink runFriendlyId={parentRunFriendlyId} />
|
||||
) : (
|
||||
<Paragraph variant="small" className="text-charcoal-500">
|
||||
<Paragraph variant="small" className="flex-1 text-charcoal-500">
|
||||
This is the root task
|
||||
</Paragraph>
|
||||
)}
|
||||
<LiveReloadingStatus rootSpanCompleted={rootSpanStatus !== "executing"} />
|
||||
</div>
|
||||
<TreeView
|
||||
parentRef={parentRef}
|
||||
@@ -355,13 +333,13 @@ function TasksTreeView({
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"delay-[25ms] flex h-8 cursor-pointer items-center overflow-hidden rounded-l-sm pr-2 transition-colors",
|
||||
"flex h-8 cursor-pointer items-center overflow-hidden rounded-l-sm pr-2",
|
||||
state.selected
|
||||
? "bg-grid-dimmed hover:bg-grid-bright"
|
||||
: "bg-transparent hover:bg-grid-dimmed"
|
||||
)}
|
||||
onClick={() => {
|
||||
toggleNodeSelection(node.id);
|
||||
selectNode(node.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 items-center">
|
||||
@@ -379,7 +357,15 @@ function TasksTreeView({
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpandNode(node.id);
|
||||
if (e.altKey) {
|
||||
if (state.expanded) {
|
||||
collapseAllBelowDepth(node.level);
|
||||
} else {
|
||||
expandAllBelowDepth(node.level);
|
||||
}
|
||||
} else {
|
||||
toggleExpandNode(node.id);
|
||||
}
|
||||
scrollToNode(node.id);
|
||||
}}
|
||||
>
|
||||
@@ -445,6 +431,50 @@ function TasksTreeView({
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
|
||||
<div className="grow @container">
|
||||
<div className="hidden items-center gap-4 @[42rem]:flex">
|
||||
<KeyboardShortcuts
|
||||
expandAllBelowDepth={expandAllBelowDepth}
|
||||
collapseAllBelowDepth={collapseAllBelowDepth}
|
||||
toggleExpandLevel={toggleExpandLevel}
|
||||
setShowDurations={setShowDurations}
|
||||
/>
|
||||
</div>
|
||||
<div className="@[42rem]:hidden">
|
||||
<Popover>
|
||||
<PopoverArrowTrigger>Shortcuts</PopoverArrowTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-[20rem] overflow-y-auto p-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="start"
|
||||
>
|
||||
<Header3 spacing>Keyboard shortcuts</Header3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<KeyboardShortcuts
|
||||
expandAllBelowDepth={expandAllBelowDepth}
|
||||
collapseAllBelowDepth={collapseAllBelowDepth}
|
||||
toggleExpandLevel={toggleExpandLevel}
|
||||
setShowDurations={setShowDurations}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
variant={"tertiary"}
|
||||
className="w-20"
|
||||
LeadingIcon={MagnifyingGlassMinusIcon}
|
||||
TrailingIcon={MagnifyingGlassPlusIcon}
|
||||
value={[scale]}
|
||||
onValueChange={(value) => setScale(value[0])}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -738,6 +768,7 @@ function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) {
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
shortcut={{ key: "p" }}
|
||||
className="flex-1"
|
||||
>
|
||||
{mouseOver ? (
|
||||
<ShowParentIconSelected className="h-4 w-4 text-indigo-500" />
|
||||
@@ -884,3 +915,92 @@ function ConnectedDevWarning() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardShortcuts({
|
||||
expandAllBelowDepth,
|
||||
collapseAllBelowDepth,
|
||||
toggleExpandLevel,
|
||||
setShowDurations,
|
||||
}: {
|
||||
expandAllBelowDepth: (depth: number) => void;
|
||||
collapseAllBelowDepth: (depth: number) => void;
|
||||
toggleExpandLevel: (depth: number) => void;
|
||||
setShowDurations: (show: (show: boolean) => boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ArrowKeyShortcuts />
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "e" }}
|
||||
action={() => expandAllBelowDepth(0)}
|
||||
title="Expand all"
|
||||
/>
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "c" }}
|
||||
action={() => collapseAllBelowDepth(1)}
|
||||
title="Collapse all"
|
||||
/>
|
||||
<NumberShortcuts toggleLevel={(number) => toggleExpandLevel(number)} />
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "d" }}
|
||||
action={() => setShowDurations((d) => !d)}
|
||||
title="Toggle durations"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrowKeyShortcuts() {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ShortcutKey shortcut={{ key: "arrowup" }} variant="medium" className="ml-0 mr-0" />
|
||||
<ShortcutKey shortcut={{ key: "arrowdown" }} variant="medium" className="ml-0 mr-0" />
|
||||
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium" className="ml-0 mr-0" />
|
||||
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium" className="ml-0 mr-0" />
|
||||
<Paragraph variant="extra-small" className="ml-1.5 whitespace-nowrap">
|
||||
Navigate
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ShortcutWithAction({
|
||||
shortcut,
|
||||
title,
|
||||
action,
|
||||
}: {
|
||||
shortcut: Shortcut;
|
||||
title: string;
|
||||
action: () => void;
|
||||
}) {
|
||||
useShortcutKeys({
|
||||
shortcut,
|
||||
action,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ShortcutKey shortcut={shortcut} variant="medium" className="ml-0 mr-0" />
|
||||
<Paragraph variant="extra-small" className="ml-1.5 whitespace-nowrap">
|
||||
{title}
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberShortcuts({ toggleLevel }: { toggleLevel: (depth: number) => void }) {
|
||||
useHotkeys(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], (event, hotkeysEvent) => {
|
||||
toggleLevel(Number(event.key));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className={cn(variants.medium, "ml-0 mr-0")}>0</span>
|
||||
<span className="text-[0.75rem] text-text-dimmed">–</span>
|
||||
<span className={cn(variants.medium, "ml-0 mr-0")}>9</span>
|
||||
<Paragraph variant="extra-small" className="ml-1.5 whitespace-nowrap">
|
||||
Toggle level
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+7
-3
@@ -6,7 +6,6 @@ import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runt
|
||||
import { TaskRunStatus } from "@trigger.dev/database";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { JSONEditor } from "~/components/code/JSONEditor";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
@@ -37,7 +36,7 @@ import {
|
||||
TestTaskPresenter,
|
||||
} from "~/presenters/v3/TestTaskPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, v3RunPath, v3TaskParamsSchema } from "~/utils/pathBuilder";
|
||||
import { docsPath, v3RunSpanPath, v3TaskParamsSchema } from "~/utils/pathBuilder";
|
||||
import { TestTaskService } from "~/v3/services/testTask.server";
|
||||
import { TestTaskData } from "~/v3/testTask";
|
||||
|
||||
@@ -77,7 +76,12 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
}
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
v3RunPath({ slug: organizationSlug }, { slug: projectParam }, { friendlyId: run.friendlyId }),
|
||||
v3RunSpanPath(
|
||||
{ slug: organizationSlug },
|
||||
{ slug: projectParam },
|
||||
{ friendlyId: run.friendlyId },
|
||||
{ spanId: run.spanId }
|
||||
),
|
||||
request,
|
||||
"Test run created"
|
||||
);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresenter.server";
|
||||
import { ApiRunResultPresenter } from "~/presenters/v3/ApiRunResultPresenter.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
/* This is the batch friendly ID */
|
||||
batchParam: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
// Authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const parsed = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsed.success) {
|
||||
return json({ error: "Invalid or missing run ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { batchParam } = parsed.data;
|
||||
|
||||
try {
|
||||
const presenter = new ApiBatchResultsPresenter();
|
||||
const result = await presenter.call(batchParam, authenticationResult.environment);
|
||||
|
||||
if (!result) {
|
||||
return json({ error: "Batch not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return json(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
} else {
|
||||
return json({ error: JSON.stringify(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { PrismaErrorSchema, prisma } from "~/db.server";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { CancelRunService } from "~/services/runs/cancelRun.server";
|
||||
import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server";
|
||||
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
/* This is the run friendly ID */
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { ApiRunResultPresenter } from "~/presenters/v3/ApiRunResultPresenter.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
/* This is the run friendly ID */
|
||||
runParam: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
// Authenticate the request
|
||||
const authenticationResult = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const parsed = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsed.success) {
|
||||
return json({ error: "Invalid or missing run ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { runParam } = parsed.data;
|
||||
|
||||
try {
|
||||
const presenter = new ApiRunResultPresenter();
|
||||
const result = await presenter.call(runParam, authenticationResult.environment);
|
||||
|
||||
if (!result) {
|
||||
return json({ error: "Run either doesn't exist or is not finished" }, { status: 404 });
|
||||
}
|
||||
|
||||
return json(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
} else {
|
||||
return json({ error: JSON.stringify(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { v3RunSpanPath } from "~/utils/pathBuilder";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectRef: z.string(),
|
||||
@@ -33,8 +34,20 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
const run = await prisma.taskRun.findUnique({
|
||||
where: {
|
||||
friendlyId: validatedParams.runParam,
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
throw new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Redirect to the project's runs page
|
||||
return redirect(
|
||||
`/orgs/${project.organization.slug}/projects/v3/${project.slug}/runs/${validatedParams.runParam}`
|
||||
v3RunSpanPath({ slug: project.organization.slug }, { slug: project.slug }, run, {
|
||||
spanId: run.spanId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
+56
-13
@@ -4,10 +4,11 @@ import {
|
||||
QueueListIcon,
|
||||
StopCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useParams } from "@remix-run/react";
|
||||
import { useFetcher, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDurationNanoseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEffect } from "react";
|
||||
import { typedjson, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
@@ -17,6 +18,7 @@ import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
|
||||
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
|
||||
import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog";
|
||||
@@ -58,19 +60,57 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
return typedjson({ span });
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const {
|
||||
span: { event },
|
||||
} = useTypedLoaderData<typeof loader>();
|
||||
export function SpanView({
|
||||
runParam,
|
||||
spanId,
|
||||
closePanel,
|
||||
}: {
|
||||
runParam: string;
|
||||
spanId: string | undefined;
|
||||
closePanel: () => void;
|
||||
}) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const { runParam } = useParams();
|
||||
const fetcher = useTypedFetcher<typeof loader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (spanId === undefined) return;
|
||||
fetcher.load(
|
||||
`/resources/orgs/${organization.slug}/projects/v3/${project.slug}/runs/${runParam}/spans/${spanId}`
|
||||
);
|
||||
}, [organization.slug, project.slug, runParam, spanId]);
|
||||
|
||||
if (spanId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fetcher.state !== "idle" || fetcher.data === undefined) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright"
|
||||
)}
|
||||
>
|
||||
<div className="mx-3 flex items-center gap-2 overflow-x-hidden border-b border-grid-dimmed">
|
||||
<div className="size-4 bg-grid-dimmed" />
|
||||
<div className="h-6 w-[60%] bg-grid-dimmed" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
span: { event },
|
||||
} = fetcher.data;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full overflow-hidden bg-background-bright",
|
||||
event.showActionBar ? "grid-rows-[2.5rem_1fr_2.5rem]" : "grid-rows-[2.5rem_1fr]"
|
||||
event.showActionBar ? "grid-rows-[2.5rem_1fr_3.25rem]" : "grid-rows-[2.5rem_1fr]"
|
||||
)}
|
||||
>
|
||||
<div className="mx-3 flex items-center justify-between gap-2 overflow-x-hidden border-b border-grid-dimmed">
|
||||
@@ -85,8 +125,8 @@ export default function Page() {
|
||||
</Header2>
|
||||
</div>
|
||||
{runParam && (
|
||||
<LinkButton
|
||||
to={v3RunPath(organization, project, { friendlyId: runParam })}
|
||||
<Button
|
||||
onClick={closePanel}
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
@@ -130,6 +170,9 @@ export default function Page() {
|
||||
)}
|
||||
<Property label="Message">{event.message}</Property>
|
||||
<Property label="Task ID">{event.taskSlug}</Property>
|
||||
{event.idempotencyKey && (
|
||||
<Property label="Idempotency key">{event.idempotencyKey}</Property>
|
||||
)}
|
||||
{event.taskPath && event.taskExportName && (
|
||||
<Property label="Task">
|
||||
<TaskPath
|
||||
@@ -185,7 +228,7 @@ export default function Page() {
|
||||
{ friendlyId: event.runId },
|
||||
{ spanId: event.spanId }
|
||||
)}
|
||||
variant="minimal/small"
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={QueueListIcon}
|
||||
shortcut={{ key: "f" }}
|
||||
>
|
||||
@@ -213,7 +256,7 @@ function RunActionButtons({ span }: { span: Span }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="danger/small" LeadingIcon={StopCircleIcon}>
|
||||
<Button variant="danger/medium" LeadingIcon={StopCircleIcon}>
|
||||
Cancel run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -233,7 +276,7 @@ function RunActionButtons({ span }: { span: Span }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="tertiary/small" LeadingIcon={ArrowPathIcon}>
|
||||
<Button variant="tertiary/medium" LeadingIcon={ArrowPathIcon}>
|
||||
Replay run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { v3RunPath } from "~/utils/pathBuilder";
|
||||
import { v3RunSpanPath } from "~/utils/pathBuilder";
|
||||
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
|
||||
|
||||
const FormSchema = z.object({
|
||||
@@ -54,12 +54,13 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const runPath = v3RunPath(
|
||||
const runPath = v3RunSpanPath(
|
||||
{
|
||||
slug: taskRun.project.organization.slug,
|
||||
},
|
||||
{ slug: taskRun.project.slug },
|
||||
{ friendlyId: newRun.friendlyId }
|
||||
{ friendlyId: newRun.friendlyId },
|
||||
{ spanId: newRun.spanId }
|
||||
);
|
||||
|
||||
return redirectWithSuccessMessage(runPath, request, `Replaying run`);
|
||||
|
||||
@@ -14,8 +14,6 @@ import { Job } from "~/models/job.server";
|
||||
import type { Organization } from "~/models/organization.server";
|
||||
import type { Project } from "~/models/project.server";
|
||||
import { objectToSearchParams } from "./searchParams";
|
||||
import { ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
|
||||
export type OrgForPath = Pick<Organization, "slug">;
|
||||
export type ProjectForPath = Pick<Project, "slug">;
|
||||
@@ -368,7 +366,7 @@ export function v3RunSpanPath(
|
||||
run: v3RunForPath,
|
||||
span: v3SpanForPath
|
||||
) {
|
||||
return `${v3RunPath(organization, project, run)}/spans/${span.spanId}`;
|
||||
return `${v3RunPath(organization, project, run)}?span=${span.spanId}`;
|
||||
}
|
||||
|
||||
export function v3TraceSpanPath(
|
||||
|
||||
@@ -63,6 +63,7 @@ export type TraceAttributes = Partial<
|
||||
| "batchId"
|
||||
| "payload"
|
||||
| "payloadType"
|
||||
| "idempotencyKey"
|
||||
>
|
||||
>;
|
||||
|
||||
@@ -371,6 +372,7 @@ export class EventRepository {
|
||||
id: event.spanId,
|
||||
parentId: event.parentId ?? undefined,
|
||||
runId: event.runId,
|
||||
idempotencyKey: event.idempotencyKey,
|
||||
data: {
|
||||
message: event.message,
|
||||
style: event.style,
|
||||
@@ -459,7 +461,7 @@ export class EventRepository {
|
||||
const links: SpanLink[] = [];
|
||||
|
||||
if (messagingEvent.success && messagingEvent.data) {
|
||||
if ("id" in messagingEvent.data.message) {
|
||||
if (messagingEvent.data.message && "id" in messagingEvent.data.message) {
|
||||
if (messagingEvent.data.message.id.startsWith("run_")) {
|
||||
links.push({
|
||||
type: "run",
|
||||
@@ -719,6 +721,7 @@ export class EventRepository {
|
||||
links: links as unknown as Prisma.InputJsonValue,
|
||||
payload: options.attributes.payload,
|
||||
payloadType: options.attributes.payloadType,
|
||||
idempotencyKey: options.attributes.idempotencyKey,
|
||||
};
|
||||
|
||||
if (options.immediate) {
|
||||
|
||||
@@ -118,7 +118,7 @@ export class DevQueueConsumer {
|
||||
completion: TaskRunExecutionResult,
|
||||
execution: TaskRunExecution
|
||||
) {
|
||||
this._inProgressAttempts.delete(completion.id);
|
||||
this._inProgressAttempts.delete(execution.attempt.id);
|
||||
|
||||
if (completion.ok) {
|
||||
this._taskSuccesses++;
|
||||
@@ -424,7 +424,7 @@ export class DevQueueConsumer {
|
||||
orderBy: { number: "desc" },
|
||||
},
|
||||
tags: true,
|
||||
batchItem: {
|
||||
batchItems: {
|
||||
include: {
|
||||
batchTaskRun: true,
|
||||
},
|
||||
@@ -499,6 +499,7 @@ export class DevQueueConsumer {
|
||||
createdAt: lockedTaskRun.createdAt,
|
||||
tags: lockedTaskRun.tags.map((tag) => tag.name),
|
||||
isTest: lockedTaskRun.isTest,
|
||||
idempotencyKey: lockedTaskRun.idempotencyKey ?? undefined,
|
||||
},
|
||||
queue: {
|
||||
id: queue.friendlyId,
|
||||
@@ -520,9 +521,10 @@ export class DevQueueConsumer {
|
||||
slug: this.env.project.slug,
|
||||
name: this.env.project.name,
|
||||
},
|
||||
batch: lockedTaskRun.batchItem?.batchTaskRun
|
||||
? { id: lockedTaskRun.batchItem.batchTaskRun.friendlyId }
|
||||
: undefined,
|
||||
batch:
|
||||
lockedTaskRun.batchItems[0] && lockedTaskRun.batchItems[0].batchTaskRun
|
||||
? { id: lockedTaskRun.batchItems[0].batchTaskRun.friendlyId }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const environmentRepository = new EnvironmentVariablesRepository();
|
||||
|
||||
@@ -39,7 +39,6 @@ const SemanticAttributes = {
|
||||
|
||||
export type MarQSOptions = {
|
||||
redis: RedisOptions;
|
||||
defaultQueueConcurrency: number;
|
||||
defaultEnvConcurrency: number;
|
||||
defaultOrgConcurrency: number;
|
||||
windowSize?: number;
|
||||
@@ -92,7 +91,7 @@ export class MarQS {
|
||||
public async getQueueConcurrencyLimit(env: AuthenticatedEnvironment, queue: string) {
|
||||
const result = await this.redis.get(this.keys.queueConcurrencyLimitKey(env, queue));
|
||||
|
||||
return result ? Number(result) : this.options.defaultQueueConcurrency;
|
||||
return result ? Number(result) : undefined;
|
||||
}
|
||||
|
||||
public async getEnvConcurrencyLimit(env: AuthenticatedEnvironment) {
|
||||
@@ -860,7 +859,6 @@ export class MarQS {
|
||||
messageQueue,
|
||||
String(this.options.visibilityTimeoutInMs ?? 300000), // 5 minutes
|
||||
String(Date.now()),
|
||||
String(this.options.defaultQueueConcurrency),
|
||||
String(this.options.defaultEnvConcurrency),
|
||||
String(this.options.defaultOrgConcurrency)
|
||||
);
|
||||
@@ -1015,16 +1013,22 @@ export class MarQS {
|
||||
concurrencyLimitKey,
|
||||
envConcurrencyLimitKey,
|
||||
orgConcurrencyLimitKey,
|
||||
String(this.options.defaultQueueConcurrency),
|
||||
String(this.options.defaultEnvConcurrency),
|
||||
String(this.options.defaultOrgConcurrency)
|
||||
);
|
||||
|
||||
const queueCurrent = Number(capacities[0]);
|
||||
const envLimit = Number(capacities[3]);
|
||||
const orgLimit = Number(capacities[5]);
|
||||
const queueLimit = capacities[1] ? Number(capacities[1]) : Math.min(envLimit, orgLimit);
|
||||
const envCurrent = Number(capacities[2]);
|
||||
const orgCurrent = Number(capacities[4]);
|
||||
|
||||
// [queue current, queue limit, env current, env limit, org current, org limit]
|
||||
return {
|
||||
queue: { current: Number(capacities[0]), limit: Number(capacities[1]) },
|
||||
env: { current: Number(capacities[2]), limit: Number(capacities[3]) },
|
||||
org: { current: Number(capacities[4]), limit: Number(capacities[5]) },
|
||||
queue: { current: queueCurrent, limit: queueLimit },
|
||||
env: { current: envCurrent, limit: envLimit },
|
||||
org: { current: orgCurrent, limit: orgLimit },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1119,13 +1123,12 @@ local currentConcurrencyKey = KEYS[7]
|
||||
local envCurrentConcurrencyKey = KEYS[8]
|
||||
local orgCurrentConcurrencyKey = KEYS[9]
|
||||
|
||||
-- Args: childQueueName, visibilityQueue, currentTime, defaultConcurrencyLimit, defaultEnvConcurrencyLimit, defaultOrgConcurrencyLimit
|
||||
-- Args: childQueueName, visibilityQueue, currentTime, defaultEnvConcurrencyLimit, defaultOrgConcurrencyLimit
|
||||
local childQueueName = ARGV[1]
|
||||
local visibilityTimeout = tonumber(ARGV[2])
|
||||
local currentTime = tonumber(ARGV[3])
|
||||
local defaultConcurrencyLimit = ARGV[4]
|
||||
local defaultEnvConcurrencyLimit = ARGV[5]
|
||||
local defaultOrgConcurrencyLimit = ARGV[6]
|
||||
local defaultEnvConcurrencyLimit = ARGV[4]
|
||||
local defaultOrgConcurrencyLimit = ARGV[5]
|
||||
|
||||
-- Check current org concurrency against the limit
|
||||
local orgCurrentConcurrency = tonumber(redis.call('SCARD', orgCurrentConcurrencyKey) or '0')
|
||||
@@ -1145,8 +1148,9 @@ end
|
||||
|
||||
-- Check current queue concurrency against the limit
|
||||
local currentConcurrency = tonumber(redis.call('SCARD', currentConcurrencyKey) or '0')
|
||||
local concurrencyLimit = tonumber(redis.call('GET', concurrencyLimitKey) or defaultConcurrencyLimit)
|
||||
local concurrencyLimit = tonumber(redis.call('GET', concurrencyLimitKey) or '1000000')
|
||||
|
||||
-- Check condition only if concurrencyLimit exists
|
||||
if currentConcurrency >= concurrencyLimit then
|
||||
return nil
|
||||
end
|
||||
@@ -1304,10 +1308,9 @@ local concurrencyLimitKey = KEYS[4]
|
||||
local envConcurrencyLimitKey = KEYS[5]
|
||||
local orgConcurrencyLimitKey = KEYS[6]
|
||||
|
||||
-- Args defaultConcurrencyLimit, defaultEnvConcurrencyLimit, defaultOrgConcurrencyLimit
|
||||
local defaultConcurrencyLimit = tonumber(ARGV[1])
|
||||
local defaultEnvConcurrencyLimit = tonumber(ARGV[2])
|
||||
local defaultOrgConcurrencyLimit = tonumber(ARGV[3])
|
||||
-- Args defaultEnvConcurrencyLimit, defaultOrgConcurrencyLimit
|
||||
local defaultEnvConcurrencyLimit = tonumber(ARGV[1])
|
||||
local defaultOrgConcurrencyLimit = tonumber(ARGV[2])
|
||||
|
||||
local currentOrgConcurrency = tonumber(redis.call('SCARD', currentOrgConcurrencyKey) or '0')
|
||||
local orgConcurrencyLimit = tonumber(redis.call('GET', orgConcurrencyLimitKey) or defaultOrgConcurrencyLimit)
|
||||
@@ -1316,7 +1319,7 @@ local currentEnvConcurrency = tonumber(redis.call('SCARD', currentEnvConcurrency
|
||||
local envConcurrencyLimit = tonumber(redis.call('GET', envConcurrencyLimitKey) or defaultEnvConcurrencyLimit)
|
||||
|
||||
local currentConcurrency = tonumber(redis.call('SCARD', currentConcurrencyKey) or '0')
|
||||
local concurrencyLimit = tonumber(redis.call('GET', concurrencyLimitKey) or defaultConcurrencyLimit)
|
||||
local concurrencyLimit = redis.call('GET', concurrencyLimitKey)
|
||||
|
||||
-- Return current capacity and concurrency limits for the queue, env, org
|
||||
return { currentConcurrency, concurrencyLimit, currentEnvConcurrency, envConcurrencyLimit, currentOrgConcurrency, orgConcurrencyLimit }
|
||||
@@ -1398,7 +1401,6 @@ declare module "ioredis" {
|
||||
childQueueName: string,
|
||||
visibilityTimeout: string,
|
||||
currentTime: string,
|
||||
defaultConcurrencyLimit: string,
|
||||
defaultEnvConcurrencyLimit: string,
|
||||
defaultOrgConcurrencyLimit: string,
|
||||
callback?: Callback<[string, string]>
|
||||
@@ -1447,7 +1449,6 @@ declare module "ioredis" {
|
||||
concurrencyLimitKey: string,
|
||||
envConcurrencyLimitKey: string,
|
||||
orgConcurrencyLimitKey: string,
|
||||
defaultConcurrencyLimit: string,
|
||||
defaultEnvConcurrencyLimit: string,
|
||||
defaultOrgConcurrencyLimit: string,
|
||||
callback?: Callback<number[]>
|
||||
@@ -1492,7 +1493,6 @@ function getMarQSClient() {
|
||||
envQueuePriorityStrategy: new SimpleWeightedChoiceStrategy({ queueSelectionCount: 12 }),
|
||||
workers: 1,
|
||||
redis: redisOptions,
|
||||
defaultQueueConcurrency: env.DEFAULT_QUEUE_EXECUTION_CONCURRENCY_LIMIT,
|
||||
defaultEnvConcurrency: env.DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT,
|
||||
defaultOrgConcurrency: env.DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT,
|
||||
visibilityTimeoutInMs: 120 * 1000, // 2 minutes,
|
||||
|
||||
@@ -812,7 +812,7 @@ class SharedQueueTasks {
|
||||
if (ok) {
|
||||
const success: TaskRunSuccessfulExecutionResult = {
|
||||
ok,
|
||||
id: attempt.friendlyId,
|
||||
id: attempt.taskRun.friendlyId,
|
||||
output: attempt.output ?? undefined,
|
||||
outputType: attempt.outputType,
|
||||
};
|
||||
@@ -820,7 +820,7 @@ class SharedQueueTasks {
|
||||
} else {
|
||||
const failure: TaskRunFailedExecutionResult = {
|
||||
ok,
|
||||
id: attempt.friendlyId,
|
||||
id: attempt.taskRun.friendlyId,
|
||||
error: attempt.error as TaskRunError,
|
||||
};
|
||||
return failure;
|
||||
@@ -848,7 +848,7 @@ class SharedQueueTasks {
|
||||
taskRun: {
|
||||
include: {
|
||||
tags: true,
|
||||
batchItem: {
|
||||
batchItems: {
|
||||
include: {
|
||||
batchTaskRun: true,
|
||||
},
|
||||
@@ -956,6 +956,7 @@ class SharedQueueTasks {
|
||||
createdAt: taskRun.createdAt,
|
||||
tags: taskRun.tags.map((tag) => tag.name),
|
||||
isTest: taskRun.isTest,
|
||||
idempotencyKey: taskRun.idempotencyKey ?? undefined,
|
||||
},
|
||||
queue: {
|
||||
id: queue.friendlyId,
|
||||
@@ -977,9 +978,10 @@ class SharedQueueTasks {
|
||||
slug: attempt.runtimeEnvironment.project.slug,
|
||||
name: attempt.runtimeEnvironment.project.name,
|
||||
},
|
||||
batch: taskRun.batchItem?.batchTaskRun
|
||||
? { id: taskRun.batchItem.batchTaskRun.friendlyId }
|
||||
: undefined,
|
||||
batch:
|
||||
taskRun.batchItems[0] && taskRun.batchItems[0].batchTaskRun
|
||||
? { id: taskRun.batchItems[0].batchTaskRun.friendlyId }
|
||||
: undefined,
|
||||
worker: {
|
||||
id: attempt.backgroundWorkerId,
|
||||
contentHash: attempt.backgroundWorker.contentHash,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { RedisOptions } from "ioredis";
|
||||
import { z } from "zod";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
|
||||
|
||||
@@ -352,6 +352,7 @@ function extractResourceProperties(attributes: KeyValue[]) {
|
||||
queueId: extractStringAttribute(attributes, SemanticInternalAttributes.QUEUE_ID),
|
||||
queueName: extractStringAttribute(attributes, SemanticInternalAttributes.QUEUE_NAME),
|
||||
batchId: extractStringAttribute(attributes, SemanticInternalAttributes.BATCH_ID),
|
||||
idempotencyKey: extractStringAttribute(attributes, SemanticInternalAttributes.IDEMPOTENCY_KEY),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BatchTriggerTaskRequestBody } from "@trigger.dev/core/v3";
|
||||
import { nanoid } from "nanoid";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { TriggerTaskService } from "./triggerTask.server";
|
||||
import { batchTaskRunItemStatusForRunStatus } from "~/models/taskRun.server";
|
||||
|
||||
export type BatchTriggerTaskServiceOptions = {
|
||||
idempotencyKey?: string;
|
||||
@@ -22,23 +22,23 @@ export class BatchTriggerTaskService extends BaseService {
|
||||
return await this.traceWithEnv("call()", environment, async (span) => {
|
||||
span.setAttribute("taskId", taskId);
|
||||
|
||||
const idempotencyKey = options.idempotencyKey ?? nanoid();
|
||||
|
||||
const existingBatch = await this._prisma.batchTaskRun.findUnique({
|
||||
where: {
|
||||
runtimeEnvironmentId_idempotencyKey: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
taskRun: true,
|
||||
const existingBatch = options.idempotencyKey
|
||||
? await this._prisma.batchTaskRun.findUnique({
|
||||
where: {
|
||||
runtimeEnvironmentId_idempotencyKey: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
taskRun: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (existingBatch) {
|
||||
span.setAttribute("batchId", existingBatch.friendlyId);
|
||||
@@ -58,7 +58,7 @@ export class BatchTriggerTaskService extends BaseService {
|
||||
data: {
|
||||
friendlyId: generateFriendlyId("batch"),
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey,
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
taskIdentifier: taskId,
|
||||
dependentTaskAttemptId: dependentAttempt?.id,
|
||||
},
|
||||
@@ -70,8 +70,6 @@ export class BatchTriggerTaskService extends BaseService {
|
||||
let index = 0;
|
||||
|
||||
for (const item of body.items) {
|
||||
const idempotencyKey = nanoid();
|
||||
|
||||
const run = await triggerTaskService.call(
|
||||
taskId,
|
||||
environment,
|
||||
@@ -83,7 +81,6 @@ export class BatchTriggerTaskService extends BaseService {
|
||||
},
|
||||
},
|
||||
{
|
||||
idempotencyKey,
|
||||
triggerVersion: options.triggerVersion,
|
||||
traceContext: options.traceContext,
|
||||
spanParentAsLink: options.spanParentAsLink,
|
||||
@@ -96,6 +93,7 @@ export class BatchTriggerTaskService extends BaseService {
|
||||
data: {
|
||||
batchTaskRunId: batch.id,
|
||||
taskRunId: run.id,
|
||||
status: batchTaskRunItemStatusForRunStatus(run.status),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { marqs } from "~/v3/marqs/index.server";
|
||||
import { devPubSub } from "../marqs/devPubSub.server";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { socketIo } from "../handleSocketIo.server";
|
||||
import { assertUnreachable } from "../utils/asserts.server";
|
||||
import { CancelAttemptService } from "./cancelAttempt.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import assertNever from "assert-never";
|
||||
|
||||
export const CANCELLABLE_STATUSES: Array<TaskRunStatus> = [
|
||||
"PENDING",
|
||||
@@ -148,7 +148,7 @@ export class CancelTaskRunService extends BaseService {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(attempt.status);
|
||||
assertNever(attempt.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,12 @@ export class CompleteAttemptService extends BaseService {
|
||||
env?: AuthenticatedEnvironment;
|
||||
checkpoint?: CheckpointData;
|
||||
}): Promise<"COMPLETED" | "RETRIED"> {
|
||||
const taskRunAttempt = await findAttempt(this._prisma, completion.id);
|
||||
const taskRunAttempt = await findAttempt(this._prisma, execution.attempt.id);
|
||||
|
||||
if (!taskRunAttempt) {
|
||||
logger.error("[CompleteAttemptService] Task run attempt not found", { id: completion.id });
|
||||
logger.error("[CompleteAttemptService] Task run attempt not found", {
|
||||
id: execution.attempt.id,
|
||||
});
|
||||
|
||||
// Update the task run to be failed
|
||||
await this._prisma.taskRun.update({
|
||||
@@ -76,7 +78,7 @@ export class CompleteAttemptService extends BaseService {
|
||||
env?: AuthenticatedEnvironment
|
||||
): Promise<"COMPLETED"> {
|
||||
await this._prisma.taskRunAttempt.update({
|
||||
where: { friendlyId: completion.id },
|
||||
where: { id: taskRunAttempt.id },
|
||||
data: {
|
||||
status: "COMPLETED",
|
||||
completedAt: new Date(),
|
||||
@@ -144,7 +146,7 @@ export class CompleteAttemptService extends BaseService {
|
||||
}
|
||||
|
||||
await this._prisma.taskRunAttempt.update({
|
||||
where: { friendlyId: completion.id },
|
||||
where: { id: taskRunAttempt.id },
|
||||
data: {
|
||||
status: "FAILED",
|
||||
completedAt: new Date(),
|
||||
|
||||
@@ -12,7 +12,7 @@ export class ResumeTaskRunDependenciesService extends BaseService {
|
||||
include: {
|
||||
taskRun: {
|
||||
include: {
|
||||
batchItem: true,
|
||||
batchItems: true,
|
||||
dependency: {
|
||||
include: {
|
||||
dependentAttempt: true,
|
||||
@@ -34,14 +34,16 @@ export class ResumeTaskRunDependenciesService extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
const { batchItem, dependency } = taskAttempt.taskRun;
|
||||
const { batchItems, dependency } = taskAttempt.taskRun;
|
||||
|
||||
if (!batchItem && !dependency) {
|
||||
if (!batchItems.length && !dependency) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (batchItem) {
|
||||
await this.#resumeBatchItem(batchItem, taskAttempt);
|
||||
if (batchItems.length) {
|
||||
for (const batchItem of batchItems) {
|
||||
await this.#resumeBatchItem(batchItem, taskAttempt);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,18 +34,20 @@ export class TriggerTaskService extends BaseService {
|
||||
return await this.traceWithEnv("call()", environment, async (span) => {
|
||||
span.setAttribute("taskId", taskId);
|
||||
|
||||
const idempotencyKey = options.idempotencyKey ?? body.options?.idempotencyKey ?? nanoid();
|
||||
const idempotencyKey = options.idempotencyKey ?? body.options?.idempotencyKey;
|
||||
|
||||
const existingRun = await this._prisma.taskRun.findUnique({
|
||||
where: {
|
||||
runtimeEnvironmentId_idempotencyKey: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey,
|
||||
},
|
||||
},
|
||||
});
|
||||
const existingRun = idempotencyKey
|
||||
? await this._prisma.taskRun.findUnique({
|
||||
where: {
|
||||
runtimeEnvironmentId_idempotencyKey: {
|
||||
runtimeEnvironmentId: environment.id,
|
||||
idempotencyKey,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (existingRun) {
|
||||
if (existingRun && existingRun.taskIdentifier === taskId) {
|
||||
span.setAttribute("runId", existingRun.friendlyId);
|
||||
return existingRun;
|
||||
}
|
||||
@@ -68,6 +70,7 @@ export class TriggerTaskService extends BaseService {
|
||||
},
|
||||
runIsTest: body.options?.test ?? false,
|
||||
batchId: options.batchId,
|
||||
idempotencyKey,
|
||||
},
|
||||
incomplete: true,
|
||||
immediate: true,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function assertUnreachable(x: never): never {
|
||||
throw new Error("Didn't expect to get here");
|
||||
}
|
||||
@@ -96,6 +96,7 @@
|
||||
"@uiw/react-codemirror": "^4.19.5",
|
||||
"@upstash/ratelimit": "^1.0.1",
|
||||
"@whatwg-node/fetch": "^0.9.14",
|
||||
"assert-never": "^1.2.1",
|
||||
"aws4fetch": "^1.0.18",
|
||||
"class-variance-authority": "^0.5.2",
|
||||
"clsx": "^1.2.1",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
The CLI `dev` command runs a server for your tasks. It will watches for changes in your `/trigger` directory and communicates with the Trigger.dev platform to register your tasks, perform runs, and send data back and forth.
|
||||
|
||||
It can also update your `@trigger.dev/*` packages to prevent version mismatches and failed deploys. You will always be prompted first.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
|
||||
@@ -21,13 +21,16 @@ yarn dlx trigger.dev@beta deploy
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Warning>Will fail in CI if any version mismatches are detected. Ensure everything runs locally first using the [dev](/v3/cli-dev) command and don't bypass the version checks!</Warning>
|
||||
|
||||
It performs a few steps to deploy:
|
||||
|
||||
1. Typechecks the code.
|
||||
2. Compiles and bundles the code.
|
||||
3. Checks that [environment variables](/v3/deploy-environment-variables) are set.
|
||||
4. Deploys the code to the cloud.
|
||||
5. Registers the tasks as a new version in the environment (prod by default).
|
||||
1. Optionally updates packages when running locally.
|
||||
2. Typechecks the code.
|
||||
3. Compiles and bundles the code.
|
||||
4. Checks that [environment variables](/v3/deploy-environment-variables) are set.
|
||||
5. Deploys the code to the cloud.
|
||||
6. Registers the tasks as a new version in the environment (prod by default).
|
||||
|
||||
## Options
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ yarn dlx trigger.dev@beta dev
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
It will first perform an update check to prevent version mismatches, failed deploys, and other errors. You will always be prompted first.
|
||||
|
||||
You will see in the terminal that the server is running and listening for requests. When you run a task, you will see it in the terminal along with a link to view it in the dashboard.
|
||||
|
||||
It is worth noting that each task runs in a separate Node process. This means that if you have a long-running task, it will not block other tasks from running.
|
||||
|
||||
@@ -35,7 +35,7 @@ export const myTask = task({
|
||||
maxAttempts: 10,
|
||||
},
|
||||
run: async (payload: string) => {
|
||||
const result = await otherTask.triggerAndWait({ payload: "some data" });
|
||||
const result = await otherTask.triggerAndWait("some data");
|
||||
//...do other stuff
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ description: "You can easily deploy your tasks with GitHub actions."
|
||||
|
||||
This simple GitHub action file will deploy you Trigger.dev tasks when new code is pushed to the `main` branch and the `trigger` directory has changes in it.
|
||||
|
||||
<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
|
||||
|
||||
@@ -42,3 +44,19 @@ If you already have a GitHub action file, you can just add the final step "🚀
|
||||
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.
|
||||
|
||||
To set it in GitHub go to your repository, click on "Settings", "Secrets and variables" and then "Actions". Add a new secret with the name `TRIGGER_ACCESS_TOKEN` and use the value of your access token.
|
||||
|
||||
## Version pinning
|
||||
|
||||
The CLI and `@trigger.dev/*` package versions need to be in sync, otherwise there will be errors and unpredictable behavior. Hence, the `deploy` command will automatically fail during CI on any version mismatches.
|
||||
|
||||
To ensure a smooth CI experience you can pin the CLI version in the deploy step, like so:
|
||||
|
||||
```yaml .github/workflows/release-trigger.yml
|
||||
- name: 🚀 Deploy Trigger.dev
|
||||
env:
|
||||
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
|
||||
run: |
|
||||
npx trigger.dev@3.0.0-beta.16 deploy
|
||||
```
|
||||
|
||||
You should use the version you run locally during dev and manual deploy. The current version is displayed in the banner, but you can also check it by appending `--version` to any command.
|
||||
|
||||
@@ -11,7 +11,6 @@ This guide highlights the differences and should help you migrate your project.
|
||||
|
||||
Here are some features you might be using in Defer that are coming this month to v3:
|
||||
|
||||
- [Scheduled tasks (including CRON)](/v3/tasks-scheduled) will be available in mid-April.
|
||||
- Triggering a task with a delay (like `assignOptions` delay in Defer) will be available soon – there is [an alternative](#delay) you can use for now.
|
||||
|
||||
You can view the full feature matrix [here](/v3/feature-matrix).
|
||||
@@ -71,7 +70,7 @@ export async function runLongRunningTask() {
|
||||
}
|
||||
```
|
||||
|
||||
In Trigger.dev your logic goes in the `run` function of a task. You can then `trigger` and `batchTrigger` that task, with a payload and options.
|
||||
In Trigger.dev your logic goes in the `run` function of a task. You can then `trigger` and `batchTrigger` that task, with a payload as the first argument.
|
||||
|
||||
```ts /app/actions/actions.ts
|
||||
"use server";
|
||||
@@ -79,7 +78,7 @@ In Trigger.dev your logic goes in the `run` function of a task. You can then `tr
|
||||
import { longRunningTask } from "@/trigger/someTasks";
|
||||
|
||||
export async function runLongRunningTask() {
|
||||
return await longRunningTask.trigger({ payload: { foo: "bar" } });
|
||||
return await longRunningTask.trigger({ foo: "bar" });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -243,7 +242,7 @@ export const longRunningTask = task({
|
||||
import { longRunningTask } from "@/trigger/longRunningTask";
|
||||
|
||||
export async function runLongRunningTask() {
|
||||
return await longRunningTask.trigger({ payload: { foo: "bar" } });
|
||||
return await longRunningTask.trigger({ foo: "bar" });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -256,6 +255,36 @@ export async function runLongRunningTask() {
|
||||
|
||||
#### Example 2: A CRON task
|
||||
|
||||
<Warning>
|
||||
"Scheduled" tasks will be available by mid-April. This will allow you to replace Defer CRON tasks.
|
||||
</Warning>
|
||||
We call these [scheduled tasks](/v3/tasks-scheduled) in Trigger.dev.
|
||||
|
||||
In Defer you might have a function like this:
|
||||
|
||||
```ts
|
||||
import { defer } from "@defer/client";
|
||||
|
||||
async function sendMondayNewletter() {
|
||||
// business logic here
|
||||
}
|
||||
|
||||
export default defer.cron(sendMondayNewletter, "0 0 * * 1");
|
||||
```
|
||||
|
||||
In Trigger.dev the task looks like this:
|
||||
|
||||
```ts
|
||||
import { schedules } from "@trigger.dev/sdk/v3";
|
||||
|
||||
//this task will run when any of the attached schedules trigger
|
||||
export const sendMondayNewletter = schedules.task({
|
||||
id: "send-monday-newsletter",
|
||||
run: async (payload) => {
|
||||
// business logic here
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then you need to attach a schedule to the task, either using the dashboard or in your code. You can attach unlimited schedules to a task.
|
||||
|
||||
<Card title="Attaching schedules" icon="clock" href="/v3/tasks-scheduled">
|
||||
How to attach a schedule to a task
|
||||
</Card>
|
||||
|
||||
@@ -107,24 +107,19 @@ export async function POST(request: Request) {
|
||||
|
||||
if (data.branch === "main") {
|
||||
//trigger the task, with a different queue
|
||||
const handle = await generatePullRequest.trigger({
|
||||
payload: data,
|
||||
options: {
|
||||
queue: {
|
||||
//the "main-branch" queue will have a concurrency limit of 10
|
||||
//this triggered run will use that queue
|
||||
name: "main-branch",
|
||||
concurrencyLimit: 10,
|
||||
},
|
||||
const handle = await generatePullRequest.trigger(data, {
|
||||
queue: {
|
||||
//the "main-branch" queue will have a concurrency limit of 10
|
||||
//this triggered run will use that queue
|
||||
name: "main-branch",
|
||||
concurrencyLimit: 10,
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json(handle);
|
||||
} else {
|
||||
//triggered with the default (concurrency of 1)
|
||||
const handle = await generatePullRequest.trigger({
|
||||
payload: data,
|
||||
});
|
||||
const handle = await generatePullRequest.trigger(data);
|
||||
return Response.json(handle);
|
||||
}
|
||||
}
|
||||
@@ -146,32 +141,26 @@ export async function POST(request: Request) {
|
||||
|
||||
if (data.isFreeUser) {
|
||||
//free users can only have 1 PR generated at a time
|
||||
const handle = await generatePullRequest.trigger({
|
||||
payload: data,
|
||||
options: {
|
||||
queue: {
|
||||
//every free user gets a queue with a concurrency limit of 1
|
||||
name: "free-users",
|
||||
concurrencyLimit: 1,
|
||||
},
|
||||
concurrencyKey: data.userId,
|
||||
const handle = await generatePullRequest.trigger(data, {
|
||||
queue: {
|
||||
//every free user gets a queue with a concurrency limit of 1
|
||||
name: "free-users",
|
||||
concurrencyLimit: 1,
|
||||
},
|
||||
concurrencyKey: data.userId,
|
||||
});
|
||||
|
||||
//return a success response with the handle
|
||||
return Response.json(handle);
|
||||
} else {
|
||||
//trigger the task, with a different queue
|
||||
const handle = await generatePullRequest.trigger({
|
||||
payload: data,
|
||||
options: {
|
||||
queue: {
|
||||
//every paid user gets a queue with a concurrency limit of 10
|
||||
name: "paid-users",
|
||||
concurrencyLimit: 10,
|
||||
},
|
||||
concurrencyKey: data.userId,
|
||||
const handle = await generatePullRequest.trigger(data, {
|
||||
queue: {
|
||||
//every paid user gets a queue with a concurrency limit of 10
|
||||
name: "paid-users",
|
||||
concurrencyLimit: 10,
|
||||
},
|
||||
concurrencyKey: data.userId,
|
||||
});
|
||||
|
||||
//return a success response with the handle
|
||||
|
||||
@@ -37,7 +37,7 @@ import { helloWorldTask } from "./trigger/hello-world";
|
||||
|
||||
async function triggerHelloWorld() {
|
||||
//This triggers the task and return a handle
|
||||
const handle = await helloWorld.trigger({ payload: { message: "Hello world!" } });
|
||||
const handle = await helloWorld.trigger({ message: "Hello world!" });
|
||||
|
||||
//You can use the handle to check the status of the task, cancel and retry it.
|
||||
console.log("Task is running with handle", handle.id);
|
||||
|
||||
@@ -53,7 +53,7 @@ export const config: TriggerConfig = {
|
||||
|
||||
## ESM-only packages
|
||||
|
||||
We'll let you know when run the CLI dev command if this is a problem. Some packages are ESM-only so they don't work directly from CJS when using Node.js. In that case you need to add them to the `dependenciesToBundle` array in your `trigger.config.ts` file.
|
||||
We'll let you know when running the CLI dev command if this is a problem. Some packages are ESM-only so they don't work directly from CJS when using Node.js. In that case you need to add them to the `dependenciesToBundle` array in your `trigger.config.ts` file.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
@@ -92,7 +92,10 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
|
||||
|
||||
<Step title="package.json postinstall `prisma generate`">
|
||||
|
||||
```json
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```json default path
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "prisma generate"
|
||||
@@ -100,6 +103,16 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
|
||||
}
|
||||
```
|
||||
|
||||
```json custom path
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "prisma generate --schema=./custom/path/to/schema.prisma"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Anything you put in `postinstall` will be run as part of the install step. This is how Next.js recommends you set up Prisma anyway.
|
||||
|
||||
</Step>
|
||||
@@ -111,7 +124,12 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
//..other stuff
|
||||
|
||||
// using the default path
|
||||
additionalFiles: ["./prisma/schema.prisma"],
|
||||
// or a custom path, for example in a monorepo
|
||||
additionalFiles: ["../../custom/path/to/schema.prisma"],
|
||||
|
||||
additionalPackages: ["prisma@5.11.0"],
|
||||
};
|
||||
```
|
||||
|
||||
+36
-29
@@ -48,7 +48,7 @@ export async function POST(request: Request) {
|
||||
const data = await request.json();
|
||||
|
||||
//trigger your task
|
||||
const handle = await emailSequence.trigger({ payload: { to: data.email, name: data.name } });
|
||||
const handle = await emailSequence.trigger({ to: data.email, name: data.name });
|
||||
|
||||
//return a success response with the handle
|
||||
return Response.json(handle);
|
||||
@@ -67,7 +67,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
const data = await request.json();
|
||||
|
||||
//trigger your task
|
||||
const handle = await emailSequence.trigger({ payload: { to: data.email, name: data.name } });
|
||||
const handle = await emailSequence.trigger({ to: data.email, name: data.name });
|
||||
|
||||
//return a success response with the handle
|
||||
return json(handle);
|
||||
@@ -91,9 +91,9 @@ export async function POST(request: Request) {
|
||||
const data = await request.json();
|
||||
|
||||
//batch trigger your task
|
||||
const batchHandle = await emailSequence.batchTrigger({
|
||||
items: data.users.map((u) => ({ payload: { to: u.email, name: u.name } })),
|
||||
});
|
||||
const batchHandle = await emailSequence.batchTrigger(
|
||||
data.users.map((u) => ({ payload: { to: u.email, name: u.name } }))
|
||||
);
|
||||
|
||||
//return a success response with the handle
|
||||
return Response.json(batchHandle);
|
||||
@@ -112,9 +112,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
const data = await request.json();
|
||||
|
||||
//batch trigger your task
|
||||
const batchHandle = await emailSequence.batchTrigger({
|
||||
items: data.users.map((u) => ({ payload: { to: u.email, name: u.name } })),
|
||||
});
|
||||
const batchHandle = await emailSequence.batchTrigger(
|
||||
data.users.map((u) => ({ payload: { to: u.email, name: u.name } }))
|
||||
);
|
||||
|
||||
//return a success response with the handle
|
||||
return json(batchHandle);
|
||||
@@ -137,7 +137,7 @@ import { myOtherTask } from "~/trigger/my-other-task";
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload: string) => {
|
||||
const handle = await myOtherTask.trigger({ payload: "some data" });
|
||||
const handle = await myOtherTask.trigger("some data");
|
||||
|
||||
//...do other stuff
|
||||
},
|
||||
@@ -154,7 +154,7 @@ import { myOtherTask } from "~/trigger/my-other-task";
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload: string) => {
|
||||
const batchHandle = await myOtherTask.batchTrigger({ items: [{ payload: "some data" }] });
|
||||
const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }]);
|
||||
|
||||
//...do other stuff
|
||||
},
|
||||
@@ -168,16 +168,18 @@ This is where it gets interesting. You can trigger a task and then wait for the
|
||||
<Accordion title="Don't use this in parallel, e.g. with `Promise.all()`">
|
||||
Instead, use `batchTriggerAndWait()` if you can, or a for loop if you can't.
|
||||
|
||||
To control concurrency using batch triggers, you can set `queue.concurrencyLimit` on the child task.
|
||||
To control concurrency using batch triggers, you can set `queue.concurrencyLimit` on the child task.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts /trigger/batch.ts
|
||||
export const batchTask = task({
|
||||
id: "batch-task",
|
||||
run: async (payload: string) => {
|
||||
const results = await childTask.batchTriggerAndWait({
|
||||
items: [{ payload: "item1" }, { payload: "item2" }],
|
||||
});
|
||||
const results = await childTask.batchTriggerAndWait([
|
||||
{ payload: "item1" },
|
||||
{ payload: "item2" },
|
||||
]);
|
||||
console.log("Results", results);
|
||||
|
||||
//...do stuff with the results
|
||||
@@ -192,7 +194,7 @@ export const loopTask = task({
|
||||
//this will be slower than the batch version
|
||||
//as we have to resume the parent after each iteration
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = await childTask.triggerAndWait({ payload: `item${i}` });
|
||||
const result = await childTask.triggerAndWait(`item${i}`);
|
||||
console.log("Result", result);
|
||||
|
||||
//...do stuff with the result
|
||||
@@ -200,6 +202,7 @@ export const loopTask = task({
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Accordion>
|
||||
@@ -208,7 +211,7 @@ export const loopTask = task({
|
||||
export const parentTask = task({
|
||||
id: "parent-task",
|
||||
run: async (payload: string) => {
|
||||
const result = await batchChildTask.triggerAndWait({ payload: "some-data" });
|
||||
const result = await batchChildTask.triggerAndWait("some-data");
|
||||
console.log("Result", result);
|
||||
|
||||
//...do stuff with the result
|
||||
@@ -223,16 +226,18 @@ You can batch trigger a task and wait for all the results. This is useful for th
|
||||
<Accordion title="Don't use this in parallel, e.g. with `Promise.all()`">
|
||||
Instead, pass in all items at once and set an appropriate `maxConcurrency`. Alternatively, use sequentially with a for loop.
|
||||
|
||||
To control concurrency, you can set `queue.concurrencyLimit` on the child task.
|
||||
To control concurrency, you can set `queue.concurrencyLimit` on the child task.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```ts /trigger/batch.ts
|
||||
export const batchTask = task({
|
||||
id: "batch-task",
|
||||
run: async (payload: string) => {
|
||||
const results = await childTask.batchTriggerAndWait({
|
||||
items: [{ payload: "item1" }, { payload: "item2" }],
|
||||
});
|
||||
const results = await childTask.batchTriggerAndWait([
|
||||
{ payload: "item1" },
|
||||
{ payload: "item2" },
|
||||
]);
|
||||
console.log("Results", results);
|
||||
|
||||
//...do stuff with the results
|
||||
@@ -247,9 +252,10 @@ export const loopTask = task({
|
||||
//this will be slower than a single batchTriggerAndWait()
|
||||
//as we have to resume the parent after each iteration
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = await childTask.batchTriggerAndWait({
|
||||
items: [{ payload: `itemA${i}` }, { payload: `itemB${i}` }],
|
||||
});
|
||||
const result = await childTask.batchTriggerAndWait([
|
||||
{ payload: `itemA${i}` },
|
||||
{ payload: `itemB${i}` },
|
||||
]);
|
||||
console.log("Result", result);
|
||||
|
||||
//...do stuff with the result
|
||||
@@ -257,6 +263,7 @@ export const loopTask = task({
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Accordion>
|
||||
@@ -265,9 +272,11 @@ export const loopTask = task({
|
||||
export const batchParentTask = task({
|
||||
id: "parent-task",
|
||||
run: async (payload: string) => {
|
||||
const results = await childTask.batchTriggerAndWait({
|
||||
items: [{ payload: "item4" }, { payload: "item5" }, { payload: "item6" }],
|
||||
});
|
||||
const results = await childTask.batchTriggerAndWait([
|
||||
{ payload: "item4" },
|
||||
{ payload: "item5" },
|
||||
{ payload: "item6" },
|
||||
]);
|
||||
console.log("Results", results);
|
||||
|
||||
//...do stuff with the result
|
||||
@@ -326,9 +335,7 @@ import { createAvatar } from "@/trigger/create-avatar";
|
||||
export async function create() {
|
||||
try {
|
||||
const handle = await createAvatar.trigger({
|
||||
payload: {
|
||||
userImage: "http://...",
|
||||
},
|
||||
userImage: "http://...",
|
||||
});
|
||||
|
||||
return { handle };
|
||||
|
||||
@@ -164,34 +164,14 @@ We've unified triggering in v3. You use `trigger()` or `batchTrigger()` which yo
|
||||
async function yourBackendFunction() {
|
||||
//call `trigger()` on any task
|
||||
const handle = await openaiTask.trigger({
|
||||
payload: {
|
||||
prompt: "Tell me a programming joke",
|
||||
},
|
||||
prompt: "Tell me a programming joke",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrading your project
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Upgrade the v2 Trigger.dev packages">
|
||||
|
||||
You can run this command to upgrade all the packages to the beta:
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli@beta update --to beta
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Follow the v3 quick start">
|
||||
|
||||
Follow the [v3 quick start](/v3/quick-start) to get started with v3.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
Just follow the [v3 quick start](/v3/quick-start) to get started with v3. Our new CLI will take care of the rest.
|
||||
|
||||
## Using v2 together with v3
|
||||
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/airtable
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/airtable",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"airtable": "^0.12.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/github
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/github",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/linear
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/linear",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/openai",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.14"
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/plain
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/plain",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"@team-plain/typescript-sdk": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/replicate
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/replicate",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"replicate": "^0.18.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/resend
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/resend",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"resend": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/sendgrid
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/sendgrid",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.14"
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/shopify
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/shopify",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/slack",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/stripe
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/stripe",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"stripe": "^12.14.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/supabase
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/supabase",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"supabase-management-js": "^1.0.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/typeform
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/typeform",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.14",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.17",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17",
|
||||
"@typeform/api-client": "^1.8.0",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @trigger.dev/astro
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- @trigger.dev/sdk@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@trigger.dev/astro",
|
||||
"description": "An Astro-native integration for Trigger.dev background jobs platform",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"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.14"
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^3.0.12",
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# trigger.dev
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b20760173: v3 CLI update command and package manager detection fix
|
||||
- @trigger.dev/core@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ed2a26c86: - Fix additionalFiles that aren't decendants
|
||||
- Stop swallowing uncaught exceptions in prod
|
||||
- Improve warnings and errors, fail early on critical warnings
|
||||
- New arg to --save-logs even for successful builds
|
||||
- Updated dependencies [ed2a26c86]
|
||||
- @trigger.dev/core@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 26093896d: When using idempotency keys, triggerAndWait and batchTriggerAndWait will still work even if the existing runs have already been completed (or even partially completed, in the case of batchTriggerAndWait)
|
||||
|
||||
- TaskRunExecutionResult.id is now the run friendlyId, not the attempt friendlyId
|
||||
- A single TaskRun can now have many batchItems, in the case of batchTriggerAndWait while using idempotency keys
|
||||
- A run’s idempotencyKey is now added to the ctx as well as the TaskEvent and displayed in the span view
|
||||
- When resolving batchTriggerAndWait, the runtimes no longer reject promises, leading to an error in the parent task
|
||||
|
||||
- b82db67b8: Add additional logging around cleaning up dev workers, and always kill them after 5 seconds if they haven't already exited
|
||||
- 62c9a5b71: Fixes an issue that caused failed tasks when resuming after calling `triggerAndWait` or `batchTriggerAndWait` in prod/staging (this doesn't effect dev).
|
||||
|
||||
The version of Node.js we use for deployed workers (latest 20) would crash with an out-of-memory error when the checkpoint was restored. This crash does not happen on Node 18x or Node21x, so we've decided to upgrade the worker version to Node.js21x, to mitigate this issue.
|
||||
|
||||
You'll need to re-deploy to production to fix the issue.
|
||||
|
||||
- Updated dependencies [374edef02]
|
||||
- Updated dependencies [26093896d]
|
||||
- Updated dependencies [62c9a5b71]
|
||||
- @trigger.dev/core@3.0.0-beta.15
|
||||
|
||||
## 3.0.0-beta.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trigger.dev",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.0.0-beta.17",
|
||||
"description": "A Command-Line Interface for Trigger.dev (v3) projects",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -85,7 +85,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.14",
|
||||
"@trigger.dev/core": "workspace:^3.0.0-beta.17",
|
||||
"@types/degit": "^2.8.3",
|
||||
"chalk": "^5.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
@@ -106,7 +106,6 @@
|
||||
"mock-fs": "^5.2.0",
|
||||
"nanoid": "^4.0.2",
|
||||
"node-fetch": "^3.3.0",
|
||||
"npm-check-updates": "^16.12.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"p-debounce": "^4.0.0",
|
||||
"p-throttle": "^6.1.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-bookworm-slim@sha256:d4cdfc305abe5ea78da7167bf78263c22596dc332f2654b662890777ea166224 AS base
|
||||
FROM node:21-bookworm-slim@sha256:fb82287cf66ca32d854c05f54251fca8b572149163f154248df7e800003c90b5 AS base
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
@@ -8,6 +8,7 @@ import { configureWhoamiCommand } from "../commands/whoami.js";
|
||||
import { COMMAND_NAME } from "../consts.js";
|
||||
import { getVersion } from "../utilities/getVersion.js";
|
||||
import { configureListProfilesCommand } from "../commands/list-profiles.js";
|
||||
import { configureUpdateCommand } from "../commands/update.js";
|
||||
|
||||
export const program = new Command();
|
||||
|
||||
@@ -23,3 +24,4 @@ configureDeployCommand(program);
|
||||
configureWhoamiCommand(program);
|
||||
configureLogoutCommand(program);
|
||||
configureListProfilesCommand(program);
|
||||
configureUpdateCommand(program);
|
||||
|
||||
@@ -43,7 +43,7 @@ import { logger } from "../utilities/logger.js";
|
||||
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
|
||||
import { login } from "./login";
|
||||
|
||||
import { Glob } from "glob";
|
||||
import { Glob, GlobOptions } from "glob";
|
||||
import type { SetOptional } from "type-fest";
|
||||
import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../utilities/build";
|
||||
import { chalkError, chalkPurple, chalkWarning } from "../utilities/cliOutput";
|
||||
@@ -57,6 +57,8 @@ import { safeJsonParse } from "../utilities/safeJsonParse";
|
||||
import { JavascriptProject } from "../utilities/javascriptProject";
|
||||
import { cliRootPath } from "../utilities/resolveInternalFilePath";
|
||||
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),
|
||||
@@ -72,6 +74,8 @@ const DeployCommandOptions = CommonCommandOptions.extend({
|
||||
projectRef: z.string().optional(),
|
||||
outputMetafile: z.string().optional(),
|
||||
apiUrl: z.string().optional(),
|
||||
saveLogs: z.boolean().default(false),
|
||||
skipUpdateCheck: z.boolean().default(false),
|
||||
});
|
||||
|
||||
type DeployCommandOptions = z.infer<typeof DeployCommandOptions>;
|
||||
@@ -88,6 +92,7 @@ export function configureDeployCommand(program: Command) {
|
||||
"prod"
|
||||
)
|
||||
.option("--skip-typecheck", "Whether to skip the pre-build typecheck")
|
||||
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
|
||||
.option(
|
||||
"--ignore-env-var-check",
|
||||
"Detected missing environment variables won't block deployment"
|
||||
@@ -140,6 +145,12 @@ export function configureDeployCommand(program: Command) {
|
||||
"If provided, will save the esbuild metafile for the build to the specified path"
|
||||
).hideHelp()
|
||||
)
|
||||
.addOption(
|
||||
new CommandOption(
|
||||
"--save-logs",
|
||||
"If provided, will save logs even for successful builds"
|
||||
).hideHelp()
|
||||
)
|
||||
.action(async (path, options) => {
|
||||
await handleTelemetry(async () => {
|
||||
await printStandloneInitialBanner(true);
|
||||
@@ -159,6 +170,10 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
|
||||
intro("Deploying project");
|
||||
|
||||
if (!options.skipUpdateCheck) {
|
||||
await updateTriggerPackages(dir, { ...options }, true, true);
|
||||
}
|
||||
|
||||
const authorization = await login({
|
||||
embedded: true,
|
||||
defaultApiUrl: options.apiUrl,
|
||||
@@ -306,25 +321,42 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
|
||||
const image = await buildImage();
|
||||
|
||||
const warnings = checkLogsForWarnings(image.logs);
|
||||
|
||||
if (!warnings.ok) {
|
||||
await failDeploy(
|
||||
deploymentResponse.data.shortCode,
|
||||
warnings.summary,
|
||||
image.logs,
|
||||
deploymentSpinner,
|
||||
warnings.warnings,
|
||||
warnings.errors
|
||||
);
|
||||
|
||||
throw new SkipLoggingError(`Failed to build project image: ${warnings.summary}`);
|
||||
}
|
||||
|
||||
if (!image.ok) {
|
||||
deploymentSpinner.stop(`Failed to build project.`);
|
||||
|
||||
// If there are logs, let's write it out to a temporary file and include the path in the error message
|
||||
if (image.logs.trim() !== "") {
|
||||
const logPath = join(await createTempDir(), `build-${deploymentResponse.data.shortCode}.log`);
|
||||
|
||||
await writeFile(logPath, image.logs);
|
||||
|
||||
logger.log(
|
||||
`${chalkError("X Error:")} ${image.error}. Full build logs have been saved to ${logPath})`
|
||||
);
|
||||
} else {
|
||||
logger.log(`${chalkError("X Error:")} ${image.error}.`);
|
||||
}
|
||||
await failDeploy(
|
||||
deploymentResponse.data.shortCode,
|
||||
image.error,
|
||||
image.logs,
|
||||
deploymentSpinner,
|
||||
warnings.warnings
|
||||
);
|
||||
|
||||
throw new SkipLoggingError(`Failed to build project image: ${image.error}`);
|
||||
}
|
||||
|
||||
const preExitTasks = async () => {
|
||||
printWarnings(warnings.warnings);
|
||||
|
||||
if (options.saveLogs) {
|
||||
const logPath = await saveLogs(deploymentResponse.data.shortCode, image.logs);
|
||||
log.info(`Build logs have been saved to ${logPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
const imageReference = options.selfHosted
|
||||
? `${selfHostedRegistryHost ? `${selfHostedRegistryHost}/` : ""}${image.image}${
|
||||
image.digest ? `@${image.digest}` : ""
|
||||
@@ -340,6 +372,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
`Project image built: ${imageReference}. Skipping deployment as requested`
|
||||
);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipCommandError("Skipping deployment as requested");
|
||||
}
|
||||
|
||||
@@ -359,6 +393,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
if (!startIndexingResponse.success) {
|
||||
deploymentSpinner.stop(`Failed to start indexing: ${startIndexingResponse.error}`);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError(`Failed to start indexing: ${startIndexingResponse.error}`);
|
||||
}
|
||||
|
||||
@@ -370,12 +406,16 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
if (!finishedDeployment) {
|
||||
deploymentSpinner.stop(`Deployment failed to complete`);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError("Deployment failed to complete: unknown issue");
|
||||
}
|
||||
|
||||
if (typeof finishedDeployment === "string") {
|
||||
deploymentSpinner.stop(`Deployment failed to complete: ${finishedDeployment}`);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError(`Deployment failed to complete: ${finishedDeployment}`);
|
||||
}
|
||||
|
||||
@@ -386,7 +426,13 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
|
||||
switch (finishedDeployment.status) {
|
||||
case "DEPLOYED": {
|
||||
deploymentSpinner.stop("Deployment completed");
|
||||
if (warnings.warnings.length > 0) {
|
||||
deploymentSpinner.stop("Deployment completed with warnings");
|
||||
} else {
|
||||
deploymentSpinner.stop("Deployment completed");
|
||||
}
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
const taskCount = finishedDeployment.worker?.tasks.length ?? 0;
|
||||
|
||||
@@ -417,6 +463,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
|
||||
logTaskMetadataParseError(parsedError.data.zodIssues, parsedError.data.tasks);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError(
|
||||
`Deployment encountered an error: ${finishedDeployment.errorData.name}`
|
||||
);
|
||||
@@ -439,6 +487,8 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
logESMRequireError(parsedError, resolvedConfig);
|
||||
}
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError(
|
||||
`Deployment encountered an error: ${finishedDeployment.errorData.name}`
|
||||
);
|
||||
@@ -447,22 +497,189 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
||||
`Deployment failed with an unknown error. Please contact eric@trigger.dev for help. ${deploymentLink}`
|
||||
);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError("Deployment failed with an unknown error");
|
||||
}
|
||||
}
|
||||
case "CANCELED": {
|
||||
deploymentSpinner.stop(`Deployment was canceled. ${deploymentLink}`);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError("Deployment was canceled");
|
||||
}
|
||||
case "TIMED_OUT": {
|
||||
deploymentSpinner.stop(`Deployment timed out. ${deploymentLink}`);
|
||||
|
||||
await preExitTasks();
|
||||
|
||||
throw new SkipLoggingError("Deployment timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printErrors(errors?: string[]) {
|
||||
for (const error of errors ?? []) {
|
||||
log.error(`${chalkError("Error:")} ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printWarnings(warnings?: string[]) {
|
||||
for (const warning of warnings ?? []) {
|
||||
log.warn(`${chalkWarning("Warning:")} ${warning}`);
|
||||
}
|
||||
}
|
||||
|
||||
type WarningsCheckReturn =
|
||||
| {
|
||||
ok: true;
|
||||
warnings: string[];
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
summary: string;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
type LogParserOptions = Array<{
|
||||
regex: RegExp;
|
||||
message: string;
|
||||
shouldFail?: boolean;
|
||||
}>;
|
||||
|
||||
// Try to extract useful warnings from logs. Sometimes we may even want to fail the build. This won't work if the step is cached.
|
||||
function checkLogsForWarnings(logs: string): WarningsCheckReturn {
|
||||
const warnings: LogParserOptions = [
|
||||
{
|
||||
regex: /prisma:warn We could not find your Prisma schema/,
|
||||
message: `Prisma generate failed to find the default schema. Did you include it in config.additionalFiles? ${terminalLink(
|
||||
"Config docs",
|
||||
docs.config.prisma
|
||||
)}\nCustom schema paths require a postinstall script like this: \`prisma generate --schema=./custom/path/to/schema.prisma\``,
|
||||
shouldFail: true,
|
||||
},
|
||||
];
|
||||
|
||||
const errorMessages: string[] = [];
|
||||
const warningMessages: string[] = [];
|
||||
|
||||
let shouldFail = false;
|
||||
|
||||
for (const warning of warnings) {
|
||||
const matches = logs.match(warning.regex);
|
||||
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = getMessageFromTemplate(warning.message, matches.groups);
|
||||
|
||||
if (warning.shouldFail) {
|
||||
shouldFail = true;
|
||||
errorMessages.push(message);
|
||||
} else {
|
||||
warningMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFail) {
|
||||
return {
|
||||
ok: false,
|
||||
summary: "Build succeeded with critical warnings. Will not proceed",
|
||||
warnings: warningMessages,
|
||||
errors: errorMessages,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
warnings: warningMessages,
|
||||
};
|
||||
}
|
||||
|
||||
// Try to extract useful error messages from the logs
|
||||
function checkLogsForErrors(logs: string) {
|
||||
const errors: LogParserOptions = [
|
||||
{
|
||||
regex: /Error: Provided --schema at (?<schema>.*) doesn't exist/,
|
||||
message: `Prisma generate failed to find the specified schema at "$schema".\nDid you include it in config.additionalFiles? ${terminalLink(
|
||||
"Config docs",
|
||||
docs.config.prisma
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
regex: /sh: 1: (?<packageOrBinary>.*): not found/,
|
||||
message: `$packageOrBinary not found\n\nIf it's a package: Include it in ${terminalLink(
|
||||
"config.additionalPackages",
|
||||
docs.config.prisma
|
||||
)}\nIf it's a binary: Please ${terminalLink(
|
||||
"get in touch",
|
||||
getInTouch
|
||||
)} and we'll see what we can do!`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const error of errors) {
|
||||
const matches = logs.match(error.regex);
|
||||
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = getMessageFromTemplate(error.message, matches.groups);
|
||||
|
||||
log.error(`${chalkError("Error:")} ${message}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageFromTemplate(template: string, replacer: RegExpMatchArray["groups"]) {
|
||||
let message = template;
|
||||
|
||||
if (replacer) {
|
||||
for (const [key, value] of Object.entries(replacer)) {
|
||||
message = message.replaceAll(`$${key}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
async function saveLogs(shortCode: string, logs: string) {
|
||||
const logPath = join(await createTempDir(), `build-${shortCode}.log`);
|
||||
await writeFile(logPath, logs);
|
||||
return logPath;
|
||||
}
|
||||
|
||||
async function failDeploy(
|
||||
shortCode: string,
|
||||
errorSummary: string,
|
||||
logs: string,
|
||||
deploymentSpinner: ReturnType<typeof spinner>,
|
||||
warnings?: string[],
|
||||
errors?: string[]
|
||||
) {
|
||||
deploymentSpinner.stop(`Failed to deploy project`);
|
||||
|
||||
// If there are logs, let's write it out to a temporary file and include the path in the error message
|
||||
if (logs.trim() !== "") {
|
||||
const logPath = await saveLogs(shortCode, logs);
|
||||
|
||||
printWarnings(warnings);
|
||||
printErrors(errors);
|
||||
|
||||
checkLogsForErrors(logs);
|
||||
|
||||
outro(`${chalkError("Error:")} ${errorSummary}. Full build logs have been saved to ${logPath}`);
|
||||
} else {
|
||||
outro(`${chalkError("Error:")} ${errorSummary}.`);
|
||||
}
|
||||
|
||||
// TODO: Let platform know so it can fail the deploy with an appropriate error
|
||||
}
|
||||
|
||||
async function checkEnvVars(
|
||||
envVars: string[],
|
||||
config: ResolvedConfig,
|
||||
@@ -944,7 +1161,7 @@ async function compileProject(
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
const workerSetupPath = join(cliRootPath(), "workers", "dev", "worker-setup.js");
|
||||
const workerSetupPath = join(cliRootPath(), "workers", "prod", "worker-setup.js");
|
||||
|
||||
let workerContents = workerFacade
|
||||
.replace("__TASKS__", createTaskFileImports(taskFiles))
|
||||
@@ -1139,9 +1356,22 @@ async function compileProject(
|
||||
|
||||
await writeJSONFile(join(tempDir, "package.json"), packageJsonContents);
|
||||
|
||||
await copyAdditionalFiles(config, tempDir);
|
||||
const copyResult = await copyAdditionalFiles(config, tempDir);
|
||||
|
||||
compileSpinner.stop("Project built successfully");
|
||||
if (!copyResult.ok) {
|
||||
compileSpinner.stop("Project built with warnings");
|
||||
|
||||
log.warn(
|
||||
`No additionalFiles matches for:\n\n${copyResult.noMatches
|
||||
.map((glob) => `- "${glob}"`)
|
||||
.join("\n")}\n\nIf this is unexpected you should check your ${terminalLink(
|
||||
"glob patterns",
|
||||
"https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer"
|
||||
)} are valid.`
|
||||
);
|
||||
} else {
|
||||
compileSpinner.stop("Project built successfully");
|
||||
}
|
||||
|
||||
const resolvingDependenciesResult = await resolveDependencies(
|
||||
tempDir,
|
||||
@@ -1443,11 +1673,24 @@ async function gatherRequiredDependencies(
|
||||
return Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
|
||||
}
|
||||
|
||||
async function copyAdditionalFiles(config: ResolvedConfig, tempDir: string) {
|
||||
type AdditionalFilesReturn =
|
||||
| {
|
||||
ok: true;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
noMatches: string[];
|
||||
};
|
||||
|
||||
async function copyAdditionalFiles(
|
||||
config: ResolvedConfig,
|
||||
tempDir: string
|
||||
): Promise<AdditionalFilesReturn> {
|
||||
const additionalFiles = config.additionalFiles ?? [];
|
||||
const noMatches: string[] = [];
|
||||
|
||||
if (additionalFiles.length === 0) {
|
||||
return;
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
return await tracer.startActiveSpan(
|
||||
@@ -1463,25 +1706,77 @@ async function copyAdditionalFiles(config: ResolvedConfig, tempDir: string) {
|
||||
additionalFiles,
|
||||
});
|
||||
|
||||
const glob = new Glob(additionalFiles, {
|
||||
const globOptions = {
|
||||
withFileTypes: true,
|
||||
ignore: ["node_modules"],
|
||||
cwd: config.projectDir,
|
||||
nodir: true,
|
||||
});
|
||||
} satisfies GlobOptions;
|
||||
|
||||
for await (const file of glob) {
|
||||
const relativeDestinationPath = join(
|
||||
tempDir,
|
||||
relative(config.projectDir, file.fullpath())
|
||||
);
|
||||
const globs: Array<GlobOptions> = [];
|
||||
let i = 0;
|
||||
|
||||
logger.debug(`Copying file ${file.fullpath()} to ${relativeDestinationPath}`);
|
||||
await mkdir(dirname(relativeDestinationPath), { recursive: true });
|
||||
await copyFile(file.fullpath(), relativeDestinationPath);
|
||||
for (const additionalFile of additionalFiles) {
|
||||
let glob: GlobOptions | Glob<typeof globOptions>;
|
||||
|
||||
if (i === 0) {
|
||||
glob = new Glob(additionalFile, globOptions);
|
||||
} else {
|
||||
const previousGlob = globs[i - 1];
|
||||
if (!previousGlob) {
|
||||
logger.error("No previous glob, this shouldn't happen", { i, additionalFiles });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the previous glob's options and cache
|
||||
glob = new Glob(additionalFile, previousGlob);
|
||||
}
|
||||
|
||||
if (!(Symbol.asyncIterator in glob)) {
|
||||
logger.error("Glob should be an async iterator", { glob });
|
||||
throw new Error("Unrecoverable error while copying additional files");
|
||||
}
|
||||
|
||||
let matches = 0;
|
||||
for await (const file of glob) {
|
||||
matches++;
|
||||
|
||||
// Any additional files that aren't a child of projectDir will be moved inside tempDir, so they can be part of the build context
|
||||
// The file "../foo/bar" will be written to "tempDir/foo/bar"
|
||||
// The file "../../bar/baz" will be written to "tempDir/bar/baz"
|
||||
const pathInsideTempDir = relative(config.projectDir, file.fullpath())
|
||||
.split(posix.sep)
|
||||
.filter((p) => p !== "..")
|
||||
.join(posix.sep);
|
||||
|
||||
const relativeDestinationPath = join(tempDir, pathInsideTempDir);
|
||||
|
||||
logger.debug(`Copying file ${file.fullpath()} to ${relativeDestinationPath}`);
|
||||
|
||||
await mkdir(dirname(relativeDestinationPath), { recursive: true });
|
||||
await copyFile(file.fullpath(), relativeDestinationPath);
|
||||
}
|
||||
|
||||
if (matches === 0) {
|
||||
noMatches.push(additionalFile);
|
||||
}
|
||||
|
||||
globs[i] = glob;
|
||||
i++;
|
||||
}
|
||||
|
||||
span.end();
|
||||
|
||||
if (noMatches.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
noMatches,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
} as const;
|
||||
} catch (error) {
|
||||
recordSpanException(span, error);
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import { findUp, pathExists } from "find-up";
|
||||
import { cliRootPath } from "../utilities/resolveInternalFilePath";
|
||||
import { escapeImportPath } from "../utilities/windows";
|
||||
import { updateTriggerPackages } from "./update";
|
||||
|
||||
let apiClient: CliApiClient | undefined;
|
||||
|
||||
@@ -61,6 +62,7 @@ const DevCommandOptions = CommonCommandOptions.extend({
|
||||
debugOtel: z.boolean().default(false),
|
||||
config: z.string().optional(),
|
||||
projectRef: z.string().optional(),
|
||||
skipUpdateCheck: z.boolean().default(false),
|
||||
});
|
||||
|
||||
type DevCommandOptions = z.infer<typeof DevCommandOptions>;
|
||||
@@ -78,6 +80,7 @@ export function configureDevCommand(program: Command) {
|
||||
)
|
||||
.option("--debugger", "Enable the debugger")
|
||||
.option("--debug-otel", "Enable OpenTelemetry debugging")
|
||||
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
|
||||
).action(async (path, options) => {
|
||||
wrapCommandAction("dev", DevCommandOptions, options, async (opts) => {
|
||||
await devCommand(path, opts);
|
||||
@@ -132,7 +135,13 @@ async function startDev(
|
||||
}
|
||||
|
||||
await printStandloneInitialBanner(true);
|
||||
printDevBanner();
|
||||
|
||||
if (!options.skipUpdateCheck) {
|
||||
console.log(); // spacing
|
||||
await updateTriggerPackages(dir, { ...options }, false, true);
|
||||
}
|
||||
|
||||
printDevBanner(!options.skipUpdateCheck);
|
||||
|
||||
logger.debug("Starting dev session", { dir, options, authorization });
|
||||
|
||||
|
||||
@@ -1,148 +1,300 @@
|
||||
import { confirm } from "@clack/prompts";
|
||||
import { RunOptions, run } from "npm-check-updates";
|
||||
import path from "path";
|
||||
import { confirm, intro, isCancel, log, outro } from "@clack/prompts";
|
||||
import { z } from "zod";
|
||||
import { chalkError, chalkSuccess } from "../utilities/cliOutput.js";
|
||||
import { readJSONFileSync, writeJSONFile } from "../utilities/fileSystem.js";
|
||||
import { installDependencies } from "../utilities/installDependencies.js";
|
||||
import { readJSONFile, removeFile, writeJSONFile } from "../utilities/fileSystem.js";
|
||||
import { spinner } from "../utilities/windows.js";
|
||||
import { CommonCommandOptions, OutroCommandError, wrapCommandAction } from "../cli/common.js";
|
||||
import { Command } from "commander";
|
||||
import { logger } from "../utilities/logger.js";
|
||||
import { PackageJson } from "type-fest";
|
||||
import { printStandloneInitialBanner, updateCheck } from "../utilities/initialBanner.js";
|
||||
import { join, resolve } from "path";
|
||||
import { JavascriptProject } from "../utilities/javascriptProject.js";
|
||||
import { PackageManager } from "../utilities/getUserPackageManager.js";
|
||||
import { getVersion } from "../utilities/getVersion.js";
|
||||
import { chalkError, prettyWarning } from "../utilities/cliOutput.js";
|
||||
|
||||
export const UpdateCommandOptionsSchema = z.object({
|
||||
to: z.string().optional(),
|
||||
export const UpdateCommandOptions = CommonCommandOptions.pick({
|
||||
logLevel: true,
|
||||
skipTelemetry: true,
|
||||
});
|
||||
|
||||
export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptionsSchema>;
|
||||
export type UpdateCommandOptions = z.infer<typeof UpdateCommandOptions>;
|
||||
|
||||
type NcuRunOptionTarget = "latest" | `@${string}`;
|
||||
|
||||
export async function updateCommand(projectPath: string, anyOptions: any) {
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start("Checking settings");
|
||||
|
||||
const parseRes = UpdateCommandOptionsSchema.safeParse(anyOptions);
|
||||
if (!parseRes.success) {
|
||||
loadingSpinner.stop(chalkError(parseRes.error.message));
|
||||
return;
|
||||
}
|
||||
const options = parseRes.data;
|
||||
|
||||
const triggerDevPackage = "@trigger.dev";
|
||||
const packageJSONPath = path.join(projectPath, "package.json");
|
||||
const packageData = readJSONFileSync(packageJSONPath);
|
||||
if (!packageData) {
|
||||
loadingSpinner.stop(chalkError("Couldn't load package.json"));
|
||||
return;
|
||||
}
|
||||
|
||||
loadingSpinner.message("Checking for updates");
|
||||
|
||||
const packageMaps: { [k: string]: { type: string; version: string } } = {};
|
||||
const packageDependencies = packageData.dependencies || {};
|
||||
const packageDevDependencies = packageData.devDependencies || {};
|
||||
Object.keys(packageDependencies).forEach((i) => {
|
||||
packageMaps[i] = { type: "dependencies", version: packageDependencies[i] };
|
||||
});
|
||||
Object.keys(packageDevDependencies).forEach((i) => {
|
||||
packageMaps[i] = {
|
||||
type: "devDependencies",
|
||||
version: packageDevDependencies[i],
|
||||
};
|
||||
});
|
||||
|
||||
const targetVersion = getTargetVersion(options.to);
|
||||
|
||||
// Use npm-check-updates to get updated dependency versions
|
||||
const ncuOptions: RunOptions = {
|
||||
packageData,
|
||||
upgrade: true,
|
||||
jsonUpgraded: true,
|
||||
target: targetVersion,
|
||||
};
|
||||
|
||||
// Can either give a json like package.json or just with deps and their new versions
|
||||
const updatedDependencies: { [k: string]: any } | void = await run(ncuOptions);
|
||||
|
||||
if (!updatedDependencies) {
|
||||
loadingSpinner.stop(chalkError("Couldn't update dependencies"));
|
||||
return;
|
||||
}
|
||||
|
||||
const ifUpdatedDependenciesIsPackageJSON =
|
||||
updatedDependencies.hasOwnProperty("dependencies") ||
|
||||
updatedDependencies.hasOwnProperty("devDependencies");
|
||||
|
||||
const dependencies = updatedDependencies.dependencies || {};
|
||||
const devDependencies = updatedDependencies.devDependencies || {};
|
||||
|
||||
const allDependencies = ifUpdatedDependenciesIsPackageJSON
|
||||
? Object.keys({ ...dependencies, ...devDependencies })
|
||||
: Object.keys(updatedDependencies);
|
||||
|
||||
const triggerPackages = allDependencies.filter((pkg) => pkg.startsWith(triggerDevPackage));
|
||||
|
||||
// If there are no @trigger.dev packages
|
||||
if (triggerPackages.length === 0) {
|
||||
loadingSpinner.stop(chalkSuccess(`All @trigger.dev/* packages are already up to date.`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter the packages with null and what don't match what
|
||||
// they are installed with so that they can be updated
|
||||
const packagesToUpdate = triggerPackages.filter((pkg: string) => updatedDependencies[pkg]);
|
||||
|
||||
// If no packages require any updation
|
||||
if (packagesToUpdate.length === 0) {
|
||||
loadingSpinner.stop(chalkSuccess(`All @trigger.dev/* packages are already up to date.`));
|
||||
return;
|
||||
}
|
||||
|
||||
let applyUpdates = targetVersion !== "latest";
|
||||
|
||||
if (targetVersion === "latest") {
|
||||
applyUpdates = await hasUserConfirmed(packagesToUpdate, packageMaps, updatedDependencies);
|
||||
}
|
||||
|
||||
if (applyUpdates) {
|
||||
const newPackageJSON = packageData;
|
||||
packagesToUpdate.forEach((packageName) => {
|
||||
const tmp = packageMaps[packageName];
|
||||
if (tmp) {
|
||||
newPackageJSON[tmp.type][packageName] = updatedDependencies[packageName];
|
||||
}
|
||||
export function configureUpdateCommand(program: Command) {
|
||||
return program
|
||||
.command("update")
|
||||
.description("Updates all @trigger.dev/* packages to match the CLI version")
|
||||
.argument("[path]", "The path to the directory that contains the package.json file", ".")
|
||||
.option(
|
||||
"-l, --log-level <level>",
|
||||
"The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.",
|
||||
"log"
|
||||
)
|
||||
.option("--skip-telemetry", "Opt-out of sending telemetry")
|
||||
.action(async (path, options) => {
|
||||
wrapCommandAction("dev", UpdateCommandOptions, options, async (opts) => {
|
||||
await printStandloneInitialBanner(true);
|
||||
await updateCommand(path, opts);
|
||||
});
|
||||
});
|
||||
await writeJSONFile(packageJSONPath, newPackageJSON);
|
||||
await installDependencies(projectPath);
|
||||
}
|
||||
}
|
||||
|
||||
// expects a version number, or latest.
|
||||
// if version number is specified, prepend it with '@' for ncu.
|
||||
function getTargetVersion(toVersion?: string): NcuRunOptionTarget {
|
||||
if (!toVersion) {
|
||||
return "latest";
|
||||
}
|
||||
return toVersion === "latest" ? "latest" : `@${toVersion}`;
|
||||
const triggerPackageFilter = /^@trigger\.dev/;
|
||||
|
||||
export async function updateCommand(dir: string, options: UpdateCommandOptions) {
|
||||
await updateTriggerPackages(dir, options);
|
||||
}
|
||||
|
||||
async function hasUserConfirmed(
|
||||
packagesToUpdate: string[],
|
||||
packageMaps: { [x: string]: { type: string; version: string } },
|
||||
updatedDependencies: { [x: string]: any }
|
||||
): Promise<boolean> {
|
||||
// Inform the user of the dependencies that can be updated
|
||||
console.log("\nNewer versions found for the following packages:");
|
||||
console.table(
|
||||
packagesToUpdate.map((i) => ({
|
||||
name: i,
|
||||
old: packageMaps[i]?.version,
|
||||
new: updatedDependencies[i],
|
||||
}))
|
||||
export async function updateTriggerPackages(
|
||||
dir: string,
|
||||
options: UpdateCommandOptions,
|
||||
embedded?: boolean,
|
||||
requireUpdate?: boolean
|
||||
) {
|
||||
if (!embedded) {
|
||||
intro("Updating packages");
|
||||
}
|
||||
|
||||
const projectPath = resolve(process.cwd(), dir);
|
||||
|
||||
const { packageJson, readonlyPackageJson, packageJsonPath } = await getPackageJson(projectPath);
|
||||
|
||||
if (!packageJson) {
|
||||
log.error("Failed to load package.json. Try to re-run with `-l debug` to see what's going on.");
|
||||
return;
|
||||
}
|
||||
|
||||
const cliVersion = getVersion();
|
||||
const newCliVersion = await updateCheck();
|
||||
|
||||
if (newCliVersion) {
|
||||
prettyWarning(
|
||||
"You're not running the latest CLI version, please consider updating ASAP",
|
||||
`Current: ${cliVersion}\nLatest: ${newCliVersion}`,
|
||||
"Run latest: npx trigger.dev@beta"
|
||||
);
|
||||
}
|
||||
|
||||
const triggerDependencies = getTriggerDependencies(packageJson);
|
||||
|
||||
function getVersionMismatches(deps: Dependency[], targetVersion: string): Dependency[] {
|
||||
const mismatches: Dependency[] = [];
|
||||
|
||||
for (const dep of deps) {
|
||||
if (dep.version === targetVersion) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mismatches.push(dep);
|
||||
}
|
||||
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
const versionMismatches = getVersionMismatches(triggerDependencies, cliVersion);
|
||||
|
||||
if (versionMismatches.length === 0) {
|
||||
if (!embedded) {
|
||||
outro(`Nothing to do${newCliVersion ? " ..but you should really update your CLI!" : ""}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
prettyWarning(
|
||||
"Mismatch between your CLI version and installed packages",
|
||||
"We recommend pinned versions for guaranteed compatibility"
|
||||
);
|
||||
|
||||
// Ask the user if they want to update the dependencies
|
||||
const shouldContinue = await confirm({
|
||||
message: "Do you want to update these packages in package.json and re-install dependencies?",
|
||||
});
|
||||
if (!process.stdout.isTTY) {
|
||||
// Running in CI with version mismatch detected
|
||||
outro("Deploy failed");
|
||||
|
||||
return shouldContinue as boolean;
|
||||
console.log(
|
||||
`ERROR: Version mismatch detected while running in CI. This won't end well. Aborting.
|
||||
|
||||
Please run the dev command locally and check that your CLI version matches the one printed below. Additionally, all \`@trigger.dev/*\` packages also need to match this version.
|
||||
|
||||
If your local CLI version doesn't match the one below, you may want to add the \`trigger.dev\` package to your dependencies. You will also have to update your workflow deploy command to \`npx trigger.dev deploy\` to ensure your pinned CLI version is used.
|
||||
|
||||
CLI version: ${cliVersion}
|
||||
|
||||
Current package versions that don't match the CLI:
|
||||
${versionMismatches.map((dep) => `- ${dep.name}@${dep.version}`).join("\n")}\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.message(""); // spacing
|
||||
|
||||
// Always require user confirmation
|
||||
const userWantsToUpdate = await updateConfirmation(versionMismatches, cliVersion);
|
||||
|
||||
if (isCancel(userWantsToUpdate)) {
|
||||
throw new OutroCommandError();
|
||||
}
|
||||
|
||||
if (!userWantsToUpdate) {
|
||||
if (requireUpdate) {
|
||||
outro("You shall not pass!");
|
||||
|
||||
logger.log(
|
||||
`${chalkError(
|
||||
"X Error:"
|
||||
)} Update required: Version mismatches are a common source of bugs and errors. Please update or use \`--skip-update-check\` at your own risk.\n`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!embedded) {
|
||||
outro("You've been warned!");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const installSpinner = spinner();
|
||||
installSpinner.start("Writing new package.json file");
|
||||
|
||||
// Backup package.json
|
||||
const packageJsonBackupPath = `${packageJsonPath}.bak`;
|
||||
await writeJSONFile(packageJsonBackupPath, readonlyPackageJson, true);
|
||||
|
||||
const exitHandler = async (sig: any) => {
|
||||
log.warn(
|
||||
`You may have to manually roll back any package.json changes. Backup written to ${packageJsonBackupPath}`
|
||||
);
|
||||
};
|
||||
|
||||
// Add exit handler to warn about manual rollback of package.json
|
||||
// Automatically rolling back can end up overwriting with an empty file instead
|
||||
process.prependOnceListener("exit", exitHandler);
|
||||
|
||||
// Update package.json
|
||||
mutatePackageJsonWithUpdatedPackages(packageJson, versionMismatches, cliVersion);
|
||||
await writeJSONFile(packageJsonPath, packageJson, true);
|
||||
|
||||
async function revertPackageJsonChanges() {
|
||||
await writeJSONFile(packageJsonPath, readonlyPackageJson, true);
|
||||
await removeFile(packageJsonBackupPath);
|
||||
}
|
||||
|
||||
installSpinner.message("Installing new package versions");
|
||||
|
||||
const jsProject = new JavascriptProject(projectPath);
|
||||
|
||||
let packageManager: PackageManager | undefined;
|
||||
|
||||
try {
|
||||
packageManager = await jsProject.getPackageManager();
|
||||
|
||||
installSpinner.message(`Installing new package versions with ${packageManager}`);
|
||||
|
||||
await jsProject.install();
|
||||
} catch (error) {
|
||||
installSpinner.stop(
|
||||
`Failed to install new package versions${packageManager ? ` with ${packageManager}` : ""}`
|
||||
);
|
||||
|
||||
// Remove exit handler in case of failure
|
||||
process.removeListener("exit", exitHandler);
|
||||
|
||||
await revertPackageJsonChanges();
|
||||
throw error;
|
||||
}
|
||||
|
||||
installSpinner.stop("Installed new package versions");
|
||||
|
||||
// Remove exit handler once packages have been updated, also delete backup file
|
||||
process.removeListener("exit", exitHandler);
|
||||
await removeFile(packageJsonBackupPath);
|
||||
|
||||
if (!embedded) {
|
||||
outro(
|
||||
`Packages updated${newCliVersion ? " ..but you should really update your CLI too!" : ""}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type Dependency = {
|
||||
type: "dependencies" | "devDependencies";
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
function getTriggerDependencies(packageJson: PackageJson): Dependency[] {
|
||||
const deps: Dependency[] = [];
|
||||
|
||||
for (const type of ["dependencies", "devDependencies"] as const) {
|
||||
for (const [name, version] of Object.entries(packageJson[type] ?? {})) {
|
||||
if (!version) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (version.startsWith("workspace")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!triggerPackageFilter.test(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ignoredPackages = ["@trigger.dev/companyicons"];
|
||||
|
||||
if (ignoredPackages.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
deps.push({ type, name, version });
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
function mutatePackageJsonWithUpdatedPackages(
|
||||
packageJson: PackageJson,
|
||||
depsToUpdate: Dependency[],
|
||||
targetVersion: string
|
||||
) {
|
||||
for (const { type, name, version } of depsToUpdate) {
|
||||
if (!packageJson[type]) {
|
||||
throw new Error(
|
||||
`No ${type} entry found in package.json. Please try to upgrade manually instead.`
|
||||
);
|
||||
}
|
||||
|
||||
packageJson[type]![name] = targetVersion;
|
||||
}
|
||||
}
|
||||
|
||||
function printUpdateTable(depsToUpdate: Dependency[], targetVersion: string): void {
|
||||
log.message("Suggested updates");
|
||||
|
||||
const tableData = depsToUpdate.map((dep) => ({
|
||||
package: dep.name,
|
||||
old: dep.version,
|
||||
new: targetVersion,
|
||||
}));
|
||||
|
||||
logger.table(tableData);
|
||||
}
|
||||
|
||||
async function updateConfirmation(depsToUpdate: Dependency[], targetVersion: string) {
|
||||
printUpdateTable(depsToUpdate, targetVersion);
|
||||
|
||||
let confirmMessage = "Would you like to apply those updates?";
|
||||
|
||||
return await confirm({
|
||||
message: confirmMessage,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPackageJson(absoluteProjectPath: string) {
|
||||
const packageJsonPath = join(absoluteProjectPath, "package.json");
|
||||
|
||||
const readonlyPackageJson = Object.freeze((await readJSONFile(packageJsonPath)) as PackageJson);
|
||||
|
||||
const packageJson = structuredClone(readonlyPackageJson);
|
||||
|
||||
return { packageJson, readonlyPackageJson, packageJsonPath };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function assertExhaustive(x: never): never {
|
||||
throw new Error("Unexpected object: " + x);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import chalk from "chalk";
|
||||
|
||||
export const green = "#4FFF54";
|
||||
@@ -63,3 +64,26 @@ export function prettyPrintDate(date: Date = new Date()) {
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
export function prettyWarning(header: string, body?: string, footer?: string) {
|
||||
const prefix = "Warning: ";
|
||||
const indent = Array(prefix.length).fill(" ").join("");
|
||||
const spacing = "\n\n";
|
||||
|
||||
const prettyPrefix = chalkWarning(prefix);
|
||||
|
||||
const withIndents = (text?: string) =>
|
||||
text
|
||||
?.split("\n")
|
||||
.map((line) => `${indent}${line}`)
|
||||
.join("\n");
|
||||
|
||||
const prettyBody = withIndents(body);
|
||||
const prettyFooter = withIndents(footer);
|
||||
|
||||
log.warn(
|
||||
`${prettyPrefix}${header}${prettyBody ? `${spacing}${prettyBody}` : ""}${
|
||||
prettyFooter ? `${spacing}${prettyFooter}` : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { relative } from "node:path";
|
||||
import { chalkError, chalkPurple, chalkGrey, chalkGreen, chalkWarning } from "./cliOutput";
|
||||
import { logger } from "./logger";
|
||||
import { ReadConfigResult } from "./configFiles";
|
||||
import { TaskMetadataParseError } from "../workers/common/errors";
|
||||
import { z } from "zod";
|
||||
import { groupTaskMetadataIssuesByTask } from "@trigger.dev/core/v3";
|
||||
import terminalLink from "terminal-link";
|
||||
import { docs } from "./links";
|
||||
|
||||
export type ESMRequireError = {
|
||||
type: "esm-require-error";
|
||||
@@ -86,6 +87,10 @@ export function logESMRequireError(parsedError: ESMRequireError, resolvedConfig:
|
||||
)}. This will bundle the module with your code.\n`
|
||||
);
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`${chalkGrey("○")} For more info see the ${terminalLink("relevant docs", docs.config.esm)}.\n`
|
||||
);
|
||||
}
|
||||
|
||||
export type PackageNotFoundError = {
|
||||
|
||||
@@ -67,8 +67,8 @@ export async function safeFeadJSONFile(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJSONFile(path: string, json: any) {
|
||||
await writeFile(path, JSON.stringify(json), "utf8");
|
||||
export async function writeJSONFile(path: string, json: any, pretty = false) {
|
||||
await writeFile(path, JSON.stringify(json, undefined, pretty ? 2 : undefined), "utf8");
|
||||
}
|
||||
|
||||
export function readJSONFileSync(path: string) {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { findUp } from "find-up";
|
||||
import { basename } from "path";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export type PackageManager = "npm" | "pnpm" | "yarn";
|
||||
|
||||
export async function getUserPackageManager(path: string): Promise<PackageManager> {
|
||||
const packageManager = await detectPackageManager(path);
|
||||
logger.debug("Detected package manager", { packageManager });
|
||||
return packageManager;
|
||||
}
|
||||
|
||||
async function detectPackageManager(path: string): Promise<PackageManager> {
|
||||
try {
|
||||
return await detectPackageManagerFromArtifacts(path);
|
||||
} catch (error) {
|
||||
@@ -29,19 +37,30 @@ function detectPackageManagerFromCurrentCommand(): PackageManager {
|
||||
}
|
||||
|
||||
async function detectPackageManagerFromArtifacts(path: string): Promise<PackageManager> {
|
||||
const packageFiles = [
|
||||
{ name: "yarn.lock", pm: "yarn" } as const,
|
||||
{ name: "pnpm-lock.yaml", pm: "pnpm" } as const,
|
||||
{ name: "package-lock.json", pm: "npm" } as const,
|
||||
{ name: "npm-shrinkwrap.json", pm: "npm" } as const,
|
||||
];
|
||||
const artifacts = {
|
||||
yarn: "yarn.lock",
|
||||
pnpm: "pnpm-lock.yaml",
|
||||
npm: "package-lock.json",
|
||||
npmShrinkwrap: "npm-shrinkwrap.json",
|
||||
};
|
||||
|
||||
for (const { name, pm } of packageFiles) {
|
||||
const foundPath = await findUp(name, { cwd: path });
|
||||
if (typeof foundPath === "string") {
|
||||
return pm;
|
||||
}
|
||||
const foundPath = await findUp(Object.values(artifacts), { cwd: path });
|
||||
|
||||
if (!foundPath) {
|
||||
throw new Error("Could not detect package manager from artifacts");
|
||||
}
|
||||
|
||||
throw new Error("Could not detect package manager from artifacts");
|
||||
logger.debug("Found path from package manager artifacts", { foundPath });
|
||||
|
||||
switch (basename(foundPath)) {
|
||||
case artifacts.yarn:
|
||||
return "yarn";
|
||||
case artifacts.pnpm:
|
||||
return "pnpm";
|
||||
case artifacts.npm:
|
||||
case artifacts.npmShrinkwrap:
|
||||
return "npm";
|
||||
default:
|
||||
throw new Error(`Unhandled package manager detection path: ${foundPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { logger } from "./logger.js";
|
||||
import { spinner } from "./windows";
|
||||
|
||||
export async function printInitialBanner(performUpdateCheck = true) {
|
||||
const packageVersion = getVersion();
|
||||
const text = `\n${logo()} ${chalkGrey(`(${packageVersion})`)}\n`;
|
||||
const cliVersion = getVersion();
|
||||
const text = `\n${logo()} ${chalkGrey(`(${cliVersion})`)}\n`;
|
||||
|
||||
logger.info(text);
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function printInitialBanner(performUpdateCheck = true) {
|
||||
// Log a slightly more noticeable message if this is a major bump
|
||||
if (maybeNewVersion !== undefined) {
|
||||
loadingSpinner.stop(`Update available ${chalk.green(maybeNewVersion)}`);
|
||||
const currentMajor = parseInt(packageVersion.split(".")[0]!);
|
||||
const currentMajor = parseInt(cliVersion.split(".")[0]!);
|
||||
const newMajor = parseInt(maybeNewVersion.split(".")[0]!);
|
||||
if (newMajor > currentMajor) {
|
||||
logger.warn(
|
||||
@@ -38,9 +38,9 @@ After installation, run Trigger.dev with \`npx trigger.dev\`.`
|
||||
}
|
||||
|
||||
export async function printStandloneInitialBanner(performUpdateCheck = true) {
|
||||
const packageVersion = getVersion();
|
||||
const cliVersion = getVersion();
|
||||
|
||||
logger.log(`\n${logo()} ${chalkGrey("(v3 Developer Preview)")}`);
|
||||
logger.log(`\n${logo()} ${chalkGrey(`(${cliVersion})`)}\n`);
|
||||
|
||||
if (performUpdateCheck) {
|
||||
const maybeNewVersion = await updateCheck();
|
||||
@@ -54,7 +54,11 @@ export async function printStandloneInitialBanner(performUpdateCheck = true) {
|
||||
logger.log(`${chalkGrey("-".repeat(54))}`);
|
||||
}
|
||||
|
||||
export function printDevBanner() {
|
||||
export function printDevBanner(printTopBorder = true) {
|
||||
if (printTopBorder) {
|
||||
logger.log(chalkGrey("-".repeat(54)));
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`${chalkGrey("Key:")} ${chalkWorker("Version")} ${chalkGrey("|")} ${chalkTask(
|
||||
"Task"
|
||||
@@ -68,7 +72,7 @@ async function doUpdateCheck(): Promise<string | undefined> {
|
||||
try {
|
||||
// default cache for update check is 1 day
|
||||
update = await checkForUpdate(pkg, {
|
||||
distTag: pkg.version.startsWith("0.0.0") ? "beta" : "latest",
|
||||
distTag: pkg.version.startsWith("3.0.0-beta") ? "beta" : "latest",
|
||||
});
|
||||
} catch (err) {
|
||||
// ignore error
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { execa } from "execa";
|
||||
import { getUserPackageManager, type PackageManager } from "./getUserPackageManager.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { spinner } from "./windows.js";
|
||||
|
||||
export async function installDependencies(projectDir: string) {
|
||||
logger.info("Installing dependencies...");
|
||||
|
||||
const pkgManager = await getUserPackageManager(projectDir);
|
||||
|
||||
const installSpinner = await runInstallCommand(pkgManager, projectDir);
|
||||
|
||||
// If the spinner was used to show the progress, use succeed method on it
|
||||
// If not, use the succeed on a new spinner
|
||||
(installSpinner || spinner()).stop(chalk.green("Successfully installed dependencies!\n"));
|
||||
}
|
||||
|
||||
async function runInstallCommand(
|
||||
pkgManager: PackageManager,
|
||||
projectDir: string
|
||||
): Promise<ReturnType<typeof spinner> | null> {
|
||||
switch (pkgManager) {
|
||||
// When using npm, inherit the stderr stream so that the progress bar is shown
|
||||
case "npm":
|
||||
await execa(pkgManager, ["install"], {
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
});
|
||||
|
||||
return null;
|
||||
// When using yarn or pnpm, use the stdout stream and ora spinner to show the progress
|
||||
case "pnpm": {
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start("Running pnpm install...");
|
||||
const pnpmSubprocess = execa(pkgManager, ["install"], {
|
||||
cwd: projectDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await new Promise<void>((res, rej) => {
|
||||
pnpmSubprocess.stdout?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
|
||||
if (text.includes("Progress")) {
|
||||
loadingSpinner.message(text.includes("|") ? text.split(" | ")[1] ?? "" : text);
|
||||
}
|
||||
});
|
||||
pnpmSubprocess.on("error", (e) => rej(e));
|
||||
pnpmSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return loadingSpinner;
|
||||
}
|
||||
case "yarn": {
|
||||
const loadingSpinner = spinner();
|
||||
loadingSpinner.start("Running yarn...");
|
||||
const yarnSubprocess = execa(pkgManager, [], {
|
||||
cwd: projectDir,
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
await new Promise<void>((res, rej) => {
|
||||
yarnSubprocess.stdout?.on("data", (data: Buffer) => {
|
||||
loadingSpinner.message(data.toString());
|
||||
});
|
||||
yarnSubprocess.on("error", (e) => rej(e));
|
||||
yarnSubprocess.on("close", () => res());
|
||||
});
|
||||
|
||||
return loadingSpinner;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,17 +25,6 @@ export async function installPackages(
|
||||
);
|
||||
}
|
||||
|
||||
async function getPackageVersion(path: string) {
|
||||
try {
|
||||
const packageJsonPath = join(path, "package.json");
|
||||
const packageJson = await readJSONFile(packageJsonPath);
|
||||
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Expects path to be in the format:
|
||||
// - source-map-support/register.js
|
||||
// - @opentelemetry/api
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user