Redmineプラグイン開発の様子について #ginowan_study で紹介した

YassLabという会社で働いています。

yasslab.jp

今年からアジャイルウェアさんのところでRedmineプラグイン開発のお手伝いをさせて頂いています。

はじめたてのころにいろいろと溜まった情報をRailsDMで話したりしました。

Railsdm 2019に参加した - hanachin temporary

#ginowan_studyRedmineプラグイン開発どんな様子なのか話しましたが、私の機材のトラブルで囲われての発表だったので聞こえない人もいたかと思いブログにしました。

Redmineの動作環境ついておさらい

課題管理ソフトウェアです。どういうことができるかの話は割愛します。

ここでRedmineのビルドマトリックスを眺めましょう、めっちゃマトリックスじゃないですか?

Redmine build status

RedmineというのはRailsで書かれています

はい

Redmineのバージョンはいくつかあります。アジャイルウェアのLychee Redmineでは現時点でRedmine 3.2、3.3、3.4、4.0をサポートしています

サポート | Lychee Redmine(ライチレッドマイン)でプロジェクトの見える化を。そして管理ももっと楽にしよう。

はい!

RedmineRailsなので、複数のRubyのバージョンで動きます

はい!!

Redmineは複数のRDBMSに対応しています。

参考: RedmineInstall - Redmine

はい!!!

広すぎる動作環境を支えるCIと自動テスト

例えば私のRedmineプラグイン開発実体験だとこういうことをがありました

  • 仕事のプラグイン開発で特定のRDBMSでは動かないSQLを意図せず書いた
  • 仕事のプラグイン開発でRDBMSごとにSQLを書き分けた
  • Redmineのバージョンによっては実装されていないAPIを使うプラグインを意図せず書いた
  • Rubyのバージョンによっては実装されていないAPIを使うプラグインを意図せず書いた
  • RedmineプラグインからはRedmineのコードを直接変更できないのでモンキーパッチをして機能を追加・変更しており、モンキーパッチで本体のメソッドが壊れてしまった

自分はうっかりものなので、こういうのを未然に防ぐために自動テストを整備してRedmine本体同様マトリックスでテストを流そうとしています。 マトリックスで流すとこれらのバグを防げます。ちゃんとテストが書けていれば!

ふつうのサービスのRailsアプリだとこういうマトリックス整えることないと思うんですが、Redmineプラグインはこれを仕事で出来てたのしい。モンキーパッチも仕事で書ける。

一応誤解がないように書いておくけど上記のような問題もだいたい優秀なレビュワーによるコードレビューとテスターによるテストで防いでもらえています。

それでもなお全力をつくして問題がおきるならCIが悪い。

CIの実装上の課題

Redmineプラグイン特有の問題なんですが、プラグインリポジトリ単体をcloneしてきても動きません。プラグインなのでRedmineプラグインしなきゃ動かない。 そしてLychee RedmineRedmineプラグインの集合なのでリポジトリごとにテストの設定がある。 ふつうにやると.circleci/config.ymlコピペまつりになってしまいます。

どうするか?

そうですね、CircleCI orbを使います。 ということでRedmineプラグイン開発用のorbを書いて業務で必要が出てきたときにちょいちょい更新しています。

CircleCI Orb Registry - agileware-jp/redmine-plugin

こういうのをちょいと書くだけで以下のようなことができます。

  • Redmineのダウンロード
  • Redmineのインストール
  • Rubyのバージョンを指定してテストを実行
  • DBMSを指定してテストを実行
orbs:
  redmine: agileware-jp/redmine-plugin@0.0.15
version: 2.1
workflows:
  version: 2
  test:
    jobs:
      - redmine/download:
          redmine_version: trunk
      - redmine/test:
          executor: redmine/ruby-26-pg
          requires: [redmine/download]

さらっと流しましたがいろいろとあるんですよね

RedmineのGemfile

RedmineではGemfileからconfig/database.ymlの中身を読んでDBMSに対応したアダプターのgemを使っています。 何をいっているかさっぱりわからないと思いますがこういうことです。

