Skip to content

Commit 80a3b0a

Browse files
dcramercodex
andcommitted
fix(nitro): Emit Vercel deployment wiring
Move Junior's heartbeat cron and conversation work queue trigger into Nitro's Vercel Build Output config so Vercel deploys the routes Nitro actually emits. Teach junior check to catch stale source-file queue mappings and missing juniorNitro() wiring before production deploys can silently miss inbound work. Co-Authored-By: GPT-5 Codex <codex@openai.com>
1 parent b331912 commit 80a3b0a

18 files changed

Lines changed: 724 additions & 150 deletions

File tree

apps/example/vercel.json

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,4 @@
11
{
22
"framework": "nitro",
3-
"buildCommand": "pnpm build",
4-
"crons": [
5-
{
6-
"path": "/api/internal/heartbeat",
7-
"schedule": "* * * * *"
8-
}
9-
],
10-
"functions": {
11-
"api/internal/agent/continue.ts": {
12-
"maxDuration": 300,
13-
"experimentalTriggers": [
14-
{
15-
"type": "queue/v2beta",
16-
"topic": "junior_conversation_work"
17-
}
18-
]
19-
}
20-
}
3+
"buildCommand": "pnpm build"
214
}

packages/docs/src/content/docs/cli/check.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: "junior check"
3-
description: "Validate Junior plugin manifests and skill files under app/ before build or deploy."
3+
description: "Validate Junior content and deployment config before build or deploy."
44
type: reference
55
prerequisites:
66
- /start-here/quickstart/
@@ -11,7 +11,7 @@ related:
1111
- /cli/snapshot-create/
1212
---
1313

14-
`junior check` validates local app content and installed plugin package content before build or deploy. It ignores legacy top-level `plugins/` and `skills/` directories, and it only runs app-file checks when the target already looks like a Junior app.
14+
`junior check` validates local app content, installed plugin package content, and Junior deployment config before build or deploy. It ignores legacy top-level `plugins/` and `skills/` directories, and it only runs app-file checks when the target already looks like a Junior app.
1515

1616
## Usage
1717

@@ -65,6 +65,10 @@ When the target already contains Junior app markers such as `app/SOUL.md`, `app/
6565

6666
If a skill file has frontmatter but no instructions after it, the command emits a warning instead of failing.
6767

68+
For Nitro/Vercel apps, the command checks deployment wiring when it sees Junior markers such as `@sentry/junior`, `juniorNitro()`, or app content files. It fails when `nitro.config.ts` omits `juniorNitro()`, because that module emits Junior's heartbeat cron and Vercel Queue trigger into the Nitro build output. It also fails when root `vercel.json` still targets `functions["api/internal/agent/continue.ts"]`; Nitro does not deploy that source file as a Vercel function.
69+
70+
Root `vercel.json` heartbeat crons emit a warning. `juniorNitro()` now emits `/api/internal/heartbeat` into `.vercel/output/config.json`, so keeping the root cron can drift from the deployed Nitro config.
71+
6872
## Example output
6973

7074
Successful validation:

packages/docs/src/content/docs/extend/scheduler-plugin.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,19 @@ import { schedulerPlugin } from "@sentry/junior-scheduler";
3232
export const plugins = defineJuniorPlugins([schedulerPlugin()]);
3333
```
3434

