GitHub WebHook receiver as Plack application
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Plack::App::GitHub::WebHook - GitHub WebHook receiver as Plack application


Build Status Coverage Status Kwalitee Score


use Plack::App::GitHub::WebHook;

# Basic Usage
    hook => sub {
        my $payload = shift;
    events => ['pull'],  # optional
    secret => $secret,   # optional
    access => 'github',  # default

# Multiple hooks
use IPC::Run3;
    hook => [
        sub { $_[0]->{repository}{name} eq 'foo' },
        sub {
            my ($payload, $event, $delivery, $logger) = @_;
            run3 \@cmd, undef, $logger->{info}, $logger->{error}; 
        sub { ...  }, # some more action


This PSGI application receives HTTP POST requests with body parameter payload set to a JSON object. The default use case is to receive GitHub WebHooks, for instance PushEvents.

The response of a HTTP request to this application is one of:

  • HTTP 403 Forbidden

    If access was not granted (for instance because it did not origin from GitHub).

  • HTTP 405 Method Not Allowed

    If the request was no HTTP POST.

  • HTTP 400 Bad Request

    If the payload was no well-formed JSON or the X-GitHub-Event header did not match configured events.

  • HTTP 200 OK

    Otherwise, if the hook was called and returned a true value.

  • HTTP 202 Accepted

    Otherwise, if the hook was called and returned a false value.

  • HTTP 500 Internal Server Error

    If a hook died with an exception, the error is returned as content body. Use configuration parameter safe to disable HTTP 500 errors.

This module requires at least Perl 5.10.


  • hook

    A hook can be any of a code reference, an object instance with method code, a class name, or a class name mapped to parameters. You can also pass a list of hooks as array reference. Class names are prepended by GitHub::WebHook unless prepended by +.

      hook => sub {
          my ($payload, $event, $delivery, $logger) = @_;
      hook => 'Foo'
      hook => '+GitHub::WebHook::Foo'
      hook => GitHub::WebHook::Foo->new
      hook => { Bar => [ doz => 'baz' ] }
      hook => GitHub::WebHook::Bar->new( doz => 'baz' )

    Each hook gets passed the encoded payload, the type of webhook event, a unique delivery ID, and a logger object. If the hook returns a true value, the next the hook is called or HTTP status code 200 is returned. If a hook returns a false value (or if no hook was given), HTTP status code 202 is returned immediately. Information can be passed from one hook to the next by modifying the payload.

  • events

    A list of event types expected to be send with the X-GitHub-Event header (e.g. ['pull']).

  • logger

    Object or function reference to hande logging events. An object must implement method log that is called with named arguments:

      $logger->log( level => $level, message => $message );

    For instance Log::Dispatch can be used as logger this way. A function reference is called with hash reference arguments:

      $logger->({ level => $level, message => $message });

    By default PSGI::Extensions is used as logger (if set).

  • secret

    Secret token set at GitHub Webhook setting to validate payload. See for details. Requires Plack::Middleware::HubSignature.

  • access

    Access restrictions, as passed to Plack::Middleware::Access. A recent list of official GitHub WebHook IPs is vailable at The default value

      access => 'github'

    is a shortcut for these official IP ranges

      access => [
          allow => "",
          allow => "",
          deny  => 'all'


      access => [
          allow => 'github',

    is a shortcut for

      access => [
          allow => "",
          allow => "",

    To disable access control via IP ranges use any of

      access => 'all'
      access => []
  • safe

    Wrap all hooks in eval { ... } blocks to catch exceptions. Error messages are send to the PSGI error stream psgi.errors. A dying hook in safe mode is equivalent to a hook that returns a false value, so it will result in a HTTP 202 response.

    If you want errors to result in a HTTP 500 response, don't use this option but wrap the application in an eval block such as this:

      sub {
          eval { $app->(@_) } || do {
              my $msg = $@ || 'Server Error';
              [ 500, [ 'Content-Length' => length $msg ], [ $msg ] ];


Each hook is passed a logger object to facilitate logging to PSGI::Extensions. The logger provides logging methods for each log level and a general log method:

sub sample_hook {
    my ($payload, $event, $delivery, $log) = @_;

    $log->debug('message');  $log->{debug}->('message');
    $log->info('message');   $log->{info}->('message');
    $log->warn('message');   $log->{warn}->('message');
    $log->error('message');  $log->{error}->('message');
    $log->fatal('message');  $log->{fatal}->('message');

    $log->log( warn => 'message' );

    run3 \@system_command, undef,
        $log->{info},   # STDOUT to log level info
        $log->{error};  # STDERR to log level error

Trailing newlines on log messages are trimmed.


Synchronize with a GitHub repository

The following application automatically pulls the master branch of a GitHub repository into a local working directory.

use Plack::App::GitHub::WebHook;
use IPC::Run3;

my $branch = "master";
my $work_tree = "/some/path";

    events => ['push','ping'],
    hook => [
        sub { 
            my ($payload, $event, $delivery, $log) = @_;
            $log->info("$event $delivery");
            $event eq 'ping' or $payload->{ref} eq "refs/heads/$branch";
        sub {
            my ($payload, $event, $delivery, $log) = @_;
            my $origin = $payload->{repository}->{clone_url} 
                       or die "missing clone_url\n";
            my $cmd;
            if ( -d "$work_tree/.git") {
                chdir $work_tree;
                $cmd = ['git','pull',$origin,$branch];
            } else {
                $cmd = ['git','clone',$origin,'-b',$branch,$work_tree];
            $log->info(join ' ', '$', @$cmd);
            run3 $cmd, undef, $log->{debug}, $log->{warn};
        # sub { ...optional action after each pull... } 

See GitHub::WebHook::Clone for before copy and pasting this code.


Many deployment methods exist. An easy option might be to use Apache webserver with mod_cgi and Plack::Handler::CGI. First install Apache, Plack and Plack::App::GitHub::WebHook:

sudo apt-get install apache2
sudo apt-get install cpanminus libplack-perl
sudo cpanm Plack::App::GitHub::WebHook

Then add this section to /etc/apache2/sites-enabled/default (or another host configuration) and restart Apache.

<Directory /var/www/webhooks>
   Options +ExecCGI -Indexes +SymLinksIfOwnerMatch
   AddHandler cgi-script .cgi

You can now put webhook applications in directory /var/www/webhooks as long as they are executable, have file extension .cgi and shebang line #!/usr/bin/env plackup. You might further want to run webhooks scripts as another user instead of www-data by using Apache module SuExec.



Copyright Jakob Voss, 2014-

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.