Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 20 additions & 256 deletions packages/core/src/v3/schemas/webhooks.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Massive removal of exported schemas and types breaks multiple consumers

This PR removes the following exports that are actively used across the codebase: Webhook (discriminated union), AlertWebhookDeploymentSuccessObject, AlertWebhookDeploymentFailedObject, AlertWebhookErrorGroupObject, DeployError, RunFailedWebhook, DeploymentSuccessWebhook, DeploymentFailedWebhook, ErrorWebhook, and the AlertWebhookRunFailedObject type alias. These are imported and used in:

  • packages/trigger-sdk/src/v3/webhooks.ts:1 — imports Webhook and calls Webhook.parse() at line 101
  • apps/webapp/app/v3/services/alerts/deliverAlert.server.ts:11-14 — imports DeploymentFailedWebhook, DeploymentSuccessWebhook, RunFailedWebhook as types
  • apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts:2 — imports ErrorWebhook type
  • apps/webapp/test/errorGroupWebhook.test.ts:2 and apps/webapp/test/webhookErrorAlerts.test.ts:2 — import Webhook for test validation

While TypeScript CI should catch the compilation failures, the semantic impact is worth noting: the entire SDK webhook verification flow (webhooks.constructEvent()) is broken since it depends on Webhook.parse(). This is a breaking change for the published @trigger.dev/sdk package, and per packages/core/CLAUDE.md, changes to @trigger.dev/core affect both the customer-facing SDK and the server-side webapp.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -2,290 +2,54 @@ import { z } from "zod";
import { RunStatus } from "./api.js";
import { RuntimeEnvironmentTypeSchema, TaskRunError } from "./common.js";

