Skip to content

Extract GitHub webhook route#367

Closed
TUPM96 wants to merge 1 commit into
ramimbo:mainfrom
TUPM96:codex/bounty-320-webhook-route
Closed

Extract GitHub webhook route#367
TUPM96 wants to merge 1 commit into
ramimbo:mainfrom
TUPM96:codex/bounty-320-webhook-route

Conversation

@TUPM96
Copy link
Copy Markdown
Contributor

@TUPM96 TUPM96 commented May 26, 2026

Refs #320

Summary

  • Extract the /webhooks/github FastAPI route wiring into app/webhooks/routes.py.
  • Move GitHub webhook header normalization and unauthorized-to-401 status mapping behind focused helpers while preserving the existing core handle_github_webhook behavior.
  • Add route-level regression tests for canonical GitHub headers, ignored signed events, persisted webhook status, and bad-signature 401 responses.
  • Keep current checks green by removing redundant ledger casts reported by mypy app and by sanitizing percent-decoded OAuth next paths covered by the existing OAuth regression test.

Complexity reduced

app/main.py no longer owns the GitHub webhook HTTP adapter details. The webhook domain handler stays in app/webhooks/github.py, while request header shaping, FastAPI route registration, and HTTP status response mapping live in a small webhook routes module with direct tests.

Tests

  • python -m pytest -q -> 337 passed
  • python -m pytest tests/test_webhook_routes.py tests/test_webhooks.py::test_github_signature_verification tests/test_webhooks.py::test_accepted_label_pays_bounty_once tests/test_wallet_api.py::test_github_login_stores_safe_default_for_backslash_next -q -> 5 passed
  • python -m ruff format --check app/main.py app/ledger/service.py app/webhooks/routes.py tests/test_webhook_routes.py
  • python -m ruff check app/main.py app/ledger/service.py app/webhooks/routes.py tests/test_webhook_routes.py
  • python -m mypy app

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced security by validating redirect paths more strictly, checking both raw and decoded forms for unsafe patterns.
  • Refactor

    • Reorganized GitHub webhook handling into a dedicated routing module with improved structure.
    • Removed unused typing imports from ledger service.
  • Tests

    • Added webhook route tests verifying signature validation and proper header handling.

Review Change Stack

Copilot AI review requested due to automatic review settings May 26, 2026 02:12
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The PR refactors GitHub webhook handling by extracting routing logic into a dedicated module with header normalization and status mapping, integrates it into the main application, hardens redirect path validation for URL-decoded inputs, adds endpoint tests, and removes unused SQLAlchemy typing support.

Changes

GitHub Webhook Routing and Main Application Integration

Layer / File(s) Summary
GitHub Webhook Route Abstractions
app/webhooks/routes.py
New module defines GITHUB_WEBHOOK_HEADERS constant, github_webhook_headers() for header normalization, github_webhook_status_code() for status mapping, and register_github_webhook_route() that registers an async POST endpoint accepting raw body, normalizing headers, and forwarding to handle_github_webhook with configuration.
Main Application Wiring and Redirect Hardening
app/main.py
Adds unquote import; updates _safe_next_path() to validate both raw and decoded paths against unsafe patterns; replaces inline webhook handler with register_github_webhook_route() call wiring database_url, webhook_secret, and accepted_labelers.

Webhook Tests and Type Cleanup

Layer / File(s) Summary
Webhook Route Tests
tests/test_webhook_routes.py
New test module with _signature() helper computing SHA-256 HMAC. Tests verify the endpoint normalizes headers and persists WebhookEvent with ignored status on valid signature, and returns 401 with unauthorized status on invalid X-Hub-Signature-256.
SQLAlchemy Type Cleanup
app/ledger/service.py
Removes unused cast and CursorResult imports; simplifies session.execute() assignments in pay_bounty() and close_bounty() by removing typed cast wrappers while preserving update logic and rowcount checks.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Extract GitHub webhook route' directly and clearly summarizes the main change: moving the webhook route from app/main.py to a new app/webhooks/routes.py module.
Description check ✅ Passed The description covers all required template sections: Summary with clear bullet points, Evidence of the problem addressed (Refs #320), and comprehensive Test Evidence including all specified checks (ruff format, ruff check, mypy, pytest).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR factors the GitHub webhook endpoint into a dedicated routes module, adds tests around webhook behavior, and hardens redirect “next” path validation.

Changes:

  • Added app/webhooks/routes.py with header normalization + status code mapping and registered it in create_app.
  • Added webhook route tests for ignored events and bad signatures.
  • Tightened _safe_next_path by validating both raw and percent-decoded forms; simplified SQLAlchemy typing around rowcount.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
tests/test_webhook_routes.py Adds coverage for GitHub webhook route behavior and persistence of webhook events.
app/webhooks/routes.py Introduces a dedicated module to register the GitHub webhook route and normalize headers.
app/main.py Switches to the new route registrar and strengthens _safe_next_path validation.
app/ledger/service.py Removes explicit CursorResult casts for update calls that rely on rowcount.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +26 to +28
"X-GitHub-Delivery": "delivery-route-ignored",
"X-GitHub-Event": "issues",
"X-Hub-Signature-256": _signature("secret", body),
Comment thread app/webhooks/routes.py
webhook_secret,
accepted_labelers,
)
return JSONResponse(result, status_code=github_webhook_status_code(result))
Comment thread app/main.py
Comment on lines 280 to 294
def _safe_next_path(next_path: str | None) -> str:
decoded_next_path = unquote(next_path) if next_path else ""
if (
not next_path
or not next_path.startswith("/")
or next_path.startswith("//")
or len(next_path) > 2048
or "\\" in next_path
or decoded_next_path.startswith("//")
or "\\" in decoded_next_path
or any(ord(char) < 32 or 127 <= ord(char) < 160 for char in next_path)
or any(ord(char) < 32 or 127 <= ord(char) < 160 for char in decoded_next_path)
):
return "/me"
return next_path
Comment thread app/ledger/service.py
Comment on lines +659 to 670
claimed = session.execute(
update(Bounty)
.where(Bounty.id == bounty.id, Bounty.awards_paid < Bounty.max_awards)
.values(
awards_paid=Bounty.awards_paid + 1,
status=case(
(Bounty.awards_paid + 1 >= Bounty.max_awards, "paid"),
else_="open",
),
)
)
if claimed.rowcount != 1:
@TUPM96
Copy link
Copy Markdown
Contributor Author

TUPM96 commented May 26, 2026

Superseded by the cleaned, scoped PR #368.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants