feat(api): GitHub webhooks for phase-1 title-cache invalidation (phase 2 PR A)#288
Conversation
… timingSafeEqual absent in Node tests)
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
uploads-api | e12a6af | Commit Preview URL Branch Preview URL |
Jul 20 2026, 04:52 PM |
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (2)
🚫 Excluded labels (none allowed) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a GitHub App webhook endpoint with HMAC verification, event-specific cache invalidation, route-order protection, tolerant error handling, and Vitest coverage for helpers, routing, mounting, and fake KV deletion. ChangesGitHub webhook handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant GitHub
participant githubWebhook
participant verifySignature
participant handleWebhook
participant GITHUB_CACHE
GitHub->>githubWebhook: POST signed webhook
githubWebhook->>verifySignature: verify raw body
verifySignature-->>githubWebhook: valid signature
githubWebhook->>handleWebhook: event type and JSON payload
handleWebhook->>GITHUB_CACHE: delete derived keys
githubWebhook-->>GitHub: 204 response
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
CodeRabbit (@coderabbitai) review |
✅ Action performedReview finished.
|
…generated types are git-ignored)
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/index.ts`:
- Around line 118-121: Move the githubWebhook mount in apps/api/src/index.ts
into the workspace-scoped API surface so workspaceAuth applies. In
apps/api/src/routes/github-webhook.ts, resolve the workspace and invoke
writeRateLimit before any cache invalidation. Update
apps/api/src/routes/github-webhook-route.test.ts to cover workspace scoping and
rate limiting, and replace the bypass-auth assertion in
apps/api/src/github-webhook-mount.test.ts with the required scoped-mount
contract.
In `@apps/api/src/routes/github-webhook.ts`:
- Around line 17-21: Update the webhook handler around verifySignature to
replace the bodyless 503 and 401 returns with the appropriate AppError
subclasses, allowing the standard error response path to produce the required
error contract. Use a configuration-related error for a missing secret and one
generic unauthorized error for every signature verification failure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f00aab9a-d83a-4a8f-9b1f-42bb816c686d
📒 Files selected for processing (7)
apps/api/src/github-webhook-mount.test.tsapps/api/src/github-webhook.test.tsapps/api/src/github-webhook.tsapps/api/src/index.tsapps/api/src/routes/github-webhook-route.test.tsapps/api/src/routes/github-webhook.tsapps/api/test/fake-kv.ts
| if (!secret) return c.body(null, 503); // honest "not configured"; never pretend to process. | ||
|
|
||
| const raw = await c.req.text(); | ||
| if (!(await verifySignature(raw, c.req.header("x-hub-signature-256") ?? null, secret))) { | ||
| return c.body(null, 401); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use the standard error response path.
The bodyless 503 and 401 responses bypass the required { error: { code, type, message, details? } } contract. Throw the appropriate AppError subclasses and retain one generic 401 error for every signature failure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/routes/github-webhook.ts` around lines 17 - 21, Update the
webhook handler around verifySignature to replace the bodyless 503 and 401
returns with the appropriate AppError subclasses, allowing the standard error
response path to produce the required error contract. Use a
configuration-related error for a missing secret and one generic unauthorized
error for every signature verification failure.
Source: Coding guidelines
Phase 2 (PR A) of the GitHub App integration — webhooks that keep the phase-1 title cache fresh. Advances #284; phase 1 shipped in #282.
What
An HMAC-verified GitHub webhook endpoint on the api worker that invalidates the phase-1
GITHUB_CACHEentries the moment upstream state changes, instead of waiting out the TTL (up to 24h for a settled item, 1h for installs).POST /v1/github/webhook— unauthenticated except for theX-Hub-Signature-256HMAC (verified over the raw body, constant-time, before any JSON parse). Mounted ahead of the/v1/:workspace/*guard (that pattern matches this path, so registration order is load-bearing — locked in by a test).src/github-webhook.ts— signature verification + an event dispatcher doing targeted KV deletes only (no prefix scans):installation→ dropghtok:{id}+ghinst:{repo}per repo (token-only when the payload carries no repo list).installation_repositories→ dropghinst:{repo}for added + removed.issues/pull_request(any action) → dropghref:{owner/repo#n}, so both renames and state changes propagate.ping/ unknown events → no-op; malformed payloads never throw.Response contract (degrade-safe on every path)
503when the secret is unset,401on missing/bad signature,204on a verified delivery (includingping, unknown events, and an unparseable-after-HMAC body). A rejecting KV delete is swallowed (Promise.allSettled) so it never surfaces as a 500 — the entry just self-heals on its TTL. No webhook failure can affect uploads, the rail, or the titles endpoint.Scope / decisions
GITHUB_APP_WEBHOOK_SECRET— matches the phase-1GITHUB_APP_*convention and the secret already provisioned on the live worker (already declared by generatedworker-configuration.d.ts, so intentionally not redeclared inenv.d.ts).issues:write/pull_requests:writeremain the next step (PR B, GitHub App phase 2: webhooks + bot-owned attachments comment #284).Tests
Plain vitest + in-process fakes, mirroring phase 1: signature accept/tamper/wrong-secret/malformed-header; each event type deletes exactly the right keys; unknown events no-op; malformed payloads never throw; a rejecting-
deleteKV still resolves; route503/401/204incl. unparseable-but-signed body; mount-ordering (secret-absent POST reaches the handler →503, not the guard). Full api suite green (659 tests).Operator steps before this is live (all degrade-safe until done)
The endpoint returns
503/401and the cache behaves exactly as today until:GITHUB_APP_WEBHOOK_SECRETis set on the api worker — already provisioned; verify rather than overwrite.https://api.uploads.sh/v1/github/webhook, set the same secret, and subscribe to theinstallation,installation_repositories,issues, andpull_requestevents.pingdelivery returns204.Summary by CodeRabbit
New Features
Tests