Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24557a7324 | |||
| 8b10a592f3 | |||
| d7089e12a9 | |||
| 162d864517 | |||
| 8ba9987944 | |||
| 7056ce53f2 | |||
| 2d6fd62fb8 | |||
| e8993ee882 | |||
| e0cdebbe09 | |||
| d6c6dc993d | |||
| 27de884f93 | |||
| c2707800aa | |||
| dbda820a71 | |||
| e417aca879 | |||
| 5a3b4a450b | |||
| f91a118958 | |||
| 9971de6a1e | |||
| 0847bee5b8 | |||
| 6152b2f3dc | |||
| ec8381e2d0 | |||
| 76a15fd278 | |||
| 824a6205f7 | |||
| a0984d5292 | |||
| c61d8ed890 | |||
| 76a0f61dee | |||
| d934feb02a | |||
| 7fdedf3137 | |||
| f6e23e94d1 | |||
| 3d6cfe5ead | |||
| 946efd1fb4 | |||
| 3d645e2b2c | |||
| 45cb75b8a3 | |||
| 91d45e5ba6 | |||
| fa5b3fe11e | |||
| 90506322b5 | |||
| e59dd465d8 | |||
| 98d5f005e4 | |||
| a6b5d2edc5 | |||
| f4a1d5f36d | |||
| 6fb6cb6aaf | |||
| 0b6417f1d8 | |||
| a685f5a4ec | |||
| 62532d5ce1 | |||
| 786d5508f0 | |||
| 187f02780b | |||
| b8b3ea07eb | |||
| e341df806c | |||
| ecf1110ab7 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Increase span attribute value length limit to 2048
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
- Prevent uncaught exceptions when handling WebSocket messages
|
||||
- Improve CLI dev command WebSocket debug and error logging
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Improve prisma errors for missing postinstall
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
try/catch opening the login URL
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Added config option extraCACerts to ProjectConfig type. This copies the ca file along with additionalFiles and sets NODE_EXTRA_CA_CERTS environment variable in built image as well as running the task.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
v3: Export AbortTaskRunError from @trigger.dev/sdk/v3
|
||||
@@ -46,6 +46,7 @@
|
||||
"changesets": [
|
||||
"afraid-sheep-joke",
|
||||
"angry-eagles-trade",
|
||||
"angry-trees-drop",
|
||||
"beige-pears-explode",
|
||||
"beige-pens-dance",
|
||||
"big-tomatoes-deliver",
|
||||
@@ -60,6 +61,7 @@
|
||||
"cool-comics-burn",
|
||||
"cool-glasses-bake",
|
||||
"cuddly-feet-approve",
|
||||
"curly-monkeys-tell",
|
||||
"dry-walls-check",
|
||||
"dull-mangos-press",
|
||||
"eight-pumas-float",
|
||||
@@ -67,9 +69,11 @@
|
||||
"famous-boats-tease",
|
||||
"fast-colts-relax",
|
||||
"few-students-share",
|
||||
"fifty-lions-think",
|
||||
"five-toes-destroy",
|
||||
"friendly-walls-repair",
|
||||
"funny-swans-destroy",
|
||||
"gorgeous-cycles-guess",
|
||||
"gorgeous-gorillas-compete",
|
||||
"green-bags-wink",
|
||||
"hot-buckets-behave",
|
||||
@@ -97,7 +101,9 @@
|
||||
"mighty-parrots-sin",
|
||||
"modern-stingrays-end",
|
||||
"nasty-jars-pump",
|
||||
"nervous-baboons-sin",
|
||||
"nervous-planets-sparkle",
|
||||
"nervous-seas-shave",
|
||||
"new-pants-beg",
|
||||
"new-rivers-tell",
|
||||
"nice-bulldogs-turn",
|
||||
@@ -161,6 +167,7 @@
|
||||
"tender-turkeys-compete",
|
||||
"thick-carrots-sneeze",
|
||||
"thin-parents-heal",
|
||||
"thirty-hotels-raise",
|
||||
"thirty-islands-kiss",
|
||||
"tidy-balloons-suffer",
|
||||
"tidy-dryers-sleep",
|
||||
@@ -175,6 +182,7 @@
|
||||
"violet-clocks-notice",
|
||||
"warm-olives-provide",
|
||||
"warm-planes-taste",
|
||||
"yellow-roses-arrive",
|
||||
"young-snails-sell"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Added declarative cron schedules
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add more package exports that can be used from the web app
|
||||
+33
-10
@@ -1,13 +1,36 @@
|
||||
{
|
||||
"extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node", "prettier"],
|
||||
"rules": {
|
||||
"@typescript-eslint/strict-boolean-expressions": [
|
||||
"error",
|
||||
{
|
||||
"allowNullableBoolean": true,
|
||||
"allowNullableString": true,
|
||||
"allowNullableNumber": true
|
||||
"plugins": [
|
||||
"@trigger.dev/eslint-plugin",
|
||||
"react-hooks",
|
||||
"@typescript-eslint/eslint-plugin",
|
||||
"import"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {
|
||||
// Autofixes imports from "@trigger.dev/core" to fine grained modules
|
||||
"@trigger.dev/no-trigger-core-import": "error",
|
||||
// Normalize `import type {}` and `import { type }`
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
// the "type" annotation can get tangled and cause syntax errors
|
||||
// during some autofixes, so easier to just turn it off
|
||||
"prefer": "type-imports",
|
||||
"disallowTypeAnnotations": true,
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
// no-trigger-core-import splits imports into multiple lines
|
||||
// this one merges them back into a single line
|
||||
// if they still import from the same module
|
||||
"import/no-duplicates": ["warn", { "prefer-inline": true }],
|
||||
// lots of undeclared vars, enable this rule if you want to clean them up
|
||||
"turbo/no-undeclared-env-vars": "off"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["seed.js", "seedCloud.ts", "populate.js"]
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function UsageBar({
|
||||
return (
|
||||
<div className="h-fit w-full py-12">
|
||||
<div className="relative h-3 w-full rounded-sm bg-background-bright">
|
||||
{billingLimit && (
|
||||
{billingLimit !== undefined && (
|
||||
<motion.div
|
||||
initial={{ width: billingLimitPercentage / startFactor + "%" }}
|
||||
animate={{ width: billingLimitPercentage + "%" }}
|
||||
@@ -57,11 +57,10 @@ export function UsageBar({
|
||||
value={formatCurrency(billingLimit, false)}
|
||||
position="bottomRow2"
|
||||
percentage={billingLimitPercentage}
|
||||
tooltipContent={`Billing limit: ${formatCurrency(billingLimit, false)}`}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{tierLimit && (
|
||||
{tierLimit !== undefined && (
|
||||
<motion.div
|
||||
initial={{ width: tierRunLimitPercentage / startFactor + "%" }}
|
||||
animate={{ width: tierRunLimitPercentage + "%" }}
|
||||
@@ -74,14 +73,10 @@ export function UsageBar({
|
||||
value={formatCurrency(tierLimit, false)}
|
||||
position="bottomRow1"
|
||||
percentage={tierRunLimitPercentage}
|
||||
tooltipContent={`${isPaying ? "Included usage" : "Tier limit"}: ${formatCurrency(
|
||||
tierLimit,
|
||||
false
|
||||
)}`}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{projectedUsage && projectedUsage !== 0 && (
|
||||
{projectedUsage !== undefined && projectedUsage !== 0 && (
|
||||
<motion.div
|
||||
initial={{ width: projectedRunsPercentage / startFactor + "%" }}
|
||||
animate={{ width: projectedRunsPercentage + "%" }}
|
||||
@@ -94,7 +89,6 @@ export function UsageBar({
|
||||
value={formatCurrency(projectedUsage, false)}
|
||||
position="topRow2"
|
||||
percentage={projectedRunsPercentage}
|
||||
tooltipContent={`Projected runs: ${formatCurrency(projectedUsage, false)}`}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -113,7 +107,6 @@ export function UsageBar({
|
||||
value={formatCurrency(current, false)}
|
||||
position="topRow1"
|
||||
percentage={usagePercentage}
|
||||
tooltipContent={`Used: ${formatCurrency(current, false)}`}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
@@ -140,7 +133,7 @@ type LegendProps = {
|
||||
value: number | string;
|
||||
percentage: number;
|
||||
position: keyof typeof positions;
|
||||
tooltipContent: string;
|
||||
tooltipContent?: string;
|
||||
};
|
||||
|
||||
function Legend({ text, value, position, percentage, tooltipContent }: LegendProps) {
|
||||
@@ -154,17 +147,24 @@ function Legend({ text, value, position, percentage, tooltipContent }: LegendPro
|
||||
flipLegendPosition === true ? "-translate-x-full border-r" : "border-l"
|
||||
)}
|
||||
>
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<Paragraph className="mr-px h-fit whitespace-nowrap bg-background-bright px-1.5 text-xs text-text-bright">
|
||||
{text}
|
||||
<span className="ml-1 text-text-dimmed">{value}</span>
|
||||
</Paragraph>
|
||||
}
|
||||
side="top"
|
||||
content={tooltipContent}
|
||||
className="z-50 h-fit"
|
||||
/>
|
||||
{tooltipContent ? (
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<Paragraph className="mr-px h-fit whitespace-nowrap bg-background-bright px-1.5 text-xs text-text-bright">
|
||||
{text}
|
||||
<span className="ml-1 text-text-dimmed">{value}</span>
|
||||
</Paragraph>
|
||||
}
|
||||
side="top"
|
||||
content={tooltipContent}
|
||||
className="z-50 h-fit"
|
||||
/>
|
||||
) : (
|
||||
<Paragraph className="mr-px h-fit whitespace-nowrap bg-background-bright px-1.5 text-xs text-text-bright">
|
||||
{text}
|
||||
<span className="ml-1 text-text-dimmed">{value}</span>
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -552,7 +552,6 @@ function V3ProjectSideMenu({
|
||||
name="Tasks"
|
||||
icon={TaskIcon}
|
||||
iconColor="text-blue-500"
|
||||
count={project.jobCount}
|
||||
to={v3ProjectPath(organization, project)}
|
||||
data-action="tasks"
|
||||
/>
|
||||
|
||||
@@ -8,12 +8,8 @@ const Alert = AlertDialogPrimitive.Root;
|
||||
|
||||
const AlertTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
const AlertPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
|
||||
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
const AlertPortal = ({ children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => (
|
||||
<AlertDialogPrimitive.Portal {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,8 @@ const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
const DialogPortal = ({ children, ...props }: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -28,8 +28,8 @@ interface SheetPortalProps
|
||||
extends SheetPrimitive.DialogPortalProps,
|
||||
VariantProps<typeof portalVariants> {}
|
||||
|
||||
const SheetPortal = ({ position, className, children, ...props }: SheetPortalProps) => (
|
||||
<SheetPrimitive.Portal className={cn(className)} {...props}>
|
||||
const SheetPortal = ({ position, children, ...props }: SheetPortalProps) => (
|
||||
<SheetPrimitive.Portal {...props}>
|
||||
<div className={portalVariants({ position })}>{children}</div>
|
||||
</SheetPrimitive.Portal>
|
||||
);
|
||||
|
||||
@@ -11,22 +11,32 @@ import { cn } from "~/utils/cn";
|
||||
|
||||
export function DeploymentStatus({
|
||||
status,
|
||||
isBuilt,
|
||||
className,
|
||||
}: {
|
||||
status: WorkerDeploymentStatus;
|
||||
isBuilt: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn("flex items-center gap-1", className)}>
|
||||
<DeploymentStatusIcon status={status} className="h-4 w-4" />
|
||||
<DeploymentStatusLabel status={status} />
|
||||
<DeploymentStatusLabel status={status} isBuilt={isBuilt} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeploymentStatusLabel({ status }: { status: WorkerDeploymentStatus }) {
|
||||
export function DeploymentStatusLabel({
|
||||
status,
|
||||
isBuilt,
|
||||
}: {
|
||||
status: WorkerDeploymentStatus;
|
||||
isBuilt: boolean;
|
||||
}) {
|
||||
return (
|
||||
<span className={deploymentStatusClassNameColor(status)}>{deploymentStatusTitle(status)}</span>
|
||||
<span className={deploymentStatusClassNameColor(status)}>
|
||||
{deploymentStatusTitle(status, isBuilt)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +89,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus):
|
||||
}
|
||||
}
|
||||
|
||||
export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
|
||||
export function deploymentStatusTitle(status: WorkerDeploymentStatus, isBuilt: boolean): string {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "Pending…";
|
||||
@@ -92,9 +102,17 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus): string {
|
||||
case "CANCELED":
|
||||
return "Canceled";
|
||||
case "TIMED_OUT":
|
||||
return "Timed out";
|
||||
if (!isBuilt) {
|
||||
return "Build timed out";
|
||||
}
|
||||
|
||||
return "Indexing timed out";
|
||||
case "FAILED":
|
||||
return "Failed";
|
||||
if (!isBuilt) {
|
||||
return "Build failed";
|
||||
}
|
||||
|
||||
return "Indexing failed";
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ArrowPathIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useNavigation } from "@remix-run/react";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
} from "~/components/primitives/Dialog";
|
||||
|
||||
type RetryDeploymentIndexingDialogProps = {
|
||||
projectId: string;
|
||||
deploymentShortCode: string;
|
||||
redirectPath: string;
|
||||
};
|
||||
|
||||
export function RetryDeploymentIndexingDialog({
|
||||
projectId,
|
||||
deploymentShortCode,
|
||||
redirectPath,
|
||||
}: RetryDeploymentIndexingDialogProps) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const formAction = `/resources/${projectId}/deployments/${deploymentShortCode}/retry-indexing`;
|
||||
const isLoading = navigation.formAction === formAction;
|
||||
|
||||
return (
|
||||
<DialogContent key="retry-indexing">
|
||||
<DialogHeader>Retry indexing this deployment?</DialogHeader>
|
||||
<DialogDescription>
|
||||
Retrying can be useful if indexing failed due to missing environment variables. Make sure
|
||||
you set them before retrying. In most other cases, indexing will keep failing until you fix
|
||||
any errors and re-deploy.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Form
|
||||
action={`/resources/${projectId}/deployments/${deploymentShortCode}/retry-indexing`}
|
||||
method="post"
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
name="redirectUrl"
|
||||
value={redirectPath}
|
||||
variant="primary/small"
|
||||
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
|
||||
disabled={isLoading}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
>
|
||||
{isLoading ? "Retrying..." : "Retry indexing"}
|
||||
</Button>
|
||||
</Form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import { type RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import { useCallback } from "react";
|
||||
import { z } from "zod";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../primitives/SimpleSelect";
|
||||
import { ScheduleTypeCombo } from "./ScheduleType";
|
||||
|
||||
export const ScheduleListFilters = z.object({
|
||||
page: z.coerce.number().default(1),
|
||||
@@ -28,6 +29,7 @@ export const ScheduleListFilters = z.object({
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => (value ? value.split(",") : undefined)),
|
||||
type: z.union([z.literal("declarative"), z.literal("imperative")]).optional(),
|
||||
search: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -48,7 +50,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
const navigate = useNavigate();
|
||||
const location = useOptimisticLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const { environments, tasks, page, search } = ScheduleListFilters.parse(
|
||||
const { environments, tasks, page, search, type } = ScheduleListFilters.parse(
|
||||
Object.fromEntries(searchParams.entries())
|
||||
);
|
||||
|
||||
@@ -73,6 +75,10 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
handleFilterChange("environments", value === "ALL" ? undefined : value);
|
||||
}, []);
|
||||
|
||||
const handleTypeChange = useCallback((value: string | typeof All) => {
|
||||
handleFilterChange("type", value === "ALL" ? undefined : value);
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = useThrottle((value: string) => {
|
||||
handleFilterChange("search", value.length === 0 ? undefined : value);
|
||||
}, 300);
|
||||
@@ -97,6 +103,30 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
defaultValue={search}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
<SelectGroup>
|
||||
<Select name="type" value={type ?? "ALL"} onValueChange={handleTypeChange}>
|
||||
<SelectTrigger size="minimal" width="full">
|
||||
<SelectValue placeholder={"Select type"} className="ml-2 whitespace-nowrap p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"ALL"}>
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="whitespace-nowrap pl-0.5 transition group-hover:text-text-bright"
|
||||
>
|
||||
All types
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
<SelectItem value={"declarative"}>
|
||||
<ScheduleTypeCombo type="DECLARATIVE" className="text-xs text-text-dimmed" />
|
||||
</SelectItem>
|
||||
<SelectItem value={"imperative"}>
|
||||
<ScheduleTypeCombo type="IMPERATIVE" className="text-xs text-text-dimmed" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ArchiveBoxIcon, ArrowsRightLeftIcon } from "@heroicons/react/20/solid";
|
||||
import type { ScheduleType } from "@trigger.dev/database";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export function ScheduleTypeCombo({ type, className }: { type: ScheduleType; className?: string }) {
|
||||
return (
|
||||
<div className={cn("flex items-center space-x-1", className)}>
|
||||
<ScheduleTypeIcon type={type} />
|
||||
<span>{scheduleTypeName(type)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScheduleTypeIcon({ type, className }: { type: ScheduleType; className?: string }) {
|
||||
switch (type) {
|
||||
case "IMPERATIVE":
|
||||
return <ArrowsRightLeftIcon className={cn("size-4", className)} />;
|
||||
case "DECLARATIVE":
|
||||
return <ArchiveBoxIcon className={cn("size-4", className)} />;
|
||||
}
|
||||
}
|
||||
export function scheduleTypeName(type: ScheduleType) {
|
||||
switch (type) {
|
||||
case "IMPERATIVE":
|
||||
return "Imperative";
|
||||
case "DECLARATIVE":
|
||||
return "Declarative";
|
||||
}
|
||||
}
|
||||
@@ -230,8 +230,12 @@ export function TaskRunsTable({
|
||||
formatDuration(new Date(run.createdAt), new Date(run.startedAt), {
|
||||
style: "short",
|
||||
})
|
||||
) : (
|
||||
) : run.isCancellable ? (
|
||||
<LiveTimer startTime={new Date(run.createdAt)} />
|
||||
) : (
|
||||
formatDuration(new Date(run.createdAt), new Date(run.updatedAt), {
|
||||
style: "short",
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -118,6 +118,7 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
v2Enabled: true,
|
||||
v3Enabled: true,
|
||||
members: {
|
||||
select: {
|
||||
|
||||
@@ -25,7 +25,7 @@ export class ApiRunPresenter {
|
||||
}: ApiRunOptions) {
|
||||
const take = Math.min(maxTasks, 50);
|
||||
|
||||
return await prisma.jobRun.findUnique({
|
||||
return await prisma.jobRun.findFirst({
|
||||
where: {
|
||||
id: runId,
|
||||
},
|
||||
|
||||
@@ -88,6 +88,7 @@ export class DeploymentListPresenter {
|
||||
version: string;
|
||||
status: WorkerDeploymentStatus;
|
||||
environmentId: string;
|
||||
builtAt: Date | null;
|
||||
deployedAt: Date | null;
|
||||
tasksCount: BigInt | null;
|
||||
userId: string | null;
|
||||
@@ -107,6 +108,7 @@ export class DeploymentListPresenter {
|
||||
u."name" AS "userName",
|
||||
u."displayName" AS "userDisplayName",
|
||||
u."avatarUrl" AS "userAvatarUrl",
|
||||
wd."builtAt",
|
||||
wd."deployedAt"
|
||||
FROM
|
||||
${sqlDatabaseSchema}."WorkerDeployment" as wd
|
||||
@@ -121,7 +123,7 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
|
||||
return {
|
||||
currentPage: page,
|
||||
totalPages: Math.ceil(totalCount / pageSize),
|
||||
deployments: deployments.map((deployment) => {
|
||||
deployments: deployments.map((deployment, index) => {
|
||||
const environment = project.environments.find((env) => env.id === deployment.environmentId);
|
||||
if (!environment) {
|
||||
throw new Error(`Environment not found for deployment ${deployment.id}`);
|
||||
@@ -136,11 +138,14 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
|
||||
shortCode: deployment.shortCode,
|
||||
version: deployment.version,
|
||||
status: deployment.status,
|
||||
builtAt: deployment.builtAt,
|
||||
deployedAt: deployment.deployedAt,
|
||||
tasksCount: deployment.tasksCount ? Number(deployment.tasksCount) : null,
|
||||
label: label?.label,
|
||||
isBuilt: !!deployment.builtAt,
|
||||
isCurrent: label?.label === "current",
|
||||
isDeployed: deployment.status === "DEPLOYED",
|
||||
isLatest: page === 1 && index === 0,
|
||||
environment: {
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
|
||||
@@ -90,6 +90,7 @@ export class DeploymentPresenter {
|
||||
},
|
||||
},
|
||||
status: true,
|
||||
builtAt: true,
|
||||
deployedAt: true,
|
||||
createdAt: true,
|
||||
promotions: {
|
||||
@@ -134,6 +135,7 @@ export class DeploymentPresenter {
|
||||
version: deployment.version,
|
||||
status: deployment.status,
|
||||
createdAt: deployment.createdAt,
|
||||
builtAt: deployment.builtAt,
|
||||
deployedAt: deployment.deployedAt,
|
||||
tasks: deployment.worker?.tasks,
|
||||
label: deployment.promotions?.[0]?.label,
|
||||
@@ -152,6 +154,7 @@ export class DeploymentPresenter {
|
||||
projectId: deployment.projectId,
|
||||
organizationId: project.organizationId,
|
||||
errorData: DeploymentPresenter.prepareErrorData(deployment.errorData),
|
||||
isBuilt: !!deployment.builtAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export class EditSchedulePresenter {
|
||||
const schedule = await this.#prismaClient.taskSchedule.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
friendlyId: true,
|
||||
generatorExpression: true,
|
||||
externalId: true,
|
||||
|
||||
@@ -78,26 +78,11 @@ export class EnvironmentVariablesPresenter {
|
||||
where: {
|
||||
project: {
|
||||
slug: projectSlug,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
type: {
|
||||
in: ["PREVIEW", "STAGING", "PRODUCTION"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "DEVELOPMENT",
|
||||
orgMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const sortedEnvironments = sortEnvironments(environments).filter(
|
||||
(e) => e.orgMember?.userId === userId || e.orgMember === null
|
||||
);
|
||||
const sortedEnvironments = sortEnvironments(environments);
|
||||
|
||||
const repository = new EnvironmentVariablesRepository(this.#prismaClient);
|
||||
const variables = await repository.getProject(project.id);
|
||||
@@ -119,7 +104,9 @@ export class EnvironmentVariablesPresenter {
|
||||
}, {} as Record<string, { value: string | undefined; environment: { type: string; id: string } }>),
|
||||
};
|
||||
}),
|
||||
environments: sortedEnvironments.map((environment) => ({
|
||||
environments: sortedEnvironments.filter(
|
||||
(e) => e.orgMember?.userId === userId || e.orgMember === null
|
||||
).map((environment) => ({
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
})),
|
||||
|
||||
@@ -69,8 +69,6 @@ async function getSlackChannelsForToken(integration: AuthenticatableIntegration)
|
||||
return (channels ?? [])
|
||||
.filter((channel) => !channel.is_archived)
|
||||
.filter((channel) => channel.is_channel)
|
||||
.filter((channel) => !channel.is_ext_shared)
|
||||
.filter((channel) => channel.unlinked === 0)
|
||||
.filter((channel) => channel.num_members)
|
||||
.sort((a, b) => a.name!.localeCompare(b.name!));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
|
||||
import { Prisma, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
|
||||
import parse from "parse-duration";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
@@ -94,12 +94,16 @@ export class RunListPresenter extends BasePresenter {
|
||||
});
|
||||
|
||||
//get all possible tasks
|
||||
const possibleTasksAsync = this._replica.backgroundWorkerTask.findMany({
|
||||
distinct: ["slug"],
|
||||
where: {
|
||||
projectId: project.id,
|
||||
},
|
||||
});
|
||||
const possibleTasksAsync = this._replica.$queryRaw<
|
||||
{
|
||||
slug: string;
|
||||
triggerSource: TaskTriggerSource
|
||||
}[]
|
||||
>`
|
||||
SELECT DISTINCT(slug), "triggerSource"
|
||||
FROM ${sqlDatabaseSchema}."BackgroundWorkerTask"
|
||||
WHERE "projectId" = ${project.id}
|
||||
ORDER BY slug ASC;`;
|
||||
|
||||
//get possible bulk actions
|
||||
const bulkActionsAsync = this._replica.bulkActionGroup.findMany({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Prisma, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { Prisma, RuntimeEnvironmentType, ScheduleType } from "@trigger.dev/database";
|
||||
import { ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
@@ -16,6 +16,7 @@ const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
export type ScheduleListItem = {
|
||||
id: string;
|
||||
type: ScheduleType;
|
||||
friendlyId: string;
|
||||
taskIdentifier: string;
|
||||
deduplicationKey: string | null;
|
||||
@@ -44,10 +45,17 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
environments,
|
||||
search,
|
||||
page,
|
||||
type,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
}: ScheduleListOptions) {
|
||||
const hasFilters =
|
||||
tasks !== undefined || environments !== undefined || (search !== undefined && search !== "");
|
||||
type !== undefined ||
|
||||
tasks !== undefined ||
|
||||
environments !== undefined ||
|
||||
(search !== undefined && search !== "");
|
||||
|
||||
const filterType =
|
||||
type === "declarative" ? "DECLARATIVE" : type === "imperative" ? "IMPERATIVE" : undefined;
|
||||
|
||||
// Find the project scoped to the organization
|
||||
const project = await this._replica.project.findFirstOrThrow({
|
||||
@@ -105,6 +113,7 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
environmentId: environments ? { in: environments } : undefined,
|
||||
},
|
||||
},
|
||||
type: filterType,
|
||||
AND: search
|
||||
? {
|
||||
OR: [
|
||||
@@ -141,6 +150,7 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
const rawSchedules = await this._replica.taskSchedule.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
friendlyId: true,
|
||||
taskIdentifier: true,
|
||||
deduplicationKey: true,
|
||||
@@ -166,6 +176,7 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
type: filterType,
|
||||
AND: search
|
||||
? {
|
||||
OR: [
|
||||
@@ -215,11 +226,12 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
ON t."scheduleId" = r."scheduleId" AND t."createdAt" = r."LatestRun";`
|
||||
: [];
|
||||
|
||||
const schedules = rawSchedules.map((schedule) => {
|
||||
const schedules: ScheduleListItem[] = rawSchedules.map((schedule) => {
|
||||
const latestRun = latestRuns.find((r) => r.scheduleId === schedule.id);
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
type: schedule.type,
|
||||
friendlyId: schedule.friendlyId,
|
||||
taskIdentifier: schedule.taskIdentifier,
|
||||
deduplicationKey: schedule.deduplicationKey,
|
||||
|
||||
@@ -78,8 +78,10 @@ export class UsagePresenter extends BasePresenter {
|
||||
${sqlDatabaseSchema}."TaskRun" tr
|
||||
JOIN ${sqlDatabaseSchema}."Project" pr ON pr.id = tr."projectId"
|
||||
JOIN ${sqlDatabaseSchema}."Organization" org ON org.id = pr."organizationId"
|
||||
JOIN ${sqlDatabaseSchema}."RuntimeEnvironment" env ON env."projectId" = pr.id
|
||||
WHERE
|
||||
tr."createdAt" > ${startOfMonth}
|
||||
env.type <> 'DEVELOPMENT'
|
||||
AND tr."createdAt" > ${startOfMonth}
|
||||
AND tr."createdAt" < ${endOfMonth}
|
||||
AND org.id = ${organizationId}
|
||||
GROUP BY
|
||||
|
||||
@@ -21,6 +21,7 @@ export class ViewSchedulePresenter {
|
||||
const schedule = await this.#prismaClient.taskSchedule.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
friendlyId: true,
|
||||
generatorExpression: true,
|
||||
generatorDescription: true,
|
||||
@@ -99,6 +100,7 @@ export class ViewSchedulePresenter {
|
||||
public toJSONResponse(result: NonNullable<Awaited<ReturnType<ViewSchedulePresenter["call"]>>>) {
|
||||
const response: ScheduleObject = {
|
||||
id: result.schedule.friendlyId,
|
||||
type: result.schedule.type,
|
||||
task: result.schedule.taskIdentifier,
|
||||
active: result.schedule.active,
|
||||
nextRun: result.schedule.nextRuns[0],
|
||||
|
||||
@@ -12,7 +12,7 @@ const ParamsSchema = z.object({
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { organizationId } = ParamsSchema.parse(params);
|
||||
|
||||
const org = await prisma.organization.findUnique({
|
||||
const org = await prisma.organization.findFirst({
|
||||
select: {
|
||||
slug: true,
|
||||
_count: {
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ const ParamsSchema = z.object({
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { organizationId } = ParamsSchema.parse(params);
|
||||
|
||||
const org = await prisma.organization.findUnique({
|
||||
const org = await prisma.organization.findFirst({
|
||||
select: {
|
||||
slug: true,
|
||||
_count: {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const ParamsSchema = z.object({
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { organizationId } = ParamsSchema.parse(params);
|
||||
|
||||
const org = await prisma.organization.findUnique({
|
||||
const org = await prisma.organization.findFirst({
|
||||
select: {
|
||||
slug: true,
|
||||
_count: {
|
||||
|
||||
+16
-1
@@ -132,7 +132,11 @@ export default function Page() {
|
||||
</Property>
|
||||
<Property label="Version">{deployment.version}</Property>
|
||||
<Property label="Status">
|
||||
<DeploymentStatus status={deployment.status} className="text-sm" />
|
||||
<DeploymentStatus
|
||||
status={deployment.status}
|
||||
isBuilt={deployment.isBuilt}
|
||||
className="text-sm"
|
||||
/>
|
||||
</Property>
|
||||
<Property label="Tasks">{deployment.tasks ? deployment.tasks.length : "–"}</Property>
|
||||
<Property label="SDK Version">
|
||||
@@ -143,6 +147,17 @@ export default function Page() {
|
||||
<DateTimeAccurate date={deployment.createdAt} /> UTC
|
||||
</Paragraph>
|
||||
</Property>
|
||||
<Property label="Built at">
|
||||
<Paragraph variant="small/bright">
|
||||
{deployment.builtAt ? (
|
||||
<>
|
||||
<DateTimeAccurate date={deployment.builtAt} /> UTC
|
||||
</>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</Paragraph>
|
||||
</Property>
|
||||
<Property label="Deployed at">
|
||||
<Paragraph variant="small/bright">
|
||||
{deployment.deployedAt ? (
|
||||
|
||||
+36
-5
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ArrowUturnLeftIcon,
|
||||
BookOpenIcon,
|
||||
CommandLineIcon,
|
||||
ServerIcon,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Fragment } from "react/jsx-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
@@ -38,6 +40,7 @@ import {
|
||||
} from "~/components/primitives/Table";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus";
|
||||
import { RetryDeploymentIndexingDialog } from "~/components/runs/v3/RetryDeploymentIndexingDialog";
|
||||
import { RollbackDeploymentDialog } from "~/components/runs/v3/RollbackDeploymentDialog";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
@@ -54,6 +57,7 @@ import {
|
||||
v3EnvironmentVariablesPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { createSearchParams } from "~/utils/searchParams";
|
||||
import { deploymentIndexingIsRetryable } from "~/v3/deploymentStatus";
|
||||
|
||||
const SearchParams = z.object({
|
||||
page: z.coerce.number().optional(),
|
||||
@@ -124,7 +128,12 @@ export default function Page() {
|
||||
user.id !== deployment.environment.userId
|
||||
? deployment.environment.userName
|
||||
: undefined;
|
||||
const path = v3DeploymentPath(organization, project, deployment);
|
||||
const path = v3DeploymentPath(
|
||||
organization,
|
||||
project,
|
||||
deployment,
|
||||
currentPage
|
||||
);
|
||||
return (
|
||||
<TableRow key={deployment.id} className="group">
|
||||
<TableCell to={path}>
|
||||
@@ -143,7 +152,10 @@ export default function Page() {
|
||||
</TableCell>
|
||||
<TableCell to={path}>{deployment.version}</TableCell>
|
||||
<TableCell to={path}>
|
||||
<DeploymentStatus status={deployment.status} />
|
||||
<DeploymentStatus
|
||||
status={deployment.status}
|
||||
isBuilt={deployment.isBuilt}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{deployment.tasksCount !== null ? deployment.tasksCount : "–"}
|
||||
@@ -265,14 +277,19 @@ function DeploymentActionsCell({
|
||||
const location = useLocation();
|
||||
const project = useProject();
|
||||
|
||||
if (deployment.isCurrent || !deployment.isDeployed) return <TableCell to={path}>{""}</TableCell>;
|
||||
const canRollback = !deployment.isCurrent && deployment.isDeployed;
|
||||
const canRetryIndexing = deployment.isLatest && deploymentIndexingIsRetryable(deployment);
|
||||
|
||||
if (!canRollback && !canRetryIndexing) {
|
||||
return <TableCell to={path}>{""}</TableCell>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCellMenu isSticky>
|
||||
{!deployment.isCurrent && deployment.isDeployed && (
|
||||
{canRollback && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="small-menu-item" LeadingIcon={ArrowPathIcon}>
|
||||
<Button variant="small-menu-item" LeadingIcon={ArrowUturnLeftIcon}>
|
||||
Rollback
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -283,6 +300,20 @@ function DeploymentActionsCell({
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
{canRetryIndexing && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="small-menu-item" LeadingIcon={ArrowPathIcon}>
|
||||
Retry indexing
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<RetryDeploymentIndexingDialog
|
||||
projectId={project.id}
|
||||
deploymentShortCode={deployment.shortCode}
|
||||
redirectPath={`${location.pathname}${location.search}`}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</TableCellMenu>
|
||||
);
|
||||
}
|
||||
|
||||
+9
@@ -301,6 +301,8 @@ function EditEnvironmentVariablePanel({
|
||||
const lastSubmission = useActionData();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const hiddenValues = Object.values(variable.values).filter((value) => !environments.map(e => e.id).includes(value.environment.id));
|
||||
|
||||
const isLoading =
|
||||
navigation.state !== "idle" &&
|
||||
navigation.formMethod === "post" &&
|
||||
@@ -336,6 +338,12 @@ function EditEnvironmentVariablePanel({
|
||||
<input type="hidden" name="action" value="edit" />
|
||||
<input type="hidden" name="id" value={variable.id} />
|
||||
<input type="hidden" name="key" value={variable.key} />
|
||||
{hiddenValues.map((value, index) => (
|
||||
<Fragment key={index}>
|
||||
<input type="hidden" name={`values[${index}].environmentId`} value={value.environment.id} />
|
||||
<input type="hidden" name={`values[${index}].value`} value={value.value} />
|
||||
</Fragment>
|
||||
))}
|
||||
<FormError id={id.errorId}>{id.error}</FormError>
|
||||
<Fieldset>
|
||||
<InputGroup fullWidth className="mb-5 mt-2">
|
||||
@@ -351,6 +359,7 @@ function EditEnvironmentVariablePanel({
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-2">
|
||||
{environments.map((environment, index) => {
|
||||
const value = variable.values[environment.id]?.value;
|
||||
index += hiddenValues.length;
|
||||
return (
|
||||
<Fragment key={environment.id}>
|
||||
<input
|
||||
|
||||
+104
-67
@@ -1,15 +1,21 @@
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { BoltIcon, BoltSlashIcon, PencilSquareIcon, TrashIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
BoltIcon,
|
||||
BoltSlashIcon,
|
||||
BookOpenIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { Form, useLocation } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { token } from "morgan";
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel, EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout, variantClasses } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,6 +25,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
@@ -31,11 +38,11 @@ import {
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { EnabledStatus } from "~/components/runs/v3/EnabledStatus";
|
||||
import { ScheduleTypeCombo } from "~/components/runs/v3/ScheduleType";
|
||||
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { findProjectBySlug } from "~/models/project.server";
|
||||
import { ViewSchedulePresenter } from "~/presenters/v3/ViewSchedulePresenter.server";
|
||||
@@ -197,8 +204,15 @@ export default function Page() {
|
||||
|
||||
const isUtc = schedule.timezone === "UTC";
|
||||
|
||||
const isImperative = schedule.type === "IMPERATIVE";
|
||||
|
||||
return (
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full overflow-hidden bg-background-bright",
|
||||
isImperative ? "grid-rows-[2.5rem_1fr_3.25rem]" : "grid-rows-[2.5rem_1fr]"
|
||||
)}
|
||||
>
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
<Header2 className={cn("whitespace-nowrap")}>{schedule.friendlyId}</Header2>
|
||||
<LinkButton
|
||||
@@ -214,6 +228,9 @@ export default function Page() {
|
||||
<PropertyTable>
|
||||
<Property label="Schedule ID">{schedule.friendlyId}</Property>
|
||||
<Property label="Task ID">{schedule.taskIdentifier}</Property>
|
||||
<Property label="Type">
|
||||
<ScheduleTypeCombo type={schedule.type} className="text-sm" />
|
||||
</Property>
|
||||
<Property label="CRON (UTC)" labelClassName="self-start">
|
||||
<div className="space-y-2">
|
||||
<InlineCode variant="extra-small">{schedule.cron}</InlineCode>
|
||||
@@ -224,15 +241,19 @@ export default function Page() {
|
||||
<Property label="Environments">
|
||||
<EnvironmentLabels size="small" environments={schedule.environments} />
|
||||
</Property>
|
||||
<Property label="External ID">
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
</Property>
|
||||
<Property label="Deduplication key">
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
</Property>
|
||||
<Property label="Status">
|
||||
<EnabledStatus enabled={schedule.active} />
|
||||
</Property>
|
||||
{isImperative && (
|
||||
<>
|
||||
<Property label="External ID">
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
</Property>
|
||||
<Property label="Deduplication key">
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
</Property>
|
||||
<Property label="Status">
|
||||
<EnabledStatus enabled={schedule.active} />
|
||||
</Property>
|
||||
</>
|
||||
)}
|
||||
</PropertyTable>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3>Last 5 runs</Header3>
|
||||
@@ -288,68 +309,84 @@ export default function Page() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{!isImperative && (
|
||||
<InfoPanel
|
||||
title="Editing declarative schedules"
|
||||
icon={BookOpenIcon}
|
||||
iconClassName="text-indigo-500"
|
||||
variant="info"
|
||||
buttonLabel="Docs"
|
||||
to="https://trigger.dev/docs/v3/tasks-scheduled"
|
||||
panelClassName="max-w-full"
|
||||
>
|
||||
You can only edit a declarative schedule by updating your schedules.task and then
|
||||
running the CLI dev and deploy commands.
|
||||
</InfoPanel>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Form method="post">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={schedule.active ? BoltSlashIcon : BoltIcon}
|
||||
leadingIconClassName={schedule.active ? "text-dimmed" : "text-success"}
|
||||
name="action"
|
||||
value={schedule.active ? "disable" : "enable"}
|
||||
>
|
||||
{schedule.active ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
</Form>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
{isImperative && (
|
||||
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Form method="post">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={TrashIcon}
|
||||
leadingIconClassName="text-error"
|
||||
className="text-error"
|
||||
LeadingIcon={schedule.active ? BoltSlashIcon : BoltIcon}
|
||||
leadingIconClassName={schedule.active ? "text-dimmed" : "text-success"}
|
||||
name="action"
|
||||
value="delete"
|
||||
value={schedule.active ? "disable" : "enable"}
|
||||
>
|
||||
Delete
|
||||
{schedule.active ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>Delete schedule</DialogHeader>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this schedule? This can't be reversed.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Form method="post">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="danger/small"
|
||||
LeadingIcon={TrashIcon}
|
||||
name="action"
|
||||
value="delete"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={TrashIcon}
|
||||
leadingIconClassName="text-error"
|
||||
className="text-error"
|
||||
name="action"
|
||||
value="delete"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>Delete schedule</DialogHeader>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this schedule? This can't be reversed.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Form method="post">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="danger/small"
|
||||
LeadingIcon={TrashIcon}
|
||||
name="action"
|
||||
value="delete"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<LinkButton
|
||||
variant="tertiary/medium"
|
||||
to={`${v3EditSchedulePath(organization, project, schedule)}${location.search}`}
|
||||
LeadingIcon={PencilSquareIcon}
|
||||
>
|
||||
Edit schedule
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<LinkButton
|
||||
variant="tertiary/medium"
|
||||
to={`${v3EditSchedulePath(organization, project, schedule)}${location.search}`}
|
||||
LeadingIcon={PencilSquareIcon}
|
||||
>
|
||||
Edit schedule
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,8 +1,8 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { EditSchedulePresenter } from "~/presenters/v3/EditSchedulePresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { ProjectParamSchema, v3ScheduleParams } from "~/utils/pathBuilder";
|
||||
import { ProjectParamSchema, v3ScheduleParams, v3SchedulesPath } from "~/utils/pathBuilder";
|
||||
import { humanToCronSupported } from "~/v3/humanToCron.server";
|
||||
import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new/route";
|
||||
|
||||
@@ -17,6 +17,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
friendlyId: scheduleParam,
|
||||
});
|
||||
|
||||
if (result.schedule?.type === "DECLARATIVE") {
|
||||
throw redirect(v3SchedulesPath({ slug: organizationSlug }, { slug: projectParam }));
|
||||
}
|
||||
|
||||
return typedjson({ ...result, showGenerateField: humanToCronSupported });
|
||||
};
|
||||
|
||||
|
||||
+68
-3
@@ -10,6 +10,11 @@ import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import {
|
||||
ScheduleTypeCombo,
|
||||
ScheduleTypeIcon,
|
||||
scheduleTypeName,
|
||||
} from "~/components/runs/v3/ScheduleType";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -334,6 +339,51 @@ function SchedulesTable({
|
||||
<TableRow>
|
||||
<TableHeaderCell>ID</TableHeaderCell>
|
||||
<TableHeaderCell>Task ID</TableHeaderCell>
|
||||
<TableHeaderCell
|
||||
tooltip={
|
||||
<div className="flex max-w-xs flex-col gap-4 p-1">
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-1.5 text-sm">
|
||||
<div className={"flex items-center space-x-1"}>
|
||||
<ScheduleTypeIcon type={"DECLARATIVE"} className="text-sky-500" />
|
||||
<span className="font-medium">{scheduleTypeName("DECLARATIVE")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Paragraph variant="small" className="!text-wrap text-text-dimmed">
|
||||
Declarative schedules are defined in a{" "}
|
||||
<InlineCode variant="extra-small">schedules.task</InlineCode> with the{" "}
|
||||
<InlineCode variant="extra-small">cron</InlineCode>
|
||||
property. They sync when you update your{" "}
|
||||
<InlineCode variant="extra-small">schedules.task</InlineCode> definition and run
|
||||
the CLI dev or deploy commands.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-1.5 text-sm">
|
||||
<div className={"flex items-center space-x-1"}>
|
||||
<ScheduleTypeIcon type={"IMPERATIVE"} className="text-teal-500" />
|
||||
<span className="font-medium">{scheduleTypeName("IMPERATIVE")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Paragraph variant="small" className="!text-wrap text-text-dimmed">
|
||||
Imperative schedules are defined here in the dashboard or by using the SDK
|
||||
functions to create or delete them. They can be created, updated, disabled, and
|
||||
deleted from the dashboard or using the SDK.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<LinkButton
|
||||
variant="tertiary/medium"
|
||||
to="https://trigger.dev/docs/v3/tasks-scheduled"
|
||||
>
|
||||
View the docs
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Type
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>External ID</TableHeaderCell>
|
||||
<TableHeaderCell>CRON</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
|
||||
@@ -362,7 +412,14 @@ function SchedulesTable({
|
||||
{schedule.taskIdentifier}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
<ScheduleTypeCombo type={schedule.type} />
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.type === "IMPERATIVE"
|
||||
? schedule.externalId
|
||||
? schedule.externalId
|
||||
: "–"
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.cron}
|
||||
@@ -384,13 +441,21 @@ function SchedulesTable({
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
{schedule.type === "IMPERATIVE"
|
||||
? schedule.userProvidedDeduplicationKey
|
||||
? schedule.deduplicationKey
|
||||
: "–"
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<EnvironmentLabels environments={schedule.environments} size="small" />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnabledStatus enabled={schedule.active} />
|
||||
{schedule.type === "IMPERATIVE" ? (
|
||||
<EnabledStatus enabled={schedule.active} />
|
||||
) : (
|
||||
"N/A"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,8 @@ import { requireUserId } from "~/services/session.server";
|
||||
import { formatCurrency, formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter";
|
||||
import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { InformationCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
await requireUserId(request);
|
||||
@@ -213,52 +215,60 @@ export default function Page() {
|
||||
>
|
||||
{(tasks) => {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Task</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Runs</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Average duration</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Average cost</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Total duration</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Total cost</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.length === 0 ? (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<Paragraph variant="small">No runs.</Paragraph>
|
||||
</TableCell>
|
||||
<TableHeaderCell>Task</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Runs</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Average duration</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Average cost</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Total duration</TableHeaderCell>
|
||||
<TableHeaderCell alignment="right">Total cost</TableHeaderCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<TableRow key={task.taskIdentifier}>
|
||||
<TableCell>{task.taskIdentifier}</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatNumber(task.runCount)}
|
||||
</TableCell>
|
||||
<TableCell alignment="right">
|
||||
{formatDurationMilliseconds(task.averageDuration, {
|
||||
style: "short",
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatCurrencyAccurate(task.averageCost)}
|
||||
</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatDurationMilliseconds(task.totalDuration, {
|
||||
style: "short",
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatCurrencyAccurate(task.totalCost)}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<div className="flex items-center justify-center">
|
||||
<Paragraph variant="small">No runs for this period</Paragraph>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<TableRow key={task.taskIdentifier}>
|
||||
<TableCell>{task.taskIdentifier}</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatNumber(task.runCount)}
|
||||
</TableCell>
|
||||
<TableCell alignment="right">
|
||||
{formatDurationMilliseconds(task.averageDuration, {
|
||||
style: "short",
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatCurrencyAccurate(task.averageCost)}
|
||||
</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatDurationMilliseconds(task.totalDuration, {
|
||||
style: "short",
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell alignment="right" className="tabular-nums">
|
||||
{formatCurrencyAccurate(task.totalCost)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<InfoPanel icon={InformationCircleIcon} panelClassName="max-w-[22rem] mt-3">
|
||||
Dev environment runs are excluded from the usage data above, since they do
|
||||
not have an associated compute cost.
|
||||
</InfoPanel>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Await>
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug } = OrganizationParamsSchema.parse(params);
|
||||
|
||||
const organization = await prisma.organization.findUnique({
|
||||
const organization = await prisma.organization.findFirst({
|
||||
where: { slug: organizationSlug, members: { some: { userId } } },
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -51,6 +51,7 @@ export default function ChoosePlanPage() {
|
||||
subscription={v3Subscription}
|
||||
organizationSlug={organizationSlug}
|
||||
hasPromotedPlan
|
||||
showGithubVerificationBadge
|
||||
/>
|
||||
</MainCenteredContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { createEnvironment } from "~/models/organization.server";
|
||||
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
|
||||
import { marqs } from "~/v3/marqs/index.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
organizationId: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* It will create a staging environment for all the projects where there isn't one already
|
||||
*/
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
// Next authenticate the request
|
||||
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
|
||||
|
||||
if (!authenticationResult) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: authenticationResult.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!user.admin) {
|
||||
return json({ error: "You must be an admin to perform this action" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { organizationId } = ParamsSchema.parse(params);
|
||||
|
||||
const organization = await prisma.organization.findUnique({
|
||||
where: {
|
||||
id: organizationId,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
include: { environments: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return json({ error: "Organization not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
for (const project of organization.projects) {
|
||||
const stagingEnvironment = project.environments.find((env) => env.type === "STAGING");
|
||||
|
||||
if (!stagingEnvironment) {
|
||||
const staging = await createEnvironment(organization, project, "STAGING");
|
||||
await marqs?.updateEnvConcurrencyLimits({ ...staging, organization, project });
|
||||
created++;
|
||||
} else {
|
||||
await marqs?.updateEnvConcurrencyLimits({ ...stagingEnvironment, organization, project });
|
||||
}
|
||||
}
|
||||
|
||||
return json({ success: true, created, total: organization.projects.length });
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||
import { Form } from "@remix-run/react";
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
@@ -16,8 +16,7 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { adminGetOrganizations, setV3Enabled } from "~/models/admin.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { adminGetOrganizations } from "~/models/admin.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { createSearchParams } from "~/utils/searchParams";
|
||||
|
||||
@@ -40,23 +39,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
return typedjson(result);
|
||||
};
|
||||
|
||||
const FormSchema = z.object({ id: z.string(), v3: z.enum(["enable", "disable"]) });
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
if (request.method.toLowerCase() !== "post") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const payload = Object.fromEntries(await request.formData());
|
||||
|
||||
const { id, v3 } = FormSchema.parse(payload);
|
||||
|
||||
const result = await setV3Enabled(userId, id, v3 === "enable");
|
||||
|
||||
return redirectWithSuccessMessage("/admin/orgs", request, `v3 ${v3}d for org ${id}`);
|
||||
}
|
||||
export default function AdminDashboardRoute() {
|
||||
const { organizations, filters, page, pageCount } = useTypedLoaderData<typeof loader>();
|
||||
|
||||
@@ -87,6 +69,7 @@ export default function AdminDashboardRoute() {
|
||||
<TableHeaderCell>Slug</TableHeaderCell>
|
||||
<TableHeaderCell>Members</TableHeaderCell>
|
||||
<TableHeaderCell>id</TableHeaderCell>
|
||||
<TableHeaderCell>v2?</TableHeaderCell>
|
||||
<TableHeaderCell>v3?</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -114,34 +97,9 @@ export default function AdminDashboardRoute() {
|
||||
))}
|
||||
</TableCell>
|
||||
<TableCell>{org.id}</TableCell>
|
||||
<TableCell>{org.v2Enabled ? "✅" : ""}</TableCell>
|
||||
<TableCell>{org.v3Enabled ? "✅" : ""}</TableCell>
|
||||
<TableCell isSticky={true}>
|
||||
<Form method="post" reloadDocument>
|
||||
<input type="hidden" name="id" value={org.id} />
|
||||
|
||||
{org.v3Enabled ? (
|
||||
<Button
|
||||
type="submit"
|
||||
name="v3"
|
||||
value="disable"
|
||||
className="mr-2"
|
||||
variant="tertiary/small"
|
||||
>
|
||||
Disable v3
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
name="v3"
|
||||
value="enable"
|
||||
className="mr-2"
|
||||
variant="tertiary/small"
|
||||
>
|
||||
Enable v3
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
</TableCell>
|
||||
<TableCell isSticky={true}> </TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -3,7 +3,11 @@ import { CreateBackgroundWorkerRequestBody } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { CreateBackgroundWorkerService } from "~/v3/services/createBackgroundWorker.server";
|
||||
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||
import {
|
||||
CreateBackgroundWorkerService,
|
||||
CreateDeclarativeScheduleError,
|
||||
} from "~/v3/services/createBackgroundWorker.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectRef: z.string(),
|
||||
@@ -42,14 +46,26 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
|
||||
const service = new CreateBackgroundWorkerService();
|
||||
|
||||
const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data);
|
||||
try {
|
||||
const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data);
|
||||
|
||||
return json(
|
||||
{
|
||||
id: backgroundWorker.friendlyId,
|
||||
version: backgroundWorker.version,
|
||||
contentHash: backgroundWorker.contentHash,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
return json(
|
||||
{
|
||||
id: backgroundWorker.friendlyId,
|
||||
version: backgroundWorker.version,
|
||||
contentHash: backgroundWorker.contentHash,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Failed to create background worker", { error: e });
|
||||
|
||||
if (e instanceof ServiceValidationError) {
|
||||
return json({ error: e.message }, { status: 400 });
|
||||
} else if (e instanceof CreateDeclarativeScheduleError) {
|
||||
return json({ error: e.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ error: "Failed to create background worker" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
|
||||
const responseObject: ScheduleObject = {
|
||||
id: schedule.id,
|
||||
type: schedule.type,
|
||||
task: schedule.task,
|
||||
active: schedule.active,
|
||||
generator: {
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
const responseObject: ScheduleObject = {
|
||||
id: schedule.id,
|
||||
type: schedule.type,
|
||||
task: schedule.task,
|
||||
active: schedule.active,
|
||||
generator: {
|
||||
@@ -107,6 +108,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
return {
|
||||
data: result.schedules.map((schedule) => ({
|
||||
id: schedule.friendlyId,
|
||||
type: schedule.type,
|
||||
task: schedule.taskIdentifier,
|
||||
generator: {
|
||||
type: "CRON",
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ActionFunction, json } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { deploymentIndexingIsRetryable } from "~/v3/deploymentStatus";
|
||||
import { RetryDeploymentIndexingService } from "~/v3/services/retryDeploymentIndexing.server";
|
||||
|
||||
export const rollbackSchema = z.object({
|
||||
redirectUrl: z.string(),
|
||||
});
|
||||
|
||||
const ParamSchema = z.object({
|
||||
projectId: z.string(),
|
||||
deploymentShortCode: z.string(),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectId, deploymentShortCode } = ParamSchema.parse(params);
|
||||
|
||||
console.log("projectId", projectId);
|
||||
console.log("deploymentShortCode", deploymentShortCode);
|
||||
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema: rollbackSchema });
|
||||
|
||||
if (!submission.value) {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
try {
|
||||
const project = await prisma.project.findUnique({
|
||||
where: {
|
||||
id: projectId,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return redirectWithErrorMessage(submission.value.redirectUrl, request, "Project not found");
|
||||
}
|
||||
|
||||
const deployment = await prisma.workerDeployment.findUnique({
|
||||
where: {
|
||||
projectId_shortCode: {
|
||||
projectId: project.id,
|
||||
shortCode: deploymentShortCode,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
return redirectWithErrorMessage(
|
||||
submission.value.redirectUrl,
|
||||
request,
|
||||
"Deployment not found"
|
||||
);
|
||||
}
|
||||
|
||||
if (!deploymentIndexingIsRetryable(deployment)) {
|
||||
return redirectWithErrorMessage(
|
||||
submission.value.redirectUrl,
|
||||
request,
|
||||
"Deployment indexing not in retryable state"
|
||||
);
|
||||
}
|
||||
|
||||
const startIndexing = new RetryDeploymentIndexingService();
|
||||
await startIndexing.call(deployment.id);
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
submission.value.redirectUrl,
|
||||
request,
|
||||
"Retrying deployment indexing"
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Failed to retry deployment indexing", {
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
projectId,
|
||||
deploymentShortCode,
|
||||
});
|
||||
submission.error = { runParam: error.message };
|
||||
return json(submission);
|
||||
} else {
|
||||
logger.error("Failed to retry deployment indexing", {
|
||||
error,
|
||||
projectId,
|
||||
deploymentShortCode,
|
||||
});
|
||||
submission.error = { runParam: JSON.stringify(error) };
|
||||
return json(submission);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -133,6 +133,7 @@ type PricingPlansProps = {
|
||||
subscription?: SubscriptionResult;
|
||||
organizationSlug: string;
|
||||
hasPromotedPlan: boolean;
|
||||
showGithubVerificationBadge?: boolean;
|
||||
};
|
||||
|
||||
export function PricingPlans({
|
||||
@@ -140,6 +141,7 @@ export function PricingPlans({
|
||||
subscription,
|
||||
organizationSlug,
|
||||
hasPromotedPlan,
|
||||
showGithubVerificationBadge,
|
||||
}: PricingPlansProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-col">
|
||||
@@ -148,6 +150,7 @@ export function PricingPlans({
|
||||
plan={plans.free}
|
||||
subscription={subscription}
|
||||
organizationSlug={organizationSlug}
|
||||
showGithubVerificationBadge={showGithubVerificationBadge}
|
||||
/>
|
||||
<TierHobby
|
||||
plan={plans.hobby}
|
||||
@@ -168,10 +171,12 @@ export function TierFree({
|
||||
plan,
|
||||
subscription,
|
||||
organizationSlug,
|
||||
showGithubVerificationBadge,
|
||||
}: {
|
||||
plan: FreePlanDefinition;
|
||||
subscription?: SubscriptionResult;
|
||||
organizationSlug: string;
|
||||
showGithubVerificationBadge?: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const navigation = useNavigation();
|
||||
@@ -184,7 +189,7 @@ export function TierFree({
|
||||
<TierContainer>
|
||||
<div className="relative">
|
||||
<PricingHeader title={plan.title} cost={0} />
|
||||
{status === "approved" && (
|
||||
{showGithubVerificationBadge && status === "approved" && (
|
||||
<SimpleTooltip
|
||||
buttonClassName="absolute right-1 top-1"
|
||||
button={
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
function projectForUpdates(id: string) {
|
||||
return prisma.project.findUnique({
|
||||
return prisma.project.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
|
||||
@@ -153,6 +153,10 @@ export async function authenticatedEnvironmentForAuthentication(
|
||||
projectRef: string,
|
||||
slug: string
|
||||
): Promise<AuthenticatedEnvironment> {
|
||||
if (slug === "staging") {
|
||||
slug = "stg";
|
||||
}
|
||||
|
||||
switch (auth.type) {
|
||||
case "apiKey": {
|
||||
if (auth.result.environment.project.externalRef !== projectRef) {
|
||||
|
||||
@@ -590,11 +590,11 @@ function getWorkerQueue() {
|
||||
},
|
||||
"v3.triggerScheduledTask": {
|
||||
priority: 0,
|
||||
maxAttempts: 3,
|
||||
maxAttempts: 3, // total delay of 30 seconds
|
||||
handler: async (payload, job) => {
|
||||
const service = new TriggerScheduledTaskService();
|
||||
|
||||
return await service.call(payload.instanceId);
|
||||
return await service.call(payload.instanceId, job.attempts === job.max_attempts);
|
||||
},
|
||||
},
|
||||
"v3.performTaskAttemptAlerts": {
|
||||
|
||||
@@ -442,9 +442,11 @@ export function v3DeploymentsPath(organization: OrgForPath, project: ProjectForP
|
||||
export function v3DeploymentPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
deployment: DeploymentForPath
|
||||
deployment: DeploymentForPath,
|
||||
currentPage: number
|
||||
) {
|
||||
return `${v3DeploymentsPath(organization, project)}/${deployment.shortCode}`;
|
||||
const query = currentPage ? `?page=${currentPage}` : "";
|
||||
return `${v3DeploymentsPath(organization, project)}/${deployment.shortCode}${query}`;
|
||||
}
|
||||
|
||||
export function v3BillingPath(organization: OrgForPath) {
|
||||
|
||||
@@ -2,10 +2,12 @@ import { clientWebsocketMessages, serverWebsocketMessages } from "@trigger.dev/c
|
||||
import { ZodMessageHandler, ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
|
||||
import { Evt } from "evt";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { CloseEvent, ErrorEvent, MessageEvent, WebSocket } from "ws";
|
||||
import type { CloseEvent, ErrorEvent, MessageEvent } from "ws";
|
||||
import { WebSocket } from "ws";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { DevQueueConsumer } from "./marqs/devQueueConsumer.server";
|
||||
import { HeartbeatService } from "./services/heartbeatService.server";
|
||||
|
||||
export class AuthenticatedSocketConnection {
|
||||
public id: string;
|
||||
@@ -14,6 +16,7 @@ export class AuthenticatedSocketConnection {
|
||||
private _sender: ZodMessageSender<typeof serverWebsocketMessages>;
|
||||
private _consumer: DevQueueConsumer;
|
||||
private _messageHandler: ZodMessageHandler<typeof clientWebsocketMessages>;
|
||||
private _pingService: HeartbeatService;
|
||||
|
||||
constructor(
|
||||
public ws: WebSocket,
|
||||
@@ -50,8 +53,42 @@ export class AuthenticatedSocketConnection {
|
||||
ws.addEventListener("close", this.#handleClose.bind(this));
|
||||
ws.addEventListener("error", this.#handleError.bind(this));
|
||||
|
||||
ws.on("ping", (data) => {
|
||||
logger.debug("[AuthenticatedSocketConnection] Received ping", {
|
||||
id: this.id,
|
||||
envId: this.authenticatedEnv.id,
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
ws.on("pong", (data) => {
|
||||
// logger.debug("[AuthenticatedSocketConnection] Received pong", {
|
||||
// id: this.id,
|
||||
// envId: this.authenticatedEnv.id,
|
||||
// data,
|
||||
// });
|
||||
});
|
||||
|
||||
this._pingService = new HeartbeatService({
|
||||
heartbeat: async () => {
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
logger.debug("[AuthenticatedSocketConnection] Websocket not open, skipping ping");
|
||||
return;
|
||||
}
|
||||
|
||||
// logger.debug("[AuthenticatedSocketConnection] Sending ping", {
|
||||
// id: this.id,
|
||||
// envId: this.authenticatedEnv.id,
|
||||
// });
|
||||
|
||||
ws.ping();
|
||||
},
|
||||
});
|
||||
this._pingService.start();
|
||||
|
||||
this._messageHandler = new ZodMessageHandler({
|
||||
schema: clientWebsocketMessages,
|
||||
logger,
|
||||
messages: {
|
||||
READY_FOR_TASKS: async (payload) => {
|
||||
await this._consumer.registerBackgroundWorker(
|
||||
@@ -99,14 +136,29 @@ export class AuthenticatedSocketConnection {
|
||||
}
|
||||
|
||||
async #handleMessage(ev: MessageEvent) {
|
||||
const data = JSON.parse(ev.data.toString());
|
||||
try {
|
||||
const data = JSON.parse(ev.data.toString());
|
||||
|
||||
await this._messageHandler.handleMessage(data);
|
||||
await this._messageHandler.handleMessage(data);
|
||||
} catch (error) {
|
||||
logger.error("[AuthenticatedSocketConnection] Failed to handle message", {
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error,
|
||||
message: ev.data.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async #handleClose(ev: CloseEvent) {
|
||||
logger.debug("[AuthenticatedSocketConnection] Websocket closed", { ev });
|
||||
|
||||
this._pingService.stop();
|
||||
|
||||
await this._consumer.stop();
|
||||
|
||||
const result = this.onClose.post(ev);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WorkerDeployment } from "@trigger.dev/database";
|
||||
|
||||
export const deploymentIndexingIsRetryable = ({
|
||||
builtAt,
|
||||
status,
|
||||
}: {
|
||||
builtAt: Date | null;
|
||||
status: WorkerDeployment["status"];
|
||||
}) => {
|
||||
return builtAt && (status === "FAILED" || status === "TIMED_OUT");
|
||||
};
|
||||
@@ -91,17 +91,13 @@ export class EnvironmentVariablesRepository implements Repository {
|
||||
return { success: false as const, error: `Environment not found` };
|
||||
}
|
||||
|
||||
// Check to see if any of the variables are `TRIGGER_SECRET_KEY` or `TRIGGER_API_URL`
|
||||
const triggerKeys = options.variables.map((v) => v.key);
|
||||
if (triggerKeys.includes("TRIGGER_SECRET_KEY") || triggerKeys.includes("TRIGGER_API_URL")) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: `You cannot set the variables TRIGGER_SECRET_KEY or TRIGGER_API_URL as they will be set automatically`,
|
||||
};
|
||||
}
|
||||
// Remove `TRIGGER_SECRET_KEY` or `TRIGGER_API_URL`
|
||||
let values = options.variables.filter(
|
||||
(v) => v.key !== "TRIGGER_SECRET_KEY" && v.key !== "TRIGGER_API_URL"
|
||||
);
|
||||
|
||||
//get rid of empty variables
|
||||
const values = options.variables.filter((v) => v.key.trim() !== "" && v.value.trim() !== "");
|
||||
values = values.filter((v) => v.key.trim() !== "" && v.value.trim() !== "");
|
||||
if (values.length === 0) {
|
||||
return { success: false as const, error: `You must set at least one value` };
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ export class MarQS {
|
||||
private redis: Redis;
|
||||
public keys: MarQSKeyProducer;
|
||||
private queuePriorityStrategy: MarQSQueuePriorityStrategy;
|
||||
#requeueingWorkers: Array<AsyncWorker> = [];
|
||||
#rebalanceWorkers: Array<AsyncWorker> = [];
|
||||
|
||||
constructor(private readonly options: MarQSOptions) {
|
||||
@@ -846,10 +845,14 @@ export class MarQS {
|
||||
});
|
||||
|
||||
stream.on("data", async (keys) => {
|
||||
stream.pause();
|
||||
|
||||
const uniqueKeys = Array.from(new Set<string>(keys));
|
||||
|
||||
if (uniqueKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
stream.pause();
|
||||
|
||||
logger.debug("Rebalancing parent queues", {
|
||||
component: "marqs",
|
||||
operation: "rebalanceParentQueues",
|
||||
|
||||
@@ -1080,7 +1080,7 @@ class SharedQueueTasks {
|
||||
|
||||
const variables = await this.#buildEnvironmentVariables(
|
||||
attempt.runtimeEnvironment,
|
||||
taskRun,
|
||||
taskRun.id,
|
||||
machinePreset
|
||||
);
|
||||
|
||||
@@ -1136,16 +1136,18 @@ class SharedQueueTasks {
|
||||
return;
|
||||
}
|
||||
|
||||
const run = await prisma.taskRun.findUnique({
|
||||
const run = await prisma.taskRun.findFirst({
|
||||
where: {
|
||||
id: runId,
|
||||
runtimeEnvironmentId: environment.id,
|
||||
},
|
||||
include: {
|
||||
lockedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
id: true,
|
||||
traceContext: true,
|
||||
friendlyId: true,
|
||||
isTest: true,
|
||||
lockedBy: {
|
||||
select: {
|
||||
attempts: true,
|
||||
machineConfig: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1156,9 +1158,20 @@ class SharedQueueTasks {
|
||||
return;
|
||||
}
|
||||
|
||||
const attemptCount = await prisma.taskRunAttempt.count({
|
||||
where: {
|
||||
taskRunId: run.id,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug("Getting lazy attempt payload for run", {
|
||||
run,
|
||||
attemptCount,
|
||||
});
|
||||
|
||||
const machinePreset = machinePresetFromConfig(run.lockedBy?.machineConfig ?? {});
|
||||
|
||||
const variables = await this.#buildEnvironmentVariables(environment, run, machinePreset);
|
||||
const variables = await this.#buildEnvironmentVariables(environment, run.id, machinePreset);
|
||||
|
||||
return {
|
||||
traceContext: run.traceContext as Record<string, unknown>,
|
||||
@@ -1169,7 +1182,7 @@ class SharedQueueTasks {
|
||||
runId: run.friendlyId,
|
||||
messageId: run.id,
|
||||
isTest: run.isTest,
|
||||
attemptCount: run._count.attempts,
|
||||
attemptCount,
|
||||
} satisfies TaskRunExecutionLazyAttemptPayload;
|
||||
}
|
||||
|
||||
@@ -1203,13 +1216,13 @@ class SharedQueueTasks {
|
||||
|
||||
async #buildEnvironmentVariables(
|
||||
environment: RuntimeEnvironment,
|
||||
run: TaskRun,
|
||||
runId: string,
|
||||
machinePreset: MachinePreset
|
||||
): Promise<Array<EnvironmentVariable>> {
|
||||
const variables = await resolveVariablesForEnvironment(environment);
|
||||
|
||||
const jwt = await generateJWTTokenForEnvironment(environment, {
|
||||
run_id: run.id,
|
||||
run_id: runId,
|
||||
machine_preset: machinePreset.name,
|
||||
});
|
||||
|
||||
@@ -1217,7 +1230,7 @@ class SharedQueueTasks {
|
||||
...variables,
|
||||
...[
|
||||
{ key: "TRIGGER_JWT", value: jwt },
|
||||
{ key: "TRIGGER_RUN_ID", value: run.id },
|
||||
{ key: "TRIGGER_RUN_ID", value: runId },
|
||||
{
|
||||
key: "TRIGGER_MACHINE_PRESET",
|
||||
value: machinePreset.name,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ZodError } from "zod";
|
||||
import { CronPattern } from "../schedules";
|
||||
import { BaseService, ServiceValidationError } from "./baseService.server";
|
||||
import { getLimit } from "~/services/platform.v3.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
type Schedule = {
|
||||
cron: string;
|
||||
timezone?: string;
|
||||
taskIdentifier: string;
|
||||
friendlyId?: string;
|
||||
};
|
||||
|
||||
export class CheckScheduleService extends BaseService {
|
||||
public async call(projectId: string, schedule: Schedule) {
|
||||
//validate the cron expression
|
||||
try {
|
||||
CronPattern.parse(schedule.cron);
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
throw new ServiceValidationError(`Invalid cron expression: ${e.issues[0].message}`);
|
||||
}
|
||||
|
||||
throw new ServiceValidationError(
|
||||
`Invalid cron expression: ${e instanceof Error ? e.message : JSON.stringify(e)}`
|
||||
);
|
||||
}
|
||||
|
||||
//chek it's a valid timezone
|
||||
if (schedule.timezone) {
|
||||
const possibleTimezones = getTimezones();
|
||||
if (!possibleTimezones.includes(schedule.timezone)) {
|
||||
throw new ServiceValidationError(
|
||||
`Invalid IANA timezone: '${schedule.timezone}'. View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//check the task exists
|
||||
const task = await this._prisma.backgroundWorkerTask.findFirst({
|
||||
where: {
|
||||
slug: schedule.taskIdentifier,
|
||||
projectId: projectId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new ServiceValidationError(
|
||||
`Task with identifier ${schedule.taskIdentifier} not found in project.`
|
||||
);
|
||||
}
|
||||
|
||||
if (task.triggerSource !== "SCHEDULED") {
|
||||
throw new ServiceValidationError(
|
||||
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
|
||||
);
|
||||
}
|
||||
|
||||
//if creating a schedule, check they're under the limits
|
||||
if (!schedule.friendlyId) {
|
||||
//check they're within their limit
|
||||
const project = await this._prisma.project.findFirst({
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new ServiceValidationError("Project not found");
|
||||
}
|
||||
|
||||
const limit = await getLimit(project.organizationId, "schedules", 500);
|
||||
const schedulesCount = await this._prisma.taskSchedule.count({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (schedulesCount >= limit) {
|
||||
throw new ServiceValidationError(
|
||||
`You have created ${schedulesCount}/${limit} schedules so you'll need to increase your limits or delete some schedules.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { projectPubSub } from "./projectPubSub.server";
|
||||
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
|
||||
import cronstrue from "cronstrue";
|
||||
import { CheckScheduleService } from "./checkSchedule.server";
|
||||
|
||||
export class CreateBackgroundWorkerService extends BaseService {
|
||||
public async call(
|
||||
@@ -68,6 +71,12 @@ export class CreateBackgroundWorkerService extends BaseService {
|
||||
});
|
||||
|
||||
await createBackgroundTasks(body.metadata.tasks, backgroundWorker, environment, this._prisma);
|
||||
await syncDeclarativeSchedules(
|
||||
body.metadata.tasks,
|
||||
backgroundWorker,
|
||||
environment,
|
||||
this._prisma
|
||||
);
|
||||
|
||||
try {
|
||||
//send a notification that a new worker has been created
|
||||
@@ -220,3 +229,155 @@ export async function createBackgroundTasks(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//CreateDeclarativeScheduleError with a message
|
||||
export class CreateDeclarativeScheduleError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "CreateDeclarativeScheduleError";
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncDeclarativeSchedules(
|
||||
tasks: TaskResource[],
|
||||
worker: BackgroundWorker,
|
||||
environment: AuthenticatedEnvironment,
|
||||
prisma: PrismaClientOrTransaction
|
||||
) {
|
||||
const tasksWithDeclarativeSchedules = tasks.filter((task) => task.schedule);
|
||||
logger.info("Syncing declarative schedules", {
|
||||
tasksWithDeclarativeSchedules,
|
||||
environment,
|
||||
});
|
||||
|
||||
const existingDeclarativeSchedules = await prisma.taskSchedule.findMany({
|
||||
where: {
|
||||
type: "DECLARATIVE",
|
||||
projectId: environment.projectId,
|
||||
},
|
||||
include: {
|
||||
instances: true,
|
||||
},
|
||||
});
|
||||
|
||||
const checkSchedule = new CheckScheduleService(prisma);
|
||||
const registerNextService = new RegisterNextTaskScheduleInstanceService(prisma);
|
||||
|
||||
//start out by assuming they're all missing
|
||||
const missingSchedules = new Set<string>(
|
||||
existingDeclarativeSchedules.map((schedule) => schedule.id)
|
||||
);
|
||||
|
||||
//create/update schedules (+ instances)
|
||||
for (const task of tasksWithDeclarativeSchedules) {
|
||||
if (task.schedule === undefined) continue;
|
||||
|
||||
const existingSchedule = existingDeclarativeSchedules.find(
|
||||
(schedule) =>
|
||||
schedule.taskIdentifier === task.id &&
|
||||
schedule.instances.some((instance) => instance.environmentId === environment.id)
|
||||
);
|
||||
|
||||
//this throws errors if the schedule is invalid
|
||||
await checkSchedule.call(environment.projectId, {
|
||||
cron: task.schedule.cron,
|
||||
timezone: task.schedule.timezone,
|
||||
taskIdentifier: task.id,
|
||||
friendlyId: existingSchedule?.friendlyId,
|
||||
});
|
||||
|
||||
if (existingSchedule) {
|
||||
const schedule = await prisma.taskSchedule.update({
|
||||
where: {
|
||||
id: existingSchedule.id,
|
||||
},
|
||||
data: {
|
||||
generatorExpression: task.schedule.cron,
|
||||
generatorDescription: cronstrue.toString(task.schedule.cron),
|
||||
timezone: task.schedule.timezone,
|
||||
},
|
||||
include: {
|
||||
instances: true,
|
||||
},
|
||||
});
|
||||
|
||||
missingSchedules.delete(existingSchedule.id);
|
||||
const instance = schedule.instances.at(0);
|
||||
if (instance) {
|
||||
await registerNextService.call(instance.id);
|
||||
} else {
|
||||
throw new CreateDeclarativeScheduleError(
|
||||
`Missing instance for declarative schedule ${schedule.id}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const newSchedule = await prisma.taskSchedule.create({
|
||||
data: {
|
||||
friendlyId: generateFriendlyId("sched"),
|
||||
projectId: environment.projectId,
|
||||
taskIdentifier: task.id,
|
||||
generatorExpression: task.schedule.cron,
|
||||
generatorDescription: cronstrue.toString(task.schedule.cron),
|
||||
timezone: task.schedule.timezone,
|
||||
type: "DECLARATIVE",
|
||||
instances: {
|
||||
create: [
|
||||
{
|
||||
environmentId: environment.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
instances: true,
|
||||
},
|
||||
});
|
||||
|
||||
const instance = newSchedule.instances.at(0);
|
||||
|
||||
if (instance) {
|
||||
await registerNextService.call(instance.id);
|
||||
} else {
|
||||
throw new CreateDeclarativeScheduleError(
|
||||
`Missing instance for declarative schedule ${newSchedule.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Delete instances for this environment
|
||||
//Delete schedules that have no instances left
|
||||
const potentiallyDeletableSchedules = await prisma.taskSchedule.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: Array.from(missingSchedules),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
instances: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const schedule of potentiallyDeletableSchedules) {
|
||||
const canDeleteSchedule =
|
||||
schedule.instances.length === 0 ||
|
||||
schedule.instances.every((instance) => instance.environmentId === environment.id);
|
||||
|
||||
if (canDeleteSchedule) {
|
||||
//we can delete schedules with no instances other than ones for the current environment
|
||||
await prisma.taskSchedule.delete({
|
||||
where: {
|
||||
id: schedule.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
//otherwise we delete the instance (other environments remain untouched)
|
||||
await prisma.taskScheduleInstance.deleteMany({
|
||||
where: {
|
||||
taskScheduleId: schedule.id,
|
||||
environmentId: environment.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ import type { BackgroundWorker } from "@trigger.dev/database";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { createBackgroundTasks } from "./createBackgroundWorker.server";
|
||||
import { createBackgroundTasks, syncDeclarativeSchedules } from "./createBackgroundWorker.server";
|
||||
import { CURRENT_DEPLOYMENT_LABEL } from "~/consts";
|
||||
import { projectPubSub } from "./projectPubSub.server";
|
||||
import { marqs } from "~/v3/marqs/index.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { ExecuteTasksWaitingForDeployService } from "./executeTasksWaitingForDeploy";
|
||||
import { PerformDeploymentAlertsService } from "./alerts/performDeploymentAlerts.server";
|
||||
import { TimeoutDeploymentService } from "./timeoutDeployment.server";
|
||||
|
||||
export class CreateDeployedBackgroundWorkerService extends BaseService {
|
||||
public async call(
|
||||
@@ -49,7 +50,39 @@ export class CreateDeployedBackgroundWorkerService extends BaseService {
|
||||
},
|
||||
});
|
||||
|
||||
await createBackgroundTasks(body.metadata.tasks, backgroundWorker, environment, this._prisma);
|
||||
try {
|
||||
await createBackgroundTasks(
|
||||
body.metadata.tasks,
|
||||
backgroundWorker,
|
||||
environment,
|
||||
this._prisma
|
||||
);
|
||||
await syncDeclarativeSchedules(
|
||||
body.metadata.tasks,
|
||||
backgroundWorker,
|
||||
environment,
|
||||
this._prisma
|
||||
);
|
||||
} catch (error) {
|
||||
const name = error instanceof Error ? error.name : "UnknownError";
|
||||
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
|
||||
await this._prisma.workerDeployment.update({
|
||||
where: {
|
||||
id: deployment.id,
|
||||
},
|
||||
data: {
|
||||
status: "FAILED",
|
||||
failedAt: new Date(),
|
||||
errorData: {
|
||||
name,
|
||||
message,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Link the deployment with the background worker
|
||||
await this._prisma.workerDeployment.update({
|
||||
@@ -101,6 +134,7 @@ export class CreateDeployedBackgroundWorkerService extends BaseService {
|
||||
|
||||
await ExecuteTasksWaitingForDeployService.enqueue(backgroundWorker.id, this._prisma);
|
||||
await PerformDeploymentAlertsService.enqueue(deployment.id, this._prisma);
|
||||
await TimeoutDeploymentService.dequeue(deployment.id, this._prisma);
|
||||
|
||||
return backgroundWorker;
|
||||
});
|
||||
|
||||
@@ -27,6 +27,20 @@ export class DeleteTaskScheduleService extends BaseService {
|
||||
}
|
||||
|
||||
try {
|
||||
const schedule = await this._prisma.taskSchedule.findFirst({
|
||||
where: {
|
||||
friendlyId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new Error("Schedule not found");
|
||||
}
|
||||
|
||||
if (schedule.type === "DECLARATIVE") {
|
||||
throw new Error("Cannot delete declarative schedules");
|
||||
}
|
||||
|
||||
await this._prisma.taskSchedule.delete({
|
||||
where: {
|
||||
friendlyId,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
type HeartbeatServiceOptions = {
|
||||
heartbeat: () => Promise<void>;
|
||||
pingIntervalInMs?: number;
|
||||
leadingEdge?: boolean;
|
||||
};
|
||||
|
||||
export class HeartbeatService {
|
||||
private _heartbeat: () => Promise<void>;
|
||||
private _heartbeatIntervalInMs: number;
|
||||
private _nextHeartbeat: NodeJS.Timeout | undefined;
|
||||
private _leadingEdge: boolean;
|
||||
|
||||
constructor(opts: HeartbeatServiceOptions) {
|
||||
this._heartbeat = opts.heartbeat;
|
||||
this._heartbeatIntervalInMs = opts.pingIntervalInMs ?? 45_000;
|
||||
this._nextHeartbeat = undefined;
|
||||
this._leadingEdge = opts.leadingEdge ?? false;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this._leadingEdge) {
|
||||
this.#doHeartbeat();
|
||||
} else {
|
||||
this.#scheduleNextHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#clearNextHeartbeat();
|
||||
}
|
||||
|
||||
#doHeartbeat = async () => {
|
||||
this.#clearNextHeartbeat();
|
||||
|
||||
await this._heartbeat();
|
||||
|
||||
this.#scheduleNextHeartbeat();
|
||||
};
|
||||
|
||||
#clearNextHeartbeat() {
|
||||
if (this._nextHeartbeat) {
|
||||
clearTimeout(this._nextHeartbeat);
|
||||
}
|
||||
}
|
||||
|
||||
#scheduleNextHeartbeat() {
|
||||
this._nextHeartbeat = setTimeout(this.#doHeartbeat, this._heartbeatIntervalInMs);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,14 @@ export class IndexDeploymentService extends BaseService {
|
||||
|
||||
// just broadcast for now - there should only ever be one provider connected
|
||||
try {
|
||||
// timeout the deployment if 180 seconds have passed and the deployment is still not indexed
|
||||
await TimeoutDeploymentService.enqueue(
|
||||
deployment.id,
|
||||
"DEPLOYING",
|
||||
"Could not index deployment in time",
|
||||
new Date(Date.now() + 180_000)
|
||||
);
|
||||
|
||||
const responses = await socketIo.providerNamespace.timeout(30_000).emitWithAck("INDEX", {
|
||||
version: "v1",
|
||||
shortCode: deployment.shortCode,
|
||||
@@ -61,15 +69,7 @@ export class IndexDeploymentService extends BaseService {
|
||||
|
||||
logger.debug("Index ACK received", { responses });
|
||||
|
||||
if (responses.length === 0) {
|
||||
// timeout the deployment if 180 seconds have passed and the deployment is still not indexed
|
||||
await TimeoutDeploymentService.enqueue(
|
||||
deployment.id,
|
||||
"DEPLOYING",
|
||||
"Could not index deployment in time",
|
||||
new Date(Date.now() + 180_000)
|
||||
);
|
||||
} else {
|
||||
if (responses.length > 0) {
|
||||
const indexFailed = new DeploymentIndexFailed();
|
||||
|
||||
for (const response of responses) {
|
||||
@@ -83,12 +83,20 @@ export class IndexDeploymentService extends BaseService {
|
||||
|
||||
const indexFailed = new DeploymentIndexFailed();
|
||||
|
||||
await indexFailed.call(
|
||||
deployment.friendlyId,
|
||||
error instanceof Error
|
||||
? { message: error.message, name: error.name }
|
||||
: { message: "Could not index deployment in time", name: "TimeoutError" }
|
||||
);
|
||||
let indexError = {
|
||||
message: `Could not index deployment: ${error}`,
|
||||
name: "IndexError",
|
||||
};
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "operation has timed out") {
|
||||
indexError = { message: "Provider failed to respond in time", name: "TimeoutError" };
|
||||
} else {
|
||||
indexError = { message: error.message, name: error.name };
|
||||
}
|
||||
}
|
||||
|
||||
await indexFailed.call(deployment.friendlyId, indexError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { deploymentIndexingIsRetryable } from "../deploymentStatus";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { IndexDeploymentService } from "./indexDeployment.server";
|
||||
|
||||
export class RetryDeploymentIndexingService extends BaseService {
|
||||
public async call(deploymentId: string) {
|
||||
const deployment = await this._prisma.workerDeployment.findUnique({
|
||||
where: {
|
||||
id: deploymentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
if (!deploymentIndexingIsRetryable(deployment)) {
|
||||
throw new Error("Deployment indexing not retryable");
|
||||
}
|
||||
|
||||
const latestDeployment = await this._prisma.workerDeployment.findFirst({
|
||||
where: {
|
||||
projectId: deployment.projectId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
if (!latestDeployment || latestDeployment.id !== deployment.id) {
|
||||
throw new Error("Deployment is not the latest");
|
||||
}
|
||||
|
||||
await this._prisma.workerDeployment.update({
|
||||
where: {
|
||||
id: deploymentId,
|
||||
},
|
||||
data: {
|
||||
status: "DEPLOYING",
|
||||
},
|
||||
});
|
||||
|
||||
await IndexDeploymentService.enqueue(deployment.id);
|
||||
|
||||
return deployment;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,20 @@ export class SetActiveOnTaskScheduleService extends BaseService {
|
||||
}
|
||||
|
||||
try {
|
||||
const schedule = await this._prisma.taskSchedule.findFirst({
|
||||
where: {
|
||||
friendlyId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new Error("Schedule not found");
|
||||
}
|
||||
|
||||
if (schedule.type === "DECLARATIVE") {
|
||||
throw new Error("Cannot enable/disable declarative schedules");
|
||||
}
|
||||
|
||||
await this._prisma.taskSchedule.update({
|
||||
where: {
|
||||
friendlyId,
|
||||
|
||||
@@ -20,6 +20,7 @@ export class StartDeploymentIndexing extends BaseService {
|
||||
? registryProxy.rewriteImageReference(body.imageReference)
|
||||
: body.imageReference,
|
||||
status: "DEPLOYING",
|
||||
builtAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export class TestTaskService extends BaseService {
|
||||
case "SCHEDULED": {
|
||||
const payload = {
|
||||
scheduleId: "sched_1234",
|
||||
type: "IMPERATIVE",
|
||||
timestamp: data.timestamp,
|
||||
lastTimestamp: data.lastTimestamp,
|
||||
timezone: data.timezone,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { logger } from "~/services/logger.server";
|
||||
import { BaseService } from "./baseService.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { PerformDeploymentAlertsService } from "./alerts/performDeploymentAlerts.server";
|
||||
import { PrismaClientOrTransaction } from "~/db.server";
|
||||
|
||||
export class TimeoutDeploymentService extends BaseService {
|
||||
public async call(id: string, fromStatus: string, errorMessage: string) {
|
||||
@@ -20,6 +21,10 @@ export class TimeoutDeploymentService extends BaseService {
|
||||
}
|
||||
|
||||
if (deployment.status !== fromStatus) {
|
||||
logger.error("Deployment is not in the correct state to be timed out", {
|
||||
currentStatus: deployment.status,
|
||||
fromStatus,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,4 +62,8 @@ export class TimeoutDeploymentService extends BaseService {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static async dequeue(deploymentId: string, tx?: PrismaClientOrTransaction) {
|
||||
await workerQueue.dequeue(`timeoutDeployment:${deploymentId}`, { tx });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import { findCurrentWorkerDeployment } from "../models/workerDeployment.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
export class TriggerScheduledTaskService extends BaseService {
|
||||
public async call(instanceId: string) {
|
||||
public async call(instanceId: string, finalAttempt: boolean) {
|
||||
const registerNextService = new RegisterNextTaskScheduleInstanceService();
|
||||
|
||||
const instance = await this._prisma.taskScheduleInstance.findUnique({
|
||||
where: {
|
||||
id: instanceId,
|
||||
@@ -30,124 +32,136 @@ export class TriggerScheduledTaskService extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldTrigger = true;
|
||||
|
||||
if (!instance.active) {
|
||||
shouldTrigger = false;
|
||||
}
|
||||
|
||||
if (!instance.taskSchedule.active) {
|
||||
shouldTrigger = false;
|
||||
}
|
||||
|
||||
if (!instance.nextScheduledTimestamp) {
|
||||
shouldTrigger = false;
|
||||
}
|
||||
|
||||
if (
|
||||
instance.environment.type === "DEVELOPMENT" &&
|
||||
(!instance.environment.currentSession || instance.environment.currentSession.disconnectedAt)
|
||||
) {
|
||||
shouldTrigger = false;
|
||||
}
|
||||
|
||||
if (instance.environment.type !== "DEVELOPMENT") {
|
||||
// Get the current backgroundWorker for this environment
|
||||
const currentWorkerDeployment = await findCurrentWorkerDeployment(instance.environment.id);
|
||||
|
||||
if (!currentWorkerDeployment) {
|
||||
logger.debug("No current worker deployment found, skipping task trigger", {
|
||||
instanceId,
|
||||
scheduleId: instance.taskSchedule.friendlyId,
|
||||
environmentId: instance.environment.id,
|
||||
});
|
||||
try {
|
||||
let shouldTrigger = true;
|
||||
|
||||
if (!instance.active) {
|
||||
shouldTrigger = false;
|
||||
} else if (
|
||||
!currentWorkerDeployment.worker ||
|
||||
!currentWorkerDeployment.worker.tasks.some(
|
||||
(t) => t.slug === instance.taskSchedule.taskIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
if (!instance.taskSchedule.active) {
|
||||
shouldTrigger = false;
|
||||
}
|
||||
|
||||
if (!instance.nextScheduledTimestamp) {
|
||||
shouldTrigger = false;
|
||||
}
|
||||
|
||||
if (
|
||||
instance.environment.type === "DEVELOPMENT" &&
|
||||
(!instance.environment.currentSession || instance.environment.currentSession.disconnectedAt)
|
||||
) {
|
||||
logger.debug(
|
||||
"Current worker deployment does not contain the scheduled task identifier, skipping task trigger",
|
||||
{
|
||||
shouldTrigger = false;
|
||||
}
|
||||
|
||||
if (instance.environment.type !== "DEVELOPMENT") {
|
||||
// Get the current backgroundWorker for this environment
|
||||
const currentWorkerDeployment = await findCurrentWorkerDeployment(instance.environment.id);
|
||||
|
||||
if (!currentWorkerDeployment) {
|
||||
logger.debug("No current worker deployment found, skipping task trigger", {
|
||||
instanceId,
|
||||
scheduleId: instance.taskSchedule.friendlyId,
|
||||
environmentId: instance.environment.id,
|
||||
workerDeploymentId: currentWorkerDeployment.id,
|
||||
workerId: currentWorkerDeployment.worker?.id,
|
||||
taskIdentifier: instance.taskSchedule.taskIdentifier,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
shouldTrigger = false;
|
||||
shouldTrigger = false;
|
||||
} else if (
|
||||
!currentWorkerDeployment.worker ||
|
||||
!currentWorkerDeployment.worker.tasks.some(
|
||||
(t) => t.slug === instance.taskSchedule.taskIdentifier
|
||||
)
|
||||
) {
|
||||
logger.debug(
|
||||
"Current worker deployment does not contain the scheduled task identifier, skipping task trigger",
|
||||
{
|
||||
instanceId,
|
||||
scheduleId: instance.taskSchedule.friendlyId,
|
||||
environmentId: instance.environment.id,
|
||||
workerDeploymentId: currentWorkerDeployment.id,
|
||||
workerId: currentWorkerDeployment.worker?.id,
|
||||
taskIdentifier: instance.taskSchedule.taskIdentifier,
|
||||
}
|
||||
);
|
||||
|
||||
shouldTrigger = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registerNextService = new RegisterNextTaskScheduleInstanceService();
|
||||
if (shouldTrigger) {
|
||||
// Enqueue triggering the task
|
||||
const triggerTask = new TriggerTaskService();
|
||||
|
||||
if (shouldTrigger) {
|
||||
// Enqueue triggering the task
|
||||
const triggerTask = new TriggerTaskService();
|
||||
|
||||
const payload = {
|
||||
scheduleId: instance.taskSchedule.friendlyId,
|
||||
timestamp: instance.nextScheduledTimestamp,
|
||||
lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
|
||||
externalId: instance.taskSchedule.externalId ?? undefined,
|
||||
timezone: instance.taskSchedule.timezone,
|
||||
upcoming: nextScheduledTimestamps(
|
||||
instance.taskSchedule.generatorExpression,
|
||||
instance.taskSchedule.timezone,
|
||||
instance.nextScheduledTimestamp!,
|
||||
10
|
||||
),
|
||||
};
|
||||
|
||||
const payloadPacket = await stringifyIO(payload);
|
||||
|
||||
logger.debug("Triggering scheduled task", {
|
||||
instance,
|
||||
payloadPacket,
|
||||
});
|
||||
|
||||
const run = await triggerTask.call(
|
||||
instance.taskSchedule.taskIdentifier,
|
||||
instance.environment,
|
||||
{ payload: payloadPacket.data, options: { payloadType: payloadPacket.dataType } },
|
||||
{ customIcon: "scheduled" }
|
||||
);
|
||||
|
||||
if (!run) {
|
||||
logger.error("Failed to trigger task", {
|
||||
instanceId,
|
||||
const payload = {
|
||||
scheduleId: instance.taskSchedule.friendlyId,
|
||||
type: instance.taskSchedule.type,
|
||||
timestamp: instance.nextScheduledTimestamp,
|
||||
lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
|
||||
externalId: instance.taskSchedule.externalId ?? undefined,
|
||||
timezone: instance.taskSchedule.timezone,
|
||||
upcoming: nextScheduledTimestamps(
|
||||
instance.taskSchedule.generatorExpression,
|
||||
instance.taskSchedule.timezone,
|
||||
instance.nextScheduledTimestamp!,
|
||||
10
|
||||
),
|
||||
};
|
||||
|
||||
const payloadPacket = await stringifyIO(payload);
|
||||
|
||||
logger.debug("Triggering scheduled task", {
|
||||
instance,
|
||||
payloadPacket,
|
||||
});
|
||||
} else {
|
||||
await this._prisma.taskRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
scheduleId: instance.taskSchedule.id,
|
||||
scheduleInstanceId: instance.id,
|
||||
},
|
||||
|
||||
const run = await triggerTask.call(
|
||||
instance.taskSchedule.taskIdentifier,
|
||||
instance.environment,
|
||||
{ payload: payloadPacket.data, options: { payloadType: payloadPacket.dataType } },
|
||||
{ customIcon: "scheduled" }
|
||||
);
|
||||
|
||||
if (!run) {
|
||||
logger.error("Failed to trigger task", {
|
||||
instanceId,
|
||||
scheduleId: instance.taskSchedule.friendlyId,
|
||||
payloadPacket,
|
||||
});
|
||||
} else {
|
||||
await this._prisma.taskRun.update({
|
||||
where: {
|
||||
id: run.id,
|
||||
},
|
||||
data: {
|
||||
scheduleId: instance.taskSchedule.id,
|
||||
scheduleInstanceId: instance.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this._prisma.taskScheduleInstance.update({
|
||||
where: {
|
||||
id: instanceId,
|
||||
},
|
||||
data: {
|
||||
lastScheduledTimestamp: instance.nextScheduledTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
await registerNextService.call(instanceId);
|
||||
} catch (e) {
|
||||
if (finalAttempt) {
|
||||
logger.error("Failed to trigger scheduled task, rescheduling the next run", {
|
||||
instanceId,
|
||||
error: e,
|
||||
});
|
||||
|
||||
await registerNextService.call(instanceId);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await this._prisma.taskScheduleInstance.update({
|
||||
where: {
|
||||
id: instanceId,
|
||||
},
|
||||
data: {
|
||||
lastScheduledTimestamp: instance.nextScheduledTimestamp,
|
||||
},
|
||||
});
|
||||
|
||||
await registerNextService.call(instanceId);
|
||||
}
|
||||
|
||||
public static async enqueue(instanceId: string, runAt: Date, tx?: PrismaClientOrTransaction) {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { Prisma, TaskSchedule } from "@trigger.dev/database";
|
||||
import cronstrue from "cronstrue";
|
||||
import { nanoid } from "nanoid";
|
||||
import { ZodError } from "zod";
|
||||
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { CronPattern, UpsertSchedule } from "../schedules";
|
||||
import { BaseService, ServiceValidationError } from "./baseService.server";
|
||||
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
|
||||
import cronstrue from "cronstrue";
|
||||
import { UpsertSchedule } from "../schedules";
|
||||
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
import { env } from "~/env.server";
|
||||
import { getLimit } from "~/services/platform.v3.server";
|
||||
import { BaseService, ServiceValidationError } from "./baseService.server";
|
||||
import { CheckScheduleService } from "./checkSchedule.server";
|
||||
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
|
||||
|
||||
export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
|
||||
|
||||
@@ -30,79 +27,9 @@ type InstanceWithEnvironment = Prisma.TaskScheduleInstanceGetPayload<{
|
||||
|
||||
export class UpsertTaskScheduleService extends BaseService {
|
||||
public async call(projectId: string, schedule: UpsertTaskScheduleServiceOptions) {
|
||||
//validate the cron expression
|
||||
try {
|
||||
CronPattern.parse(schedule.cron);
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
throw new ServiceValidationError(`Invalid cron expression: ${e.issues[0].message}`);
|
||||
}
|
||||
|
||||
throw new ServiceValidationError(
|
||||
`Invalid cron expression: ${e instanceof Error ? e.message : JSON.stringify(e)}`
|
||||
);
|
||||
}
|
||||
|
||||
const task = await this._prisma.backgroundWorkerTask.findFirst({
|
||||
where: {
|
||||
slug: schedule.taskIdentifier,
|
||||
projectId: projectId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new ServiceValidationError(
|
||||
`Task with identifier ${schedule.taskIdentifier} not found in project.`
|
||||
);
|
||||
}
|
||||
|
||||
if (task.triggerSource !== "SCHEDULED") {
|
||||
throw new ServiceValidationError(
|
||||
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
|
||||
);
|
||||
}
|
||||
|
||||
//if creating a schedule, check they're under the limits
|
||||
if (!schedule.friendlyId) {
|
||||
//check they're within their limit
|
||||
const project = await this._prisma.project.findFirst({
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
select: {
|
||||
organizationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new ServiceValidationError("Project not found");
|
||||
}
|
||||
|
||||
const limit = await getLimit(project.organizationId, "schedules", 500);
|
||||
const schedulesCount = await this._prisma.taskSchedule.count({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (schedulesCount >= limit) {
|
||||
throw new ServiceValidationError(
|
||||
`You have created ${schedulesCount}/${limit} schedules so you'll need to increase your limits or delete some schedules. Increase your limits by contacting support.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule.timezone) {
|
||||
const possibleTimezones = getTimezones();
|
||||
if (!possibleTimezones.includes(schedule.timezone)) {
|
||||
throw new ServiceValidationError(
|
||||
`Invalid IANA timezone: "${schedule.timezone}". View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
|
||||
);
|
||||
}
|
||||
}
|
||||
//this throws errors if the schedule is invalid
|
||||
const checkSchedule = new CheckScheduleService(this._prisma);
|
||||
await checkSchedule.call(projectId, schedule);
|
||||
|
||||
const result = await $transaction(this._prisma, async (tx) => {
|
||||
const deduplicationKey =
|
||||
@@ -126,6 +53,10 @@ export class UpsertTaskScheduleService extends BaseService {
|
||||
});
|
||||
|
||||
if (existingSchedule) {
|
||||
if (existingSchedule.type === "DECLARATIVE") {
|
||||
throw new ServiceValidationError("Cannot update a declarative schedule");
|
||||
}
|
||||
|
||||
return await this.#updateExistingSchedule(tx, existingSchedule, schedule, projectId);
|
||||
} else {
|
||||
return await this.#createNewSchedule(tx, schedule, projectId, deduplicationKey);
|
||||
@@ -317,6 +248,7 @@ export class UpsertTaskScheduleService extends BaseService {
|
||||
#createReturnObject(taskSchedule: TaskSchedule, instances: InstanceWithEnvironment[]) {
|
||||
return {
|
||||
id: taskSchedule.friendlyId,
|
||||
type: taskSchedule.type,
|
||||
task: taskSchedule.taskIdentifier,
|
||||
active: taskSchedule.active,
|
||||
externalId: taskSchedule.externalId,
|
||||
|
||||
@@ -96,6 +96,7 @@ export class SharedSocketConnection {
|
||||
|
||||
this._messageHandler = new ZodMessageHandler({
|
||||
schema: clientWebsocketMessages,
|
||||
logger,
|
||||
messages: {
|
||||
READY_FOR_TASKS: async (payload) => {
|
||||
this._sharedQueueConsumerPool.start();
|
||||
|
||||
@@ -39,6 +39,7 @@ class RedisZodSubscriber<TMessageCatalog extends ZodMessageCatalogSchema>
|
||||
this._subscriber = new Redis(_options.redis);
|
||||
this._messageHandler = new ZodMessageHandler({
|
||||
schema: _options.schema,
|
||||
logger: this._logger,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,20 +77,26 @@ class RedisZodSubscriber<TMessageCatalog extends ZodMessageCatalogSchema>
|
||||
|
||||
const message = this._messageHandler.parseMessage(parsedMessage);
|
||||
|
||||
if (typeof message.type !== "string") {
|
||||
if (!message.success) {
|
||||
this._logger.error(`Failed to parse message: ${message.error}`, { parsedMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
const listener = this._listeners.get(message.type);
|
||||
if (typeof message.data.type !== "string") {
|
||||
this._logger.error(`Failed to parse message: invalid type`, { parsedMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
const listener = this._listeners.get(message.data.type);
|
||||
|
||||
if (!listener) {
|
||||
this._logger.debug(`No listener for message type: ${message.type}`, { parsedMessage });
|
||||
this._logger.debug(`No listener for message type: ${message.data.type}`, { parsedMessage });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await listener(message.payload);
|
||||
await listener(message.data.payload);
|
||||
} catch (error) {
|
||||
this._logger.error("Error handling message", { error, message });
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@total-typescript/ts-reset": "^0.4.2",
|
||||
"@trigger.dev/eslint-plugin": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/eslint": "^8.4.6",
|
||||
@@ -221,6 +222,9 @@
|
||||
"esbuild": "^0.15.10",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-turbo": "^2.0.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-import": "^16.0.1",
|
||||
"postcss-loader": "^8.1.1",
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ RUN pnpm run build --filter=webapp...
|
||||
|
||||
# Runner
|
||||
FROM node:20.11.1-bullseye-slim@sha256:5a5a92b3a8d392691c983719dbdc65d9f30085d6dcd65376e7a32e6fe9bf4cbe AS runner
|
||||
RUN apt-get update && apt-get install -y openssl
|
||||
RUN apt-get update && apt-get install -y openssl netcat-openbsd
|
||||
WORKDIR /triggerdotdev
|
||||
RUN corepack enable
|
||||
ENV NODE_ENV production
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -x
|
||||
set -xe
|
||||
|
||||
if [ -n "$DATABASE_HOST" ]; then
|
||||
scripts/wait-for-it.sh ${DATABASE_HOST} -- echo "database is up"
|
||||
@@ -8,10 +8,9 @@ fi
|
||||
# Run migrations
|
||||
pnpm --filter @trigger.dev/database db:migrate:deploy
|
||||
|
||||
# Copy over required prisma files and invoke bundled seed file
|
||||
# Copy over required prisma files
|
||||
cp packages/database/prisma/schema.prisma apps/webapp/prisma/
|
||||
cp node_modules/@prisma/engines/*.node apps/webapp/prisma/
|
||||
pnpm --filter webapp db:seed
|
||||
|
||||
cd /triggerdotdev/apps/webapp
|
||||
# exec dumb-init pnpm run start:local
|
||||
|
||||
+24
-78
@@ -1,14 +1,8 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Trigger.dev",
|
||||
"openapi": [
|
||||
"/openapi.yml",
|
||||
"/v3-openapi.yaml"
|
||||
],
|
||||
"versions": [
|
||||
"v3",
|
||||
"v2"
|
||||
],
|
||||
"openapi": ["/openapi.yml", "/v3-openapi.yaml"],
|
||||
"versions": ["v3", "v2"],
|
||||
"api": {
|
||||
"playground": {
|
||||
"mode": "simple"
|
||||
@@ -76,9 +70,7 @@
|
||||
{
|
||||
"group": "",
|
||||
"version": "v3",
|
||||
"pages": [
|
||||
"v3/introduction"
|
||||
]
|
||||
"pages": ["v3/introduction"]
|
||||
},
|
||||
{
|
||||
"group": "Getting Started",
|
||||
@@ -101,10 +93,7 @@
|
||||
"v3/apikeys",
|
||||
{
|
||||
"group": "Task types",
|
||||
"pages": [
|
||||
"v3/tasks-regular",
|
||||
"v3/tasks-scheduled"
|
||||
]
|
||||
"pages": ["v3/tasks-regular", "v3/tasks-scheduled"]
|
||||
},
|
||||
"v3/trigger-config"
|
||||
]
|
||||
@@ -112,10 +101,7 @@
|
||||
{
|
||||
"group": "Development",
|
||||
"version": "v3",
|
||||
"pages": [
|
||||
"v3/cli-dev",
|
||||
"v3/run-tests"
|
||||
]
|
||||
"pages": ["v3/cli-dev", "v3/run-tests"]
|
||||
},
|
||||
{
|
||||
"group": "Deployment",
|
||||
@@ -126,9 +112,7 @@
|
||||
"v3/github-actions",
|
||||
{
|
||||
"group": "Deployment integrations",
|
||||
"pages": [
|
||||
"v3/vercel-integration"
|
||||
]
|
||||
"pages": ["v3/vercel-integration"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -154,7 +138,8 @@
|
||||
"v3/machines",
|
||||
"v3/idempotency",
|
||||
"v3/reattempting-replaying",
|
||||
"v3/notifications"
|
||||
"v3/notifications",
|
||||
"v3/run-usage"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -164,10 +149,7 @@
|
||||
"v3/management/overview",
|
||||
{
|
||||
"group": "Tasks API",
|
||||
"pages": [
|
||||
"v3/management/tasks/trigger",
|
||||
"v3/management/tasks/batch-trigger"
|
||||
]
|
||||
"pages": ["v3/management/tasks/trigger", "v3/management/tasks/batch-trigger"]
|
||||
},
|
||||
{
|
||||
"group": "Runs API",
|
||||
@@ -205,26 +187,21 @@
|
||||
},
|
||||
{
|
||||
"group": "Projects API",
|
||||
"pages": [
|
||||
"v3/management/projects/runs"
|
||||
]
|
||||
"pages": ["v3/management/projects/runs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Open source",
|
||||
"version": "v3",
|
||||
"pages": [
|
||||
"v3/github-repo",
|
||||
"v3/open-source-self-hosting",
|
||||
"v3/open-source-contributing"
|
||||
]
|
||||
"pages": ["v3/github-repo", "v3/open-source-self-hosting", "v3/open-source-contributing"]
|
||||
},
|
||||
{
|
||||
"group": "Troubleshooting",
|
||||
"version": "v3",
|
||||
"pages": [
|
||||
"v3/troubleshooting",
|
||||
"v3/upgrading-packages",
|
||||
"v3/troubleshooting-alerts",
|
||||
"v3/troubleshooting-uptime-status",
|
||||
"v3/troubleshooting-github-issues",
|
||||
@@ -234,11 +211,7 @@
|
||||
{
|
||||
"group": "Help",
|
||||
"version": "v3",
|
||||
"pages": [
|
||||
"v3/community",
|
||||
"v3/help-slack",
|
||||
"v3/help-email"
|
||||
]
|
||||
"pages": ["v3/community", "v3/help-slack", "v3/help-email"]
|
||||
},
|
||||
{
|
||||
"group": "Getting Started",
|
||||
@@ -430,10 +403,7 @@
|
||||
"pages": [
|
||||
{
|
||||
"group": "Airtable",
|
||||
"pages": [
|
||||
"integrations/apis/airtable",
|
||||
"integrations/apis/airtable-tasks"
|
||||
]
|
||||
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
|
||||
},
|
||||
{
|
||||
"group": "GitHub",
|
||||
@@ -459,25 +429,16 @@
|
||||
},
|
||||
{
|
||||
"group": "Plain",
|
||||
"pages": [
|
||||
"integrations/apis/plain",
|
||||
"integrations/apis/plain-tasks"
|
||||
]
|
||||
"pages": ["integrations/apis/plain", "integrations/apis/plain-tasks"]
|
||||
},
|
||||
"integrations/apis/replicate",
|
||||
{
|
||||
"group": "SendGrid",
|
||||
"pages": [
|
||||
"integrations/apis/sendgrid",
|
||||
"integrations/apis/sendgrid-tasks"
|
||||
]
|
||||
"pages": ["integrations/apis/sendgrid", "integrations/apis/sendgrid-tasks"]
|
||||
},
|
||||
{
|
||||
"group": "Resend",
|
||||
"pages": [
|
||||
"integrations/apis/resend",
|
||||
"integrations/apis/resend-tasks"
|
||||
]
|
||||
"pages": ["integrations/apis/resend", "integrations/apis/resend-tasks"]
|
||||
},
|
||||
{
|
||||
"group": "Shopify",
|
||||
@@ -489,10 +450,7 @@
|
||||
},
|
||||
{
|
||||
"group": "Slack",
|
||||
"pages": [
|
||||
"integrations/apis/slack",
|
||||
"integrations/apis/slack-tasks"
|
||||
]
|
||||
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
|
||||
},
|
||||
"integrations/apis/stripe",
|
||||
{
|
||||
@@ -518,9 +476,7 @@
|
||||
"sdk/triggerclient/constructor",
|
||||
{
|
||||
"group": "Instance properties",
|
||||
"pages": [
|
||||
"sdk/triggerclient/store"
|
||||
]
|
||||
"pages": ["sdk/triggerclient/store"]
|
||||
},
|
||||
{
|
||||
"group": "Instance methods",
|
||||
@@ -583,10 +539,7 @@
|
||||
"sdk/dynamictrigger/constructor",
|
||||
{
|
||||
"group": "Instance methods",
|
||||
"pages": [
|
||||
"sdk/dynamictrigger/register",
|
||||
"sdk/dynamictrigger/unregister"
|
||||
]
|
||||
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -597,10 +550,7 @@
|
||||
"sdk/dynamicschedule/constructor",
|
||||
{
|
||||
"group": "Instance methods",
|
||||
"pages": [
|
||||
"sdk/dynamicschedule/register",
|
||||
"sdk/dynamicschedule/unregister"
|
||||
]
|
||||
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -613,9 +563,7 @@
|
||||
{
|
||||
"group": "HTTP Reference",
|
||||
"version": "v2",
|
||||
"pages": [
|
||||
"sdk/api-reference/events/create-an-event"
|
||||
]
|
||||
"pages": ["sdk/api-reference/events/create-an-event"]
|
||||
},
|
||||
{
|
||||
"group": "React SDK",
|
||||
@@ -631,9 +579,7 @@
|
||||
{
|
||||
"group": "Overview",
|
||||
"version": "v2",
|
||||
"pages": [
|
||||
"examples/introduction"
|
||||
]
|
||||
"pages": ["examples/introduction"]
|
||||
}
|
||||
],
|
||||
"footerSocials": {
|
||||
@@ -641,4 +587,4 @@
|
||||
"github": "https://github.com/triggerdotdev",
|
||||
"linkedin": "https://www.linkedin.com/company/triggerdotdev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-12
@@ -16,7 +16,7 @@ paths:
|
||||
post:
|
||||
operationId: create_schedule_v1
|
||||
summary: Create a schedule
|
||||
description: Create a new schedule based on the specified options.
|
||||
description: Create a new `IMPERATIVE` schedule based on the specified options.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -127,7 +127,7 @@ paths:
|
||||
put:
|
||||
operationId: update_schedule_v1
|
||||
summary: Update Schedule
|
||||
description: Update a schedule by its ID.
|
||||
description: Update a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: schedule_id
|
||||
@@ -173,7 +173,7 @@ paths:
|
||||
delete:
|
||||
operationId: delete_schedule_v1
|
||||
summary: Delete Schedule
|
||||
description: Delete a schedule by its ID.
|
||||
description: Delete a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: schedule_id
|
||||
@@ -203,7 +203,7 @@ paths:
|
||||
post:
|
||||
operationId: deactivate_schedule_v1
|
||||
summary: Deactivate Schedule.
|
||||
description: Deactivate a schedule by its ID.
|
||||
description: Deactivate a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: schedule_id
|
||||
@@ -237,7 +237,7 @@ paths:
|
||||
post:
|
||||
operationId: activate_schedule_v1
|
||||
summary: Activate Schedule
|
||||
description: Activate a schedule by its ID.
|
||||
description: Activate a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
|
||||
parameters:
|
||||
- in: path
|
||||
name: schedule_id
|
||||
@@ -925,7 +925,6 @@ paths:
|
||||
override: false
|
||||
});
|
||||
|
||||
|
||||
"/api/v1/projects/{projectRef}/envvars/{env}/{name}":
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/projectRef"
|
||||
@@ -1314,10 +1313,6 @@ paths:
|
||||
]
|
||||
}'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
components:
|
||||
parameters:
|
||||
taskIdentifier:
|
||||
@@ -1496,14 +1491,33 @@ components:
|
||||
type: object
|
||||
properties:
|
||||
queue:
|
||||
$ref: '#/components/schemas/QueueOptions'
|
||||
$ref: "#/components/schemas/QueueOptions"
|
||||
concurrencyKey:
|
||||
type: string
|
||||
description: Scope the concurrency limit to a specific key.
|
||||
idempotencyKey:
|
||||
type: string
|
||||
description: An optional property that specifies the idempotency key used to prevent creating duplicate runs. If you provide an existing idempotency key, we will return the existing run ID.
|
||||
|
||||
ttl:
|
||||
$ref: "#/components/schemas/TTL"
|
||||
delay:
|
||||
$ref: "#/components/schemas/Delay"
|
||||
TTL:
|
||||
type:
|
||||
- string
|
||||
- number
|
||||
description: "The time-to-live for this run. If the run is not executed within this time, it will be removed from the queue and never execute. You can use a string in this format: `1h`, `1m`, `1h42m` or a number of seconds (min. 1)."
|
||||
Delay:
|
||||
type: string
|
||||
description: |
|
||||
The delay before the task is executed. This can be a Date object, a string like `1h` or a date-time string.
|
||||
|
||||
* "1h" - 1 hour
|
||||
* "30d" - 30 days
|
||||
* "15m" - 15 minutes
|
||||
* "2w" - 2 weeks
|
||||
* "60s" - 60 seconds
|
||||
* new Date("2025-01-01T00:00:00Z")
|
||||
EnvFilter:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1957,6 +1971,10 @@ components:
|
||||
example: my-scheduled-task
|
||||
description: The id of the scheduled task that will be triggered by this
|
||||
schedule
|
||||
"type":
|
||||
type: string
|
||||
example: IMPERATIVE
|
||||
description: The type of schedule, `DECLARATIVE` or `IMPERATIVE`. Declarative schedules are declared in your code by setting the `cron` property on a `schedules.task`. Imperative schedules are created in the dashboard or by using the imperative SDK functions like `schedules.create()`.
|
||||
active:
|
||||
type: boolean
|
||||
example: true
|
||||
|
||||
@@ -14,12 +14,11 @@ description: "What features are currently available in the Developer Preview"
|
||||
| [Atomic versioning](/v3/versioning) | Each deploy creates a new version | ✅ |
|
||||
| [Deploy via CLI](/v3/cli-deploy) | Deploy from the command line | ✅ |
|
||||
| [Deploy via GitHub Actions](/v3/github-actions) | Deploy using GitHub Actions | ✅ |
|
||||
| Alerts | Add alerts in the UI for errors and deploys | ✅ |
|
||||
| [Alerts](/v3/troubleshooting-alerts) | Add alerts in the UI for errors and deploys | ✅ |
|
||||
| [Scheduled tasks](/v3/tasks-scheduled) | A task that can be triggered on a schedule | ✅ |
|
||||
| [Zod tasks](/v3/tasks-zod) | Define tasks using Zod schemas | ⏳ |
|
||||
| Full text search of runs | Find a run by searching the payload and output | ⏳ |
|
||||
| Logs view with search | All logs view with filtering and full text search | ⏳ |
|
||||
| Notifications | Send data to your web app from a run | ⏳ |
|
||||
| Rollbacks | Easily rollback changes when errors happen | ⏳ |
|
||||
|
||||
[Let us know](https://trigger.dev/discord) what we should prioritize and what we are missing.
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "GitHub Actions"
|
||||
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.
|
||||
This simple GitHub action file will deploy your 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
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
|
||||
If you already have a GitHub action file, you can just add the final step "🚀 Deploy Trigger.dev" to your existing file.
|
||||
|
||||
You need to add the `TRIGGER_ACCESS_TOKEN` secret to your repository. You can create a new access token by going to your profile page and then clicking on the "Personal Access Tokens" tab.
|
||||
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"](https://cloud.trigger.dev/account/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.
|
||||
|
||||
|
||||
@@ -39,3 +39,5 @@ export const config: TriggerConfig = {
|
||||
| medium-2x | 2 | 4 |
|
||||
| large-1x | 4 | 8 |
|
||||
| large-2x | 8 | 16 |
|
||||
|
||||
You can view the Trigger.dev cloud pricing for these machines [here](https://trigger.dev/pricing#computePricing).
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Usage"
|
||||
description: "Get compute duration and cost from inside a run, or for a specific block of code."
|
||||
---
|
||||
|
||||
## Getting the run cost and duration
|
||||
|
||||
You can get the cost and duration of the current including retries of the same run.
|
||||
|
||||
```ts
|
||||
export const heavyTask = task({
|
||||
id: "heavy-task",
|
||||
machine: {
|
||||
preset: "medium-2x",
|
||||
},
|
||||
run: async (payload, { ctx }) => {
|
||||
// Do some compute
|
||||
const result = await convertVideo(payload.videoUrl);
|
||||
|
||||
// Get the current cost and duration up until this line of code
|
||||
// This includes the compute time of the previous lines
|
||||
let currentUsage = usage.getCurrent();
|
||||
/* currentUsage = {
|
||||
compute: {
|
||||
attempt: {
|
||||
costInCents: 0.01700,
|
||||
durationMs: 1000,
|
||||
},
|
||||
total: {
|
||||
costInCents: 0.0255,
|
||||
durationMs: 1500,
|
||||
},
|
||||
},
|
||||
baseCostInCents: 0.0025,
|
||||
totalCostInCents: 0.028,
|
||||
}
|
||||
*/
|
||||
|
||||
// In the cloud product we do not count waits towards the compute cost or duration.
|
||||
// We also don't include time between attempts or before the run starts executing your code.
|
||||
// So this line does not affect the cost or duration.
|
||||
await wait.for({ seconds: 5 });
|
||||
|
||||
// This will give the same result as before the wait.
|
||||
currentUsage = usage.getCurrent();
|
||||
|
||||
// Do more compute
|
||||
const result = await convertVideo(payload.videoUrl);
|
||||
|
||||
// This would give a different value
|
||||
currentUsage = usage.getCurrent();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
In Trigger.dev cloud we do not include time between attempts, before your code executes, or waits
|
||||
towards the compute cost or duration.
|
||||
</Note>
|
||||
|
||||
## Getting the cost and duration of a block of code
|
||||
|
||||
You can also wrap code with `usage.measure` to get the cost and duration of that block of code:
|
||||
|
||||
```ts
|
||||
// Inside a task run function, or inside a function that's called from there.
|
||||
const { result, compute } = await usage.measure(async () => {
|
||||
//...Do something for 1 second
|
||||
return {
|
||||
foo: "bar",
|
||||
};
|
||||
});
|
||||
|
||||
logger.info("Result", { result, compute });
|
||||
/* result = {
|
||||
foo: "bar"
|
||||
}
|
||||
compute = {
|
||||
costInCents: 0.01700,
|
||||
durationMs: 1000,
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
This will work from inside the `run` function, our lifecycle hooks (like `onStart`, `onFailure`, `onSuccess`, etc.), or any function you're calling from the `run` function. It won't work for code that's not executed using Trigger.dev.
|
||||
+62
-10
@@ -3,21 +3,13 @@ title: "Scheduled tasks"
|
||||
description: "A task that is triggered on a recurring schedule using CRON syntax."
|
||||
---
|
||||
|
||||
To use scheduled tasks you need to do two things:
|
||||
|
||||
1. Define a task in your code using `schedules.task()`.
|
||||
2. Attach a schedule to the task either using the dashboard or the SDK.
|
||||
|
||||
<Info>A task can have multiple schedules attached to it.</Info>
|
||||
|
||||
Like all tasks they don't have timeouts, they should be placed inside a [/trigger folder](/v3/trigger-folder), and you [can configure them](/v3/tasks-overview#defining-a-task).
|
||||
|
||||
## Defining a scheduled task
|
||||
|
||||
This task will run when any of the attached schedules trigger. They have a predefined payload with some useful properties:
|
||||
|
||||
```ts
|
||||
import { schedules } from "@trigger.dev/sdk/v3";
|
||||
|
||||
//this task will run when any of the attached schedules trigger
|
||||
export const firstScheduledTask = schedules.task({
|
||||
id: "first-scheduled-task",
|
||||
run: async (payload) => {
|
||||
@@ -68,6 +60,66 @@ You can see from the comments that the payload has several useful properties:
|
||||
to do that.
|
||||
</Note>
|
||||
|
||||
Like all tasks they don't have timeouts, they should be placed inside a [/trigger folder](/v3/trigger-folder), and you [can configure them](/v3/tasks-overview#defining-a-task).
|
||||
|
||||
## How to attach a schedule
|
||||
|
||||
Now that we've defined a scheduled task, we need to define when it will actually run. To do this we need to attach one or more schedules.
|
||||
|
||||
There are two ways of doing this:
|
||||
|
||||
- **Declarative:** defined on your `schedules.task`. They sync when you run the dev command or deploy.
|
||||
- **Imperative:** created from the dashboard or by using the imperative SDK functions like `schedules.create()`.
|
||||
|
||||
<Info>
|
||||
A scheduled task can have multiple schedules attached to it, including a declarative schedule
|
||||
and/or many imperative schedules.
|
||||
</Info>
|
||||
|
||||
### Declarative schedules
|
||||
|
||||
These sync when you run the [dev](/v3/cli-dev) or [deploy](/v3/cli-deploy) commands.
|
||||
|
||||
To create them you add the `cron` property to your `schedules.task()`. This property is optional and is only used if you want to add a declarative schedule to your task:
|
||||
|
||||
```ts
|
||||
export const firstScheduledTask = schedules.task({
|
||||
id: "first-scheduled-task",
|
||||
//every two hours (UTC timezone)
|
||||
cron: "0 */2 * * *",
|
||||
run: async (payload, { ctx }) => {
|
||||
//do something
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you use a string it will be in UTC. Alternatively, you can specify a timezone like this:
|
||||
|
||||
```ts
|
||||
export const secondScheduledTask = schedules.task({
|
||||
id: "second-scheduled-task",
|
||||
cron: {
|
||||
//5am every day Tokyo time
|
||||
pattern: "0 5 * * *",
|
||||
timezone: "Asia/Tokyo",
|
||||
},
|
||||
run: async (payload) => {},
|
||||
});
|
||||
```
|
||||
|
||||
When you run the [dev](/v3/cli-dev) or [deploy](/v3/cli-deploy) commands, declarative schedules will be synced. If you add, delete or edit the `cron` property it will be updated when you run these commands. You can view your schedules on the Schedules page in the dashboard.
|
||||
|
||||
### Imperative schedules
|
||||
|
||||
Alternatively you can explicitly attach schedules to a `schedules.task`. You can do this in the Schedules page in the dashboard by just pressing the "New schedule" button, or you can use the SDK to create schedules.
|
||||
|
||||
The advantage of imperative schedules is that they can be created dynamically, for example, you could create a schedule for each user in your database. They can also be activated, disabled, edited, and deleted without deploying new code by using the SDK or dashboard.
|
||||
|
||||
To use imperative schedules you need to do two things:
|
||||
|
||||
1. Define a task in your code using `schedules.task()`.
|
||||
2. Attach 1+ schedules to the task either using the dashboard or the SDK.
|
||||
|
||||
## Supported CRON syntax
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: "How to upgrade the Trigger.dev packages"
|
||||
sidebarTitle: "Upgrading packages"
|
||||
description: "When we release fixes and new features we recommend you upgrade your Trigger.dev packages."
|
||||
---
|
||||
|
||||
## Update command
|
||||
|
||||
Run this command in your project:
|
||||
|
||||
```sh
|
||||
npx trigger.dev@beta update
|
||||
```
|
||||
|
||||
This will update all of the Trigger.dev packages in your project to the latest version.
|
||||
|
||||
## Running the CLI locally
|
||||
|
||||
When you run the CLI locally use the latest version for the `dev` and `deploy` commands:
|
||||
|
||||
```sh
|
||||
npx trigger.dev@beta dev
|
||||
```
|
||||
|
||||
```sh
|
||||
npx trigger.dev@beta deploy
|
||||
```
|
||||
|
||||
These commands will also give you the option to upgrade if you are behind on versions.
|
||||
|
||||
## Deploying with GitHub Actions
|
||||
|
||||
You can deploy using [GitHub Actions](/v3/github-actions). We recommend that you lock your version in the workflow file so make sure to upgrade.
|
||||
|
||||
<Warning>
|
||||
The deploy step will fail if version mismatches are detected. It's important that you update the
|
||||
version using the steps below.
|
||||
</Warning>
|
||||
|
||||
<Steps>
|
||||
<Step title="Find your workflow file">
|
||||
In your `.githubs/workflows` folder you can find your workflow yml files. You may have a prod
|
||||
and staging one.
|
||||
</Step>
|
||||
<Step title="Update the version for the run command">
|
||||
In the steps you'll see a `run` command. It will run the trigger.dev deploy CLI command. Make
|
||||
sure to update this version to the latest version (e.g. `npx trigger.dev@3.0.0-beta.48 deploy`).
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## package.json dev dependency
|
||||
|
||||
Instead of using `npx`, `pnpm dlx` or `yarn dlx` you can add the Trigger.dev CLI as a dev dependency to your package.json file.
|
||||
|
||||
For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"trigger.dev": "3.0.0-beta.48"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you've done this make sure to update the version to match the `@trigger.dev/sdk` package.
|
||||
|
||||
Once you have added the `trigger.dev` package to your `devDependencies`, you can use `npm exec trigger.dev`, `pnpm exec trigger.dev`, or `yarn exec trigger.dev` to run the CLI.
|
||||
|
||||
But we recommend adding your dev and deploy commands to the `scripts` section of your `package.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev:trigger": "trigger.dev dev",
|
||||
"deploy:trigger": "trigger.dev deploy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then you can run `npm run dev:trigger` and `npm run deploy:trigger` to run the CLI.
|
||||
@@ -19,8 +19,3 @@ Before digging deeper into the details of writing tasks, you should read the [fu
|
||||
| [Idempotency](/v3/idempotency) | Protect against mutations happening twice. |
|
||||
| [Reattempting & Replaying](/v3/reattempting-replaying) | You can reattempt a task that has failed all of its attempts. You can also replay a task with a new version of your code. |
|
||||
| [Notifications](/v3/notifications) | Send realtime notifications from your task that you can subscribe to from your backend or frontend. |
|
||||
| [Rollbacks](/v3/rollbacks) | Rollback code inside a task when errors happen to provide transactional guarantees. |
|
||||
| [Using APIs](/v3/using-apis) | How to use APIs from within your tasks. |
|
||||
| [Trigger filters](/v3/trigger-filters) | Prevent unwanted filters where the payload doesn't match your filter. |
|
||||
| [Middleware](/v3/middleware) | Middleware can wrap the task run function. |
|
||||
| [Automated tests](/v3/automated-tests) | Write automated tests in code. |
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/airtable
|
||||
|
||||
## 3.0.0-beta.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8ba998794]
|
||||
- @trigger.dev/sdk@3.0.0-beta.50
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.50
|
||||
|
||||
## 3.0.0-beta.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.49
|
||||
- @trigger.dev/sdk@3.0.0-beta.49
|
||||
|
||||
## 3.0.0-beta.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ecf1110ab]
|
||||
- @trigger.dev/sdk@3.0.0-beta.48
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.48
|
||||
|
||||
## 3.0.0-beta.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/airtable",
|
||||
"version": "3.0.0-beta.47",
|
||||
"version": "3.0.0-beta.50",
|
||||
"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.47",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.47",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.50",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.50",
|
||||
"airtable": "^0.12.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/github
|
||||
|
||||
## 3.0.0-beta.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8ba998794]
|
||||
- @trigger.dev/sdk@3.0.0-beta.50
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.50
|
||||
|
||||
## 3.0.0-beta.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.49
|
||||
- @trigger.dev/sdk@3.0.0-beta.49
|
||||
|
||||
## 3.0.0-beta.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ecf1110ab]
|
||||
- @trigger.dev/sdk@3.0.0-beta.48
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.48
|
||||
|
||||
## 3.0.0-beta.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/github",
|
||||
"version": "3.0.0-beta.47",
|
||||
"version": "3.0.0-beta.50",
|
||||
"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.47",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.47",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.50",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.50",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/linear
|
||||
|
||||
## 3.0.0-beta.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8ba998794]
|
||||
- @trigger.dev/sdk@3.0.0-beta.50
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.50
|
||||
|
||||
## 3.0.0-beta.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.49
|
||||
- @trigger.dev/sdk@3.0.0-beta.49
|
||||
|
||||
## 3.0.0-beta.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ecf1110ab]
|
||||
- @trigger.dev/sdk@3.0.0-beta.48
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.48
|
||||
|
||||
## 3.0.0-beta.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/linear",
|
||||
"version": "3.0.0-beta.47",
|
||||
"version": "3.0.0-beta.50",
|
||||
"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.47",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.47",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.50",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.50",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 3.0.0-beta.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8ba998794]
|
||||
- @trigger.dev/sdk@3.0.0-beta.50
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.50
|
||||
|
||||
## 3.0.0-beta.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.49
|
||||
- @trigger.dev/sdk@3.0.0-beta.49
|
||||
|
||||
## 3.0.0-beta.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ecf1110ab]
|
||||
- @trigger.dev/sdk@3.0.0-beta.48
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.48
|
||||
|
||||
## 3.0.0-beta.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/openai",
|
||||
"version": "3.0.0-beta.47",
|
||||
"version": "3.0.0-beta.50",
|
||||
"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.47",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.47"
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.50",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.50"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/plain
|
||||
|
||||
## 3.0.0-beta.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8ba998794]
|
||||
- @trigger.dev/sdk@3.0.0-beta.50
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.50
|
||||
|
||||
## 3.0.0-beta.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.49
|
||||
- @trigger.dev/sdk@3.0.0-beta.49
|
||||
|
||||
## 3.0.0-beta.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ecf1110ab]
|
||||
- @trigger.dev/sdk@3.0.0-beta.48
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.48
|
||||
|
||||
## 3.0.0-beta.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/plain",
|
||||
"version": "3.0.0-beta.47",
|
||||
"version": "3.0.0-beta.50",
|
||||
"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.47",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.47",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.50",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.50",
|
||||
"@team-plain/typescript-sdk": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/replicate
|
||||
|
||||
## 3.0.0-beta.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8ba998794]
|
||||
- @trigger.dev/sdk@3.0.0-beta.50
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.50
|
||||
|
||||
## 3.0.0-beta.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.49
|
||||
- @trigger.dev/sdk@3.0.0-beta.49
|
||||
|
||||
## 3.0.0-beta.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ecf1110ab]
|
||||
- @trigger.dev/sdk@3.0.0-beta.48
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.48
|
||||
|
||||
## 3.0.0-beta.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/replicate",
|
||||
"version": "3.0.0-beta.47",
|
||||
"version": "3.0.0-beta.50",
|
||||
"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.47",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.47",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.50",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.50",
|
||||
"replicate": "^0.18.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @trigger.dev/resend
|
||||
|
||||
## 3.0.0-beta.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8ba998794]
|
||||
- @trigger.dev/sdk@3.0.0-beta.50
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.50
|
||||
|
||||
## 3.0.0-beta.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.49
|
||||
- @trigger.dev/sdk@3.0.0-beta.49
|
||||
|
||||
## 3.0.0-beta.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ecf1110ab]
|
||||
- @trigger.dev/sdk@3.0.0-beta.48
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.48
|
||||
|
||||
## 3.0.0-beta.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/resend",
|
||||
"version": "3.0.0-beta.47",
|
||||
"version": "3.0.0-beta.50",
|
||||
"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.47",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.47",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.50",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.50",
|
||||
"resend": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user