# Include database gems for the adapters found in the database
# configuration file
require 'erb'
require 'yaml'
database_file = File.join(File.dirname(__FILE__), "config/database.yml")
if File.exist?(database_file)
  database_config = YAML::load(ERB.new(IO.read(database_file)).result)
  adapters = database_config.values.map {|c| c['adapter']}.compact.uniq
  if adapters.any?
    adapters.each do |adapter|
      case adapter
      when 'mysql2'
        gem "mysql2", "~> 0.5.0", :platforms => [:mri, :mingw, :x64_mingw]
      when /postgresql/
        gem "pg", "~> 1.1.4", :platforms => [:mri, :mingw, :x64_mingw]
      when /sqlite3/
        gem "sqlite3", "~> 1.4.0", :platforms => [:mri, :mingw, :x64_mingw]
      when /sqlserver/
        gem "tiny_tds", "~> 1.0.5", :platforms => [:mri, :mingw, :x64_mingw]
        gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw, :x64_mingw]
      else
        warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems")
      end
    end
  else
    warn("No adapter found in config/database.yml, please configure it first")
  end
else
  warn("Please configure your config/database.yml first")
end

https://github.com/redmine/redmine/blob/be7d39a4d3c2f567f4cc315969d6a5c09221fb45/Gemfile#L42-L71

プラグインのGemfile

RedmineプラグインのGemfileがRedmineのGemfileの中からeval_gemfileで読み込まれます。 ここで例えばプラグインAがrspecのバージョンを3.8系に固定、プラグインBがrspecのバージョンを3.7系に固定するとどうなるか? おめでとうございます、bundle install失敗です👏 これを防ぐために実行時に必要のないgemについてはプラグインごとにGemfile.localというファイルを用意し、プラグインの開発時に対応するプラグインのGemfile.localをRedmine本体に入れてつかう、みたいなことをやっています。詳しくは参考資料を見てください。

参考: Redmine4時代のプラグイン開発 redmine.tokyo #13

orbではどうやっているか

粛々とこの辺がいい感じになって何も設定せずに動くよう直しています。 orbの具体的な使い方についてはもうちょっとorbが成長したら別記事を書きたいと思います。

CIでのレビューで面倒だと思うこととHeroku Review Apps

前述した通り多用な動作環境なので

  • 私がレビューするとき、実際に触るためにfetchしてcheckoutして動かしてみて実際さわってみよ!、、、が面倒(めんどうくさがりなので)
  • テストするときにテスト環境にブランチをデプロイが手動で不便そう
  • 使ってみた感じをチェックしたいのでデプロイお願いします依頼
  • テスターの方に動作確認してもらっているのですが(この環境ほんとによくて安心してバグったコードが書けて、なおせる、頭が上がりません。。。)、Redmine/Rubyのこのバージョンだと動きません、あるRDBMSだと動きませんみたいな報告をうけたとき、自分の手元で動作環境合わせるのがちょっと面倒

ということでYassLab社内でも利用実績のあるHeroku Review Apps導入いかがですか?と聞いてみたところ

  • テスト用の秘伝のデータがあってそれがないと不便そう
  • シード流すのに時間がかかってしまってすばやく確認できなさそう
  • テスターというよりマネージャに触ってもらった感じを確認してもらうのに便利そう

などのフィードバックをもらっていたのでいろいろと解決策を考えていたのでした

RedmineプラグインでHeroku Review Appsをつかう

問題: プラグインなので単体で動かない

再掲: Redmineプラグイン特有の問題なんですが、プラグインリポジトリ単体をcloneしてきても動きません。 今回解決方法としてnull buildpackを使いました。 口頭だと伝わりづらかったと思うので、流れの図です。

developerdeveloperGitHubGitHubHerokuHerokuCircleCICircleCIcreate pull requestdeploy the plugin with null buildpackcreated review appgit pushrun testtest resultdeploy the plugin with redmine/ruby buildpack

こういう流れで単体では動かないコードでもデプロイがうまく動きます。

Heroku Review AppsがPull Requestごとに作成され、消えます。 初回はnull buildpackで無をデプロイするのでデプロイに成功します。 最初からrubyのビルドパック使うと諸事情でコケるのですが、null buildpackを使うとコケないので、Heroku Review Appsのデプロイに失敗した通知がとびません。

app.json解説

なにもしないビルドパックを使うことでとりえあずデプロイが成功します。DBと必要な環境変数を2つ用意しておきます。

{
  "addons": ["heroku-postgresql"],
  "buildpacks": [{"url": "https://github.com/ryandotsmith/null-buildpack"}],
  "env": {
    "SECRET_KEY_BASE": {"generator": "secret"},
    "REDMINE_LANG": {"value": "en"}
  }
}

問題: seed長い

レビューアプリの親元からコピーするようにします。 具体的にはheroku pg:copyをします。

他まだ試せていませんが以下のような感じで短縮できそうなのが見えてきました

  • seed全部を流さず一部のみ流す
  • データ投入を並列で流す/バルクで流す

サンプルリポジトリ

