Skip to content

User-to-repo mapping and GitHub notification routing #203

@dcellison

Description

@dcellison

Problem

GitHub integration settings (PR_REVIEW_ENABLED, ISSUE_TRIAGE_ENABLED, GITHUB_NOTIFY_CHAT_ID) are global env vars. The webhook processes events with no awareness of which user cares about which repo. All notifications go to a single hardcoded chat ID.

In multi-user, each user has their own GitHub identity, subscribes to different repos, and wants notifications routed to their chosen destination (preserving the PR #182 separation between GitHub notifications and the main Kai chat).

Parent issue: #196 (sub-issue #5)

Proposal

1. Add repo subscriptions to users.yaml

users:
  - telegram_id: 2114582497
    name: Daniel
    github: dcellison
    github_repos:
      - dcellison/kai
      - dcellison/anvil
    github_notify_chat_id: -100123456789
    pr_review: true
    issue_triage: true

2. Route webhook events by repo ownership

When a GitHub webhook event arrives for dcellison/kai:

  1. Extract the repo full name from the payload (repository.full_name)
  2. Look up which user(s) have that repo in their github_repos list
  3. For each matched user, check their pr_review / issue_triage flags
  4. Route notifications to each user's github_notify_chat_id (falls back to telegram_id if unset)

3. Telegram commands for managing subscriptions

/github                         - show current repo subscriptions and GitHub settings
/github token <ghp_...>        - store GitHub PAT for webhook management
/github token clear             - remove stored token
/github add <owner/repo>        - subscribe + register webhook (if token available)
/github remove <owner/repo>     - unsubscribe + deregister webhook (if last subscriber)
/github notify <chat_id>        - set notification destination chat
/github notify reset            - reset to main chat (telegram_id)
/github reviews on|off          - toggle PR review notifications
/github triage on|off           - toggle issue triage notifications

4. Webhook registration (ingestion side)

Everything above handles routing - which user sees which repo's events. But events only arrive if the repo has a GitHub webhook pointing to Kai's endpoint. Today only dcellison/kai has this configured. A user who runs /github add dcellison/anvil would be subscribed to... silence.

/github add must handle both sides: subscribe the user to notifications AND register the webhook on the repo so events actually flow in.

GitHub token requirement

Registering a webhook requires admin:repo_hook scope on a GitHub personal access token. Each user stores their own token via Telegram:

/github token <ghp_...>    - store GitHub PAT (one-time setup)
/github token clear         - remove stored token
  • Stored in the database per-user (settings table, key github_token:{chat_id})
  • Never echoed back, never logged, never included in error messages
  • The token is also used to verify repo existence during /github add

Registration flow

When a user runs /github add owner/repo:

  1. Validate format (owner/repo)
  2. Check if the user has a stored GitHub token. If not, reply with manual setup instructions and the webhook URL/secret
  3. Use the GitHub API to check if a webhook already exists on that repo pointing to Kai's endpoint
  4. If no webhook exists, create one via POST /repos/{owner}/{repo}/hooks with:
    • url: the public webhook URL (from TELEGRAM_WEBHOOK_URL base, i.e., https://api.syrinx.net/webhook/github)
    • secret: the shared WEBHOOK_SECRET
    • events: ["push", "pull_request", "issues", "issue_comment", "pull_request_review"]
    • content_type: json
  5. If webhook creation fails (403 - no admin access, 404 - repo not found), reply with the specific error and manual setup instructions
  6. On success, add the repo to the user's subscription list

Shared secret model

All repos use the same WEBHOOK_SECRET for HMAC signature validation. This is intentional - the endpoint is already behind Cloudflare with signature verification. Per-repo secrets would require identifying the repo before validating the signature (the body must be read to get the repo name, but the signature covers the body), creating a chicken-and-egg problem.

Manual fallback

If the user has no token or lacks admin access on the repo, /github add still subscribes them to notifications but prints:

Subscribed to owner/repo notifications.

I could not register the webhook automatically (no GitHub token / insufficient permissions).
To receive events, add a webhook manually in the repo settings:

  URL: https://api.syrinx.net/webhook/github
  Content type: application/json
  Secret: (ask your Kai admin)
  Events: Pushes, Pull requests, Issues, Issue comments, Pull request reviews

Deregistration

/github remove owner/repo unsubscribes the user. If no other users are subscribed to that repo, Kai removes the webhook from GitHub (if the user has a token with access). If other users are still subscribed, the webhook stays.

Display

/github with no arguments shows:

GitHub: dcellison
Notifications: group "Kai Dev" (-100123456789)
PR reviews: on
Issue triage: on

Subscribed repos:
  dcellison/kai
  dcellison/anvil

Storage

Two layers:

  1. users.yaml (admin baseline) - github_repos, github_notify_chat_id, pr_review, issue_triage fields
  2. Database (user overrides via Telegram) - for repos added/removed via /github add/remove and setting toggles

Database storage options:

  • github_token:{chat_id} in settings table (PAT for webhook management - never logged or echoed)
  • github_repos:{chat_id} in settings table as JSON array (for user-added repos beyond the yaml baseline)
  • github_notify_chat:{chat_id} in settings table
  • pr_review:{chat_id} and issue_triage:{chat_id} in settings table

The effective repo list is the union of users.yaml repos and database-added repos, minus database-removed repos.

Webhook routing changes

Current flow (webhook.py):

  • GitHub events arrive at /webhook/github
  • PR review and issue triage handlers use global config
  • Notifications go to GITHUB_NOTIFY_CHAT_ID or fall back to first allowed user

New flow:

  • GitHub events arrive at /webhook/github
  • Extract repository.full_name from payload
  • Look up subscribed users for that repo
  • For each user: check their pr_review/issue_triage flags, route to their notification chat
  • If no user is subscribed to the repo, log a warning (don't silently drop)

Validation

  • /github add validates the repo format (owner/repo)
  • Repo existence is verified automatically during webhook registration (when a token is available). Without a token, format validation only.
  • /github notify validates the chat_id is a valid integer

Backward compatibility

  • PR_REVIEW_ENABLED env var becomes global default for users without a pr_review field
  • ISSUE_TRIAGE_ENABLED env var becomes global default for users without an issue_triage field
  • GITHUB_NOTIFY_CHAT_ID env var becomes fallback for users without github_notify_chat_id
  • Existing single-user installs work unchanged

Context

  • GitHub webhook handler: webhook.py
  • PR review agent: triggered by pull_request events (opened/reopened/synchronize)
  • Issue triage: triggered by issues events
  • Current notification routing: uses GITHUB_NOTIFY_CHAT_ID with fallback to first allowed user
  • PR Route GitHub notifications to a separate Telegram group #182 added the separation between GitHub notifications and main chat
  • UserConfig dataclass: config.py lines 86-111 (needs new fields: github_repos, github_notify_chat_id, pr_review, issue_triage)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions