Githubの各種イベント通知をPubSubHubbubで受け取るの巻

tl;dr - 通常のHookではなくPubSubHubbubのほうのHookを使えばGithubのすべてのイベントをひとつのWebHookで受け取ることができる。

Github の WebHook ではレポジトリの更新しか受け取れず、issue とかも受け取れたら便利なのになーと思いつつ API ドキュメントを見てみると Hook を API から登録したりすることができるようになっていた。 だがこれは所詮は Web から登録できる Hook をいじるもので、このリストにあるものしか登録できない。 また、それぞれの Hook について登録できるイベントはリストで定義されている物に制限されているようで、たとえば WebHook だと push イベントしか設定することはできないみたい。(API 経由でも設定できなかった)

一方、それとは別に用意されている PubSubHubbub の Hook をつかうと、どうやら一つの WebHook URL に任意のイベントを送ることができるようだ。

やり方は簡単で、https://github.com/typester/p5-UVpush イベントを http://unknownplace.org/webhook で受け取りたいとすると、

$ curl -u typester -i \
  https://api.github.com/hub \
  -F "hub.mode=subscribe" \
  -F "hub.topic=https://github.com/typester/p5-UV/events/push" \
  -F "hub.callback=http://unknownplace.org/webhook"

というような感じで API をたたけば登録完了。これで指定した URL に対してイベントが送られてくるようになる。 他のイベントも受け取りたいというときは、これを受け取りたいイベント分繰り返せば良い。

Hook に送られてくるのは JSON だが、デフォルトだと通常の WebHook と同じように payload={json} みたいな形で送られてくる。 ドキュメントによれば、-H "Accept: application/json" を上のリクエストにつけるか、イベントタイプを https://github.com/typester/p5-UV/events/push.json みたいに .json を加えてやるかすると、生の JSON が POST されるようになるらしい。が、僕は今までの WebHook スクリプトを流用したのでこれはためしていない。

登録した Hook の一覧は普通の Hook API で、

$ curl -u typester -i https://api.github.com/repos/typester/p5-UV/hooks

このようにするとで取得可能っぽい。僕の場合は WebHook と同じ分類で(イベントだけが複数ある)リストに載っていたが、生 JSON を受け取るように設定していた場合どのように載るのかは不明。

WebHook スクリプトは以下のような通知を受け取ったら IRC に投げる、みたいのを使っている。

#!/usr/bin/env perl

use strict;
use warnings;
use utf8;

use Twiggy::Server;
use Getopt::Long;
use File::chdir;
use Encode;

use AnyEvent;
use AnyEvent::IRC::Client;
use Log::Minimal;

use Plack;
use Plack::Request;
use JSON::XS;

my $conf = {
    irc => {
        server   => 'irc.example.com',
        port     => 6667,
        ssl      => 1,
        channel  => '#example-project',
        nick     => 'github',
    },

    commit_hook => sub {
        my ($branch) = @_;

        if ($branch eq 'develop') {
            local $CWD = '/path/to/project';
            system 'git', qw!--git-dir=.git pull!;
            system 'curl', 'http://127.0.0.1:8080/jenkins-ci/job/project/build';
        }
    },
};

GetOptions(
    \my %option,
    qw/host=s port=i/,
);
$option{host} ||= '0.0.0.0';
$option{port} ||= 5000;

my $cv = AnyEvent->condvar;

my $irc = AnyEvent::IRC::Client->new;
$irc->enable_ssl if $conf->{irc}{ssl};

my $connector = sub {
    $irc->connect($conf->{irc}{server}, $conf->{irc}{port}, {
        nick => $conf->{irc}{nick},
        $conf->{irc}{password} ? (password => $conf->{irc}{password}) : (),
    });
};

$irc->reg_cb(
    registered => sub {
        my ($irc) = @_;
        $irc->send_srv( JOIN => $conf->{irc}{channel} );
    },

    disconnect => sub {
        warnf 'IRC Disconnected: %s', $_[1];
        $connector->();
    },

    debug_send => sub {
        my ($irc, $command, @params) = @_;
        debugf '> %s %s', $command, join ' ', @params;
    },
    debug_recvd => sub {
        my ($irc, $msg) = @_;
        debugf '< %s', $msg;
    },
);
$connector->();

my $app = sub {
    my $req = Plack::Request->new(shift);

    if ($req->method eq 'POST' and my $payload = $req->param('payload')) {
        my $json    = decode_json($payload);
        my $channel = $conf->{irc}{channel};

        if ($json->{commits}) {
            # push event
            (my $branch = $json->{ref}) =~ s!refs/heads/!!;

            if ($channel) {
                for my $commit (@{ $json->{commits} }) {
                    $irc->send_srv(
                        NOTICE => $channel,
                        encode_utf8 sprintf '%s committed to %s "%s" %s',
                        $commit->{author}{username} || $commit->{author}{name}, $branch,
                        $commit->{message}, $commit->{url},
                    );
                }

                my $hook = $conf->{commit_hook};
                $hook->($branch) if $hook;
            }
        }
        elsif (my $issue = $json->{issue}) {
            my $sender = $json->{sender};

            if (my $comment = $json->{comment}) {
                # issue comment event
                my ($body) = $comment->{body} =~ /^(.*\S.*)$/m;

                $irc->send_srv(
                    NOTICE => $channel,
                    encode_utf8 sprintf '%s commented issue #%d %s %s',
                    $sender->{login}, $issue->{number}, $body || '', $issue->{html_url},
                );
            }
            else {
                # issues event
                $irc->send_srv(
                    NOTICE => $channel,
                    encode_utf8 sprintf '%s %s issue #%d %s %s',
                    $sender->{login}, $json->{action}, $issue->{number}, $issue->{title}, $issue->{html_url},
                );
            }
        }

        return [200, [], ['']];
    }

    return [403, [], ['Forbidden']];
};

AnyEvent::post_detect {
    if ($AnyEvent::MODEL eq 'AnyEvent::Impl::EV') {
        no warnings 'once';
        $EV::DIED = sub { $cv->croak($@) };
    }
};

my $server = Twiggy::Server->new(%option);
$server->register_service($app);

$cv->recv;

現在 push, issues, issue-comment イベントを受け取ってる。 issues とれるとやっぱりだいぶ良い感じ。 あと pullreq 多用しているプロジェクトだとそのイベントもあるので通知させるようにすると捗りそう。

by typester / at 2012-08-08T10:55:00 / github · perl / Comment

ローカルのgitレポジトリをgistにアップするgist.pl

とか作ってみた。

gist.pl

git レポジトリのルートで実行するとそのレポジトリの内容を gist にポストし、レポジトリの remote に投稿した gist を追加します。

いろいろ手抜きなので下に注意事項を書いておきます。

  1. 正確にはレポジトリの中身ではなくてカレントディレクトリ以下のすべてのファイルを投稿しちゃう (.git以下はのぞく)
  2. git config で github.user と github.token を登録しておく必要がある
  3. remote 名 は origin きめうち
  4. プライベートgistとかしらない

などという感じです。特に1は注意。必要に応じてverupする感じで。

gisty つかってたんですが、僕は何か作るときとりあえず git init するので、gisty だと gist に投稿したらそれ以降違う場所のレポジトリを使うという設計なのでちょっと面倒だったんですね。

by typester / at 2009-01-27T20:24:00 / git · github / Comment