35-
The scaffolded `vercel.json` includes the internal heartbeat route:
36-
37-
```json title="vercel.json"
38-
{
39-
"crons": [
40-
{
41-
"path": "/api/internal/heartbeat",
42-
"schedule": "* * * * *"
43-
}
44-
]
45-
}
35+
`juniorNitro()` emits the internal heartbeat route into Nitro's Vercel Build Output config:
36+
37+
```ts title="nitro.config.ts"
38+
import { defineConfig } from "nitro";
39+
import { juniorNitro } from "@sentry/junior/nitro";
40+
41+
export default defineConfig({
42+
preset: "vercel",
43+
modules: [juniorNitro()],
44+
});
4645
```
4746

48-
If you manage routes manually, call the heartbeat route on a one-minute cadence:
47+
If you deploy outside Vercel, call the heartbeat route on a one-minute cadence:
4948

5049
| Route | Purpose |
5150
| ------------------------- | ------------------------------- |

packages/docs/src/content/docs/reference/api/functions/createApp.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ title: "createApp"
77

88
> **createApp**(`options?`): `Promise`\<`Hono`\<`BlankEnv`, `BlankSchema`, `"/"`\>\>
99
10-
Defined in: [app.ts:252](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L252)
10+
Defined in: [app.ts:259](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L259)
1111

1212
Create a Hono app with all Junior routes.
1313

packages/docs/src/content/docs/reference/api/functions/juniorNitro.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ title: "juniorNitro"
77

88
> **juniorNitro**(`options?`): `object`
99
10-
Defined in: [nitro.ts:183](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L183)
10+
Defined in: [nitro.ts:258](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L258)
1111

12-
Nitro module that copies app and plugin content into the Vercel build output.
12+
Nitro module that configures deployment wiring and copies app/plugin content into the Vercel build output.
1313

1414
## Parameters
1515

packages/docs/src/content/docs/reference/api/functions/juniorVercelConfig.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ title: "juniorVercelConfig"
99
1010
Defined in: [vercel.ts:6](https://github.com/getsentry/junior/blob/main/packages/junior/src/vercel.ts#L6)
1111

12-
Return a minimal Vercel config for scaffolded Junior apps.
12+
Return the root Vercel project config for scaffolded Junior apps.
1313

1414
## Parameters
1515

packages/docs/src/content/docs/reference/api/interfaces/JuniorAppOptions.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,42 @@ prev: false
55
title: "JuniorAppOptions"
66
---
77

8-
Defined in: [app.ts:48](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L48)
8+
Defined in: [app.ts:53](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L53)
99

1010
## Properties
1111

1212
### configDefaults?
1313

1414
> `optional` **configDefaults?**: `Record`\<`string`, `unknown`\>
1515
16-
Defined in: [app.ts:50](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L50)
16+
Defined in: [app.ts:55](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L55)
1717

1818
Install-wide provider defaults (`provider.key` format). Channel overrides take precedence.
1919

20-
***
20+
---
21+
22+
### conversationWork?
23+
24+
> `optional` **conversationWork?**: `VercelConversationWorkCallbackOptions`
25+
26+
Defined in: [app.ts:57](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L57)
27+
28+
Queue consumer wiring for the durable conversation worker.
29+
30+
---
2131

2232
### plugins?
2333

2434
> `optional` **plugins?**: [`JuniorPluginSet`](/reference/api/interfaces/juniorpluginset/)
2535
26-
Defined in: [app.ts:52](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L52)
36+
Defined in: [app.ts:59](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L59)
2737

2838
Direct plugin set override. Usually omitted when `juniorNitro()` uses a plugin module.
2939

30-
***
40+
---
3141

3242
### waitUntil?
3343

3444
> `optional` **waitUntil?**: `WaitUntilFn`
3545
36-
Defined in: [app.ts:53](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L53)
46+
Defined in: [app.ts:60](https://github.com/getsentry/junior/blob/main/packages/junior/src/app.ts#L60)

packages/docs/src/content/docs/reference/api/interfaces/JuniorNitroOptions.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,53 @@ prev: false
55
title: "JuniorNitroOptions"
66
---
77

8-
Defined in: [nitro.ts:33](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L33)
8+
Defined in: [nitro.ts:39](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L39)
99

1010
## Properties
1111

12+
### conversationWorkQueueTopic?
13+
14+
> `optional` **conversationWorkQueueTopic?**: `string`
15+
16+
Defined in: [nitro.ts:43](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L43)
17+
18+
Vercel Queue topic for durable conversation work. Must match the runtime queue producer topic.
19+
20+
---
21+
1222
### cwd?
1323

1424
> `optional` **cwd?**: `string`
1525
16-
Defined in: [nitro.ts:34](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L34)
26+
Defined in: [nitro.ts:40](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L40)
1727

18-
***
28+
---
1929

2030
### includeFiles?
2131

2232
> `optional` **includeFiles?**: `string`[]
2333
24-
Defined in: [nitro.ts:44](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L44)
34+
Defined in: [nitro.ts:52](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L52)
2535

2636
Extra file patterns to copy into the server output for files that the
2737
bundler cannot trace (e.g. dynamically imported providers).
2838
Each entry is `"<package-name>/<subpath-glob>"`, resolved via Node
2939
module resolution. Example: `"@earendil-works/pi-ai/dist/providers/*.js"`
3040

31-
***
41+
---
3242

3343
### maxDuration?
3444

3545
> `optional` **maxDuration?**: `number`
3646
37-
Defined in: [nitro.ts:35](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L35)
47+
Defined in: [nitro.ts:41](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L41)
3848

39-
***
49+
---
4050

4151
### plugins?
4252

4353
> `optional` **plugins?**: `JuniorNitroPluginSource`
4454
45-
Defined in: [nitro.ts:37](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L37)
55+
Defined in: [nitro.ts:45](https://github.com/getsentry/junior/blob/main/packages/junior/src/nitro.ts#L45)
4656

4757
Plugin catalog set or runtime-safe plugin module. Direct sets must not include trusted hooks.

packages/docs/src/content/docs/start-here/deploy-to-vercel.md

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ related:
1212
- /start-here/verify-and-troubleshoot/
1313
---
1414

15-
The scaffolded app is already shaped for Vercel. Deployment mainly means linking the project, setting env vars, enabling the heartbeat cron, enabling snapshot warmup support, and pointing Slack at the production URL.
15+
The scaffolded app is already shaped for Vercel. Deployment mainly means linking the project, keeping `juniorNitro()` in Nitro config, setting env vars, enabling snapshot warmup support, and pointing Slack at the production URL.
1616

1717
## Link the project
1818

@@ -41,35 +41,26 @@ The scaffolded `package.json` includes the production build script:
4141

4242
Keep the Vercel build command as `pnpm build`. `junior snapshot create` prepares sandbox runtime dependencies declared by enabled plugins before request handling starts.
4343

44-
## Keep Vercel runtime entries
44+
## Enable Junior's Nitro deployment module
4545

46-
Junior uses a one-minute internal heartbeat to run trusted plugin heartbeats and recover stale agent dispatches. The scheduler plugin uses this heartbeat when scheduled tasks are enabled. The scaffolded `vercel.json` should include these runtime entries:
46+
Junior uses a one-minute internal heartbeat to run trusted plugin heartbeats and recover stale agent dispatches. Durable agent work is also resumed by a Vercel Queue consumer. Both pieces are emitted by `juniorNitro()` into Nitro's Vercel Build Output config, which is the config Vercel deploys for Nitro apps.
4747

48-
```json title="vercel.json"
49-
{
50-
"framework": "nitro",
51-
"buildCommand": "pnpm build",
52-
"crons": [
53-
{
54-
"path": "/api/internal/heartbeat",
55-
"schedule": "* * * * *"
56-
}
57-
],
58-
"functions": {
59-
"api/internal/agent/continue.ts": {
60-
"maxDuration": 300,
61-
"experimentalTriggers": [
62-
{
63-
"type": "queue/v2beta",
64-
"topic": "junior_conversation_work"
65-
}
66-
]
67-
}
68-
}
69-
}
48+
Keep `juniorNitro()` installed in `nitro.config.ts`:
49+
50+
```ts title="nitro.config.ts"
51+
import { defineConfig } from "nitro";
52+
import { juniorNitro } from "@sentry/junior/nitro";
53+
54+
export default defineConfig({
55+
preset: "vercel",
56+
modules: [juniorNitro()],
57+
routes: {
58+
"/**": { handler: "./server.ts" },
59+
},
60+
});
7061
```
7162

