agileware-jp/redmine-plugin orb実装詳解
agileware-jp/redmine-plugin orbの実装について詳解をします。
Redmine Advent Calendar 2019の2日目の記事です。 1日めはredmineエバンジェリストの会 2019年まとめでした。
orbはこちらで公開されています。
https://circleci.com/orbs/registry/orb/agileware-jp/redmine-plugin
開発は以下のリポジトリで行われています。
なにをするものか
CircleCIでRedmineプラグインのテストを行う際に様々な組み合わせでテストするのを補助するためのorbです。
なぜCircleCIのorbか
CircleCIを使っているからです。またorbとして公開すると複数のプラグインのリポジトリで設定を共通化できます。
ディレクトリ構造
2019/12/01時点では以下のような構造になっています。
% tree . ├── Makefile ├── README.md ├── gen_executors_latest.rb ├── gen_executors_mariadb.rb ├── gen_executors_mysql.rb ├── gen_executors_pg.rb ├── gen_executors_sqlite3.rb └── src ├── @orb.yml ├── commands │ ├── download.yml │ ├── generate_database_yml-mysql.yml │ ├── generate_database_yml-pg.yml │ ├── generate_database_yml-sqlite3.yml │ ├── generate_database_yml.yml │ ├── install-rspec.yml │ ├── install_plugin.yml │ └── setup.yml ├── examples │ ├── redmine-plugin-rspec.yml │ └── redmine-plugin-test.yml ├── executors │ ├── downloader.yml │ ├── ruby-19-mariadb.yml │ ├── ruby-19-mysql.yml │ ├── ruby-19-pg.yml │ ├── ruby-19-sqlite3.yml │ ├── ruby-20-mariadb.yml │ ├── ruby-20-mysql.yml │ ├── ruby-20-pg.yml │ ├── ruby-20-sqlite3.yml │ ├── ruby-21-mariadb.yml │ ├── ruby-21-mysql.yml │ ├── ruby-21-pg.yml │ ├── ruby-21-sqlite3.yml │ ├── ruby-22-mariadb.yml │ ├── ruby-22-mysql.yml │ ├── ruby-22-pg.yml │ ├── ruby-22-sqlite3.yml │ ├── ruby-23-mariadb.yml │ ├── ruby-23-mysql.yml │ ├── ruby-23-pg.yml │ ├── ruby-23-sqlite3.yml │ ├── ruby-24-mariadb.yml │ ├── ruby-24-mysql.yml │ ├── ruby-24-pg.yml │ ├── ruby-24-sqlite3.yml │ ├── ruby-25-mariadb.yml │ ├── ruby-25-mysql.yml │ ├── ruby-25-pg.yml │ ├── ruby-25-sqlite3.yml │ ├── ruby-26-mariadb.yml │ ├── ruby-26-mysql.yml │ ├── ruby-26-pg.yml │ ├── ruby-26-sqlite3.yml │ ├── ruby-mariadb.yml │ ├── ruby-mysql.yml │ ├── ruby-pg.yml │ └── ruby-sqlite3.yml └── jobs ├── download.yml ├── rspec.yml └── test.yml 5 directories, 58 files
src以下の構造について
circleci config pack
コマンドを使って1つのYAMLにまとめられるようになっています。
どうまとめられるのか詳しくはドキュメントを参照して下さい。
circleci config pack
によりまとめられたYAMLをorbとして公開しています。
src/@orb.yml
circleci config pack
によりまとめられたYAMLのトップにかかれている説明です。
version: 2.1 description: | Orbs for the Redmine plugins automated testing. https://github.com/agileware-jp/redmine-plugin-orb
src/commands
主にテストで使用するためのコマンド群をまとめています。
src/examples
CircleCIの設定ファイルでどうorbを使うかのサンプルコードを置いています。
src/executors
テストを実行する環境を定義しています。 どのexecutorを使うかでRubyとRDBMSの組み合わせが決まります。
src/jobs
一般的なテストの実行方法についてjobにまとめています。
.circleci/config.yml
agileware-jp/redmine-plugin orb自体のCIの設定が書かれています。
gen_executors_*.rb
実行環境を定義するyamlを動的生成するためのスクリプト群です。
Makefile
実行環境を定義するyamlを動的生成するためのルールを書いています。
agileware-jp/redmine-plugin orbリリースの流れ
orbのCIにはCircleCIのorb-tools orbを使っています。 Pull Requestが出るとこのような流れでCIが流れます。
- まずYAMLの形式が正しいか
orb-tools/lint
をかけます。 - つぎに
orb-tools/pack
で1ファイルにまとめます。 - 最後に
orb-tools/publish-dev
が実行され、開発版としてコミットハッシュ値の先頭7文字をとった@dev:xxxxxx
バージョンをリリースします
開発版のバージョンを使ってみて問題がなさそうならマージしています。
マージされると orb-tools/publish-dev
の代わりに orb-tools/increment
が実行され、正式なバージョンとしてorbがリリースされます。
# .circleci/config.yml version: 2.1 orbs: orb-tools: circleci/orb-tools@8.5.0 workflows: release: jobs: - orb-tools/lint - orb-tools/pack: requires: - orb-tools/lint - orb-tools/publish-dev: orb-name: agileware-jp/redmine-plugin requires: - orb-tools/pack filters: branches: ignore: master - orb-tools/increment: attach-workspace: true orb-ref: agileware-jp/redmine-plugin requires: - orb-tools/pack filters: branches: only: master
CircleCIへのorbの導入方法
デフォルトでは純正orb以外は使うことができません。Orb Security Settingsで第三者製のorbを使えるように設定を行って下さい。
注 : あなたの組織でサードパーティ製 Orbs を利用するにあたっては、あらかじめ CircleCI の Settings > Security ページにアクセスし、[Orb Security Settings] で [Yes] を選んでおく必要があります。 https://circleci.com/docs/ja/2.0/orb-intro/
参考: https://github.com/ishikawa999/redmine_message_customize/pull/22#issuecomment-514052286
設定例についてはexamplesディレクトリをみてもらいたいのですが、基本的にはこの程度の設定で使い始めることができます
usage: version: 2.1 orbs: redmine: agileware-jp/redmine-plugin@0.0.24 workflows: version: 2 test: jobs: - redmine/download: redmine_version: trunk # or version string e.g. '4.0.4' - redmine/test: executor: redmine/ruby-pg requires: [redmine/download]
実装の詳解
これから各ファイルの実装について詳解していきます。
Makefile
Makefileでは以下のように src/executors
以下のYAMLを生成しています。
.PHONY: all all: src/executors/ruby-sqlite3.yml src/executors/ruby-26-pg.yml src/executors/ruby-26-mysql.yml src/executors/ruby-26-mariadb.yml src/executors/ruby-26-sqlite3.yml; src/executors/ruby-%-pg.yml: gen_executors_pg.rb ruby gen_executors_pg.rb src/executors/ruby-%-mariadb.yml: gen_executors_mariadb.rb ruby gen_executors_mariadb.rb src/executors/ruby-%-mysql.yml: gen_executors_mysql.rb ruby gen_executors_mysql.rb src/executors/ruby-%-sqlite3.yml: gen_executors_sqlite3.rb ruby gen_executors_sqlite3.rb src/executors/ruby-mysql.yml src/executors/ruby-pg.yml src/executors/ruby-sqlite3.yml: gen_executors_latest.rb ruby gen_executors_latest.rb .PHONY: clean clean: rm src/executors/ruby-*.yml
書いていて気づいたんですがここで circleci config pack
してもいいかもしれませんね。
src/executorsを動的に生成しているのはなぜか
agileware-jp/redmine-plugin orbではRubyのバージョンとRDBMSの組み合わせをjobのexecutorを使って切り替えています。
なのでRubyのバージョンとRDBMSの組み合わせの分だけexecutorのYAMLを生成する必要があります。 見ての通りこの組み合わせが大量にあります。
% ls src/executors | wc -l 37
手動で書き換えるとかなり手間なのでスクリプトで生成するようにしています。
executor生成スクリプトでの工夫
例としてPostgreSQLの生成スクリプトを紹介します。
# gen_executors_pg.rb require 'erb' VERSION = <<~POSTGRES.freeze parameters: pg_version: description: version of PostgreSQL type: string default: <%= pg_version == :'latest-ram' ? 'latest-ram' : "'" + pg_version.to_s + "'" %> docker: - image: circleci/ruby:<%= ruby_version %>-node-browsers environment: DATABASE_ADAPTER: postgresql - image: circleci/postgres:<< parameters.pg_version >> POSTGRES template = ERB.new(VERSION) { 'latest-ram': %w[2.6 2.5], '9-ram': %w[2.4 2.3 2.2 2.1 2.0 1.9] }.each do |pg_version, ruby_versions| ruby_versions.each do |ruby_version| File.write("src/executors/ruby-#{ruby_version.sub('.', '')}-pg.yml", template.result(binding)) end end
PostgreSQLのバージョンとRubyのバージョンの組み合わせが定義されているのが見えるでしょうか。 Ruby 2.5, 2.6では最新版のPostgreSQLを使用しています。 しかしRuby 2.4以下ではPostgreSQL 9を使用しています。 なぜかというと、Ruby 2.4以下をサポートしているRedmineでは古いバージョンのpg gemを使っていてPostgreSQL 10にうまくつながらないことがあるためです。
参考: https://help.heroku.com/WKJ027JH/rails-error-after-upgrading-to-postgres-10
MySQLでも同様につながらないことがあります。なのでつながる組み合わせでYAMLを生成しています。
データベースのイメージはテストの実行時間の短縮を意図してデフォルトだとram版のイメージを指定しています。
環境変数の DATABASE_ADAPTER
は config/database.yml
の接続情報を書き出すために使われます。
src/executors/ruby-pg.yml
例として1つexecutorを見てみます。これがruby最新版とPostgreSQL最新版のexecutorです。
環境変数の DATABASE_ADAPTER
には postgresql
が設定されていますね。
parameters: ruby_version: description: version of Ruby type: string default: latest pg_version: description: version of PostgreSQL type: string default: latest-ram docker: - image: circleci/ruby:<< parameters.ruby_version >>-node-browsers environment: DATABASE_ADAPTER: postgresql - image: circleci/postgres:<< parameters.pg_version >>
src/commands/generate_database_yml.yml
executorで使用しているデータベースの情報をもとに config/database.yml
を生成するためのコマンドです。
RedmineではGemfileから config/database.yml
を読みその情報をもとに使用するgemを決めるようになっています。
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/f7fcd111e9b0e127950634435bf4e63d0f69d4bf/Gemfile#L46-L71
なので bundle install
を実行する前にexecutorが用意しているデータベースの接続情報を config/database.yml
に書き出す必要があります。
agileware-jp/redmine-plugin orbではexecutorの環境変数で DATABASE_ADAPTER
を定義しています。
環境変数をもとに config/database.yml
を生成します。
# src/commands/generate_database_yml.yml description: Generate database.yml steps: - run: name: Generate database.yml command: | if [ $DATABASE_ADAPTER = "mysql2" ]; then cat \<<-'EOF' > config/database.yml test: adapter: mysql2 database: circle_test host: 127.0.0.1 username: root password: encoding: utf8mb4 EOF fi if [ $DATABASE_ADAPTER = "postgresql" ]; then cat \<<-'EOF' > config/database.yml test: adapter: postgresql database: circle_test host: 127.0.0.1 username: postgres password: postgres EOF fi if [ $DATABASE_ADAPTER = "sqlite3" ]; then cat \<<-'EOF' > config/database.yml test: adapter: sqlite3 database: db/test.sqlite3 EOF fi
src/commands/download.yml
Redmineをダウンロードします。バージョン指定は必須です。
最近RedMicaがリリースされたので download-redmine
にリネームして download-redmica
を増やしたいですね。
ここに trunk
を指定すると最新のリリースされていないRedmineがダウンロードされます。
description: Download Redmine << parameters.redmine_version >> parameters: cache_key_prefix: description: prefix of cache key type: string default: redmine-plugin-commands-download- redmine_version: description: version of Redmine type: string destination: description: destination path type: string default: '' steps: - restore_cache: keys: - '<< parameters.cache_key_prefix >><< parameters.redmine_version >>' - run: name: Download Redmine << parameters.redmine_version >> command: | set -e if [ << parameters.redmine_version >> = trunk ]; then svn co --non-interactive --config-option servers:global:ssl-authority-files=<(curl -sL https://www.gandi.net/static/CAs/GandiStandardSSLCA2.pem) https://svn.redmine.org/redmine/trunk redmine-trunk exit fi if [ ! -d redmine-<< parameters.redmine_version >> ]; then curl https://redmine.org/releases/redmine-<< parameters.redmine_version >>.tar.gz | tar zx fi - save_cache: key: '<< parameters.cache_key_prefix >><< parameters.redmine_version >>' paths: redmine-<< parameters.redmine_version >> - run: name: Move redmine-<< parameters.redmine_version >> to destination command: | if [ -n "<< parameters.destination >>" ]; then mv redmine-<< parameters.redmine_version >> << parameters.destination >> fi
src/commands/generate_database_yml-mysql.yml
特に面白いことはないです。executorのRDBMSのイメージに合わせて接続するデータベースを指定しています。
description: Generate database.yml for MySQL steps: - run: name: Generate database.yml command: | cat \<<'EOF' > config/database.yml test: adapter: mysql2 database: circle_test host: 127.0.0.1 username: root password: encoding: utf8mb4 EOF
src/commands/generate_database_yml-pg.yml
Ditto
description: Generate database.yml for PostgreSQL steps: - run: name: Generate database.yml command: | cat \<<'EOF' > config/database.yml test: adapter: postgresql database: circle_test host: 127.0.0.1 username: postgres password: postgres EOF
src/commands/generate_database_yml-sqlite3.yml
Ditto
description: Generate database.yml for SQLite3 steps: - run: name: Generate database.yml command: | cat \<<'EOF' > config/database.yml test: adapter: sqlite3 database: db/test.sqlite3 EOF
src/commands/install_plugin.yml
install-plugin-from-git
みたいな名前にリネームしたほうがよさそう。
工夫している点がいくつかあります。
git clone
に時間がかからないように --depth 1
を付けています。
ブランチを指定できます。指定されていない場合masterをチェックアウトします。
なんといっても特徴的なのは checkout-circle-branch
オプションです。
デフォルトで true
です。
複数のプラグインにまたがる回収が入った際「プラグインAのブランチfoobarをテストするためには、プラグインBのブランチfoobarが必要」みたいなときがなくはないと思います。たぶんある。
そういうときのために install_plugin
でプラグインBをcloneする際にプラグインBにfoobarブランチがあればそれを使うようになっています。
git ls-remote
で確認しているのですが -h
オプションがないとタグの番号であっても合わせようとしてしまうので大変危険です。(事故った)
description: Install Redmine plugin << parameters.redmine_plugin_repository >> parameters: redmine_plugin_repository: description: repository of Redmine plugin type: string branch: description: branch of Redmine plugin type: string default: '' checkout-circle-branch: description: "checkout $CIRCLE_BRANCH (default: true)" type: boolean default: true steps: - run: name: Install plugin << parameters.redmine_plugin_repository >> working_directory: plugins command: | branch="<< parameters.branch >>" if [ -z "$branch" ]; then branch='master' <<# parameters.checkout-circle-branch >> if [ -n "$(git ls-remote -h << parameters.redmine_plugin_repository >> $CIRCLE_BRANCH)" ]; then branch=$CIRCLE_BRANCH fi <</ parameters.checkout-circle-branch >> fi git clone --depth 1 --branch $branch << parameters.redmine_plugin_repository >>
src/commands/setup.yml
bundle install
してマイグレーション等を流してプラグインをインストールしています。
ちょっと気をつけてほしいのがキャッシュのキー部分です。
をキーに含むようにしています。
また bundle check
を行って bundle install
をなるべく避けています。
description: Setup Redmine parameters: cache_key_prefix: description: prefix of cache key type: string default: redmine-plugin-commands-setup- steps: - run: name: Check ruby version command: | ruby -v > /tmp/ruby-version - restore_cache: keys: - '<< parameters.cache_key_prefix >>{{ checksum "/tmp/ruby-version" }}-{{ checksum "Gemfile" }}-{{ checksum "config/database.yml" }}' - run: name: Setup Redmine command: | bundle check --path .bundle || bundle install --path .bundle - save_cache: key: '<< parameters.cache_key_prefix >>{{ checksum "/tmp/ruby-version" }}-{{ checksum "Gemfile" }}-{{ checksum "config/database.yml" }}' paths: - .bundle - Gemfile.lock - run: name: Setup Database command: | bundle exec rake db:create db:migrate - run: name: Setup plugins command: | bundle exec rake redmine:plugins
src/jobs/download.yml
Redmineをダウンロードしてくるだけのjobです。 一応ダウンロードが完了したRedmineについてはキャッシュに保存するようにしています。 trunkのダウンロードだけはキャッシュできません。なのでキャッシュしていないつもり。 downloadだけでjobを分けているのは各Redmineのダウンロードを1回で抑えるための工夫です。
ただしdownloadを別jobに分けるとテスト実行するjobの方で requires
の記述が増えるのでちょっと微妙な気もしています...
parameters: cache_key_prefix: description: prefix of cache key type: string default: redmine-plugin-commands-download- redmine_version: description: version of Redmine type: string after-download: description: steps to run after download command. type: steps default: [] executor: downloader working_directory: /tmp steps: - download: cache_key_prefix: << parameters.cache_key_prefix >> redmine_version: << parameters.redmine_version >> destination: ~/project - steps: << parameters.after-download >> - persist_to_workspace: root: '~/project' paths: ['*']
src/examples/redmine-plugin-rspec.yml
RSpecのテストを実行するためのjobです。
before-
とか after-
でいろんなフックを挟むようにしています。
などなどいろいろあると思います。
特に工夫しているのがプラグインリポジトリ内の Gemfile.local
をコピーするあたりです。
Gemfile.local
の役割についてはdouhashiさんの資料をご覧ください。
あと通常リポジトリの .rspec
にかかれている設定とは別でRspecJunitFormatterを使いたいとかあると思います。
そのへんは .circleci/.rspec
を用意しておくとRedmine本体のルートディレクトリにcpしておいてくれるようにしました。
parameters: after-script: description: steps to execute after script type: steps default: - persist_to_workspace: root: ~/project paths: ['*'] before-script: description: steps to execute before script type: steps default: [] before-setup: description: steps to execute before setup type: steps default: [] cache_key_prefix: description: key of cache type: string default: '' executor: description: executor for rspec type: executor plugin_name: description: name of Redmine plugin type: string default: '' script: description: steps to execute the rspec type: steps default: - run: | if [ -z "$REDMINE_PLUGIN_NAME" ]; then REDMINE_PLUGIN_NAME=$CIRCLE_PROJECT_REPONAME fi if [ -f plugins/$REDMINE_PLUGIN_NAME/.circleci/.rspec ]; then cp plugins/$REDMINE_PLUGIN_NAME/.circleci/.rspec . fi bundle exec rspec plugins/$REDMINE_PLUGIN_NAME/spec environment: RAILS_ENV: test REDMINE_PLUGIN_NAME: << parameters.plugin_name >> executor: << parameters.executor >> steps: - attach_workspace: at: ~/project - checkout: path: /tmp/plugin - run: name: Install plugin command: | if [ -z "$REDMINE_PLUGIN_NAME" ]; then REDMINE_PLUGIN_NAME=$CIRCLE_PROJECT_REPONAME fi mv /tmp/plugin plugins/$REDMINE_PLUGIN_NAME touch plugins/$REDMINE_PLUGIN_NAME/Gemfile if [ -f plugins/$REDMINE_PLUGIN_NAME/Gemfile.local ]; then cat plugins/$REDMINE_PLUGIN_NAME/Gemfile.local >> plugins/$REDMINE_PLUGIN_NAME/Gemfile fi cp plugins/$REDMINE_PLUGIN_NAME/Gemfile /tmp/Gemfile - generate_database_yml - steps: << parameters.before-setup >> - setup: cache_key_prefix: << parameters.cache_key_prefix >>{{ checksum "/tmp/Gemfile" }} - steps: << parameters.before-script >> - steps: << parameters.script >> - steps: << parameters.after-script >>
src/jobs/test.yml
だいたいRSpecと同じなので書くことなし
parameters: after-script: description: steps to execute after script type: steps default: - persist_to_workspace: root: ~/project paths: ['*'] before-script: description: steps to execute before script type: steps default: [] before-setup: description: steps to execute before setup type: steps default: [] cache_key_prefix: description: key of cache type: string default: '' executor: description: executor for test type: executor plugin_name: description: name of Redmine plugin type: string default: '' script: description: steps to execute the test type: steps default: - run: | if [ -z "$REDMINE_PLUGIN_NAME" ]; then REDMINE_PLUGIN_NAME=$CIRCLE_PROJECT_REPONAME fi bundle exec rake redmine:plugins:test NAME=$REDMINE_PLUGIN_NAME environment: RAILS_ENV: test REDMINE_PLUGIN_NAME: << parameters.plugin_name >> executor: << parameters.executor >> steps: - attach_workspace: at: ~/project - checkout: path: /tmp/plugin - run: name: Install plugin command: | if [ -z "$REDMINE_PLUGIN_NAME" ]; then REDMINE_PLUGIN_NAME=$CIRCLE_PROJECT_REPONAME fi mv /tmp/plugin plugins/$REDMINE_PLUGIN_NAME touch plugins/$REDMINE_PLUGIN_NAME/Gemfile if [ -f plugins/$REDMINE_PLUGIN_NAME/Gemfile.local ]; then cat plugins/$REDMINE_PLUGIN_NAME/Gemfile.local >> plugins/$REDMINE_PLUGIN_NAME/Gemfile fi cp plugins/$REDMINE_PLUGIN_NAME/Gemfile /tmp/Gemfile - generate_database_yml - steps: << parameters.before-setup >> - setup: cache_key_prefix: << parameters.cache_key_prefix >>{{ checksum "/tmp/Gemfile" }} - steps: << parameters.before-script >> - steps: << parameters.script >> - steps: << parameters.after-script >>
やりたいこと
いろいろとあるので困ったらissueたてたい
- commandの名前を
_
区切りから-
区切りにしたい - RedMica対応したい
- SQLServer対応したい
- checkoutをrspec/test jobでやるのは名前とあってなくてよくなさそうなのでdownload jobにうつしたい
- orbの動作を保証するテストを書きたい
まとめ
今年書いたorbの実装について来年思い出せなくなる前に書き出しておこうと思ってはや12月。書けたのでよかた。 Redmineプラグイン開発についてウェブ上の情報と、orb情報をもっと充実させていきたい。
明日は yamasaki さんのJSで妄想する話の予定です。たのしみ!