Skip to content

Commit 8540138

Browse files
Merge branch 'main' into fix/batch-trigger-task-identifier
2 parents fe7e5ed + c55af7b commit 8540138

File tree

16 files changed

+291
-42
lines changed

16 files changed

+291
-42
lines changed

apps/supervisor/src/env.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ const Env = z.object({
112112
KUBERNETES_SCHEDULER_NAME: z.string().optional(), // Custom scheduler name for pods
113113
KUBERNETES_LARGE_MACHINE_POOL_LABEL: z.string().optional(), // if set, large-* presets affinity for machinepool=<value>
114114

115+
// Project affinity settings - pods from the same project prefer the same node
116+
KUBERNETES_PROJECT_AFFINITY_ENABLED: BoolEnv.default(false),
117+
KUBERNETES_PROJECT_AFFINITY_WEIGHT: z.coerce.number().int().min(1).max(100).default(50),
118+
KUBERNETES_PROJECT_AFFINITY_TOPOLOGY_KEY: z.string().trim().min(1).default("kubernetes.io/hostname"),
119+
115120
// Placement tags settings
116121
PLACEMENT_TAGS_ENABLED: BoolEnv.default(false),
117122
PLACEMENT_TAGS_PREFIX: z.string().default("node.cluster.x-k8s.io"),

apps/supervisor/src/workloadManager/kubernetes.ts

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class KubernetesWorkloadManager implements WorkloadManager {
120120
},
121121
spec: {
122122
...this.addPlacementTags(this.#defaultPodSpec, opts.placementTags),
123-
affinity: this.#getNodeAffinity(opts.machine),
123+
affinity: this.#getAffinity(opts.machine, opts.projectId),
124124
terminationGracePeriodSeconds: 60 * 60,
125125
containers: [
126126
{
@@ -390,50 +390,86 @@ export class KubernetesWorkloadManager implements WorkloadManager {
390390
return preset.name.startsWith("large-");
391391
}
392392

393-
#getNodeAffinity(preset: MachinePreset): k8s.V1Affinity | undefined {
393+
#getAffinity(preset: MachinePreset, projectId: string): k8s.V1Affinity | undefined {
394+
const nodeAffinity = this.#getNodeAffinityRules(preset);
395+
const podAffinity = this.#getProjectPodAffinity(projectId);
396+
397+
if (!nodeAffinity && !podAffinity) {
398+
return undefined;
399+
}
400+
401+
return {
402+
...(nodeAffinity && { nodeAffinity }),
403+
...(podAffinity && { podAffinity }),
404+
};
405+
}
406+
407+
#getNodeAffinityRules(preset: MachinePreset): k8s.V1NodeAffinity | undefined {
394408
if (!env.KUBERNETES_LARGE_MACHINE_POOL_LABEL) {
395409
return undefined;
396410
}
397411

398412
if (this.#isLargeMachine(preset)) {
399413
// soft preference for the large-machine pool, falls back to standard if unavailable
400414
return {
401-
nodeAffinity: {
402-
preferredDuringSchedulingIgnoredDuringExecution: [
403-
{
404-
weight: 100,
405-
preference: {
406-
matchExpressions: [
407-
{
408-
key: "node.cluster.x-k8s.io/machinepool",
409-
operator: "In",
410-
values: [env.KUBERNETES_LARGE_MACHINE_POOL_LABEL],
411-
},
412-
],
413-
},
415+
preferredDuringSchedulingIgnoredDuringExecution: [
416+
{
417+
weight: 100,
418+
preference: {
419+
matchExpressions: [
420+
{
421+
key: "node.cluster.x-k8s.io/machinepool",
422+
operator: "In",
423+
values: [env.KUBERNETES_LARGE_MACHINE_POOL_LABEL],
424+
},
425+
],
414426
},
415-
],
416-
},
427+
},
428+
],
417429
};
418430
}
419431

420432
// not schedulable in the large-machine pool
421433
return {
422-
nodeAffinity: {
423-
requiredDuringSchedulingIgnoredDuringExecution: {
424-
nodeSelectorTerms: [
425-
{
434+
requiredDuringSchedulingIgnoredDuringExecution: {
435+
nodeSelectorTerms: [
436+
{
437+
matchExpressions: [
438+
{
439+
key: "node.cluster.x-k8s.io/machinepool",
440+
operator: "NotIn",
441+
values: [env.KUBERNETES_LARGE_MACHINE_POOL_LABEL],
442+
},
443+
],
444+
},
445+
],
446+
},
447+
};
448+
}
449+
450+
#getProjectPodAffinity(projectId: string): k8s.V1PodAffinity | undefined {
451+
if (!env.KUBERNETES_PROJECT_AFFINITY_ENABLED) {
452+
return undefined;
453+
}
454+
455+
return {
456+
preferredDuringSchedulingIgnoredDuringExecution: [
457+
{
458+
weight: env.KUBERNETES_PROJECT_AFFINITY_WEIGHT,
459+
podAffinityTerm: {
460+
labelSelector: {
426461
matchExpressions: [
427462
{
428-
key: "node.cluster.x-k8s.io/machinepool",
429-
operator: "NotIn",
430-
values: [env.KUBERNETES_LARGE_MACHINE_POOL_LABEL],
463+
key: "project",
464+
operator: "In",
465+
values: [projectId],
431466
},
432467
],
433468
},
434-
],
469+
topologyKey: env.KUBERNETES_PROJECT_AFFINITY_TOPOLOGY_KEY,
470+
},
435471
},
436-
},
472+
],
437473
};
438474
}
439475
}

apps/webapp/app/components/AskAI.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,30 +118,31 @@ function AskAIProvider({ websiteId, isCollapsed = false }: AskAIProviderProps) {
118118
<motion.div layout="position" transition={{ duration: 0.2, ease: "easeInOut" }}>
119119
<TooltipProvider disableHoverableContent>
120120
<Tooltip>
121-
<div className={isCollapsed ? "w-full" : "inline-flex"}>
122-
<TooltipTrigger asChild>
121+
<TooltipTrigger asChild>
122+
<span className={cn("inline-flex h-8", isCollapsed && "w-full")}>
123123
<Button
124124
variant="small-menu-item"
125125
data-action="ask-ai"
126-
shortcut={{ modifiers: ["mod"], key: "/", enabledOnInputElements: true }}
126+
shortcut={{ modifiers: ["mod"], key: "i", enabledOnInputElements: true }}
127127
hideShortcutKey
128128
data-modal-override-open-class-ask-ai="true"
129129
onClick={() => openAskAI()}
130-
className={isCollapsed ? "w-full justify-center" : ""}
130+
fullWidth={isCollapsed}
131+
className={cn("h-full", isCollapsed && "justify-center")}
131132
>
132133
<AISparkleIcon className="size-5" />
133134
</Button>
134-
</TooltipTrigger>
135-
</div>
135+
</span>
136+
</TooltipTrigger>
136137
<TooltipContent
137-
side={isCollapsed ? "right" : "top"}
138-
sideOffset={isCollapsed ? 8 : 4}
139-
className="flex items-center gap-1 py-1.5 pl-2.5 pr-2 text-xs"
138+
side="right"
139+
sideOffset={8}
140+
className="flex items-center gap-2 text-xs"
140141
>
141142
Ask AI
142143
<span className="flex items-center">
143144
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
144-
<ShortcutKey shortcut={{ key: "/" }} variant="medium/bright" />
145+
<ShortcutKey shortcut={{ key: "i" }} variant="medium/bright" />
145146
</span>
146147
</TooltipContent>
147148
</Tooltip>

apps/webapp/app/components/Shortcuts.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ function ShortcutContent() {
7676
<ShortcutKey shortcut={{ key: "enter" }} variant="medium/bright" />
7777
</Shortcut>
7878
<Shortcut name="Ask AI">
79-
<ShortcutKey shortcut={{ modifiers: ["mod"], key: "/" }} variant="medium/bright" />
79+
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
80+
<ShortcutKey shortcut={{ key: "i" }} variant="medium/bright" />
8081
</Shortcut>
8182
<Shortcut name="Filter">
8283
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,10 @@ function CollapseToggle({
993993
return (
994994
<div className="absolute -right-3 top-1/2 z-10 -translate-y-1/2">
995995
{/* Vertical line to mask the side menu border */}
996-
<div className="pointer-events-none absolute left-1/2 top-1/2 h-10 w-px -translate-y-1/2 bg-background-bright" />
996+
<div className={cn(
997+
"pointer-events-none absolute left-1/2 top-1/2 h-10 w-px -translate-y-1/2 transition-colors duration-200",
998+
isHovering ? "bg-charcoal-750" : "bg-background-bright"
999+
)} />
9971000
<TooltipProvider disableHoverableContent>
9981001
<Tooltip>
9991002
<TooltipTrigger asChild>

docs/deploy-environment-variables.mdx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ You can edit an environment variable's values. You cannot edit the key name, you
6666

6767
</Steps>
6868

69+
## Local development
70+
71+
When running `npx trigger.dev dev`, the CLI automatically loads environment variables from these files in order (later files override any duplicate keys from earlier ones):
72+
73+
- `.env`
74+
- `.env.development`
75+
- `.env.local`
76+
- `.env.development.local`
77+
- `dev.vars`
78+
79+
These variables are available to your tasks via `process.env`. You don't need to use the `--env-file` flag for this automatic loading.
80+
6981
## In your code
7082

7183
You can use our SDK to get and manipulate environment variables. You can also easily sync environment variables from another service into Trigger.dev.
@@ -360,4 +372,55 @@ This will read your .env.production file using dotenvx and sync the variables to
360372

361373
- Trigger.dev does not automatically detect .env.production or dotenvx files
362374
- You can paste them manually into the dashboard
363-
- Or sync them automatically using a build extension
375+
- Or sync them automatically using a build extension
376+
377+
## Multi-tenant applications
378+
379+
If you're building a multi-tenant application where each tenant needs different environment variables (like tenant-specific API keys or database credentials), you don't need a separate project for each tenant. Instead, use a single project and load tenant-specific secrets at runtime.
380+
381+
<Note>
382+
This is different from [syncing environment variables at deploy time](#sync-env-vars-from-another-service).
383+
Here, secrets are loaded dynamically during task execution, not synced to Trigger.dev's environment variables.
384+
</Note>
385+
386+
### Recommended approach
387+
388+
Use a secrets service (Infisical, AWS Secrets Manager, HashiCorp Vault, etc.) to store tenant-specific secrets, then retrieve them at the start of each task run based on the tenant identifier in your payload or context.
389+
390+
**Important:** Never pass secrets in the task payload, as payloads are logged and visible in the dashboard.
391+
392+
### Example implementation
393+
394+
```ts
395+
import { task } from "@trigger.dev/sdk";
396+
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
397+
398+
export const processTenantData = task({
399+
id: "process-tenant-data",
400+
run: async (payload: { tenantId: string; data: unknown }) => {
401+
// Retrieve tenant-specific secret at runtime
402+
const client = new SecretsManagerClient({ region: "us-east-1" });
403+
const response = await client.send(
404+
new GetSecretValueCommand({
405+
SecretId: `tenants/${payload.tenantId}/supabase-key`,
406+
})
407+
);
408+
409+
const supabaseKey = JSON.parse(response.SecretString!).SUPABASE_SERVICE_KEY;
410+
411+
// Your task logic using the tenant-specific secret
412+
// ...
413+
},
414+
});
415+
```
416+
417+
You can use any secrets service - see the [sync env vars section](#sync-env-vars-from-another-service) for an example with Infisical.
418+
419+
### Benefits
420+
421+
- **Single codebase** - Deploy once, works for all tenants
422+
- **Secure** - Secrets never appear in payloads or logs
423+
- **Scalable** - No project limit constraints
424+
- **Flexible** - Easy to add new tenants without redeploying
425+
426+
This approach allows you to support unlimited tenants with a single Trigger.dev project, avoiding the [project limit](/limits#projects) while maintaining security and separation of tenant data.

docs/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,8 @@
352352
"guides/frameworks/webhooks-guides-overview",
353353
"guides/frameworks/nextjs-webhooks",
354354
"guides/frameworks/remix-webhooks",
355-
"guides/examples/stripe-webhook"
355+
"guides/examples/stripe-webhook",
356+
"guides/examples/hookdeck-webhook"
356357
]
357358
}
358359
]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: "Trigger tasks from Hookdeck webhooks"
3+
sidebarTitle: "Hookdeck webhooks"
4+
description: "This example demonstrates how to use Hookdeck to receive webhooks and trigger Trigger.dev tasks."
5+
---
6+
7+
## Overview
8+
9+
This example shows how to use [Hookdeck](https://hookdeck.com) as your webhook infrastructure to trigger Trigger.dev tasks. Hookdeck receives webhooks from external services, and forwards them directly to the Trigger.dev API. This gives you the best of both worlds: Hookdeck's webhook management, logging, and replay capabilities, combined with Trigger.dev's reliable task execution.
10+
11+
## Key features
12+
13+
- Use Hookdeck as your webhook endpoint for external services
14+
- Hookdeck forwards webhooks directly to Trigger.dev tasks via the API
15+
- All webhooks are logged and replayable in Hookdeck
16+
17+
## Setting up Hookdeck
18+
19+
You'll configure everything in the [Hookdeck dashboard](https://dashboard.hookdeck.com). No code changes needed in your app.
20+
21+
### 1. Create a destination
22+
23+
In Hookdeck, create a new [destination](https://hookdeck.com/docs/destinations) with the following settings:
24+
25+
- **URL**: `https://api.trigger.dev/api/v1/tasks/<task-id>/trigger` (replace `<task-id>` with your task ID)
26+
- **Method**: POST
27+
- **Authentication**: Bearer token (use your `TRIGGER_SECRET_KEY` from Trigger.dev)
28+
29+
### 2. Add a transformation
30+
31+
Create a [transformation](https://hookdeck.com/docs/transformations) to wrap the webhook body in the `payload` field that Trigger.dev expects:
32+
33+
```javascript
34+
addHandler("transform", (request, context) => {
35+
request.body = { payload: { ...request.body } };
36+
return request;
37+
});
38+
```
39+
40+
### 3. Create a connection
41+
42+
Create a [connection](https://hookdeck.com/docs/connections) that links your source (where webhooks come from) to the destination and transformation you created above.
43+
44+
## Task code
45+
46+
This task will be triggered when Hookdeck forwards a webhook to the Trigger.dev API.
47+
48+
```ts trigger/webhook-handler.ts
49+
import { task } from "@trigger.dev/sdk";
50+
51+
export const webhookHandler = task({
52+
id: "webhook-handler",
53+
run: async (payload: Record<string, unknown>) => {
54+
// The payload contains the original webhook data from the external service
55+
console.log("Received webhook:", payload);
56+
57+
// Add your custom logic here
58+
},
59+
});
60+
```
61+
62+
## Testing your setup
63+
64+
To test everything is working:
65+
66+
1. Set up your destination, transformation, and connection in [Hookdeck](https://dashboard.hookdeck.com)
67+
2. Send a test webhook to your Hookdeck source URL (use the Hookdeck Console or cURL)
68+
3. Check the Hookdeck dashboard to verify the webhook was received and forwarded
69+
4. Check the [Trigger.dev dashboard](https://cloud.trigger.dev) to see the successful run of your task
70+
71+
For more information on setting up Hookdeck, refer to the [Hookdeck Documentation](https://hookdeck.com/docs).

docs/guides/frameworks/webhooks-guides-overview.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ A webhook handler is code that executes in response to an event. They can be end
3131
How to create a Stripe webhook handler and trigger a task when a 'checkout session completed'
3232
event is received.
3333
</Card>
34+
<Card title="Hookdeck webhooks" icon="webhook" href="/guides/examples/hookdeck-webhook">
35+
Use Hookdeck to receive webhooks and forward them to Trigger.dev tasks with logging and replay
36+
capabilities.
37+
</Card>
3438
<Card
3539
title="Supabase database webhooks guide"
3640
icon="webhook"

docs/guides/introduction.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Get set up fast using our detailed walk-through guides.
3737
| [Python web crawler](/guides/python/python-crawl4ai) | Use Python, Crawl4AI and Playwright to create a headless web crawler |
3838
| [Sequin database triggers](/guides/frameworks/sequin) | Trigger tasks from database changes using Sequin |
3939
| [Stripe webhooks](/guides/examples/stripe-webhook) | Trigger tasks from incoming Stripe webhook events |
40+
| [Hookdeck webhooks](/guides/examples/hookdeck-webhook) | Use Hookdeck to receive webhooks and forward them to Trigger.dev tasks |
4041
| [Supabase database webhooks](/guides/frameworks/supabase-edge-functions-database-webhooks) | Trigger tasks using Supabase database webhooks |
4142
| [Supabase edge function hello world](/guides/frameworks/supabase-edge-functions-basic) | Trigger tasks from Supabase edge function |
4243
| [Using webhooks in Next.js](/guides/frameworks/nextjs-webhooks) | Trigger tasks from a webhook in Next.js |

0 commit comments

Comments
 (0)