spatie/laravel-github-webhooks
Handle GitHub webhooks in Laravel: verify signatures, log valid calls, and dispatch jobs/events per webhook type. Includes a GitHubWebhookCall model to access payloads and queueable handlers for event-driven integrations.
Installation:
composer require spatie/laravel-github-webhooks
php artisan vendor:publish --provider="Spatie\GitHubWebhooks\GitHubWebhooksServiceProvider" --tag="github-webhooks-config"
php artisan vendor:publish --provider="Spatie\GitHubWebhooks\GitHubWebhooksServiceProvider" --tag="github-webhooks-migrations"
php artisan migrate
Configure GitHub Webhook:
GITHUB_WEBHOOK_SECRET in .env (found in GitHub repo settings).routes/api.php:
Route::githubWebhooks('github-webhook');
First Use Case:
Create a job for a specific event (e.g., issues.opened) in app/Jobs/GitHubWebhooks/HandleIssueOpenedWebhookJob.php:
namespace App\Jobs\GitHubWebhooks;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Spatie\GitHubWebhooks\Models\GitHubWebhookCall;
class HandleIssueOpenedWebhookJob implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public GitHubWebhookCall $webhookCall) {}
public function handle()
{
$issue = $this->webhookCall->payload('issue');
// Logic to handle new issue
}
}
Register Job:
Update config/github-webhooks.php:
'jobs' => [
'issues.opened' => \App\Jobs\GitHubWebhooks\HandleIssueOpenedWebhookJob::class,
],
Job-Based Handling:
pull_request.opened, push).* in config to handle all events with a fallback job.'jobs' => [
'push' => \App\Jobs\GitHubWebhooks\HandlePushWebhook::class,
'*' => \App\Jobs\GitHubWebhooks\HandleAllWebhooks::class,
],
Event-Based Handling:
github-webhooks::<event> events in EventServiceProvider:
protected $listen = [
'github-webhooks::issues.opened' => [
\App\Listeners\NotifyIssueOpened::class,
],
];
Payload Access:
GitHubWebhookCall methods to access data:
$login = $webhookCall->payload('issue.user.login');
$action = $webhookCall->eventActionName(); // e.g., "issues.opened"
Queue Optimization:
class HandleWebhookJob implements ShouldQueue { ... }
Local Development:
config/github-webhooks.php:
'verify_signature' => env('APP_ENV') !== 'local',
Testing:
GitHubWebhookCall factory or seeders to simulate webhooks:
$webhook = GitHubWebhookCall::factory()->create(['event' => 'issues.opened']);
Logging and Debugging:
github_webhook_calls table for payloads and exceptions.try {
$this->handle();
} catch (\Exception $e) {
\Log::error('Webhook failed', ['error' => $e->getMessage()]);
}
GitHub Actions:
push to main):
on:
repository_dispatch:
types: [deploy]
Third-Party Services:
public function handle()
{
Http::post('https://api.slack.com/...', ['text' => 'New issue: ' . $this->webhookCall->payload('issue.title')]);
}
Webhook Retries:
ProcessGitHubWebhookJob to retry failed calls:
dispatch(new ProcessGitHubWebhookJob(GitHubWebhookCall::find($id)));
Signature Verification:
GITHUB_WEBHOOK_SECRET or misconfiguring it in GitHub repo settings..env and GitHub’s webhook configuration.APP_DEBUG to see WebhookFailed exceptions.Route Misconfiguration:
routes/api.php matches GitHub’s webhook URL exactly.Route::get('/health', fn() => 'OK'); to test connectivity.Payload Parsing:
payload('key.subkey') for nested data.$title = $webhookCall->payload('issue.title'); // Correct
$title = $webhookCall->payload('issue.title'); // Works
$title = $webhookCall->payload('issue.title'); // Fails if 'title' is missing
Queue Timeouts:
CSRF Exemptions:
VerifyCsrfToken::$except if using web.php.except array in app/Http/Middleware/VerifyCsrfToken.php.Log Webhook Payloads:
protected $listen = [
'github-webhooks::*' => [
\App\Listeners\LogWebhookPayload::class,
],
];
class LogWebhookPayload
{
public function handle(GitHubWebhookCall $webhookCall)
{
\Log::info('Webhook received', ['event' => $webhookCall->event, 'payload' => $webhookCall->payload]);
}
}
Inspect Database:
github_webhook_calls table for failed calls:
SELECT * FROM github_webhook_calls WHERE exception IS NOT NULL;
Test Locally:
ngrok http 8000
https://<ngrok-subdomain>.ngrok.io/github-webhook.ProcessEverythingWebhookProfile:
ProcessEverythingWebhookProfile to filter events:
class CustomWebhookProfile extends ProcessEverythingWebhookProfile
{
public function shouldProcess(GitHubWebhookCall $webhookCall): bool
{
return $webhookCall->event === 'issues.opened';
}
}
Update config:
'profile' => \App\Profiles\CustomWebhookProfile::class,
Pruning:
prune_webhook_calls_after_days).app/Console/Kernel.php:
$schedule->command('model:prune', [
'--model' => [\Spatie\GitHubWebhooks\Models\GitHubWebhookCall::class],
])->daily();
GitHubWebhookCall to add fields or logic:
class CustomGitHubWebhookCall extends GitHubWebhookCall
{
public function getIssueUrl(): string
{
return 'https://github.com/' . $this->payload('repository.full_name') . '/issues/' . $this->payload('issue.number');
}
}
Update config:
'model' => \App\Models\CustomGitHub
How can I help you explore Laravel packages today?