You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The live-update path: a GitHub webhook endpoint that reacts to push events and enqueues dashboard_sync jobs for every binding whose tracked ref was actually affected by the push.
Scope:
Handler. New src/docverse/handlers/webhooks/github.py exposing /docverse/webhooks/github, using safir.github's GitHubWebhookRouter (or the equivalent registration API) for HMAC verification and event dispatch. Do not hand-roll HMAC or payload parsing. Mount the router in src/docverse/main.py.
Service. New src/docverse/services/dashboard_templates/push_processor.py — PushEventProcessor.process(event):
Find all bindings matching (github_owner, github_repo, github_ref).
tests/handlers/webhooks_github_test.py — signed POST with a fabricated push payload lands the expected dashboard_sync jobs in MockArqQueue; unsigned/wrong-signature → 401; unrelated event types → 200 no-op.
tests/services/dashboard_templates/push_processor_test.py — (a) binding whose root_path intersects the push is synced; binding whose root_path does not intersect is skipped. (b) Truncated payload triggers compare-API fallback (exercised via the mock_github fixture from DM-54689: GitHub App client + tree fetcher + respx fake-GitHub fixture #236). (c) Push to a ref with no bindings is a no-op.
Periodic reconciliation / drift-audit job (also out-of-scope in the PRD).
Acceptance criteria
Webhook endpoint rejects unsigned or mis-signed payloads with 401/403; accepts correctly-signed payloads.
PushEventProcessor filters bindings by root_path intersection with the push's changed paths; only surviving bindings get a dashboard_sync job.
Truncation branch: simulated truncated payload falls back to the compare API and the processor still correctly identifies affected bindings (test covers both branches).
Metadata
tickets/DM-54689-github-webhookWhat to build
The live-update path: a GitHub webhook endpoint that reacts to
pushevents and enqueuesdashboard_syncjobs for every binding whose tracked ref was actually affected by the push.Scope:
src/docverse/handlers/webhooks/github.pyexposing/docverse/webhooks/github, usingsafir.github'sGitHubWebhookRouter(or the equivalent registration API) for HMAC verification and event dispatch. Do not hand-roll HMAC or payload parsing. Mount the router insrc/docverse/main.py.src/docverse/services/dashboard_templates/push_processor.py—PushEventProcessor.process(event):(github_owner, github_repo, github_ref).GET /repos/{owner}/{repo}/compare/{before}...{after}via the helper from DM-54689: GitHub App client + tree fetcher + respx fake-GitHub fixture #236'schanged_paths.py.root_path.dashboard_syncjob per surviving binding viaDashboardSyncEnqueuer(from DM-54689: dashboard_sync worker + DASHBOARD_TEMPLATE lock + syncer + fanout + force-sync endpoint #237).Tests:
tests/handlers/webhooks_github_test.py— signed POST with a fabricated push payload lands the expecteddashboard_syncjobs inMockArqQueue; unsigned/wrong-signature → 401; unrelated event types → 200 no-op.tests/services/dashboard_templates/push_processor_test.py— (a) binding whoseroot_pathintersects the push is synced; binding whoseroot_pathdoes not intersect is skipped. (b) Truncated payload triggers compare-API fallback (exercised via themock_githubfixture from DM-54689: GitHub App client + tree fetcher + respx fake-GitHub fixture #236). (c) Push to a ref with no bindings is a no-op.Out of scope:
Acceptance criteria
PushEventProcessorfilters bindings byroot_pathintersection with the push's changed paths; only surviving bindings get adashboard_syncjob.uv run --only-group=nox nox -s typingandtestpass.User stories addressed
From PRD #232:
root_pathis a no-op)root_path, not repo root)safir.github's webhook router, not hand-rolled HMAC)