Plack::App::GitHub::WebHook - GitHub WebHook receiver as Plack application
use Plack::App::GitHub::WebHook; # Basic Usage Plack::App::GitHub::WebHook->new( hook => sub { my $payload = shift; ... }, events => ['pull'], # optional secret => $secret, # optional access => 'github', # default )->to_app; # Multiple hooks use IPC::Run3; Plack::App::GitHub::WebHook->new( hook => [ sub { $_[0]->{repository}{name} eq 'foo' }, sub { my ($payload, $event, $delivery, $logger) = @_; run3 \@cmd, undef, $logger->{info}, $logger->{error}; }, sub { ... }, # some more action ] )->to_app;
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.
payload
The response of a HTTP request to this application is one of:
If access was not granted (for instance because it did not origin from GitHub).
If the request was no HTTP POST.
If the payload was no well-formed JSON or the X-GitHub-Event header did not match configured events.
X-GitHub-Event
Otherwise, if the hook was called and returned a true value.
Otherwise, if the hook was called and returned a false value.
If a hook died with an exception, the error is returned as content body. Use configuration parameter safe to disable HTTP 500 errors.
safe
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 +.
code
+
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']).
['pull']
Object or function reference to hande logging events. An object must implement method log that is called with named arguments:
log
$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 => "204.232.175.64/27", allow => "192.30.252.0/22", deny => 'all' ]
and
access => [ allow => 'github', ... ]
is a shortcut for
access => [ allow => "204.232.175.64/27", allow => "192.30.252.0/22", ... ]
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.
eval { ... }
psgi.errors
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.
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"; Plack::App::GitHub::WebHook->new( 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}; 1; }, # sub { ...optional action after each pull... } ], )->to_app;
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.
/etc/apache2/sites-enabled/default
<Directory /var/www/webhooks> Options +ExecCGI -Indexes +SymLinksIfOwnerMatch AddHandler cgi-script .cgi </Directory>
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.
/var/www/webhooks
.cgi
#!/usr/bin/env plackup
www-data
GitHub WebHooks are documented at http://developer.github.com/webhooks/.
See GitHub::WebHook for a collection of handlers for typical tasks.
WWW::GitHub::PostReceiveHook uses Web::Simple to receive GitHub web hooks. A listener as exemplified by the module can also be created like this:
use Plack::App::GitHub::WebHook; use Plack::Builder; build { mount '/myProject' => Plack::App::GitHub::WebHook->new( hook => sub { my $payload = shift; } ); mount '/myOtherProject' => Plack::App::GitHub::WebHook->new( hook => sub { run3 \@cmd ... } ); };
Net::GitHub and Pithub provide access to GitHub APIs.
Github::Hooks::Receiver and App::GitHubWebhooks2Ikachan are alternative application that receive GitHub WebHooks.
Copyright Jakob Voss, 2014-
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
To install Plack::App::GitHub::WebHook, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Plack::App::GitHub::WebHook
CPAN shell
perl -MCPAN -e shell install Plack::App::GitHub::WebHook
For more information on module installation, please visit the detailed CPAN module installation guide.