追記
2017/10/02 ライブラリを HTTP::Tiny
に変更しました。
2017/03/09 Slack APIの変更に追随するのとあわせて、文面を更新しました。
2014/07/21 スクリプトを全面的に書き直し、文面を追記/修正しました。
目的
以前、「gitの共有リポジトリにpushしたらChatworkにメッセージを通知する」という記事を書いたのですが、最近になって、チャットツールにSlackも使うようになったので、同じ目的でgit push時にどちらにも通知を送ることにします。
bashスクリプトをperlで書きなおした理由
SlackのAPIはPOSTパラメーターにJSONを埋め込むという形式になっています。Chatworkだけに通知を送っていた時はbash
を使っていたのですが、bash
ではJSONの取扱いに難があったので、perl
を使って全面的に書き換えることにしました。perl
を採用した最も大きな理由は私が一番慣れている言語ということですが、多くのLinuxシステムに標準でインストールされているということも理由の一つです。
Slack Incoming WebHooks
Slackには多くの魅力がありますが、Integrationのための機能が非常に充実しているということが最も大きいと個人的には思います。既存の有名サービスから簡単に通知を送ることができるのはもちろん、Incoming Webhooksを用いて、自作ツールの中にSlackへの通知機能を簡単に実装することができます。今回はこのIncoming Webhooksを用いてgitリポジトリから通知を送ることにします。
詳細は下記の公式APIドキュメントをお読みください。
post-update
おおよその内容の理解は前述の記事を読んでいただきたいと思います。
作成されたhooksスクリプトは下記のとおりです。不必要なほど重厚な作りになってしまいましたね。
ムシャクシャしてやった。後悔はしていない。
#!/usr/bin/env perl
package MyApp::Notifier::Base;
use warnings;
use HTTP::Tiny;
sub new {
my $class = shift;
my $self = { @_ };
bless( $self, $class );
return $self;
}
sub send {
my $self = shift;
my ( $repos, $branch, $message ) = @_;
my ( $url, $post_data, $headers ) = $self->_prepare_request_parameters( $repos, $branch, $message );
my $ua = HTTP::Tiny->new();
my $response = $ua->post($url, {
'headers' => $headers,
'content' => $post_data,
} );
unless ($response->{success}) {
warn $response->{reason};
return 0;
}
return 1;
}
sub _prepare_request_parameters {
die "[ERROR] Have to override this method";
}
package MyApp::Notifier::Slack;
use warnings;
use JSON::PP;
@MyApp::Notifier::Slack::ISA = qw(MyApp::Notifier::Base);
sub _prepare_request_parameters {
my $self = shift;
my ( $repos, $branch, $message ) = @_;
my $endpoint = "https://hooks.slack.com/services";
my $path_token = $self->{path_token};
my $channel = $self->{channel};
my $username = 'Git hooks';
my $icon = ':jack_o_lantern:';
my $text = "(\`${repos}\`) [\`${branch}\`]";
if ( $branch eq 'master' ) {
$text = ":bell: PUSHED INTO \`${branch}\` BRANCH!!!\n${text} ${message}";
}
elsif ( ! -f "refs/heads/$branch" ) {
$text = "${text} Branch was deleted.";
}
else {
$text = "${text} ${message}";
}
my $headers = {
'Content-Type' => 'application/x-www-form-urlencoded',
};
my $post_data = "payload=" . encode_json +{
channel => $channel,
username => $username,
icon_emoji => $icon,
text => $text,
};
return "$endpoint/$path_token", $post_data, $headers;
}
package MyApp::Notifier::Chatwork;
use warnings;
@MyApp::Notifier::Chatwork::ISA = qw(MyApp::Notifier::Base);
sub _prepare_request_parameters {
my $self = shift;
my ( $repos, $branch, $message ) = @_;
my $endpoint = "https://api.chatwork.com/v1/rooms";
my $token = $self->{token};
my $room_id = $self->{room_id};
my $url = "$endpoint/$room_id/messages";
my $text = "(${repos}) [${branch}] ${message}";
if ( $branch eq 'master') {
$text = "[info][title]PUSHED INTO \`master\` BRANCH!!![/title]${text}[/info]";
}
my $post_data = 'body=' . $text;
my $headers = {
'X-ChatWorkToken' => $token,
};
return $url, $post_data, $headers;
}
package MyApp::Git;
use warnings;
use FindBin qw/$Bin/;
use File::Basename qw/dirname/;
use Encode;
use Encode::Guess qw/shift-jis euc-jp 7bit-jis/;
sub new {
my $class = shift;
my $self = { @_ };
bless( $self, $class );
return $self;
}
sub repos_name {
my $self = shift;
unless ( defined $self->{repos_name} ) {
my $dir = (split( /\//, dirname( $Bin ) ))[-1];
$dir =~s/\.git$//;
$self->{repos_name} = $dir;
}
return $self->{repos_name};
}
sub branch_name {
my $self = shift;
unless ( defined $self->{branch_name} ) {
$self->{branch_name} = `git rev-parse --symbolic --abbrev-ref $_[0] 2> /dev/null`;
chomp $self->{branch_name};
}
return $self->{branch_name};
}
sub commit_message {
my $self = shift;
my $branch = shift;
unless ( defined $self->{commit_message} ) {
$self->{commit_message} = `git log -1 --pretty=format:"%h - %an : %s" $branch 2> /dev/null`;
}
return unless $self->{commit_message};
my $decoder = Encode::Guess->guess( $self->{commit_message} );
if ( ref $decoder ) {
$self->{commit_message} = $decoder->decode( $self->{commit_message} );
}
return $self->{commit_message};
}
package main;
use warnings;
use JSON::PP;
scalar @ARGV == 0 && die "[ERROR] Arguments not enough";
my $git = MyApp::Git->new();
my $repos = $git->repos_name();
my $branch = $git->branch_name( @ARGV );
my $message = $git->commit_message( $branch );
my %notifiers = (
'slack' => sub { new MyApp::Notifier::Slack(@_) },
'chatwork' => sub { new MyApp::Notifier::Chaktwork(@_) },
);
my $config = _load_config();
while ( my ( $name, $generator ) = each %notifiers ) {
my $notifier = $generator->( %{$config->{$name}} );
$notifier->send( $repos, $branch, $message )
|| warn "[WARN] Faild to send message.";
}
exit;
sub _load_config {
return decode_json( join '', <DATA> );
}
1; # magic number
__DATA__
{
"slack": {
"path_token": "<CHANGE_THIS>",
"channel": "<CHANGE_THIS>"
},
"chatwork": {
"token": "<CHANGE_THIS>",
"room_id": "<CHANGE_THIS>"
}
}
最新のバージョンはgistに置いてありますので、興味のある方は是非どうぞ。
下記の環境で動作確認しています。
OS: macOS Sierra 10.12.3(16D32)
Perl: v5.20.3 (via plenv)
できるだけ標準な環境で動作するように考えましたが、Perlのライブラリを1つだけインストールする必要があります。
$ cpanm IO::Socket::SSL
通知対象としたいすべてのリモートリポジトリの $REPOSITORY_ROOT/hooks/
に post-update
という名前で前述のファイルを置きます。実行権限をつけることも忘れないでください。
$ vi post-update # ファイル末尾の設定を変更
$ chmod 755 post-update
$ scp ./post-update $REMOTE_HOST:$REPOSITORY_ROOT/hooks/
$REPOSITORY_ROOT
と$REMOTE_HOST
はそれぞれ、リポジトリの最上位ディレクトリとリモートリポジトリを置いてあるホストの名前を示す任意の値とします。
次項以降でこのスクリプトの内容を解説します。
構成
1つのファイルですが、4つのpackageに分かれています。このスクリプトの実行時に呼び出されるのは main
パッケージです。その他はgitのコミットの情報などを取得するモジュールやチャットに送信するモジュールです。送信する機能は MyApp::Notifier::Base
に記載されており、このモジュールを継承して通知先サービスごとの送信モジュールを作成します。これらのモジュールは、リクエストパラメーターを作成するサブルーチン_prepare_request_parameters
を上書きします。
設定値
編集すべき設定値は、スクリプトの最後、__DATA__
の下に記載されたJSONにまとめられています。ご自分の環境に合わせて編集してください。
{
"slack": {
"path_token": "hogehoge/hogehoge/hogehoge",
"channel": "general"
},
"chatwork": {
"token": "hogehogehoge",
"room_id": "12345678"
}
}
上記の値に何を設定すべきかは、各サービスの設定画面から取得してください。Slackの場合は、前述のIntegrationsのページでIncoming Webhooksを選択し、画面の指示通りに簡単に作成することができます。
投稿テキストの作成
投稿するメッセージは、前述の _prepare_request_parameters()
と commit_message()
というメソッドの中で構築しています。
commit_message()
の中では git log
コマンドを使ってコミットメッセージを取得しています。コミットメッセージは指定された(pushされた)ブランチの最後のものを利用します。取得する際にはメッセージのエンコーディングを判定してデコードしておきます。
sub commit_message {
my $self = shift;
my $branch = shift;
unless ( defined $self->{commit_message} ) {
$self->{commit_message} = `git log -1 --pretty=format:"%h - %an : %s" $branch 2> /dev/null`;
}
return unless $self->{commit_message};
my $decoder = Encode::Guess->guess( $self->{commit_message} );
if ( ref $decoder ) {
$self->{commit_message} = $decoder->decode( $self->{commit_message} );
}
return $self->{commit_message};
}
次に _prepare_request_parameters()
の中で、プラットフォーム合わせた形式にに加工します。Slackでは次のようにしています。
my $text = "(\`${repo}\`) [\`${branch}\`] ${message}";
if ( $branch eq 'master' ) {
$text = ":bell: PUSHED INTO \`master\` BRANCH!!!\n$text";
}
リポジトリ名やブランチ名を入れることをおすすめします。 master
ブランチにpushしたときには、より強めの注意喚起が必要でしょうから、さらに加工するようにしています。
Slackに関してはさらに、 username
と icon_emoji
は任意に設定することができます。 icon_emoji
はEmoji Cheet Sheetに記載されているものが使える他、下記のページ(参考URL)で自分で作成したアイコンを利用することもできます。
- Custom Emoji - https://my.slack.com/customize/emoji
icon_emoji
は本文中でも使えます。この辺りは自由に創造性を持って編集していただければと思います。ちなみに、Qiitaでも表示できますよ
投稿
実際に投稿してみます。
ちゃんと動作したでしょうか?