Skip to content

Instantly share code, notes, and snippets.

@benvinegar
Created October 30, 2025 17:48
Show Gist options
  • Select an option

  • Save benvinegar/505364ac2e8747eafd2bd4bc489cd6ec to your computer and use it in GitHub Desktop.

Select an option

Save benvinegar/505364ac2e8747eafd2bd4bc489cd6ec to your computer and use it in GitHub Desktop.
DEV-652: GitHub Closed Issue Webhook Investigation

DEV-652: GitHub Closed Issue Webhook Investigation

Date: 2025-10-30 Linear Ticket: DEV-652 Issue: GitHub app not properly handling webhook updates when an issue is closed

Executive Summary

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.


Investigation Details

1. Webhook Flow Verification

✅ 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 /webhook in apps/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_QUEUE with DLQ fallback ✓

2. Event Type Support

✅ 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.

3. APIEvent Type System

✅ 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.

4. Root Cause: Event Type Determination

❌ 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)

5. Metadata Preservation

✅ 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:

  1. We can query for closed issues using metadata.github_issue_state
  2. We can identify the action using metadata.github_event_action
  3. We have the closed timestamp in metadata.closed_at

Bad News: The event_type field in the database is incorrect, which affects:

  1. Event processing logic that filters by event type
  2. Analytics and reporting on event types
  3. Downstream systems that rely on event type classification

6. GitHub Webhook Action Types

According to GitHub's webhook documentation, the issues event includes these actions:

Actions that should be MessageCreated:

  • opened - Issue was opened
  • transferred - Issue was transferred (arguably could be MessageUpdated)

Actions that should be MessageUpdated:

  • closed - Issue was closed ❌ Currently misclassified
  • reopened - Issue was reopened ❌ Currently misclassified
  • edited - Issue title/body edited ✓ Currently correct
  • labeled - Label added ✓ Currently correct
  • unlabeled - Label removed ✓ Currently correct
  • assigned - Assignee added ✓ Currently correct
  • unassigned - Assignee removed ✓ Currently correct
  • milestoned - Milestone added ✓ Currently correct
  • demilestoned - Milestone removed ✓ Currently correct
  • locked - Issue locked ✓ Currently correct
  • unlocked - Issue unlocked ✓ Currently correct
  • pinned - Issue pinned ✓ Currently correct
  • unpinned - Issue unpinned ✓ Currently correct

Actions that should be MessageDeleted:

  • deleted - Issue was deleted ✓ Currently correct

7. Database Storage

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.


Recommended Fix

Code Change Required

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',
];

Testing Recommendations

  1. Unit Test: Add test cases to verify determineAPIEventType('closed') returns MessageUpdated
  2. 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_type in database
  3. 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"

Data Migration Considerations

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.


Related Files

Primary Files to Modify

  • apps/github/src/lib/event-transformer.ts:692-731 - Add closed/reopened to updateActions

Files for Testing

  • apps/github/src/lib/event-transformer.test.ts - Add unit tests
  • apps/github/src/lib/queue-handler.test.ts - Add integration tests

Files for Reference

  • packages/api-types/src/types/events.ts - APIEvent type definitions
  • packages/database/src/schema/events.ts - Database schema
  • apps/github/src/types/github.ts - GitHub webhook types
  • apps/github/src/lib/webhook-handler.ts - Webhook processing
  • apps/github/src/lib/queue-handler.ts - Event queue processing

Conclusion

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:

  1. Apply the recommended code change
  2. Add unit and integration tests
  3. Deploy to staging and verify with test webhook
  4. Deploy to production
  5. Optionally run data migration for historical events
  6. Monitor for correct classification of new closed/reopened events
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment