Skip to content

feat(api): GitHub webhooks for phase-1 title-cache invalidation (phase 2 PR A)#288

Merged
Zach Dunn (zachdunn) merged 8 commits into
mainfrom
claude/github-integration-next-825278
Jul 20, 2026
Merged

feat(api): GitHub webhooks for phase-1 title-cache invalidation (phase 2 PR A)#288
Zach Dunn (zachdunn) merged 8 commits into
mainfrom
claude/github-integration-next-825278

Conversation

@zachdunn

@zachdunn Zach Dunn (zachdunn) commented Jul 20, 2026

Copy link
Copy Markdown
Member

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_CACHE entries 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 the X-Hub-Signature-256 HMAC (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 → drop ghtok:{id} + ghinst:{repo} per repo (token-only when the payload carries no repo list).
    • installation_repositories → drop ghinst:{repo} for added + removed.
    • issues / pull_request (any action) → drop ghref:{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)

503 when the secret is unset, 401 on missing/bad signature, 204 on a verified delivery (including ping, 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

  • KV-only invalidation, no D1 — every event names the specific repo/installation, so no listing is ever required.
  • Secret name GITHUB_APP_WEBHOOK_SECRET — matches the phase-1 GITHUB_APP_* convention and the secret already provisioned on the live worker (already declared by generated worker-configuration.d.ts, so intentionally not redeclared in env.d.ts).
  • Synchronous, not queue-backed — the workload is idempotent and self-healing, so a DLQ has little to catch. A queue is the right foundation only if later phases add durable, non-self-healing event handling; tracked in Evaluate CF Queue + DLQ for GitHub webhook ingestion #287.
  • No permission upgrade, no CLI changes. Bot-owned attachments comment + issues:write/pull_requests:write remain the next step (PR B, GitHub App phase 2: webhooks + bot-owned attachments comment #284).
  • No changeset (api-only; changeset-ignored).

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-delete KV still resolves; route 503/401/204 incl. 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/401 and the cache behaves exactly as today until:

  1. GITHUB_APP_WEBHOOK_SECRET is set on the api worker — already provisioned; verify rather than overwrite.
  2. On the "uploads.sh" GitHub App: set the webhook URL to https://api.uploads.sh/v1/github/webhook, set the same secret, and subscribe to the installation, installation_repositories, issues, and pull_request events.
  3. Confirm the first ping delivery returns 204.

Summary by CodeRabbit

  • New Features

    • Added a GitHub webhook endpoint that validates signed requests and processes supported event notifications.
    • Automatically refreshes affected cached GitHub installation, repository, issue, and pull request data.
    • Returns clear responses for missing configuration, invalid signatures, malformed payloads, and successful requests.
  • Tests

    • Added comprehensive coverage for webhook routing, signature validation, event handling, cache invalidation, and failure resilience.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 20, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (2)
  • coderabbit:review
  • review
🚫 Excluded labels (none allowed) (1)
  • wip

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74ffcf31-6f0f-4521-b22b-ef578b12a2d5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

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

Changes

GitHub webhook handling

Layer / File(s) Summary
Verification and cache invalidation
apps/api/src/github-webhook.ts, apps/api/test/fake-kv.ts, apps/api/src/github-webhook.test.ts
Adds HMAC-SHA256 signature verification and event-based cache-key deletion with tolerant handling of malformed payloads and KV failures.
Route mounting and request handling
apps/api/src/routes/github-webhook.ts, apps/api/src/index.ts, apps/api/src/routes/github-webhook-route.test.ts, apps/api/src/github-webhook-mount.test.ts
Adds POST /v1/github/webhook, verifies signatures before JSON parsing, returns defined status codes, forwards valid payloads, and mounts the route before workspace authentication.

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
Loading

Suggested labels: coderabbit:review

Poem

A rabbit checks the webhook beat,
With HMAC paws and timing neat.
Cache keys hop out of sight,
Bad JSON fades without a fight,
And routes bloom in order right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding GitHub webhook support to invalidate phase-1 title cache entries.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/github-integration-next-825278

Comment @coderabbitai help to get the list of available commands.

@zachdunn

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ec8b92d and 5069391.

📒 Files selected for processing (7)
  • apps/api/src/github-webhook-mount.test.ts
  • apps/api/src/github-webhook.test.ts
  • apps/api/src/github-webhook.ts
  • apps/api/src/index.ts
  • apps/api/src/routes/github-webhook-route.test.ts
  • apps/api/src/routes/github-webhook.ts
  • apps/api/test/fake-kv.ts

Comment thread apps/api/src/index.ts
Comment thread apps/api/src/routes/github-webhook.ts Outdated
Comment on lines +17 to +21
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant