NAME
Plack::App::GitHub::WebHook - GitHub WebHook receiver as Plack application
SYNOPSIS
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;
DESCRIPTION
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.
CONFIGURATION
- 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 https://developer.github.com/webhooks/securing/ 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 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 => []
- safe
-
Wrap all hooks in
eval { ... }
blocks to catch exceptions. Error messages are send to the PSGI error streampsgi.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 ] ]; }; };
LOGGING
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.
EXAMPLES
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";
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.
DEPLOYMENT
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
</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.
SEE ALSO
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 AND LICENSE
Copyright Jakob Voss, 2014-
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.