Skip to content

Webhook GitHub

Trigger: HTTP | State: stateless | Guarantee: at-most-once | Difficulty: intermediate

Overview

This recipe documents the GitHub webhook receiver in examples/apis-and-ingress/webhook_github/. It combines signature verification, JSON validation, event-type dispatch, and structured response payloads.

The endpoint is anonymous by design, but it is protected by HMAC-SHA256 verification using the X-Hub-Signature-256 header and a shared secret from GITHUB_WEBHOOK_SECRET.

It handles push, pull_request, and issues events with dedicated handlers, then falls back for unknown events.

When to Use

  • You need to receive GitHub events securely in Azure Functions.
  • You want a clean event-dispatch pattern for multiple webhook event types.
  • You need a reference for rejecting invalid signatures and malformed JSON.

When NOT to Use

  • You need a public endpoint without shared-secret validation.
  • You need to do long-running processing inline instead of acknowledging quickly and offloading work.
  • You need guaranteed ordered event processing without additional queueing or deduplication.

Architecture

flowchart LR
    github[GitHub Webhooks]
    route[/POST /api/github/webhook/]
    verify[Verify X-Hub-Signature-256]
    dispatch[Dispatch by X-GitHub-Event]
    handlers[push / pull_request / issues handlers]
    response[JSON response]

    github --> route --> verify --> dispatch --> handlers --> response

Behavior

sequenceDiagram
    participant GitHub
    participant Function as Azure Function

    GitHub->>Function: POST webhook + signature + event header
    Function->>Function: Load GITHUB_WEBHOOK_SECRET
    Function->>Function: Validate HMAC signature
    alt signature valid
        Function->>Function: Parse JSON and dispatch event
        Function-->>GitHub: 200 OK with event summary
    else signature invalid
        Function-->>GitHub: 401 Invalid or missing webhook signature
    end

Project Structure

examples/apis-and-ingress/webhook_github/
├── function_app.py
├── host.json
├── local.settings.json.example
├── pyproject.toml
└── README.md

Implementation

The function uses helper methods for consistency:

  • _json_response standardizes JSON serialization and content type.
  • _is_signature_valid verifies request authenticity.
  • _handle_push, _handle_pull_request, and _handle_issues map event payloads to outputs.

Signature verification from function_app.py:

def _is_signature_valid(signature_header: str | None, payload: bytes, secret: str) -> bool:
    if not signature_header or not signature_header.startswith("sha256="):
        return False
    expected = (
        "sha256="
        + hmac.new(
            key=secret.encode("utf-8"),
            msg=payload,
            digestmod=hashlib.sha256,
        ).hexdigest()
    )
    return hmac.compare_digest(expected, signature_header)

Main route and validation sequence:

@app.route(route="github/webhook", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
def github_webhook(req: func.HttpRequest) -> func.HttpResponse:
    secret = os.environ.get("GITHUB_WEBHOOK_SECRET", "")
    if not secret:
        return _json_response({"error": "Server is not configured with GITHUB_WEBHOOK_SECRET."}, status_code=500)

    signature = req.headers.get("X-Hub-Signature-256")
    raw_body = req.get_body()
    if not _is_signature_valid(signature, raw_body, secret):
        return _json_response({"error": "Invalid or missing webhook signature."}, status_code=401)

Event dispatch behavior:

event_type = req.headers.get("X-GitHub-Event", "")
if event_type == "push":
    return _json_response(_handle_push(payload), status_code=200)
if event_type == "pull_request":
    return _json_response(_handle_pull_request(payload), status_code=200)
if event_type == "issues":
    return _json_response(_handle_issues(payload), status_code=200)

Status code intent:

  • 500 when the server is misconfigured (missing secret).
  • 401 when signature is invalid or absent.
  • 400 when JSON is malformed or not an object.
  • 200 when event is accepted and processed, including unhandled event types with a generic message.

Run Locally

Prerequisites:

  • Python 3.10+
  • Azure Functions Core Tools v4
  • azure-functions dependency from pyproject.toml
  • GITHUB_WEBHOOK_SECRET configured in environment/local settings
  • Ability to compute test signatures for local webhook replay
cd examples/apis-and-ingress/webhook_github
pip install -e ".[dev]"
func start

Expected Output

POST /api/github/webhook with valid signature and X-GitHub-Event: push

-> 200 OK
{
  "event": "push",
  "repository": "octo/repo",
  "ref": "refs/heads/main",
  "commits": 1,
  "message": "Processed push with 1 commit(s)."
}

POST with invalid signature -> 401
{"error":"Invalid or missing webhook signature."}

Production Considerations

  • Scaling: Webhooks can burst during high repo activity; keep handlers fast and offload heavy work to queues.
  • Retries: GitHub retries failed deliveries, so return non-2xx only for true failures and ensure deterministic handling.
  • Idempotency: Use X-GitHub-Delivery or payload identifiers to deduplicate repeated deliveries.
  • Observability: Log event type, delivery ID, repository, and outcome with structured fields for triage.
  • Security: Rotate webhook secrets, enforce TLS end to end, and never trust payloads before signature validation.