/** Represents a failed run alert webhook payload */
const AlertWebhookRunFailedObject = z.object({
/** Task information */
const ID_PATTERNS = {
RUN: /^run_[a-zA-Z0-9]+$/,
TASK: /^task_[a-zA-Z0-9]+$/,
ENV: /^env_[a-zA-Z0-9]+$/,
ORG: /^org_[a-zA-Z0-9]+$/,
PROJECT: /^proj_[a-zA-Z0-9]+$/,
};
Comment on lines +5 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 environment.id, organization.id, and project.id regex patterns reject all valid database CUIDs

The ID patterns ^env_[a-zA-Z0-9]+$, ^org_[a-zA-Z0-9]+$, and ^proj_[a-zA-Z0-9]+$ require specific prefixes, but the actual values used in webhook payloads are Prisma CUIDs (e.g., clxxxxxxxxxxxxxxxxxx) with no prefix. In deliverAlert.server.ts:428, environment.id is set to alert.environment.id (a raw CUID). Similarly, organization.id is alert.project.organizationId (line 434) and project.id is alert.project.id (line 439). All Prisma model primary keys use @default(cuid()) per the schema. Any validation of a real server-generated webhook payload against this schema will fail for all three of these fields.

Suggested change
const ID_PATTERNS = {
RUN: /^run_[a-zA-Z0-9]+$/,
TASK: /^task_[a-zA-Z0-9]+$/,
ENV: /^env_[a-zA-Z0-9]+$/,
ORG: /^org_[a-zA-Z0-9]+$/,
PROJECT: /^proj_[a-zA-Z0-9]+$/,
};
const ID_PATTERNS = {
RUN: /^run_[a-zA-Z0-9]+$/,
};
const idWithPrefix = (pattern: RegExp) => z.string().regex(pattern, "Invalid ID format");
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const idWithPrefix = (pattern: RegExp) => z.string().regex(pattern, "Invalid ID format");

export const AlertWebhookRunFailedObject = z.object({
task: z.object({
/** Unique identifier for the task */
id: z.string(),
/** File path where the task is defined */
filePath: z.string(),
/** Name of the exported task function */
id: idWithPrefix(ID_PATTERNS.TASK),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 task.id regex pattern rejects all valid task identifiers

The TASK pattern /^task_[a-zA-Z0-9]+$/ requires a task_ prefix, but the actual value populated in webhook payloads is alert.taskRun.taskIdentifier (deliverAlert.server.ts:397), which is a user-defined slug like "process-payment", "email-sequence", etc. These identifiers contain hyphens and have no task_ prefix, so any call to AlertWebhookRunFailedObject.parse() or .safeParse() on a real webhook payload will fail validation.

Suggested change
id: idWithPrefix(ID_PATTERNS.TASK),
id: z.string().min(1),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

filePath: z.string().min(1),
exportName: z.string().optional(),
/** Version of the task */
version: z.string(),
/** Version of the SDK used */
sdkVersion: z.string(),
/** Version of the CLI used */
cliVersion: z.string(),
}),
/** Run information */
run: z.object({
/** Unique identifier for the run */
id: z.string(),
/** Run number */
number: z.number(),
/** Current status of the run */
id: idWithPrefix(ID_PATTERNS.RUN),
number: z.number().int().positive(),
status: RunStatus,
/** When the run was created */
createdAt: z.coerce.date(),
/** When the run started executing */
startedAt: z.coerce.date().optional(),
/** When the run finished executing */
completedAt: z.coerce.date().optional(),
/** Whether this is a test run */
isTest: z.boolean(),
/** Idempotency key for the run */
idempotencyKey: z.string().optional(),
/** Associated tags */
tags: z.array(z.string()),
/** Error information */
tags: z.array(z.string().max(50)).max(20),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Tags validation constraints may reject existing valid payloads

The new schema adds .max(50) on individual tag strings and .max(20) on the tags array (tags: z.array(z.string().max(50)).max(20)), whereas the old schema had no such constraints (tags: z.array(z.string())). If any existing webhook consumers or server paths produce payloads with more than 20 tags or tags longer than 50 characters, validation would fail. This should be verified against the actual tag limits enforced elsewhere in the system (e.g., at task trigger time) to ensure consistency.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

error: TaskRunError,
/** Whether the run was an out-of-memory error */
isOutOfMemoryError: z.boolean(),
/** Machine preset used for the run */
machine: z.string(),
/** URL to view the run in the dashboard */
dashboardUrl: z.string(),
dashboardUrl: z.string().url(),
}),
/** Environment information */
environment: z.object({
/** Environment ID */
id: z.string(),
/** Environment type */
id: idWithPrefix(ID_PATTERNS.ENV),
type: RuntimeEnvironmentTypeSchema,
/** Environment slug */
slug: z.string(),
/** Environment branch name */
branchName: z.string().optional(),
}),
Comment on lines 39 to 43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Removed branchName from environment schema breaks webhook API contract

The branchName optional field was removed from the environment object in the schema, but the server still sends it in webhook payloads at deliverAlert.server.ts:431 (branchName: alert.environment.branchName ?? undefined). Zod's default behavior strips unknown keys, so consumers who validate with .parse() will silently lose the branchName field. This is a breaking change to the webhook API contract — consumers who relied on branchName (e.g., to identify preview environment branches) will no longer receive it after validation.

Suggested change
environment: z.object({
/** Environment ID */
id: z.string(),
/** Environment type */
id: idWithPrefix(ID_PATTERNS.ENV),
type: RuntimeEnvironmentTypeSchema,
/** Environment slug */
slug: z.string(),
/** Environment branch name */
branchName: z.string().optional(),
}),
environment: z.object({
id: idWithPrefix(ID_PATTERNS.ENV),
type: RuntimeEnvironmentTypeSchema,
slug: z.string(),
branchName: z.string().optional(),
}),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

/** Organization information */
organization: z.object({
/** Organization ID */
id: z.string(),
/** Organization slug */
id: idWithPrefix(ID_PATTERNS.ORG),
slug: z.string(),
/** Organization name */
name: z.string(),
}),
/** Project information */
project: z.object({
/** Project ID */
id: z.string(),
/** Project reference */
id: idWithPrefix(ID_PATTERNS.PROJECT),
ref: z.string(),
/** Project slug */
slug: z.string(),
/** Project name */
name: z.string(),
}),
});
export type AlertWebhookRunFailedObject = z.infer<typeof AlertWebhookRunFailedObject>;

/** Represents a deployment error */
export const DeployError = z.object({
/** Error name */
name: z.string(),
/** Error message */
message: z.string(),
/** Error stack trace */
stack: z.string().optional(),
/** Standard error output */
stderr: z.string().optional(),
});
export type DeployError = z.infer<typeof DeployError>;

const deploymentCommonProperties = {
/** Environment information */
environment: z.object({
id: z.string(),
type: RuntimeEnvironmentTypeSchema,
slug: z.string(),
/** Environment branch name */
branchName: z.string().optional(),
}),
/** Organization information */
organization: z.object({
id: z.string(),
slug: z.string(),
name: z.string(),
}),
/** Project information */
project: z.object({
id: z.string(),
ref: z.string(),
slug: z.string(),
name: z.string(),
}),
/** Git metadata for the deployment source code */
git: z
.object({
branch: z.string(),
commitSha: z.string(),
commitMessage: z.string(),
commitUrl: z.string(),
branchUrl: z.string(),
pullRequestNumber: z.number().optional(),
pullRequestTitle: z.string().optional(),
pullRequestUrl: z.string().optional(),
provider: z.string().optional(),
})
.optional(),
/** Vercel integration data */
vercel: z
.object({
deploymentUrl: z.string(),
})
.optional(),
};

const deploymentDeploymentCommonProperties = {
/** Deployment ID */
id: z.string(),
/** Deployment status */
status: z.string(),
/** Deployment version */
version: z.string(),
/** Short code identifier */
shortCode: z.string(),
};

/** Represents a successful deployment alert webhook payload */
export const AlertWebhookDeploymentSuccessObject = z.object({
...deploymentCommonProperties,
deployment: z.object({
...deploymentDeploymentCommonProperties,
/** When the deployment completed */
deployedAt: z.coerce.date(),
}),
/** Deployed tasks */
tasks: z.array(
z.object({
/** Task ID */
id: z.string(),
/** File path where the task is defined */
filePath: z.string(),
/** Name of the exported task function */
exportName: z.string().optional(),
/** Source of the trigger */
triggerSource: z.string(),
})
),
});

/** Represents a failed deployment alert webhook payload */
export const AlertWebhookDeploymentFailedObject = z.object({
...deploymentCommonProperties,
deployment: z.object({
...deploymentDeploymentCommonProperties,
/** When the deployment failed */
failedAt: z.coerce.date(),
}),
/** Error information */
error: DeployError,
});

export type AlertWebhookDeploymentSuccessObject = z.infer<
typeof AlertWebhookDeploymentSuccessObject
>;
export type AlertWebhookDeploymentFailedObject = z.infer<typeof AlertWebhookDeploymentFailedObject>;

/** Represents an error group alert webhook payload */
export const AlertWebhookErrorGroupObject = z.object({
/** Classification of the error alert */
classification: z.enum(["new_issue", "regression", "unignored"]),
/** Error information */
error: z.object({
/** Error fingerprint identifier */
fingerprint: z.string(),
/** Error type */
type: z.string(),
/** Error message */
message: z.string(),
/** Sample stack trace */
stackTrace: z.string().optional(),
/** When the error was first seen */
firstSeen: z.coerce.date(),
/** When the error was last seen */
lastSeen: z.coerce.date(),
/** Number of occurrences */
occurrenceCount: z.number(),
/** Task identifier where the error occurred */
taskIdentifier: z.string(),
}),
/** Environment information */
environment: z.object({
/** Environment ID */
id: z.string(),
/** Environment name */
name: z.string(),
}),
/** Organization information */
organization: z.object({
/** Organization ID */
id: z.string(),
/** Organization slug */
slug: z.string(),
/** Organization name */
name: z.string(),
}),
/** Project information */
project: z.object({
/** Project ID */
id: z.string(),
/** Project reference */
ref: z.string(),
/** Project slug */
slug: z.string(),
/** Project name */
name: z.string(),
}),
/** URL to view the error in the dashboard */
dashboardUrl: z.string(),
});

export type AlertWebhookErrorGroupObject = z.infer<typeof AlertWebhookErrorGroupObject>;

/** Common properties for all webhooks */
const commonProperties = {
/** Webhook ID */
id: z.string(),
/** When the webhook was created */
created: z.coerce.date(),
/** Version of the webhook */
webhookVersion: z.string(),
};

/** Represents all possible webhook types */
export const Webhook = z.discriminatedUnion("type", [
/** Run failed alert webhook */
z.object({
...commonProperties,
type: z.literal("alert.run.failed"),
object: AlertWebhookRunFailedObject,
}),
/** Deployment success alert webhook */
z.object({
...commonProperties,
type: z.literal("alert.deployment.success"),
object: AlertWebhookDeploymentSuccessObject,
}),
/** Deployment failed alert webhook */
z.object({
...commonProperties,
type: z.literal("alert.deployment.failed"),
object: AlertWebhookDeploymentFailedObject,
}),
/** Error group alert webhook */
z.object({
...commonProperties,
type: z.literal("alert.error"),
object: AlertWebhookErrorGroupObject,
}),
]);

export type Webhook = z.infer<typeof Webhook>;
export type RunFailedWebhook = Extract<Webhook, { type: "alert.run.failed" }>;
export type DeploymentSuccessWebhook = Extract<Webhook, { type: "alert.deployment.success" }>;
export type DeploymentFailedWebhook = Extract<Webhook, { type: "alert.deployment.failed" }>;
export type ErrorWebhook = Extract<Webhook, { type: "alert.error" }>;