CS-10217 trigger command on webhook event#4098
Conversation
Preview deployments |
There was a problem hiding this comment.
Pull request overview
Adds webhook-triggered command execution (with GitHub-specific filtering) and introduces an “async incremental indexing” mode so card creation can return an assigned ID immediately without waiting for indexing—useful for command/worker-driven writes.
Changes:
- Add webhook receiver support for looking up registered webhook commands, filtering by event payload/headers, and enqueueing run-command jobs.
- Add
X-Boxel-Async-Index/asyncIndexoption to allow “fire-and-forget” incremental indexing and a 202 response that returns an ID immediately. - Expand realm session-room notification selection to include all users when a realm is world-readable.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/runtime-common/realm.ts | Adds asyncIndex write option and 202 create-card response path to avoid blocking on indexing. |
| packages/runtime-common/db-queries/session-room-queries.ts | Adjusts session-room lookup query to include world-readable realm behavior. |
| packages/realm-server/handlers/handle-webhook-receiver.ts | Executes (enqueues) registered webhook commands after signature verification, using filter handlers. |
| packages/realm-server/handlers/webhook-filter-handlers.ts | Introduces pluggable webhook filter handlers (default + GitHub event filter). |
| packages/realm-server/tests/server-endpoints/webhook-receiver-test.ts | Adds/updates tests asserting webhook receipt and command enqueue behavior. |
| packages/realm-server/server.ts | Allows X-Boxel-Async-Index through CORS. |
| packages/matrix/scripts/register-github-webhook.ts | Adds a script to register an incoming GitHub webhook and associated webhook commands. |
| packages/host/app/services/store.ts | Adds async-index persistence path to obtain server-assigned IDs immediately in render/command contexts. |
| packages/catalog-realm/github-event/github-event.gts | Adds a card type to store GitHub webhook event payloads and derived fields. |
| packages/catalog-realm/commands/process-github-event.gts | Adds a command that saves a GithubEventCard from a webhook event. |
| packages/catalog-realm/commands/create-submission.ts | Adds an import related to card error handling (currently unused). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/runtime-common/realm.ts
Outdated
| if (!options?.asyncIndex) { | ||
| await this.indexing(); | ||
| } |
There was a problem hiding this comment.
asyncIndex skips await this.indexing(), but RealmIndexUpdater.update() overwrites #indexingDeferred (see runtime-common/realm-index-updater.ts), so calling update() while a from-scratch index is running can clobber the deferred and leave prior callers waiting on indexing() hanging. Consider still awaiting any in-progress indexing before scheduling the incremental update (even in async mode), or change RealmIndexUpdater to chain/de-dupe indexing promises instead of overwriting the single deferred.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/realm-server/tests/server-endpoints/webhook-receiver-test.ts
Outdated
Show resolved
Hide resolved
| // Step 3: Register commands for both pull_request and pull_request_review events. | ||
| // Two separate commands are needed because GitHub uses different event headers | ||
| // for PR state changes vs. review submissions. | ||
| console.log('Setting up webhook commands...'); | ||
|
|
||
| const baseFilter: Record<string, unknown> = { | ||
| type: 'github-event', | ||
| }; | ||
|
|
||
| const eventTypes = [ | ||
| 'pull_request', | ||
| 'pull_request_review', | ||
| 'pull_request_review_comment', | ||
| 'check_run', | ||
| 'commit_comment', | ||
| 'discussion_comment', | ||
| ]; | ||
| for (let eventType of eventTypes) { | ||
| const cmd = await ensureWebhookCommand(jwt, { | ||
| incomingWebhookId: webhook.id, | ||
| command: commandURL, | ||
| filter: { eventType, ...baseFilter }, | ||
| }); | ||
| console.log(` ✓ ${eventType.padEnd(22)} → command ${cmd.id}`); |
There was a problem hiding this comment.
The script registers github-event webhook-command filters that include type and eventType but omit submissionRealmUrl. In this PR’s GithubEventFilterHandler.buildCommandInput, missing submissionRealmUrl throws, so these commands would never be enqueued when events arrive. Either add submissionRealmUrl to the filter in this script, or adjust the handler to derive it when absent.
| class GithubEventFilterHandler implements WebhookFilterHandler { | ||
| matches( | ||
| headers: Koa.Context['req']['headers'], | ||
| filter: Record<string, any>, | ||
| ): boolean { | ||
| let eventType = headers['x-github-event'] as string | undefined; | ||
|
|
||
| if (filter.eventType && filter.eventType !== eventType) { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } |
There was a problem hiding this comment.
GithubEventFilterHandler.matches has an incompatible parameter list and is called as matches(payload, headers, filter) by the receiver. As written it will treat the payload object as headers, so header-based filtering will behave incorrectly at runtime. Update the method signature to accept (payload, headers, filter) and implement the documented filtering (e.g. prNumber) using the parsed payload.
packages/host/app/services/store.ts
Outdated
| isNew; | ||
| if (useAsyncIndex) { | ||
| // POST with X-Boxel-Async-Index: realm writes the file and returns | ||
| // 202 with the assigned card ID immediately (fire-and-forget index). |
There was a problem hiding this comment.
The comment says the async-index POST returns 202, but Realm.createCard returns 201 for async-index responses. Please update the comment to match the actual status code to avoid confusion for future maintainers.
| // 202 with the assigned card ID immediately (fire-and-forget index). | |
| // 201 with the assigned card ID immediately (fire-and-forget index). |
d44fccd to
08f2eba
Compare
08f2eba to
df1db1a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: df1db1a87d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (filter.eventType && filter.eventType !== eventType) { | ||
| return false; | ||
| } | ||
|
|
||
| return true; |
There was a problem hiding this comment.
Enforce PR-number matching in github-event filters
GithubEventFilterHandler.matches only compares eventType and always returns true otherwise, so a filter that includes prNumber does not constrain execution to that PR. In practice, commands configured for a single PR will still run for all pull-request events of the same type (when submissionRealmUrl is present), which mixes unrelated PR events and triggers unintended command runs.
Useful? React with 👍 / 👎.
|
from your description:
are we still doing this? |
|
@habdelra planning to remove it as we shall try increasing the worker approach in local from the office hour discussion |
| static [queryableValue](_value: any, _stack: BaseDef[]): null { | ||
| return null; | ||
| } |
|
|
||
| class ProcessGithubEventInput extends CardDef { | ||
| @field eventType = contains(StringField); // from command_filter | ||
| @field submissionRealmUrl = contains(StringField); // from command_filter |
There was a problem hiding this comment.
only one submssion realm right? field not needed. Do we need that?
There was a problem hiding this comment.
or maybe if its a write. It shud just b called realm
tintinthong
left a comment
There was a problem hiding this comment.
Think we want to move all the async index stuff out of this PR
|
|
||
| class ProcessGithubEventInput extends CardDef { | ||
| @field eventType = contains(StringField); // from command_filter | ||
| @field submissionRealmUrl = contains(StringField); // from command_filter |
There was a problem hiding this comment.
or maybe if its a write. It shud just b called realm
packages/host/app/services/store.ts
Outdated
| // New cards still need a server-assigned ID in render context so that | ||
| // downstream commands can reference them. Use async-index so the realm | ||
| // returns the ID immediately without waiting for indexing to complete. | ||
| if (!instance.id && !opts?.doNotPersist) { | ||
| return (await this.persistAndUpdate(instance, { | ||
| realm: opts?.realm, | ||
| localDir: opts?.localDir, | ||
| asyncIndex: true, | ||
| })) as T | CardErrorJSONAPI; | ||
| } | ||
| // Existing cards: skip saves in render context (no auto-save). |
There was a problem hiding this comment.
do we need this portion of code?
There was a problem hiding this comment.
Not needed because is part of async index work, will take it out
| @@ -55,12 +63,23 @@ export async function fetchRealmSessionRooms( | |||
| let rows = await query(dbAdapter, [ | |||
There was a problem hiding this comment.
why was this fetchSessionRooms changed ahh?
There was a problem hiding this comment.
@tintinthong Before that, users with read access from the public realm (eg: db permission record like catalog/submission realm with * all users) will not get a notification when a new card is created
The obvious scenario is when in the code mode file tree, the webhook is triggered and the command creates a card - the user will not see the latest file added in the LHS under the submission realm. The user has to refresh to see the latest file result
6764b98 to
60edaf6
Compare
when running github event command experience timeout and file list is not updating in submission realm
9b4feff to
91058a1
Compare
91058a1 to
3e3638b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e3638b462
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!realm) { | ||
| throw new Error( | ||
| 'realm must be provided in the filter for github-event webhook commands', |
There was a problem hiding this comment.
Reject github-event filters that omit realm
github-event filters without realm are currently accepted at creation time, but buildCommandInput throws when such a command actually matches, and handle-webhook-receiver catches that error and silently skips execution. This creates a latent misconfiguration where webhook deliveries return 200 but the command never runs for matching events; either validate realm up front or make the runtime fallback path actually work.
Useful? React with 👍 / 👎.
| try { | ||
| payload = JSON.parse(rawBody); | ||
| } catch (_error) { | ||
| console.warn('Failed to parse webhook payload for filtering'); | ||
| } |
There was a problem hiding this comment.
Preserve non-JSON webhook payloads for command input
When the body is not valid JSON, the receiver logs a warning and keeps payload as {}, then still proceeds to enqueue commands. For generic/default webhook commands this drops the original payload entirely, so downstream commands cannot inspect the real webhook body and may make incorrect decisions. The handler should either pass through the raw body or fail the delivery for handlers that require structured JSON.
Useful? React with 👍 / 👎.
| import NumberField from 'https://cardstack.com/base/number'; | ||
| import { JsonField } from 'https://cardstack.com/base/commands/search-card-result'; | ||
|
|
||
| export class GithubEventCard extends CardDef { |
There was a problem hiding this comment.
is there any value in subclassing this.
There was a problem hiding this comment.
Might not be for now. If we found more different event cards involved, we can refactor the base event card which has similar attributes

What is changing
Webhook command execution (realm-server)
github-event — matches by X-GitHub-Event header; assembles { eventType, realm, payload } as command input.
default — pass-through; always matches and forwards the raw payload.