This document describes the GitHub App integration for the Towns Protocol bot, which bridges GitHub activity with Towns channels through real-time webhooks and fallback polling.
Towns Protocol is a decentralized communication platform where users organize into spaces (communities) containing channels (topic-specific streams). Users interact via:
- Slash commands (e.g.,
/github subscribe owner/repo) - Messages with bot mentions and replies
- Reactions and threaded conversations
The bot receives Towns events via webhooks at /webhook, processes them, and sends formatted messages back to channels.
GitHub's Events API has critical limitations:
| Issue | Impact |
|---|---|
| Missing PR merge events | No notifications when PRs are closed/merged |
| Empty commit data | Push events contain commits: [] |
| 5-minute polling delay | Not real-time |
| Best-effort delivery | No guarantees on completeness |
Root cause: Events API is designed for activity feeds, not reliable event delivery.
┌─────────────────────────────────────────────┐
│ Single Hono Application │
├─────────────────────────────────────────────┤
│ │
│ POST /webhook ← Towns Protocol │
│ POST /github-webhook ← GitHub App │
│ GET /health ← Health checks │
│ │
│ • Single process, single database │
│ • Deployed on Render │
│ • Dual-mode: webhooks OR polling │
└─────────────────────────────────────────────┘
Real-time Webhooks (Preferred):
- Requires GitHub App installation on repository
- Instant delivery (< 1 second latency)
- Works for public AND private repos
- Complete event data guaranteed
Polling Fallback (Legacy):
- No installation required
- 5-minute polling interval
- Only works for public repos
- Missing events (PR merges, commit data)
- Dual-mode architecture: Automatically use webhooks when app installed, fall back to polling otherwise
- No manual webhook configuration: GitHub App manages webhooks automatically
- Idempotency: Track webhook deliveries by
X-GitHub-Deliveryheader to prevent duplicates - Database-backed state: Installation and delivery state persisted in PostgreSQL (Drizzle ORM)
- Foreign key CASCADE: Auto-cleanup when installations deleted
CREATE TABLE github_installations (
installation_id INTEGER PRIMARY KEY,
account_login TEXT NOT NULL,
account_type TEXT NOT NULL CHECK(account_type IN ('Organization', 'User')),
installed_at TIMESTAMPTZ NOT NULL,
suspended_at TIMESTAMPTZ,
app_slug TEXT NOT NULL DEFAULT 'towns-github-bot'
);CREATE TABLE installation_repositories (
installation_id INTEGER NOT NULL,
repo_full_name TEXT NOT NULL,
added_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (installation_id, repo_full_name),
FOREIGN KEY (installation_id)
REFERENCES github_installations(installation_id)
ON DELETE CASCADE
);
CREATE INDEX idx_installation_repos_by_name ON installation_repositories(repo_full_name);CREATE TABLE github_subscriptions (
id SERIAL PRIMARY KEY,
space_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
repo_full_name TEXT NOT NULL,
delivery_mode TEXT NOT NULL CHECK (delivery_mode IN ('webhook', 'polling')),
is_private BOOLEAN NOT NULL,
created_by_towns_user_id TEXT NOT NULL
REFERENCES github_user_tokens(towns_user_id) ON DELETE CASCADE,
created_by_github_login TEXT,
installation_id INTEGER
REFERENCES github_installations(installation_id) ON DELETE SET NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
event_types TEXT NOT NULL DEFAULT 'pr,issues,commits,releases',
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(space_id, channel_id, repo_full_name)
);
CREATE INDEX idx_subscriptions_by_repo ON github_subscriptions(repo_full_name);
CREATE INDEX idx_subscriptions_by_channel ON github_subscriptions(channel_id);github_user_tokens (see src/db/schema.ts) stores the encrypted user OAuth credentials referenced by created_by_towns_user_id.
CREATE TABLE webhook_deliveries (
delivery_id TEXT PRIMARY KEY, -- X-GitHub-Delivery header
installation_id INTEGER,
event_type TEXT NOT NULL,
delivered_at TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending', 'success', 'failed')),
error TEXT,
retry_count INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_deliveries_status ON webhook_deliveries(status, delivered_at);Note: installation_id in webhook_deliveries is nullable because some webhook events don't have installation context. No foreign key constraint to preserve audit trail.
Implemented in src/github-app/:
-
GitHubApp (
app.ts)- Octokit App initialization
- Webhook event registration
- Public
webhooksproperty for route registration
-
EventProcessor (
event-processor.ts)- Routes webhook events to formatters
- Filters by channel event type preferences
- Sends to subscribed Towns channels
-
InstallationService (
installation-service.ts)- Handles installation lifecycle (created/deleted)
- Manages repository additions/removals
- Checks installation status per repo
- Notifies Towns channels of mode changes
-
WebhookProcessor (
webhook-processor.ts)- Idempotency tracking via deliveryId
- Cleanup of old webhook records
- Database-backed (no in-memory state)
GitHub Event
→ POST /github-webhook
→ JWT verify + HMAC signature check (Octokit)
→ Check idempotency (webhook_deliveries table)
→ EventProcessor routes to formatter
→ Filter by channel preferences
→ Send to Towns channels
→ Mark as processed (success only)
Critical: Failed webhooks are NOT marked as processed, allowing GitHub's retry mechanism to work.
Using @octokit/webhooks official types:
import type {
EmitterWebhookEvent,
EmitterWebhookEventName,
} from "@octokit/webhooks";
type WebhookPayload<T extends EmitterWebhookEventName> =
EmitterWebhookEvent<T>["payload"];
export type PullRequestPayload = WebhookPayload<"pull_request">;
export type IssuesPayload = WebhookPayload<"issues">;
// ... etcWhy not @octokit/webhooks-types? That package is legacy (community JSON schemas). @octokit/webhooks uses official OpenAPI types.
Status: Live in production — users only leave Towns once and the subscription is created automatically during the OAuth callback.
/github subscribe owner/repo
↓
User has OAuth token?
├─ No → Show OAuth URL → User authorizes → Callback stores token
└─ Yes → Continue
↓
Check if GitHub App installed on repo
├─ Installed → delivery_mode = 'webhook'
└─ Not installed → delivery_mode = 'polling'
↓
Create subscription immediately
↓
Success message in Towns + browser success page
↓
[If polling] Auto-redirect to GitHub App installation page (5s countdown)
↓
[Optional] User installs GitHub App → Webhook upgrades subscription automatically
Private repos require the GitHub App to be installed before subscribing.
/github subscribe owner/repo
↓
User has OAuth token?
├─ No → Show OAuth URL → User authorizes → Callback stores token
└─ Yes → Continue
↓
Check if GitHub App installed on repo
├─ Installed → Create subscription with delivery_mode = 'webhook' → Success
└─ Not installed ↓
Store pending subscription in `pending_subscriptions` table
Show "Installation Required" page with redirect to GitHub App install
↓
User installs GitHub App
↓
Installation webhook fires → `completePendingSubscriptions()` runs
↓
Subscription created automatically + notification sent to channel
Pending Subscriptions:
- Stored in
pending_subscriptionstable with 1-hour expiration - Auto-completed when installation webhook fires for the target repo
- Cleaned up periodically by
cleanupExpiredPendingSubscriptions()
State parameter passed through OAuth flow:
{
"action": "subscribe",
"townsUserId": "0x...",
"spaceId": "...",
"channelId": "...",
"repo": "owner/repo",
"eventTypes": "pr,issues,commits,..."
}Purpose: Upgrade polling subscriptions and complete pending subscriptions
Implemented in InstallationService:
- Triggered by
installation.createdandinstallation_repositories.addedevents - Upgrades existing polling subscriptions to webhook mode
- Completes pending subscriptions for private repos
- Sends notification to affected channels
Query commands require 2 invocations for private repos (vs 1 for public):
/gh_pr owner/repo #123
↓
User has OAuth token?
├─ No → Show editable OAuth prompt → User authorizes → Callback
│ ↓
│ OAuth Success → Edit message to "✅ GitHub connected"
│ ↓
│ Check if GitHub App installed on repo
│ ├─ Installed → Show success page
│ └─ Not installed → Show "Installation Required" page
│ Auto-redirect to GitHub App install (3s)
│ ↓
│ User installs app → Returns to Towns
│
└─ Yes → Check if GitHub App installed
├─ Installed → Fetch and display result
└─ Not installed → Show install prompt
Second invocation after OAuth + install:
/gh_pr owner/repo #123
↓
OAuth token valid ✓
GitHub App installed ✓
↓
Fetch and display result → SUCCESS
Key differences from subscriptions:
- Query commands cannot be "pended" and auto-completed by webhooks
- Results must be returned synchronously to the user
- Users must re-run the command after OAuth + install complete
{
"action": "query",
"townsUserId": "0x...",
"spaceId": "...",
"channelId": "...",
"repo": "owner/repo",
"messageEventId": "..." // For editing the OAuth prompt message
}Filter events by branch to reduce noise from feature branches.
Syntax:
/github subscribe owner/repo --events commits --branches main,develop
/github subscribe owner/repo --events commits,ci --branches release/*
/github subscribe owner/repo --events commits --branches all # Opt-in to all branches (or *)Scope - Events affected by --branches flag:
| Event | Branch Context | Example Use Case |
|---|---|---|
| commits | Branch pushed to | Only main/develop pushes |
| ci | Workflow trigger branch | Only prod CI results |
| pr | Base branch (merge target) | Only PRs targeting main |
| reviews | PR's base branch | Same as pr |
| branches | Branch created/deleted | Only release/* events |
Not branch-specific: issues, comments, releases, stars, forks
Design Decisions:
- Default: Default branch only (breaking change from current "all branches")
- Patterns: Glob support (
release/*,feature/*) --branches allopts into all branches (preserves old behavior)
Storage:
New branch_filter column in github_subscriptions:
NULL= default branch only'all'= all branches'main,develop,release/*'= specific branches/patterns
Files to modify:
db/schema.ts- Addbranch_filtercolumngithub-subscription-handler.ts- Parse--branchesflagevent-processor.ts- Filter in specific handlers (onPush,onWorkflowRun,onPullRequest,onPullRequestReview,onBranchEvent) using typed payloads from@octokit/webhookssubscription-service.ts- Store/retrieve filter
Migration:
Breaking change: existing subscriptions get default-branch-only behavior.
Option: migrate existing commits subscriptions to branch_filter = 'all' to preserve old behavior.
Group related GitHub events into threads to reduce channel noise while preserving context.
Core Concept:
When a PR is opened, subsequent events (commits, CI runs, reviews, comments) are sent as thread replies instead of top-level messages. This keeps the channel clean while grouping all PR activity together.
Event Grouping Strategy:
| Thread Anchor | Grouped Events |
|---|---|
| PR opened | Commits pushed, CI status, reviews, review comments, PR comments, PR merged/closed |
| Issue opened | Issue comments, issue closed/reopened |
| Release published | (standalone - no grouping needed) |
| Branch created | Commits pushed to branch (optional) |
Implementation Approach:
-
Unified Message Mappings Table (implemented as
messageMappings):- Stores both anchor and child entity mappings
- Fields:
spaceId, channelId, repoFullName, githubEntityType, githubEntityId, parentType, parentNumber, townsMessageId, githubUpdatedAt, expiresAt - Composite primary key on (spaceId, channelId, repoFullName, githubEntityType, githubEntityId)
-
Event Processing Flow:
Incoming webhook event
↓
Is this a PR/issue event?
├─ PR opened / Issue opened → Send as top-level, store in messageMappings
└─ PR push / PR review / PR comment / etc.
↓
Lookup parent anchor in messageMappings
├─ Found → Send as reply with threadId
└─ Not found → Send as top-level (anchor was before bot joined or filtered)
- Architecture:
EventProcessor- Routes webhook events, handles entity contextMessageDeliveryService- Manages send/edit/delete, thread lookup, mapping storagehandleAnchorEvent()- Unified handler for PR/issue open/edit/close/reopen
Message Format in Thread:
# Anchor message (top-level)
🔀 **PR #123 opened** by @user
feat: Add dark mode support
[View PR](https://github.com/...)
# Thread replies
💬 @reviewer commented on PR #123
"Looks good! Just one suggestion..."
✅ CI passed on PR #123
All 45 tests passing
🔍 @reviewer approved PR #123
"LGTM!"
🎉 PR #123 merged by @user
Configuration Options:
/github subscribe owner/repo --threads=on # Enable threading (default)
/github subscribe owner/repo --threads=off # All events top-levelOr per-event-type:
/github subscribe owner/repo --thread-pr --no-thread-issuesEdge Cases:
- Late joins: If bot wasn't present when PR opened, send as top-level
- Thread expiration: Clean up mappings after 30 days (configurable via
MESSAGE_MAPPING_EXPIRY_DAYS) - Cross-channel: Same PR subscribed in multiple channels = separate threads
- Branch-filtered PR: PR on non-matching branch → no anchor → child events filtered
- Base branch change: PR retargeted to matching branch → create anchor retroactively
Data Retention:
- Thread mappings expire after 30 days (configurable)
- Cleanup job runs daily to remove expired entries
- No impact on message history (Towns retains messages independently)
Limitations:
- Requires Towns Protocol thread support (verified working)
- Thread lookup adds ~1 DB query per event (indexed)
GitHub fires issue_comment for both issue and PR comments. Unlike pull_request_review events, the issue_comment payload doesn't include pull_request.base.ref.
Solution: In-memory PR Branch Cache
- Cache:
Map<string, { branch: string, updatedAt: Date }>keyed by${repo}#${prNumber} - Populated by:
onPullRequest,onPullRequestReview,onPullRequestReviewComment - Read by:
onIssueComment(cache hit → use cached branch, miss → fetch via API) - Timestamp ordering: Only update if incoming
pull_request.updated_at>= existing - Cleanup: Delete entry on PR close; lazy TTL on read (>24h old = treat as miss, refetch)
API Fallback:
- On cache miss or stale entry, fetch PR via installation Octokit
- Rate limit: 5,000 req/hour per installation (rarely hit since PR events populate cache)
Update the anchor message when PR metadata changes, keeping the thread anchor current without creating new top-level messages.
Fields That Trigger Anchor Refresh:
| Field | Source Event | Example Change |
|---|---|---|
| Title | pull_request.edited |
"feat: Add X" → "feat: Add X and Y" |
| State | pull_request.closed/reopened |
open → closed/merged |
| Labels | pull_request.labeled/unlabeled |
Added bug, removed wip |
| Assignees | pull_request.assigned/unassigned |
@alice assigned |
| Review State | pull_request_review.submitted |
→ approved / changes_requested |
| CI Status | check_suite.completed (optional) |
✅ passing / ❌ failing |
Implementation:
- Store anchor message ID in
messageMappingswithisAnchor: truecontext - Detect metadata changes via
handleAnchorEvent():pull_requestevents:edited,closed,reopened- Thread replies created for
closed/reopened, anchor updated in place
- Update message via
bot.editMessage(channelId, townsMessageId, newMessage) - Timestamp ordering: Use
githubUpdatedAtto prevent stale overwrites
Base Branch Change (Retarget) Handling:
When a PR's base branch changes from non-matching to matching:
- On
pull_request.edited, check if anchor exists viaMessageDeliveryService.anchorExists() - If no anchor AND branch now matches filter → create anchor retroactively (treat as late
opened) - If anchor exists → normal edit flow (update anchor)
- Pre-existing orphaned messages (sent before anchor) remain top-level; future events thread to new anchor
Anchor Card Format:
🔀 **PR #123** open → merged
feat: Add dark mode support
👤 @user 📊 +340 -120
🏷️ enhancement, ui
👥 @reviewer1, @reviewer2
✅ 2 approvals ⏳ CI passing
🔗 https://github.com/...
Interaction with Thread Replies:
- Thread replies continue to be added as new messages
- Only the anchor (first message) is updated
- Thread context preserved
Webhook Ordering:
- GitHub webhooks may arrive out of order
- Use
pull_request.updated_atto compare with storedgithubUpdatedAt - Only process edit if incoming timestamp >= stored timestamp
- Branch cache uses same ordering to prevent stale branch overwrites
Idempotency:
X-GitHub-Deliveryheader tracked inwebhookDeliveriestable- Same event delivered twice is skipped
- Anchor regeneration deterministic based on PR state in payload
See
CONTRIBUTING.mdfor complete environment variables reference.
Located at github-app-manifest.json. Key settings:
public: true- Anyone can installdefault_permissions: read-only access to reposdefault_events: repo-level events only (NOT installation events)redirect_url: Required field (points to/health)
Note: Installation/repository events are app-level and automatically enabled.
Core Infrastructure:
- GitHub App core with Octokit integration
- Webhook endpoint with signature verification
- Database schema with foreign key CASCADE
- CHECK constraints for data integrity
- Event processor routing to formatters
- Installation lifecycle management
- Idempotency tracking (database-backed)
- Type-safe handlers with consolidated types
- Dual-mode polling service (skips repos with app)
User Experience:
- Status command shows delivery mode per repo (⚡/⏱️)
- Private repo handling with OAuth validation
- Enhanced subscription messages with delivery mode info
- Installation notifications with automatic upgrade
- Case-insensitive unsubscribe
- Ephemeral OAuth URLs for security
- Pending subscriptions for private repos (auto-complete on install)
- OAuth token renewal - automatic refresh via
oauth.refreshToken()with 5-minute buffer - Granular unsubscribe -
/github unsubscribe owner/repo --events pr,issues - Subscription management -
/github subscribe owner/repo --events releasesadds to existing
Documentation:
- Comprehensive README reorganization
- New CONTRIBUTING.md with developer guide
- Complete environment variables documentation
- Condensed AGENTS.md reference
Event Organization (Priority 1):
- Thread-based grouping for related events (PR + commits + CI)
- PR Anchor Dynamic Updates - Update anchor on title/state changes
- PR Comment Branch Filtering - Apply branch filter to
issue_commentevents on PRs - Retroactive Anchor Creation - Create anchor when PR base branch changes to match filter
- Event summaries / digests to reduce channel noise
Mentions & Filters (Priority 2):
- GitHub Mentions → Towns Mentions - Convert GitHub @mentions to Towns @mentions when users are linked
- Label-Based Event Filtering - Filter subscriptions by PR/issue labels (
--labels bug,security)
New Commands:
/gh_stat owner/repo- Repository statistics and contributor leaderboard (see Future Improvements)/gh search- Search repos, issues, PRs/gh_release list owner/repo- List releases/github test owner/repo- Webhook diagnostics
- Subscribe to public repo without app → polls successfully
- Subscribe to public repo with app → receives webhooks
- Subscribe to private repo without app → shows error, requires app
- Subscribe to private repo with app → receives webhooks
- Install app to repo → channel notified, mode switches
- Uninstall app from repo → channel notified, falls back to polling
- Webhook signature verification rejects invalid requests
- Idempotency prevents duplicate event processing
- Failed webhooks allow retry (not marked as processed)
- Old webhook deliveries cleaned up (lt() comparison works)
Platform: Render (Web Service)
Configuration:
- Build:
bun install - Start:
bun run src/index.ts - Environment: Set all variables from
.env.sample - Database: Managed PostgreSQL (Render, Neon, Supabase, etc.) via
DATABASE_URL/DATABASE_SSL
Webhook URL: Must be publicly accessible at https://your-domain.com/github-webhook
- Signature verification: All GitHub webhooks verified via Octokit
- Raw body handling: Webhook route registered before body-parsing middleware
- Idempotency: Prevents replay attacks and duplicate processing
- Foreign key constraints: Ensures data integrity
- CHECK constraints: Validates data at database level
Webhook mode:
- Latency: < 1 second from GitHub event to Towns message
- Throughput: Limited by Octokit processing (no bottleneck observed)
- Database: O(1) idempotency check, O(1) installation lookup
Polling mode:
- Latency: Up to 5 minutes
- API usage: 1 request per repo per 5 minutes
- Database: O(n) where n = subscribed repos
Cleanup:
- Webhook deliveries: Periodic cleanup of records > 7 days old
- Uses
lt()comparison (noteq())
The /gh_pr and /gh_issue commands now reuse the same OAuth-first strategy as subscriptions so private repositories behave exactly like public ones if the caller has access.
- Handlers call the GitHub REST API with the bot token first (fast path for public repos).
- Errors are classified via
classifyApiError. - On
forbiddenornot_found, the bot looks up the requester's OAuth token (GitHubOAuthService.getUserOctokit):- Token present → retry the API call with the user's credentials.
- Token missing → send the editable OAuth prompt so the user can connect without rerunning the command.
- Token present but request still fails → tell the user they don't have access to the repo.
- Rate limiting, missing args, and validation errors have dedicated responses so the UX mirrors
/github subscribe.
src/handlers/gh-pr-handler.tsandsrc/handlers/gh-issue-handler.tscontain the fallback logic.src/utils/oauth-helpers.tsprovides the editable OAuth prompts.src/api/github-client.tsaccepts optional Octokit instances so all API helpers can run with either credential set.
- Health indicators: Show delivery latency and last event timestamp
- Installation status command:
/github app-statusto view all installations - Webhook delivery monitoring: Track success/failure rates
- Rate limit handling: Graceful degradation if API limits hit
- Scheduled digests: Daily/weekly summaries for quieter channels
- Multi-region deployment: Reduce latency for global users
A fun statistics command for repository insights and contributor leaderboards.
Possible Subcommands:
/gh_stat owner/repo # Overview: stars, forks, open issues/PRs, languages
/gh_stat owner/repo contributors # Top contributors by commits (last 30/90 days)
/gh_stat owner/repo activity # Commit frequency, PR merge rate, issue close rate
/gh_stat owner/repo leaderboard # Gamified: lines added/removed, PRs merged, issues closedData Sources (GitHub API):
GET /repos/{owner}/{repo}- Basic stats (stars, forks, watchers)GET /repos/{owner}/{repo}/contributors- Contributor list with commit countsGET /repos/{owner}/{repo}/stats/contributors- Detailed contribution stats (additions/deletions per week)GET /repos/{owner}/{repo}/stats/commit_activity- Weekly commit countsGET /repos/{owner}/{repo}/stats/participation- Owner vs all commit activityGET /repos/{owner}/{repo}/languages- Language breakdown
Leaderboard Ideas:
- 🏆 Top committers (last 30 days)
- 📈 Most lines added
- 🔥 Longest streak
- 🐛 Most issues closed
- 🔀 Most PRs merged
Caching Considerations:
- GitHub stats endpoints return 202 while computing (need retry logic)
- Cache results for 1 hour to avoid rate limits
- Consider storing in
repo_stats_cachetable
Display Format:
📊 **facebook/react** Statistics
⭐ 220k stars 🍴 45k forks 👀 6.5k watchers
📝 1,200 open issues 🔀 180 open PRs
🏆 **Top Contributors (30 days)**
1. @gaearon - 45 commits (+2,340 / -890)
2. @acdlite - 38 commits (+1,200 / -450)
3. @sebmarkbage - 22 commits (+890 / -320)
📈 Activity: 156 commits this week (↑12% vs last week)
Convert GitHub @mentions to Towns @mentions when the GitHub user is linked to a Towns account.
Detection Points:
- PR body / Issue body
- PR comments / Issue comments
- Review comments
- Review body
Mapping Storage:
Leverage existing github_user_tokens table which links towns_user_id ↔ github_login:
-- Already exists in github_user_tokens:
-- towns_user_id TEXT PRIMARY KEY
-- github_login TEXT -- from OAuth profileImplementation Flow:
Incoming comment/body text
↓
Regex extract @mentions: /@([a-zA-Z0-9_-]+)/g
↓
For each @mention:
Lookup github_login in github_user_tokens
├─ Found → Replace with Towns @mention: <@{towns_user_id}>
└─ Not found → Leave as plain text @username
↓
Send formatted message with Towns mentions array
Message Format:
await handler.sendMessage(channelId, message, {
mentions: [
{ userId: "0x123...", displayName: "alice" },
{ userId: "0x456...", displayName: "bob" },
],
});Edge Cases:
- GitHub bot accounts (e.g.,
@dependabot) - leave as plain text - Case sensitivity: GitHub usernames are case-insensitive
- Multiple mentions of same user - deduplicate in mentions array
Filter PR/Issue subscriptions by labels to reduce noise.
Syntax:
/github subscribe owner/repo --labels bug,security
/github subscribe owner/repo --labels "priority:high"
/github subscribe owner/repo --labels bug --events pr,issuesBehavior:
- Only deliver events where the PR/issue has at least one matching label
- Labels are checked at event delivery time (not subscription time)
- Applies to:
pr,issues,comments,reviews,review_comments - Does NOT apply to:
commits,ci,releases,branches,forks,stars
Database Changes (comments only):
-- Add to github_subscriptions table
-- label_filter TEXT -- NULL = no filter, else comma-separated labelsInteraction with Other Features:
- PR Anchor Updates: Label changes trigger anchor refresh, then check if subscription still matches
- Thread Replies: If PR loses matching label, existing thread continues (no orphaning)
- Branch Filters: Labels and branches are independent filters (AND logic)
UI Considerations:
/github statusshould show label filters- Warning if no events match filter in last 7 days
- GitHub App Documentation: https://docs.github.com/apps
- Octokit SDK: https://github.com/octokit
- Towns Protocol: https://towns.com
- Render Deployment: https://render.com