Ruby Freaks Lounge

第36回Redmineプラグイン開発(2)

前回のおさらいと今回の概要

前回第34回では初めてのプラグイン作成ということで、サーバの稼動状態を管理するServerStatusというプラグインを作りました。Redmineのプラグインは非常に柔軟性が高く、ビルトインの機能とは無関係のものを作ることも可能です。

今回はそこから一歩進んで、Redmineのチケットやアクティビティ(日本語環境では「活動⁠⁠。Tracでいうタイムライン)と連動したプラグインを作ります。作成するのはデプロイをRedmine上から行い、実行結果や履歴を参照することのできるContinuousDeploymentというプラグインです。

なお、今回はプラグインを作成するにあたって、以下のようなシナリオを用意してみました。

シナリオ

あるWebサービスの開発現場では、プロジェクト管理にRedmineを、デプロイにcapistranoというツールを使用しています。

タスク管理にはRedmineのチケットを使い、デプロイはcapistranoにより1コマンドで可能でしたが、メンバーからは以下のような不満が挙がっていました。

  1. デプロイ前にリリース予定のチケットをディレクターがチェックするルールだったが、未チェックのままリリースされる事が多々あった。
  2. デプロイの履歴が管理されておらず、過去にどんなリリースがあったかを遡る際に非常に手間がかかっていた。

そこで、Redmineからデプロイ前のチケットのステータスチェック、デプロイの実行、そしてデプロイ後の履歴の作成という一連の処理を実行することのできるプラグインを作成することにしました。デプロイは「デプロイボタン」を用意し、これを押すだけでデプロイが行われるイメージです。

プラグインの仕様

まず1番の問題についてですが、チケットのワークフローは、新規 → 進行中 → 実装完了 → ⁠ディレクターがチェックして)チェック完了 → 解決、といった流れで進んでいるとします。問題になっているのは、実装完了のステータスになっているチケットが残っている状態でデプロイすることです。そのため、実装完了となっているチケットがある場合にはデプロイできないようにします。

また、2番の問題についてはデプロイの履歴を一覧で確認できる画面を用意するほか、デプロイ後にはアクティビティにに表示されるようにしましょう。

画面イメージは以下のようになります。

図1 デプロイ管理画面
図1 デプロイ管理画面
図2 アクティビティ
図2 アクティビティ

雛形と初期設定

では早速作っていきましょう。まずは雛形を作成します。

$ ruby script/generate redmine_plugin ContinuousDeployment

続いて初期設定です。

init.rb
Redmine::Plugin.register :redmine_continuous_deployment do
  name 'Redmine Continuous Deployment plugin'
  author 'Takayuki Kyowa'
  description 'Redmine Continuous Deployment plugin'
  version '0.0.1'

  menu :project_menu, :deploy_history,  # ①
       { :controller => 'deployments', :action => 'index' }, 
       :caption => "デプロイ", :last => true, 
       :param => :project_id # ②
  permission :view_deploy_history, { :deployments => [ :index ] }, :public => true # ③
end

