Cs 10217 trigger command on webhook event#4074
Cs 10217 trigger command on webhook event#4074richardhjtan wants to merge 13 commits intocs-9945-submission-bot-open-a-github-prfrom
Conversation
…-10217-trigger-command-on-webhook-event
…-10217-trigger-command-on-webhook-event
…-10217-trigger-command-on-webhook-event
when running github event command experience timeout and file list is not updating in submission realm
| realm: input.realm, | ||
| localDir: input.localDir, | ||
| }); | ||
| if (input.doNotWaitForPersist) { |
There was a problem hiding this comment.
Added doNotWaitForPersist option for save card command, this avoid timeout issue when running the command in headless chrome
Preview deployments |
Host Test Results 1 files ±0 1 suites ±0 1h 33m 46s ⏱️ - 2m 43s For more details on these errors, see this check. Results for commit d08e7f7. ± Comparison against base commit 36bd710. ♻️ This comment has been updated with latest results. |
…-10217-trigger-command-on-webhook-event
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d08e7f78c8
ℹ️ 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".
packages/host/app/commands/bot-requests/create-listing-pr-request.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This pull request implements webhook command execution functionality to automatically run commands when GitHub webhook events are received. It introduces a GitHub event card definition for storing webhook event data, adds a command to process GitHub events, and includes comprehensive test coverage for the new functionality.
Changes:
- Add webhook command execution with filtering support (event type, PR number)
- Introduce GitHub event card for storing webhook events as data
- Update session room queries to support world-readable realms
- Add webhook management methods to realm server service
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
packages/realm-server/handlers/handle-webhook-receiver.ts |
Implements command execution logic with event filtering and queuing |
packages/runtime-common/db-queries/session-room-queries.ts |
Modifies query to include users with world-readable realm access |
packages/realm-server/tests/server-endpoints/webhook-receiver-test.ts |
Adds comprehensive tests for webhook command execution and filtering |
packages/matrix/scripts/register-github-webhook.ts |
New script for registering GitHub webhooks with event filtering |
packages/host/app/services/realm-server.ts |
Adds webhook management API methods (list, create) |
packages/host/app/commands/save-card.ts |
Adds support for non-blocking card persistence |
packages/host/app/commands/bot-requests/create-listing-pr-request.ts |
Integrates webhook registration into PR creation flow |
packages/catalog-realm/github-event/github-event.gts |
New card definition for storing GitHub webhook events |
packages/catalog-realm/commands/process-github-event.gts |
New command to process GitHub webhook events and create event cards |
packages/base/commands/search-card-result.gts |
Adds queryableValue implementation for JsonField |
packages/base/command.gts |
Adds doNotWaitForPersist field to SaveCardInput |
packages/host/app/commands/create-listing-pr.ts |
Minor comment update |
Comments suppressed due to low confidence (2)
packages/realm-server/handlers/handle-webhook-receiver.ts:166
- Inconsistent logging approach: The code uses
console.warnandconsole.errordirectly, while other handlers in the codebase use theloggerutility from@cardstack/runtime-common. For consistency and better log management (including proper log levels and module identification), consider using the logger utility:const log = logger('webhook-receiver');and thenlog.warn(...)andlog.error(...).
console.warn('Failed to parse webhook payload for filtering');
}
let eventType = ctxt.req.headers['x-github-event'] as string | undefined;
let executedCommands = 0;
for (let commandRow of commandRows) {
let commandFilter = commandRow.command_filter as Record<
string,
any
> | null;
// Apply filter if specified
if (commandFilter) {
// Check if event type matches filter
if (commandFilter.eventType && commandFilter.eventType !== eventType) {
continue;
}
// Check if PR number matches filter (for pull_request events)
if (
commandFilter.prNumber &&
payload.pull_request?.number !== commandFilter.prNumber
) {
continue;
}
// Additional filter checks can be added here as needed
}
let commandURL = commandRow.command as string;
let submissionRealmUrl =
(commandFilter?.submissionRealmUrl as string | undefined) ??
new URL('/submissions/', commandURL).href;
// Run as the realm owner so they have write permissions in the submission realm
let realmOwnerRows = await query(dbAdapter, [
`SELECT username FROM realm_user_permissions WHERE realm_url = `,
param(submissionRealmUrl),
` AND realm_owner = true LIMIT 1`,
]);
let runAs =
(realmOwnerRows[0]?.username as string | undefined) ??
(webhook.username as string);
let commandInput = {
eventType: eventType ?? '',
submissionRealmUrl,
payload,
};
try {
await enqueueRunCommandJob(
{
realmURL: submissionRealmUrl,
realmUsername: runAs,
runAs,
command: commandURL,
commandInput,
},
queue,
dbAdapter,
userInitiatedPriority,
);
executedCommands++;
} catch (error) {
console.error(
`Failed to enqueue webhook command ${commandURL}:`,
error,
);
packages/matrix/scripts/register-github-webhook.ts:20
- Header name case mismatch: The script uses lowercase 'x-hub-signature-256' in the webhook configuration, but the tests use 'X-Hub-Signature-256' with proper capitalization. While the handler correctly normalizes headers to lowercase when reading (line 213), this inconsistency could cause confusion. Consider using the canonical capitalization 'X-Hub-Signature-256' consistently in both places, as this is the standard format used by GitHub webhooks.
header: 'x-hub-signature-256',
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import type MatrixService from '../services/matrix-service'; | ||
| import type RealmServerService from '../services/realm-server'; | ||
| import type StoreService from '../services/store'; |
There was a problem hiding this comment.
The import paths for services are incorrect. Since this file is in the bot-requests subdirectory, the imports should use '../../services/' instead of '../services/'. The correct paths should be:
import type MatrixService from '../../services/matrix-service';import type RealmServerService from '../../services/realm-server';import type StoreService from '../../services/store';
This pattern is correctly used in send-bot-trigger-event.ts in the same directory.
| import type MatrixService from '../services/matrix-service'; | |
| import type RealmServerService from '../services/realm-server'; | |
| import type StoreService from '../services/store'; | |
| import type MatrixService from '../../services/matrix-service'; | |
| import type RealmServerService from '../../services/realm-server'; | |
| import type StoreService from '../../services/store'; |
| let realmOwnerRows = await query(dbAdapter, [ | ||
| `SELECT username FROM realm_user_permissions WHERE realm_url = `, | ||
| param(submissionRealmUrl), | ||
| ` AND realm_owner = true LIMIT 1`, | ||
| ]); | ||
| let runAs = | ||
| (realmOwnerRows[0]?.username as string | undefined) ?? | ||
| (webhook.username as string); |
There was a problem hiding this comment.
The database query to fetch realm owner is not wrapped in a try-catch block, but it's inside a loop that processes webhook commands. If this query fails for any reason (e.g., database connection issues), it will cause the entire webhook request to fail with an unhandled exception, potentially losing valid webhook events. Consider wrapping this query in a try-catch block and logging the error while continuing to process other commands, or handle the error gracefully by skipping this specific command.
| let realmOwnerRows = await query(dbAdapter, [ | |
| `SELECT username FROM realm_user_permissions WHERE realm_url = `, | |
| param(submissionRealmUrl), | |
| ` AND realm_owner = true LIMIT 1`, | |
| ]); | |
| let runAs = | |
| (realmOwnerRows[0]?.username as string | undefined) ?? | |
| (webhook.username as string); | |
| let runAs = webhook.username as string; | |
| try { | |
| let realmOwnerRows = await query(dbAdapter, [ | |
| `SELECT username FROM realm_user_permissions WHERE realm_url = `, | |
| param(submissionRealmUrl), | |
| ` AND realm_owner = true LIMIT 1`, | |
| ]); | |
| if (realmOwnerRows[0]?.username) { | |
| runAs = realmOwnerRows[0].username as string; | |
| } | |
| } catch (error) { | |
| console.error( | |
| `Failed to fetch realm owner for submission realm ${submissionRealmUrl}:`, | |
| error, | |
| ); | |
| } |
| 'WHERE u.session_room_id IS NOT NULL', | ||
| 'AND (', | ||
| ' EXISTS (', | ||
| ' SELECT 1 FROM realm_user_permissions', | ||
| ' WHERE realm_url =', | ||
| param(realmURL), | ||
| 'AND (rup.read = true OR rup.write = true)', | ||
| 'AND u.session_room_id IS NOT NULL', | ||
| ' AND username = u.matrix_user_id', | ||
| ' AND (read = true OR write = true)', | ||
| ' )', | ||
| ' OR EXISTS (', | ||
| ' SELECT 1 FROM realm_user_permissions', | ||
| ' WHERE realm_url =', | ||
| param(realmURL), | ||
| " AND username = '*'", | ||
| ' AND read = true', | ||
| ' )', | ||
| ')', |
There was a problem hiding this comment.
The refactored query logic appears to have a potential issue. When a realm is world-readable (username = '*' with read = true), the query will return ALL users with session rooms, not just those who should have access to this specific realm. The OR EXISTS clause for world-readable realms doesn't restrict which users are returned - it just checks if such a permission exists for the realm, and if it does, all users in the WHERE clause match.
This could expose session room information for users who shouldn't have access to this realm. The original JOIN-based approach correctly filtered users to only those with explicit permissions for this realm. Consider revising the logic to ensure that world-readable access doesn't inadvertently expose all user session rooms.
packages/host/app/commands/bot-requests/create-listing-pr-request.ts
Outdated
Show resolved
Hide resolved
lukemelia
left a comment
There was a problem hiding this comment.
This needs some work. See comments.
packages/base/command.gts
Outdated
| @field card = linksTo(CardDef); | ||
| @field realm = contains(StringField); | ||
| @field localDir = contains(StringField); | ||
| @field doNotWaitForPersist = contains(BooleanField); |
There was a problem hiding this comment.
Why are we doing this? It seems problematic -- i.e. what if saving fails?
There was a problem hiding this comment.
Agree, reverted this
| const githubWebhook = webhooks.find( | ||
| (w: { verificationType: string }) => | ||
| w.verificationType === 'HMAC_SHA256_HEADER', | ||
| ); |
There was a problem hiding this comment.
This is not a robust way to lookup the webhook. Other webhooks could use this verification type as well couldn't they?
There was a problem hiding this comment.
Probably should go ahead and do the above TODO now
| get submissionRealmURL(): string { | ||
| return `${this.url.href}submissions/`; | ||
| } |
There was a problem hiding this comment.
This seems awkwardly specific for the realm-server service. Can we define it somewhere else?
| static [queryableValue](_value: any, _stack: BaseDef[]): null { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
@lukemelia This is to fix this card error
| if (commandFilter.eventType && commandFilter.eventType !== eventType) { | ||
| continue; | ||
| } | ||
|
|
||
| // Check if PR number matches filter (for pull_request events) | ||
| if ( | ||
| commandFilter.prNumber && | ||
| payload.pull_request?.number !== commandFilter.prNumber | ||
| ) { | ||
| continue; | ||
| } | ||
|
|
||
| // Additional filter checks can be added here as needed |
There was a problem hiding this comment.
The command filter should specify a type along with its configuration and we should use the type to look up a code block somewhere that has this specific logic of comparing the payload with the filter configuration. The logic should not be in line within this handle-webhook-receiver.
| let submissionRealmUrl = | ||
| (commandFilter?.submissionRealmUrl as string | undefined) ?? | ||
| new URL('/submissions/', commandURL).href; | ||
|
|
||
| // Run as the realm owner so they have write permissions in the submission realm | ||
| let realmOwnerRows = await query(dbAdapter, [ | ||
| `SELECT username FROM realm_user_permissions WHERE realm_url = `, | ||
| param(submissionRealmUrl), | ||
| ` AND realm_owner = true LIMIT 1`, | ||
| ]); | ||
| let runAs = | ||
| (realmOwnerRows[0]?.username as string | undefined) ?? | ||
| (webhook.username as string); | ||
|
|
||
| let commandInput = { | ||
| eventType: eventType ?? '', | ||
| submissionRealmUrl, | ||
| payload, | ||
| }; |
There was a problem hiding this comment.
This logic of assembling the command input also should not be in line here. It needs to be abstracted in some way. The idea is that this route handler should handle any webhook in the system, not just the github webhook.
| try { | ||
| await enqueueRunCommandJob( | ||
| { | ||
| realmURL: submissionRealmUrl, |
There was a problem hiding this comment.
This shouldn't be fixed. It should be based on something from the database.
| @@ -55,12 +57,23 @@ export async function fetchRealmSessionRooms( | |||
| let rows = await query(dbAdapter, [ | |||
| 'SELECT u.matrix_user_id, u.session_room_id', | |||
| 'FROM users u', | |||
| 'JOIN realm_user_permissions rup', | |||
| 'ON rup.username = u.matrix_user_id', | |||
| 'WHERE rup.realm_url =', | |||
| 'WHERE u.session_room_id IS NOT NULL', | |||
| 'AND (', | |||
| ' EXISTS (', | |||
| ' SELECT 1 FROM realm_user_permissions', | |||
| ' WHERE realm_url =', | |||
| param(realmURL), | |||
| 'AND (rup.read = true OR rup.write = true)', | |||
| 'AND u.session_room_id IS NOT NULL', | |||
| ' AND username = u.matrix_user_id', | |||
| ' AND (read = true OR write = true)', | |||
| ' )', | |||
| ' OR EXISTS (', | |||
| ' SELECT 1 FROM realm_user_permissions', | |||
| ' WHERE realm_url =', | |||
| param(realmURL), | |||
| " AND username = '*'", | |||
| ' AND read = true', | |||
| ' )', | |||
| ')', | |||
There was a problem hiding this comment.
Did you check other consumers of this method to see if there are any unintended consequences of this change?
There was a problem hiding this comment.
There are no tests about this, I have conducted a test to demonstrate different scenarios to understand and avoid unintentional behaviour
60f3787 to
7982631
Compare
|
Close this PR due to the base branch PR being closed and split into several PRs. The feedback changes and newer approach are addressed in #4098 |
Uh oh!
There was an error while loading. Please reload this page.