CircleCIの画面はHerokuのAPIキーがガッツリでていておみせできませんが(見せない方法知りたい) .circleci/config.ymlは以下のような感じです。

version: 2.1

orbs:
  heroku: circleci/heroku@0.0.8
  redmine: agileware-jp/redmine-plugin@dev:c39794f

executors:
  ruby-26:
    docker:
      - image: circleci/ruby:latest

jobs:
  deploy-review-app:
    executor: ruby-26
    steps:
      - run: |
          set -e
          if [ -z "$CIRCLE_PULL_REQUEST" ]; then
            echo "Not create pull request yet."
            exit 1
          fi
      - attach_workspace:
          at: ~/project
      - heroku/install
      - run: |
          set -e
          echo 'vendor/bundle' >> .gitignore
          cat \<<-'EOF' > Procfile
          web: bin/rails s
          EOF
          cat \<<-'EOF' > config/database.yml
          production:
            adapter: postgresql
          EOF
      - run: |
          set -e
          ruby -e 'puts "ruby #{RUBY_VERSION.inspect}"' >> Gemfile
          echo "gem 'rails_12factor', group: 'production'" >> Gemfile
          bundle lock
      - run: |
          set -e
          git config --global user.email "seiei@yasslab.jp"
          git config --global user.name "Seiei Miyagi"
          git init
          git add .
          git add -f Gemfile.lock
          git add -f config/database.yml
          git commit -m 'deploy'
      - run: |
          set -e
          export HEROKU_APP_NAME=$(heroku pipelines:info --json redmine-review-app | ruby -rjson -e 'puts JSON.parse($stdin.read).fetch("apps").detect { |a| a.dig("coupling", "stage") == "review" && a.fetch("git_url").end_with?("#{ENV.fetch("CIRCLE_PULL_REQUEST")[/\d+$/]}.git") }&.fetch("name")')
          if [ -z "$HEROKU_APP_NAME" ]; then
            echo "heroku review apps not found."
            exit 1
          fi

          heroku buildpacks:clear --app $HEROKU_APP_NAME
          git push -f https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git HEAD:master
          heroku pg:copy --app $HEROKU_APP_NAME --confirm $HEROKU_APP_NAME redmine-review-app-production::DATABASE_URL DATABASE_URL
          heroku run --app $HEROKU_APP_NAME bin/rails db:migrate
          heroku run --app $HEROKU_APP_NAME bin/rails redmine:load_default_data
          heroku ps:scale --app $HEROKU_APP_NAME web=1

workflows:
  version: 2
  test:
    jobs:
      - redmine/download:
          redmine_version: '4.0.4'
      - redmine/test:
          executor: redmine/ruby-26-pg
          requires: [redmine/download]
          after-script:
      - deploy-review-app:
          requires: [redmine/test]

https://github.com/yasslab/hello_world/blob/update_url/.circleci/config.yml

参加者の発表

いろいろトラブっていて冷静にきけなかったけど完全パソコンなしアジャイルLT最高だった...。 あとEdgeTPUおもしろそうだったので触発されてその日帰宅してすぐポチった。 機械学習するぞ〜〜〜(乗り遅れた勢いたら一緒にやりましょう)

他、発表する予定だったネタ

Quine化とAA化が間に合わなくてボツにしたけどQuineの猛者とQuineで戦うために書いた対戦型Quineの構想について懇親会でお話しました。かびさん相談のってくれてありがとうございました。

その後すくすく育ててQuineになった。

解説はこちらから

hanachin.hateblo.jp

帰り道

すごい早起きしていて疲れで脳がバグっていて歩いて帰った(12km) とはいえ学生のころの飲み会で宜野湾から那覇徒歩帰宅は普通にやっていたので問題なし、PCが重くなったぐらい。 軽いPCほしい。

Redmine課外活動

Redmineの依存gemにpull requestを出したりしています。

CIを直したり: Update .travis.yml by hanachin · Pull Request #51 · naitoh/rbpdf warningsを消したり: Fix test warnings by hanachin · Pull Request #50 · naitoh/rbpdf 不要なファイルを消したり: Remove unused lib/core/image_science.rb by hanachin · Pull Request #49 · naitoh/rbpdf

redmine trunk changesというブログもやっているけど最近はあまり見れてないです。

つながりたい

Redmine開発している方、ruby-jpのSlackの #redmine チャンネルでよろしくお願いしましょう!!!

いいなと思ったらKyashでお金を下さい
20191128011151
GitHubスポンサーも受け付けています
https://github.com/sponsors/hanachin/