++ed by:
2 non-PAUSE users
Author image Jakob Voß
and 4 contributors


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


    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.


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.



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.


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


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 token set at GitHub Webhook setting to validate payload. See https://developer.github.com/webhooks/securing/ for details. Requires Plack::Middleware::HubSignature.


Access restrictions, as passed to Plack::Middleware::Access. A recent list of official GitHub WebHook IPs is vailable at https://api.github.com/meta. 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 => []

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.