Date: 2025-10-30 Linear Ticket: DEV-652 Issue: GitHub app not properly handling webhook updates when an issue is closed
Root Cause Identified: GitHub webhook events for issue state changes (closed, reopened) are being misclassified as MessageCreated events instead of MessageUpdated events.
Impact: When a GitHub issue is closed or reopened, the system treats it as a new message creation rather than an update to an existing message. This causes:
- Incorrect event type classification in the database
- Potential for duplicate entries or incorrect grouping
- Misclassification in downstream processing (e.g., issues flagged as "burning problems" when closed)
Fix Required: Update determineAPIEventType() function in apps/github/src/lib/event-transformer.ts to include 'closed' and 'reopened' in the updateActions array.
✅ Webhooks ARE being received: The GitHub app webhook infrastructure is correctly configured and processing events.
Webhook Processing Flow:
GitHub → POST /webhook → processGitHubWebhook() → Queue → handleGitHubEventQueue() → Transform → Ingest
Verification Points:
- Webhook endpoint:
POST /webhookinapps/github/src/index.ts - Signature validation: Using
GITHUB_WEBHOOK_SECRET✓ - Duplicate detection: Redis-based with 7-day TTL ✓
- Raw payload storage: R2 bucket at
raw-webhooks/{date}/{eventType}/{deliveryId}.json✓ - Event queuing:
GITHUB_EVENTS_QUEUEwith DLQ fallback ✓
✅ Issue events ARE supported:
From apps/github/src/types/github.ts:90-104:
export type GitHubWebhookEvent =
| 'issues' // ✓ Includes closed/reopened actions
| 'issue_comment'
| 'pull_request'
| 'pull_request_review'
| 'pull_request_review_comment'
| 'push'
| 'repository'
| 'installation'
| 'installation_repositories'
| 'organization'
| 'membership'
| 'member'
| 'team'
| 'team_add';The issues event type is explicitly supported and processed by the queue handler.
✅ APIEvent types exist for state changes:
From packages/api-types/src/types/events.ts:12-20:
export const APIEventTypes = {
MessageCreated: 'message.created',
MessageUpdated: 'message.updated', // ✓ Exists for state changes
MessageDeleted: 'message.deleted',
ReactionCreated: 'reaction.created',
ReactionUpdated: 'reaction.updated',
ReactionDeleted: 'reaction.deleted',
} as const;The MessageUpdated type exists and is intended for state changes, but is not being used for closed/reopened actions.
❌ PROBLEM FOUND: In apps/github/src/lib/event-transformer.ts:692-731
The determineAPIEventType() function does NOT include 'closed' and 'reopened' in the update actions:
export function determineAPIEventType(action: string | undefined): APIEventType {
if (!action) {
return APIEventTypes.MessageCreated;
}
// Delete actions
if (action === 'deleted') {
return APIEventTypes.MessageDeleted;
}
// Update actions (content changes)
const updateActions = [
'edited',
'labeled',
'unlabeled',
'assigned',
'unassigned',
'milestoned',
'demilestoned',
'locked',
'unlocked',
'pinned',
'unpinned',
'transferred',
'converted_to_draft',
'ready_for_review',
'review_requested',
'review_request_removed',
'auto_merge_enabled',
'auto_merge_disabled',
'synchronize',
// ❌ 'closed' is MISSING
// ❌ 'reopened' is MISSING
];
if (updateActions.includes(action)) {
return APIEventTypes.MessageUpdated;
}
// Default to created for new content
// ❌ 'closed' and 'reopened' fall through to here
return APIEventTypes.MessageCreated;
}Current Behavior:
action: 'opened'→MessageCreated✓ (correct)action: 'closed'→MessageCreated❌ (WRONG - should be MessageUpdated)action: 'reopened'→MessageCreated❌ (WRONG - should be MessageUpdated)action: 'edited'→MessageUpdated✓ (correct)
✅ State information IS being captured: Even though the event type is wrong, the metadata correctly includes state information.
From apps/github/src/lib/event-transformer.ts:76-180 in transformGitHubIssueEvent():
metadata: {
github_issue_state: issue.state, // ✓ 'open' or 'closed'
github_event_action: event.action, // ✓ The webhook action
closed_at: issue.closed_at, // ✓ Timestamp when closed (or null)
// ... other metadata ...
}Good News: The raw state data is preserved in the event metadata, so:
- We can query for closed issues using
metadata.github_issue_state - We can identify the action using
metadata.github_event_action - We have the closed timestamp in
metadata.closed_at
Bad News: The event_type field in the database is incorrect, which affects:
- Event processing logic that filters by event type
- Analytics and reporting on event types
- Downstream systems that rely on event type classification
According to GitHub's webhook documentation, the issues event includes these actions:
Actions that should be MessageCreated:
opened- Issue was openedtransferred- Issue was transferred (arguably could be MessageUpdated)
Actions that should be MessageUpdated:
closed- Issue was closed ❌ Currently misclassifiedreopened- Issue was reopened ❌ Currently misclassifiededited- Issue title/body edited ✓ Currently correctlabeled- Label added ✓ Currently correctunlabeled- Label removed ✓ Currently correctassigned- Assignee added ✓ Currently correctunassigned- Assignee removed ✓ Currently correctmilestoned- Milestone added ✓ Currently correctdemilestoned- Milestone removed ✓ Currently correctlocked- Issue locked ✓ Currently correctunlocked- Issue unlocked ✓ Currently correctpinned- Issue pinned ✓ Currently correctunpinned- Issue unpinned ✓ Currently correct
Actions that should be MessageDeleted:
deleted- Issue was deleted ✓ Currently correct
Events are stored in the events table with the event_type field:
From packages/database/src/schema/events.ts:32-76:
export const eventsTbl = pgTable('events', {
eventId: uuid('id').$type<Id<'events'>>().default(sql`uuidv7()`).primaryKey(),
organizationId: ref('organization_id')
.$type<Id<'organizations'>>()
.notNull()
.references(() => organizationsTbl.id, { onDelete: 'cascade' }),
projectId: ref('project_id')
.$type<Id<'projects'>>()
.notNull()
.references(() => projectsTbl.id, { onDelete: 'cascade' }),
eventType: text('event_type').notNull(), // ❌ Stores 'message.created' for closed issues
// ... other fields ...
});Impact: Queries filtering by event_type = 'message.updated' will miss closed/reopened issues.
File: apps/github/src/lib/event-transformer.ts:692-731
Change: Add 'closed' and 'reopened' to the updateActions array:
const updateActions = [
'edited',
'closed', // ADD THIS
'reopened', // ADD THIS
'labeled',
'unlabeled',
'assigned',
'unassigned',
'milestoned',
'demilestoned',
'locked',
'unlocked',
'pinned',
'unpinned',
'transferred',
'converted_to_draft',
'ready_for_review',
'review_requested',
'review_request_removed',
'auto_merge_enabled',
'auto_merge_disabled',
'synchronize',
];- Unit Test: Add test cases to verify
determineAPIEventType('closed')returnsMessageUpdated - Integration Test: Create a test that simulates a GitHub closed webhook and verifies:
- Event is queued
- Event is transformed with
event_type: 'message.updated' - Metadata contains correct
github_issue_state: 'closed' - Event is stored with correct
event_typein database
- Manual Test: Close an actual GitHub issue in a connected repo and verify:
- Webhook is received
- Event appears in database with
event_type = 'message.updated' - Issue is not flagged as a "burning problem"
Historical Data: Existing events in the database with incorrect event types could be identified and updated:
-- Find misclassified closed issues
SELECT e.event_id, e.event_type, m.metadata
FROM events e
JOIN events_meta m ON e.event_id = m.id
WHERE e.event_type = 'message.created'
AND m.metadata->>'github_event_action' IN ('closed', 'reopened')
AND e.source_name = 'github';
-- Update misclassified events (run after code fix is deployed)
UPDATE events
SET event_type = 'message.updated'
WHERE event_type = 'message.created'
AND event_id IN (
SELECT e.event_id
FROM events e
JOIN events_meta m ON e.event_id = m.id
WHERE m.metadata->>'github_event_action' IN ('closed', 'reopened')
AND e.source_name = 'github'
);Note: This migration is optional since the metadata is correct. The impact is primarily on event type filtering/analytics.
apps/github/src/lib/event-transformer.ts:692-731- Add closed/reopened to updateActions
apps/github/src/lib/event-transformer.test.ts- Add unit testsapps/github/src/lib/queue-handler.test.ts- Add integration tests
packages/api-types/src/types/events.ts- APIEvent type definitionspackages/database/src/schema/events.ts- Database schemaapps/github/src/types/github.ts- GitHub webhook typesapps/github/src/lib/webhook-handler.ts- Webhook processingapps/github/src/lib/queue-handler.ts- Event queue processing
Problem Confirmed: GitHub closed/reopened webhook events ARE being received but are misclassified as MessageCreated instead of MessageUpdated.
Root Cause: The determineAPIEventType() function does not include 'closed' and 'reopened' in the list of update actions.
Fix Complexity: Low - single array addition in one function.
Risk Level: Low - change is isolated to event type classification logic.
Data Impact: Metadata is correct, so existing closed issues can still be identified via metadata.github_issue_state and metadata.github_event_action.
Next Steps:
- Apply the recommended code change
- Add unit and integration tests
- Deploy to staging and verify with test webhook
- Deploy to production
- Optionally run data migration for historical events
- Monitor for correct classification of new closed/reopened events