Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Github Webhook Laravel Package

swop/github-webhook

PHP package for receiving and handling GitHub webhooks. Validate webhook signatures, route GitHub events to your own handlers, and access payload data cleanly so you can react to pushes, pull requests, issues, and other repository activity.

Deep Wiki
Context7

Getting Started

Install

composer require swop/github-webhook

The package is framework-agnostic — it does not ship a Laravel service provider, so there is nothing to publish or register. It is a small library: a Config object, a GitHubWebHook dispatcher, an Event value object, a signature validator, and a set of exceptions.

First use case: a push webhook that hits a Laravel route

  1. Add the secret from your GitHub repo settings (Settings → Webhooks → Secret) to config/services.php:
'github' => [
    'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'),
],
  1. Create a controller action. The only thing that matters is that you hand the library the raw request body and the signature headers:
use Swop\GitHubWebHook\Config\Config;
use Swop\GitHubWebHook\GitHubWebHook;

public function __invoke(Request $request)
{
    $config = new Config();
    $config->setSecret(config('services.github.webhook_secret'));
    $config->addListener('push', new \App\Webhooks\PushListener());

    $webhook = new GitHubWebHook($config);

    try {
        $webhook->handle(/* headers + raw body per README */);
    } catch (\Swop\GitHubWebHook\Exception\InvalidSignatureException $e) {
        abort(401);
    }

    return response('ok');
}
  1. Register the route outside the web middleware group (or add it to VerifyCsrfToken::$except) — GitHub will never send a CSRF token:
Route::post('/webhooks/github', GitHubWebhookController::class)
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

Where to look first

  • The README’s single code sample is essentially the entire public API surface; read it before anything else.
  • Config — secret, listeners, and any global options.
  • Event — how the payload is exposed (raw array vs. typed getters). This determines how much you write yourself.
  • Exception\* — these are your HTTP status codes; map them once in the controller.

Implementation Patterns

One listener class per event, resolved from the container

Rather than closures, implement a small ListenerInterface (or just a handle(Event $event) method) per event and register them in a dedicated GitHubWebhookServiceProvider or a config array:

$config->addListener('push', app(PushListener::class));
$config->addListener('pull_request', app(PullRequestListener::class));

This keeps the controller a three-line adapter and makes listeners unit-testable in isolation.

Listener → job, not listener → work

GitHub waits roughly 10–25 seconds for a 2xx. Treat the listener as a router: validate, extract identifiers, dispatch a queued job, return. Never clone, deploy, or call slow APIs synchronously inside the listener — GitHub will retry the delivery and you will do the work twice.

Idempotency on X-GitHub-Delivery

GitHub retries failed deliveries with the same delivery UUID. Persist it (cache or a webhook_deliveries table with a unique index) and short-circuit duplicates:

$id = $request->header('X-GitHub-Delivery');
if (! Cache::add("gh:{$id}", true, now()->addDay())) {
    return response('duplicate', 200);
}

This is the single highest-value addition on top of the library, because the library does not do it for you.

Verify before you parse

Call the signature validation path first and let InvalidSignatureException short-circuit. If you log the payload before validating, you are writing attacker-controlled data into your logs.

A catch-all listener for observability

Register a wildcard handler (if the package’s Config supports *, otherwise register your known event types) that increments a counter or logs X-GitHub-Event + X-GitHub-Delivery. This is how you discover you are silently dropping pull_request_review events.

Testing

  • Build fixture payloads as JSON files (tests/Fixtures/push.json) — the library takes the raw body, so tests are just file_get_contents.
  • Compute the signature in the test the same way GitHub does:
$sig = 'sha1=' . hash_hmac('sha1', $body, $secret);
  • Assert three cases: valid signature → 200 + job dispatched; tampered body → 401; unknown event type → 200 (not 500).

Local development

Use smee.io or ngrok to forward to http://localhost:8000/webhooks/github. Set the secret identically in GitHub and in .env, otherwise you will spend an hour debugging a “signature mismatch” that is really a config mismatch. Replay a real delivery from Recent Deliveries → Redeliver instead of hand-crafting payloads.

Gotchas and Tips

The raw body is the signature input — never the parsed array

$request->all() or json_decode(...) re-serialised will not match the bytes GitHub signed. Always use $request->getContent() (Laravel) or file_get_contents('php://input'). This is the cause of ~90% of “invalid signature” issues with this class of library.

Header format is sha1=<hex>, not bare hex

GitHub sends X-Hub-Signature (sha1=…) and, for newer hooks, X-Hub-Signature-256 (sha256=…). If you pass the header through unchanged, make sure you are not stripping the prefix — and confirm which algorithm the library expects. Prefer configuring SHA-256 if the package version supports it.

If setSecret() is omitted, validation is skipped

Many implementations treat a missing secret as “no validation.” That means a misconfigured env var turns your endpoint into an unauthenticated public write API. Fail loudly: assert the secret is non-empty at boot.

The ping event fires the moment you save the hook

GitHub sends a ping payload immediately. If you have no listener for it, make sure the library/your controller still returns 200 — a 4xx/5xx on ping shows a red “last delivery failed” badge and confuses everyone during setup.

Exceptions are not HTTP responses

The library throws on invalid payloads/signatures. Unless your framework maps them, an uncaught exception becomes a 500, GitHub marks the delivery failed, and it retries. Wrap handle() once and translate: InvalidSignatureException → 401, malformed payload → 400, everything else → 500 with an error log.

This package’s last release is from late 2020

Practical implications: verify it installs and passes tests on your PHP version (8.1+), watch for dynamic-property deprecations and loose null handling, and pin it in composer.json. Budget half a day to vendor a fix if you hit an incompatibility — the codebase is tiny and readable, so forking a class is realistic. Long term, the two behaviours you actually need (HMAC verification + json_decode) are ~20 lines; keep that in mind if maintenance becomes a problem.

It parses; it does not model

Do not expect typed event classes for every GitHub event. You will get an Event with the type and an array payload, and you will build your own value objects per event. That is fine — but it means schema drift is on you. Add isset()/null checks on fields you rely on (pull_request.merged, ref, after) because payload shapes change and the library will not warn you.

Content type matters

Modern GitHub hooks are application/json. Older configurations use application/x-www-form-urlencoded with a payload field. Check what your hook is set to; the library likely assumes JSON, and a form-encoded hook will decode to an empty array (or throw) with no obvious error message.

Extension points

Config is the seam: custom listeners, the secret, and any event-type mapping live there. If you need to validate against multiple secrets (zero-downtime rotation), the cleanest path is a thin wrapper that tries the new secret, falls back to the old, then calls handle() — or a fork of the validator. Do not scatter hash_hmac calls through your app.

Do not rely on IP allowlisting

GitHub’s published hook IP ranges change and were deprecated in favour of the X-GitHub-Hook-IP header in 2023. Signature verification is the real control; treat IP checks as defence in depth only.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings