JenkinsでRailsアプリをテストする。

環境

Jenkinsのインストール

さくらのVPSにDebian squeezeをインストールする方法はこちら

$ wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add -
$ sudo vi /etc/apt/sources.list
deb http://pkg.jenkins-ci.org/debian binary
$ sudo apt-get update
$ sudo apt-get install jenkins

jenkinsユーザーが作成されて8080にjenkinsが立ち上がる。

nginxでのReverse proxyの設定

example.com:8080では味気無いのでci.example.comでアクセス出来るようにする。

$ sudo apt-get install nginx
$ sudo vi /etc/nginx/sites-available/ci.example.com
server {
  listen 80;
  server_name ci.example.com;
  location / { proxy_pass http://localhost:8080; }
}
$ ln -s /etc/nginx/sites-available/ci.example.com /etc/nginx/sites-enable/ci.example.com

rvmのインストール

rvmをsystem wideにインストールするのは大変なので単にjenkinsユーザーにインストールする。

$ sudo su - jenkins
$ bash < <( curl http://rvm.beginrescueend.com/releases/rvm-install-head )

大体nokogiriを使う段になって困るので下記をちゃんとインストールしておく。

$ rvm package install readline openssl zlib
$ rvm install 1.8.7 -C --with-openssl-dir=$HOME/.rvm/usr --with-readline-dir=$HOME/.rvm/usr --with-zlib-dir=$HOME/.rvm/usr

アプリ名でgemsetを作っておき、bundlerをインストールしておく。(jenkinsのタスクでやっても構わないと思う)

$ rvm use ruby-1.8.7-p334
$ rvm gemset create rails-example-app
$ rvm use ruby-1.8.7-p334@rails-example-app
$ gem install bundler

ユーザー認証

LAN内でない場合はユーザー認証をかける。"Manage Jenkins"の"Access Control"で"Secure Realm"を"Jenkins's own user database"にして"Allow users to sign up"にチェックを入れる。"Authorization"は"Matrix-based security"を選び、管理者ユーザーとなる予定のユーザー名を追加しておく。少し分かりづらいがここで設定した後、同じ名前でsign upをすれば良い。(間違って管理画面にはいれなくなってしまった場合はここを参考にしてリセットする。)

Configure System [Jenkins]

gitプラグインのインストール

大体が社内のgitやgithubから持ってくることになるのでgitプラグインをインストールする。"Manage Jenkins" > "Plugin Manager" > "Available"から"Git Plugin"を選択してインストールする。"Github Plugin"もついでに入れておくと良いかもしれない。

ビルドの設定

"Source Code Management"から"Git"を選択し、"Repositories" > "URL of repository"にリポジトリのURLを入れる。(githubのプライベートリポジトリを使っている場合はjenkinsユーザーで鍵を作り、githubの"Admin" > "Deploy Keys"から登録しておけばいい。)ビルドトリガーは好きに設定する。右側の"?"に親切な説明があるので迷わないだろう。ビルドはrailsなので"Execute shell"を選択する。rvmにはrvm-shellという指定の環境でshellを実行するうってつけのコマンドがあるのでそれをshebangに設定すればいい。

u2plus Config [Jenkins]

"Post-build Actions"で"E-mail Notification"を設定しておく。Debianデフォルトのexim4はメールが飛ぶようになってないのでいっそのことpostfixを入れた方が楽。

$ sudo apt-get install postfix

関連:Debian Squeeze install to Sakura VPS - komagata

Lokkathon #2 - Lokka

日時:02月27日 10:00 - 19:00
参加費:無料
開催場所:FJORD, LLC
東京都渋谷区本町1−36−11 ドエルヤマト203

Lokkaの本体、テーマ、プラグインをもくもくと開発するイベントです。

最低でも@komagataはいて作業しています。

遅刻早退自由です。

飲み物食べ物持ち込み自由です。

同じrouteを2回定義する件。

routesも要はRegexpとブロックの配列。でも最初に定義した方が呼ばれるって何か変。こういうものって後に追加した方が呼ばれる設計にする方が自然な気がする。

そこでもしかして同じroutesとか関係なくて、単にファイル内で上から下に書いてく時の優先順位がそのまんまこの設計に反映されてる的な感じなんじゃないかと思ってpassしてみた。

# public/plugin/lokka-unk/lib/lokka/unk.rb:
module Lokka
  module Unk
    def self.registered(app)
      app.get '/' do
        puts 'unk'
        pass
      end
    end
  end
end
% bundle exec ruby lokka.rb
unk
localhost - - [17/Feb/2011:10:41:33 JST] "GET / HTTP/1.1" 200 38183
- -> /

普通にページは表示されつつ、標準出力にunk。予想通りだ。これは擬似beforeみたいにも使えそう。(普通にbefore '/' do ... endした方が良いが。)

sinatraはルールがシンプルだから自由度が高いなあ。

何だか感慨深い。Java(僕のJava知識はJ2SE1.4で止まってる)とかその他の大きめなフレームワークだとChain-of-responsibilityパターンとかいってFilterChainみたいな感じで実装すると思うんだよね。こういうの。

sinatraはちっちゃいからRegexpとブロックのオブジェクトを配列にもってるから単にナメればいいでしょ?みたいな雰囲気。

Filterクラスを継承したクラスを作るとか面倒。Objective-Cだとブロック対応してない環境のためにワザワザdelegate作るのでヘッダも必要だしとか・・・だるいよなあ(ただの愚痴です・・・)

Lokkaのデータ構造についてざっくりとした図。クラス図もER図も厳密過ぎちゃうので何となくの図です。

  • Post(投稿)
    • いわゆる普通のブログの投稿。
  • Page(ページ)
    • ブログの投稿の様に時系列に並べたりしないもの。(aboutとかprofileとかfaqとか) 
  • Entry(エントリー)
    • PostとPageのこと。
  • Category(カテゴリー)
    • エントリーをツリー状に分類するためのもの。親カテゴリーの概念が有る。1エントリーに複数カテゴリーはつかない。
  • Tag(タグ)
    • エントリーをフラットな構造に分類するもの。1エントリーに複数つけられる。 
  • Comment(コメント)
    • Entryにはコメントが付けられます。
  • User(ユーザー)
    • その名の通り。
  • Site(サイト)
    • サイト自体の情報。タイトルとかテーマとか。
  • Option(オプション)
    • 必須ではないサイトの情報。プラグインなどから勝手に項目を増やせる。
  • Snippet(スニペット)
    • テンプレートの切れ端。URLは持っていない。

これらは全て共通のAPIを持っています。(DataMapper)

*あとで書く* (datamapper.orgを見て・・・)

Entry(PostやPage)はCommentやCategoryやTagやUserと相互に紐づいています。

> Post('about').category.name
mac
> Comment(1).entry.user.name
komagata

分かりにくい部分

PostとPageはEntryのサブクラス。所謂単一テーブル継承ですが持っている機能が同じです。

# entry.rb
class Entry
  ...
end

class Post < Entry; end
class Page < Entry; end

「おやおやこれはおかしいぞ、機能が同じならなんで特化するんだい?」

「名前が違います。」

今後特化した機能ができるかもね!要はLokkaの"ブログ"の部分がPostで"CMS"の部分がPageです。

(WordPressのカスタムタクソノミーはキモイので同じの作るとしてもclass Foo < Entry; endを動的に増やすとかで対応しようと思っています。)

テーマ作成者にとっては結構違います。Postはそのテンプレートに合わせたPostをLokkaがあらかじめテンプレートに渡してくれます。Pageの方はLokkaは何もしてくれないので自分で取りに行かなければいけません。

# entries.erb
<% @posts.each do |post| %>
  <%= post.title %>
<% end %>
# entry.erb
<%= @post.title %>

テンプレートに応じたPostが@postsや@postに最初から入っています。これは下記と同じ意味です。

# entries.erb
<% @posts = Post.recent(10) %>
<% @posts.each do |post| %>
  <%= post.title %>
<% end %>
# entry.erb
<% @post = Post.get_by_id_or_slug(params[:id_or_slug])
<%= @post.title %>

@postが最初から設定されてると上記のように書かなくて済むから便利!

何故同じrouteを2回定義したときの動作が気になったかというと、@miminashiさんのtweetが気になったからです。

Twitter / @miminashi: @komagata プラグインに書いたdoブロックの ...

Lokkaではまずプラグインが読み込まれるのでこの動きは好都合。

# public/plugin/lokka-unk/lib/lokka/unk.rb:
module Lokka
  module Unk
    def self.registered(app)
      app.get '/' do
        'unk'
      end
    end
  end
end
% curl http://localhost:9646
unk

プラグインから簡単にトップページを横取りできた。これはいいフリーダム。

# public/plugin/lokka-unk/lib/lokka/shit.rb:
module Lokka
  module Shit
    def self.registered(app)
      app.get %r{^/([0-9a-zA-Z-]+)$} do |slug|
        slug
      end
    end
  end
end
% curl http://localhost:9646/1
1
% curl http://localhost:9646/hoge
hoge

個別ページはこんな感じ。選ばれない方のメソッドは単に実行されないので副作用はありません。

SPAMで大変なことになっていたこのブログのコメントを@yagi_さんのAkismetプラグインをきっかけに整理してみた。とりあえず既存のSPAMを消すが大変だった。そもそもSPAM判定されようがされまいが、コメントがDBに投稿される数が多過ぎるので、

%form
  :javascript
    document.write('<input type="hidden" name="check" value="check" />')

と入れて、checkという値が無かったら404を返すようにした。SPAM BOTがJavascript を理解するかどうかというただそれだけだけど、今までは1日に数百個コメントSPAMが来るとかザラだったので大分違う気がします。何か他に簡単で99%ぐらいのスパムを弾ける方法があればいいなと思います。

Conditional Tagの追加

テーマを書くときに、今どのページなのか(カテゴリー別ページなのか、個別ページなのか等)を判断する関数をWordPressではConditional Tagと言うらしいです。@machidaさんからの要望で、個別ページ(entry?)の中でも投稿(Post)なのかページ(Page)なのかが一発で判断できるConditional Tagが欲しいと言っていたので追加しました。

%w[index search category tag yearly monthly daily post page entry entries].each do |name|                                                                     
  define_method("#{name}?") do    
    @theme_types.include?(name.to_sym)
  end
end

post?とpage?というhelperを追加しただけです。

日本語のtruncate

何文字以上だったら"xxxxxxxxxxxxx..."みたいに縮めるtruncateが日本語文字列の途中でぶった切ってしまうのを直しました。微妙に気になってたんです。

def truncate(text, options = {})                                                                                                                              
  options = {:length => 30, :ommision => '...'}.merge(options)
  if options[:length] < text.split(//u).size
    text.split(//u)[0, options[:length]].to_s + options[:ommision]
  else
    text
  end
end

split(//u)で配列にして文字数を判断するようにしました。何か場当たり的な感じで怒られそうだけど・・・。

LokkaにAkismetプラグイン来た!これで勝つる!@yagi_さんありがとうございます。

プラグインはみんなlokka-xxxxというgithubリポジトリになってるのでsubmoduleとして本体に取り込んでみました。

$ cd $LOKKA_ROOT
$ git submodule add git://github.com/yagitoshiro/lokka-akisment.git public/plugin/lokka-akismet

lokka本体をcloneした時は

$ git submodule init
$ git submodule update

git submodule initすると.git/configに下記みたいな設定が増える。

[submodule "public/plugin/lokka-akismet"]
        url = git://github.com/yagitoshiro/lokka-akismet.git

submoduleの中がmasterブランチになってないのでmasterにする。

$ cd public/plugin/lokka-akismet
$ git checkout master

しないといけないのは若干敷居が高いけど、リリースするzipには含めるからまあいいかなあ。

Lokkaのpluginを別リポジトリにするためにgit filter-branch --subdirectory-filterを使ってみました。

Pro Git - Pro Git 6.4 Git のさまざまなツール 歴史の書き換え

最強のオプション: filter-branch

歴史を書き換える方法がもうひとつあります。これは、大量のコミットの書き換えを機械的に行いたい場合 (メールアドレスを一括変更したりすべてのコミットからあるファイルを削除したりなど) に使うものです。そのためのコマンドが filter-branch です。

% git clone git@github.com:komagata/lokka.git
% cd lokka
% git filter-branch -f --subdirectory-filter public/plugin/lokka-hoptoad HEAD
Rewrite cedd2bc6856876f6a51cfbf2cf70f3ceb2b835a9 (1/1)
Ref 'refs/heads/master' was rewritten
% git st
# On branch master
# Your branch and 'origin/master' have diverged,
# and have 1 and 247 different commit(s) each, respectively.
#
nothing to commit (working directory clean)
% git log
commit 34ad15f4435ccc563f48778419fe2d48d7c58161
Author: Masaki KOMAGATA <komagata@gmail.com>
Date: Thu Dec 2 17:26:34 2010 +0900

added hoptoad plugin.
% ls
Gemfile lib views

「やだ、なにこれ・・・」

スゲー変わってる。filter-branch系は歴史を遡って書き換えちゃう系なので新たにcloneしといた方がホント安全っぽいですね。

後はgithubに新たに作ったlokka-hoptoadリポジトリにpushして完了。便利ですね!

Lokkaの標準でないプラグインを別リポジトリにして、lokka.orgにプラグインのページを作りました。

Plugins - Lokka

githubを"lokka"で検索して出てくるものを書いてみました。数が増えたらWordPressのPlugin Direcotryみたいにアップ出来るようにしたい。

他にもっと作るべきものあると思うんですが、ちょっとこのブログに必要だったのでLokkaのHoptoadプラグインとNew Relic RPMプラグインを作りました。

Hoptoad

Hoptoad : p0t Errors

Hoptoadは僕も大好きなshouldaやfactory_girlやpaperclip等のライブラリも作っているthoughtbotのサービス。アプリのエラーを管理するサービスです。最近iOSアプリのエラーにも対応してみたいです。

New Relic RPM

Heroku | docs-komagata-org | New Relic RPM

New Relic RPMはアプリのパフォーマンスを監視してくれるサービスです。

プラグイン

komagata [p0t] - Lokka

プラグイン一覧にHoptoadとNew Relic RPMが増えました。

komagata [p0t] - Lokka

New Relic RPMの方は特はHerokuの場合、Addonを入れただけでプラグインが存在すれば動きますが、HoptoadはAPIキーを上記の設定画面で入力する必要があります。

Herokuの場合、Addonはそれぞれ下記で簡単に追加できます。

% heroku addons:add hoptoad:basic
% heroku addons:add newrelic:bronze

Google Analytics, Hoptoad, New Relic RPMの三つをこのブログに入れてみてわかったことは、1日250PVぐらいしかないのに、レスポンスに1〜3秒ぐらいかかっていて、特に不具合は発生してないが、Herokuから"Backlog Too Deep"のエラーが130も起きてるとのメールが来ることです。

2dynosにしたら$36/monthかかるんですが、性能はあんまり期待出来そうにありません。