72-
If you maintain `vercel.json` manually, keep the `/api/internal/heartbeat` cron entry and the queue trigger for `api/internal/agent/continue.ts`. The scaffolded `api/internal/agent/continue.ts` file delegates queue delivery to `server.ts`, and Vercel requires the `functions` key to match a concrete source file.
63+
Do not configure `functions["api/internal/agent/continue.ts"]` in root `vercel.json`; Nitro does not deploy that source file as a Vercel function. `juniorNitro()` attaches the queue trigger to `/api/internal/agent/continue` with Nitro `vercel.functionRules`, and emits the `/api/internal/heartbeat` cron into `.vercel/output/config.json`.
7364

7465
The heartbeat endpoint returns `401` unless the incoming Vercel Cron request has a bearer token that matches `CRON_SECRET`.
7566

@@ -125,11 +116,13 @@ Reinstall the Slack app if scopes changed.
125116
Run these checks after deployment:
126117

127118
1. `GET https://<your-domain>/health` returns `status: "ok"`.
128-
2. The Vercel deployment has a cron entry for `/api/internal/heartbeat`.
129-
3. A Slack mention produces a thread reply in the expected workspace.
130-
4. App Home opens without an error.
131-
5. Queue callback and turn logs show successful processing.
132-
6. One enabled plugin workflow succeeds end to end.
119+
2. `junior check` passes without deployment config errors.
120+
3. The Vercel deployment has a cron entry for `/api/internal/heartbeat`.
121+
4. The Vercel deployment has a Queue trigger for `/api/internal/agent/continue`.
122+
5. A Slack mention produces a thread reply in the expected workspace.
123+
6. App Home opens without an error.
124+
7. Queue callback and turn logs show successful processing.
125+
8. One enabled plugin workflow succeeds end to end.
133126

134127
## Next step
135128

0 commit comments

Comments
 (0)