今回はメニューの追加先をプロジェクトメニューにしています(①⁠⁠。プロジェクトメニューはプロジェクトの配下となる為、プロジェクトIDが必須になります。:paramオプションで:project_idを指定するとURLにプロジェクトIDが付与されます(②)。また、プロジェクトメニューはデフォルトで非表示になっているため、permissionメソッドで表示/非表示を制御します(③)。

permissionメソッドの引数解説
  permission 
    :deploy_histories,              # パーミッションの名前。パーミッション全体で一意でなければならない。
    { :deployments => [ :index ] }, # 制御対象の画面をあらわすハッシュ。menuメソッドで指定したコントローラ / アクションを指定する。
    :public => true                 # trueにすると常に表示される。

permissionメソッドで注意しなければならないのは、publicオプションをtrueにしている場合、ログインユーザにのみプロジェクトを表示するように設定していたとしても、URLを直打ちすると見ることができてしまう、と言うことです。そのため、一般公開を目的としたプラグインを作成する場合には指定しない方が無難でしょう。指定しない場合は権限の管理画面(/roles/report)からロール毎に表示/非表示を設定します。

図3 権限管理画面(/roles/report)
図3 権限管理画面(/roles/report)

マイグレーション

次にマイグレーションを作成します。今回必要になるのはデプロイの履歴を表現するモデルです。名前はDeployHistoryとしましょう。

$ ruby script/generate redmine_plugin_model ContinuousDeployment DeployHistory
$ mv db/migrate/20100314093908_create_deploy_histories.rb db/migrate/001_create_deploy_histories.rb

DeployHistoryモデルは関連モデルに以下の3つを持たせます。

  1. デプロイ時のプロジェクト
  2. デプロイを実施したユーザ(ボタンを押した人)
  3. デプロイ時のチェンジセット

他にもデプロイ開始時刻や終了時刻、実行結果などの属性を持たせます。

001_create_deploy_histories.rb
class CreateDeployHistories < ActiveRecord::Migration
  def self.up
    create_table :deploy_histories do |t|
      t.column :project_id, :integer    # デプロイ時のプロジェクト
      t.column :deployer_id, :integer   # デプロイしたユーザ
      t.column :changeset_id, :integer  # デプロイ時のチェンジセット(リビジョン)
      t.column :started_on, :datetime   # 開始時間
      t.column :deployed_on, :datetime  # 終了時間
      t.column :return_code, :integer   # デプロイの実行結果。コマンドの終了ステータスを保存する。
      t.column :log, :text              # デプロイのログ
    end
  end

  def self.down
    drop_table :deploy_histories
  end
end

一通り作成したらmigrateを実行します。

$ RAILS_ENV=production rake db:migrate_plugins

モデルの実装

続いてモデルです。関連と終了ステータスの名前だけを書いたシンプルなコードです。

deploy_history.rb
class DeployHistory < ActiveRecord::Base
  belongs_to :project
  belongs_to :deployer, :class_name => 'User', :foreign_key => 'deployer_id'
  belongs_to :changeset

  def result_name
    self.return_code == 0 ? "成功" : "失敗"
  end
end

デプロイ履歴画面の実装

さて、まずはデプロイの履歴画面を実装していきましょう。始めにコントローラとビューのファイルを作成します。コントローラの名前はdeployments、履歴画面はindexアクションとしました。

$ ruby script/generate redmine_plugin_controller ContinuousDeployment deployments index

コントローラから実装していきます。

class DeploymentsController < ApplicationController

  before_filter :find_project # ①

  def index
    @histories = DeployHistory.find(:all)
  end

  private

  def find_project
    @project = Project.find(params[:project_id])
  end

プロジェクト配下では@projectインスタンス変数が必須になる為、URLで渡されたプロジェクトIDからProjectモデルのインスタンスを取得し、@projectインスタンス変数にセットしています。基本的にすべての画面で必要になるため、before_filterで記述しています⁠。

次にビューの実装です。デプロイボタンがあること以外は前回とほぼ同様のコードになっています。デプロイを実施するアクション名はdeployとしました。

<style type="text/css">
  table.deploy-histories {
    width: 80%;
  }
  tr.deploy-history {
    text-align:center;
  }
</style>

<h2>デプロイ</h2>

<div style='margin-bottom: 10px'>
  <%= button_to "デプロイ!", { :action => "deploy", :project_id => params[:project_id] }, :confirm => "デプロイします。よろしいですか?" %>
</div>

<table class="list deploy-histories">
  <thead><tr>
    <th>完了日時</th>
    <th>開始日時</th>
    <th>結果</th>
    <th>担当者</th>
    <th>リビジョン</th>
    <th>詳細</th>
  </tr></thead>
  <tbody>
<%- @histories.each do |history| -%>
  <tr class="<%= cycle("odd", "even") %> deploy-history">
    <td><%=h history.deployed_on.strftime("%F %H:%M") %></td>
    <td><%=h history.started_on.strftime("%F %H:%M") %></td>
    <td><%=h history.result_name %></td>
    <td><%=h history.deployer.login %></td>
    <td><%=h history.changeset.revision %></td>
    <td><%= link_to "URL", :action => "show", :id => history.id, :project_id => history.project_id %></td>
  </tr>
<%- end -%>
  </tbody>
</table>

デプロイコマンドの実装

では核心のdeployアクションに入りましょう。このアクションでチケットのステータスの確認や、履歴の保存などを行います。

def deploy
  ### ① チケットの状態チェック
  warning_status = IssueStatus.find(:first, :conditions => { :name => "実装完了" })
  if @project.issues.map(&:status).include?(warning_status)
    flash[:error] = "未チェックのチケットが存在します!"
    redirect_to :action => "index", :project_id => params[:project_id]
    return
  end

  ### ② リポジトリの更新、最新リビジョン番号の取得
  repository = @project.repository
  repository.fetch_changesets
  latest_changeset = Changeset.find(:first, :conditions => { :repository_id => repository }, :order => "revision DESC")

  ### ③ デプロイの実行
  start = Time.now
  cmd = "cap deploy 2>&1"
  finish = Time.now

  ### ④ 実行結果の確認
  out = `#{cmd}`
  return_code = $? ? $?.exitstatus : 8929

  ### ⑤ 履歴の作成
  DeployHistory.create!(:project => @project,
                        :changeset => latest_changeset,
                        :log => out,
                        :started_on => start,
                        :deployed_on => finish,
                        :deployer => User.current)

  ### ⑥ indexアクションに戻る
  flash[:notice] = "デプロイに成功しました! リビジョン: #{latest_changeset.revision}"
  redirect_to :action => "index", :project_id => params[:project_id]
end

だいぶ長いコードになりました。順々に説明していきます。

まず、では実装完了ステータスのチケットがあるかどうかをチェックし、ある場合にはエラーメッセージをflash領域に格納してリダイレクトしています。flashには値を入れておけばRedmineが自動でレンダリングしてくれます。あらかじめ準備されているキーはerror, warning, noticeの3つで、それぞれデザインが変わります。

図4 flash[:error]とflash[:notice]のデザイン
図4 flash[:error]とflash[:notice]のデザイン
図5
画像

では履歴の保存用にリポジトリの最新リビジョンを取得しています。注意しなければならないのは、Redmineではリポジトリとの同期を定期的に行っているわけではなく、リポジトリ画面にアクセスした際に同期している、という点です。つまり、リポジトリ画面にアクセスしていない場合はRedmine上のリポジトリデータが古い可能性があります。そのため、Repository#fetch_changesetsメソッドを呼び、最新の情報に更新しています。

, ではデプロイの実施と前後の処理時間を取得しています。Linuxのコマンドを実行しているだけなのでcapistranoである必要性は特になく、自作のシェルスクリプトでもかまいません。

そしてで履歴を作成し、で完了メッセージをflashに格納し、indexアクションにリダイレクトします。

まだデプロイしてもアクティビティへ表示されるようにはなっていませんが、とりあえずコアな部分はできました。デプロイ!ボタンを押して、実際に動くことを確認してみましょう。

また、ログなどを表示するデプロイ履歴の詳細画面の作成については今回は省略します。Redmineとは関連しない画面なので、前回のステータスの変更履歴画面と同じように実装すれば問題なく作れるかと思います。

アクティビティへの表示

さて、晴れてRedmine上からデプロイができるようになり、そのログも確認できるようになりました。デプロイはコミットやWikiの更新などと同様にプロジェクトへ加える変更となりますから、デプロイした際にはアクティビティに表示されるようにしてみましょう。

アクティビティ機能はメニューなどと同様にプラグインから利用する事が可能ですが、やや手間がかかります。具体的には以下の2つの作業が必要です。

  1. アクティビティの提供元となるモデルを指定する
  2. モデルに対してアクティビティを生成するための共通インターフェースを実装する

今回はもちろんDeployHistoryモデルが提供元です。

まず、提供元となるモデルはinit.rbで定義します。activity_providerというメソッドで、アクティビティの名前とモデルのクラス名を指定します。

init.rb(追加分のみ)
  activity_provider :deploy_histories, :class_name => "DeployHistory"

次にモデルで実装する共通インターフェースですが、これはacts_as_activity_providerとacts_as_eventという2つのクラスメソッドで実装します。acts_as_activity_providerではアクティビティの取得処理を、acts_as_eventでは表示内容を定義しています。

deploy_history.rb(追加分のみ)
  acts_as_activity_provider :timestamp => "deployed_on",
                            :author_key => :deployer_id,
                            :find_options => { :include => :project } # ①

  acts_as_event :title => Proc.new {|inst| "リビジョン#{inst.changeset.revision}番がデプロイされました。"},
                :description => :log, # 120文字に短縮されます。
                :datetime => :deployed_on,
                :url => Proc.new {|inst| {:controller => 'deployments', :action => 'show', :id => inst.id, :project_id => inst.project_id }},
                :author => :deployer

ではfind_optionsにprojectを指定しています。アクティビティの検索条件にはプロジェクトのIDが含まれるため、必ずincludeに指定する必要があります。

これで完了です。再起動してアクティビティを見ると、先ほどデプロイした際の履歴が追加されているはずです。実はRedmineのアクティビティはそれ専用のテーブルが用意されているわけではなく、提供元となっている複数のモデルからレコードを取得し、それらを内部でマージする実装になっています。ですので、アクティビティを有効にする前のオペレーションでも表示される、というわけです。

おわりに

今回はContinuousDeploymentの開発を通して、チケットのステータスの取得やアクティビティへの表示、リポジトリの操作など、Redmineのビルトインの機能をプラグインから利用する、ということを解説しました。

ちなみにデプロイやビルドとRedmineとの連動、と言うことであればHudsonというCIサーバとRedmineを連動させるプラグインが既にリリースされていますリンク⁠。ですが、CIサーバの導入はわりとヘビーな話になりますし、既にcapistranoや自作のシェルスクリプトで運用している現場にとってはまずはこうして自作のプラグインから始める、というのは気軽で選択肢の一つとしてあってよいかと思います。

最終回となる次回はプラグイン開発の落ち穂拾いと、社内用プラグイン開発のススメ、と言う開発とはちょっと離れた内容についてお話したいと思います。どうぞお楽しみに。

おすすめ記事

記事・ニュース一覧