Ruby の gem を自動的にリリースする GitHub Action を書いた

ソースはここ。

github.com

何をするかというと

  1. gemspec の version を見て、rubygems.org に登録されているバージョンと比較
  2. GitHubリポジトリに "v1.2.3" の形式のタグを生成
  3. gem を build し rubygems.org に push

するだけ。 普通なら Ruby で書くのだろうけど、GitHub Actions が Node.js 推しのようなのと TypeScript に挑戦してみたかったので TypeScript 製。

Dockerコンテナでの設定ファイル生成にテンプレートとしてERBを使う

Dockerコンテナを起動する際に、環境変数で渡したパラメーターを使って設定ファイルを作りたいことがある。 例えばsambaにアクセスするとき、ユーザー名が foo なら許可、それ以外は不許可としたければ設定ファイルは以下のようにする。

[global]
        log file = /var/log/samba/%m

[share]
        path = /mount
        guest ok = no
        valid users = foo # ユーザー foo ならアクセスを許可する

このユーザー名を可変にしたいとしよう。Docker で docker run -e USER=foo -e PASSWORD=pass ... のようにしたいなら、以下のようになる。

Dockerfile

FROM ubuntu:latest

RUN apt update \
    && apt install -y \
    samba

COPY samba.sh /root/

EXPOSE 139 445

CMD ["/root/samba.sh"]

samba.sh

#!/bin/sh

# /etc/samba/smb.conf を生成する
cat <<SMBCONF >> /etc/samba/smb.conf
[global]
        log file = /var/log/samba/%m

[share]
        path = /mount
        guest ok = no
        valid users = $USER
SMBCONF

# システムに foo ユーザーを追加し、foo ユーザーとしてアクセスできるようにする
useradd $USER
cat <<PASSWORD | passwd $USER
$PASSWORD
$PASSWORD
PASSWORD
cat <<PASSWORD | smbpasswd -a -s $USER
$PASSWORD
$PASSWORD
PASSWORD

# samba サーバーを起動する
/usr/sbin/smbd -F -S --no-process-group

しかしこのファイル生成方法は、smb.conf のテンプレートがスクリプト中にあって気持ちが悪い。 こんなとき Ruby なら ERB が使えるのだが、インストールすると依存する複数のパッケージも入ってしまうし、テンプレートを使いたいだけなのにちょっと大げさだ。

# Ubuntu 19.10 の場合
$ root@f86b2af49215:/# apt install ruby
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following additional packages will be installed:
  ca-certificates fonts-lato javascript-common libgdbm-compat4 libgdbm6 libjs-jquery libruby2.5 libyaml-0-2
  openssl rake ruby-did-you-mean ruby-minitest ruby-net-telnet ruby-power-assert ruby-test-unit ruby-xmlrpc
  ruby2.5 rubygems-integration unzip zip
Suggested packages:
  apache2 | lighttpd | httpd gdbm-l10n ri ruby-dev bundler
The following NEW packages will be installed:
  ca-certificates fonts-lato javascript-common libgdbm-compat4 libgdbm6 libjs-jquery libruby2.5 libyaml-0-2
  openssl rake ruby ruby-did-you-mean ruby-minitest ruby-net-telnet ruby-power-assert ruby-test-unit
  ruby-xmlrpc ruby2.5 rubygems-integration unzip zip
0 upgraded, 21 newly installed, 0 to remove and 23 not upgraded.
Need to get 7568 kB of archives.
After this operation, 31.9 MB of additional disk space will be used.
Do you want to continue? [Y/n]

Ruby には軽量版の mruby がある。 素の mruby のバイナリは Ubuntu 19.10 なら apt install mruby でインストールできるが、これには ERB がバンドルされておらず利用できないので、代わりに mitamae を使うことにする。 プロビジョニングツールであり ERB を使ってテンプレートから設定ファイルを生成し配置する機能をもともと備えているし、シングルバイナリで 2.28MB (v1.10.5の場合)とコンパクトなので、インストールも手間がかからない。

Dockerfile

FROM ubuntu:latest

RUN apt update \
    && apt install -y \
    samba

# mitamae の latest の URL を指定する。
# 最新版のバイナリへのリダイレクトを伴うためか、変更がなくとも docker build の度に毎回ダウンロードが走るので、気になる場合は指定バージョンの URL を使う。
ADD https://github.com/itamae-kitchen/mitamae/releases/latest/download/mitamae-x86_64-linux /root/mitamae
RUN chmod +x /root/mitamae

COPY samba.sh mitamae /root/

EXPOSE 139 445

CMD ["/root/samba.sh"]

samba.sh

#!/bin/sh

(cd /root && mitamae local setup.rb)
# mitamae にはユーザー追加機能も任意のコマンド実行機能もあるので、`passwd` `smbpasswd` も `setup.rb` に切り出してしまう。

# samba サーバーを起動する
/usr/sbin/smbd -F -S --no-process-group

setup.rb

# /etc/samba/smb.conf 生成
template "/etc/samba/smb.conf" do
  variables(user: ENV["USER"])
end

# ユーザー追加
user ENV["USER"] do
  password ENV["password"]
end

execute "Add a samba user" do
  command <<-COMMAND.gsub(/^ */, "")
    cat <<PASSWORD | smbpasswd -a -s #{ENV["USER"]}
    #{ENV["PASSWORD"]}
    #{ENV["PASSWORD"]}
    PASSWORD
  COMMAND
end

mitamae/templates/etc/samba/smb.conf.erb

[global]
        log file = /var/log/samba/%m

[share]
        path = /mount
        guest ok = no
        valid users = <%= @user %>

テンプレート部を別ファイルに切り出せた。

また mitamae 化の他の利点として、実行時にシステムへの変更がわかりやすく差分表示されることを記しておきたい。

パラメーターを JSON にしてみる

mitamae は JSON をパースできる。パラメーターを JSON で渡すことを考えてみる。

param = JSON.parse(ENV["SMB_PARAM"])
user = param["user"]

template "/etc/samba/smb.conf" do
  variables(user: user["name"])
end

user user["name"] do
  password user["password"]
end

execute "Add a samba user" do
  command <<-COMMAND.gsub(/^ */, "")
    cat <<PASSWORD | smbpasswd -a -s #{user["name"]}
    #{user["password"]}
    #{user["password"]}
    PASSWORD
  COMMAND
end
$ smb_param=$(cat <<PARAM
{
  "user": {
    "name": "foo",
    "password": "pass"
  }
}
PARAM

$ sudo docker run --rm -p 139:139 -p 445:445 -e SMB_PARAM="$smb_param"

コード量をそれほど変えることなく JSON に対応することができた。 これにより

  • パラメーターを階層化でき、追加や削除、環境変数名前空間管理が楽 (プレフィックス付きの長い環境変数名にしなくていい)
  • 配列が扱える (上記の例だとユーザーを複数にするとき USER1, USER2, ... などとせず "user": [...] とできる)
  • 環境ごとにパラメーターをまとめて管理できる (それぞれの JSON ファイルを作ればいい)

などの利便性を得られる。

なお実装は nowlinuxing/docker-samba においてあるので、気になる方は参照されたし。

GitHub ActionsでRubyプロジェクトのCIをしてみた

エントリ要約

はじめに

しばらく前からGitHub Actionsがパブリックベータとして公開されていた。 申請をしてから結構待つ必要があった(1〜2週間かかったと思う)が利用できるようになったので、自前のRubyプロジェクトのリポジトリに適用してみた。 なお今は申請するとすぐ利用できるとのこと。

基本設定

いずれ何もしなくても使えるようになるのかもしれないが、現時点では申請が必要。
こちらからできる。

申請が通るとGitHubリポジトリ上部に"Actions"というタブが表示される。

GitHub Actionsの設定ページ
GitHub Actionsの設定ページ

ここからワークフローの雛形を選べるので、"Ruby"を選択。 ワークフローの設定ファイルの編集画面になるので、適宜編集してコミット。 あとはこのファイルをカスタムしていく。

テスト

jobs.build.stepsを以下のようにする。

jobs:
  build:
    # 省略
    steps:
    - uses: actions/checkout@v1

    - name: Set up Ruby 2.6
      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.6.x

    - name: Build and test
      run: |
        # 依存するgemのインストールに必要なOSパッケージのインストールなどはこのあたりに記載する
        # sudo apt-get update
        # sudo apt-get install -y libsqlite3-dev
        gem install bundler
        bundle install --jobs 4 --retry 3
        # rspec を実行する
        bundle exec rspec

マトリックステスト

複数のバージョンのrubyでテストする場合はjobs.build.strategy.matrixを追加し、jobs.build.stepsSet Up Ruby 2.6アクションのruby-versionを書き換える。

jobs:
  build:
    runs-on: ubuntu-latest

    # ここを追加。なお現時点で利用できる Ruby は 2.3.7, 2.4.6, 2.5.5, 2.6.3 の模様
    strategy:
      matrix:
        ruby:
          - 2.5.x
          - 2.6.x

    # 略

    steps:
    - uses: actions/checkout@v1

    - name: Set up Ruby 
      uses: actions/setup-ruby@v1
      with:
        # matrix.ruby に置き換える
        ruby-version: ${{ matrix.ruby }}

依存するgemをマトリックスにする場合は、bundlerの環境変数BUNDLE_GEMFILEGemfileを指定して実行する機能を利用する。 自力で頑張ることもできるだろうが、thoughtbot/appraisalを使えばローカルでの複数バージョンテスト一括実行も楽に行えるのでおすすめ。

    strategy:
      matrix:
        ruby:
          - 2.5.x
          - 2.6.x

        gemfile:
          - rails_5.2.gemfile
          - rails_6.0.gemfile

    # 略

    steps:
      # 略

      - name: Build and test
        # env を追加
        env:
          BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}
        run: |
          gem install bundler
          bundle install --jobs 4 --retry 3
          bundle exec rspec

Code Climateでテストカバレッジを確認する

"Build and test"アクションの前後にcc-test-reporterを実行するアクションを追加する。

事前にCode Climate側でリポジトリを追加し、Test Reporter IDを取得しておく必要がある。
リポジトリSettingSecretsに登録しておくとYAML内で参照できるようになるため、CC_TEST_REPORTER_IDとして登録する。

    steps:
    - uses: actions/checkout@v1
    - name: Set up Ruby
      uses: actions/setup-ruby@v1
      with:
        ruby-version: ${{ matrix.ruby }}

    # 
    - name: Setup Code Climate test-reporter
      env:
        CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
      run: |
        curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
        chmod +x ./cc-test-reporter
        ./cc-test-reporter before-build

    - name: Build and test
      env:
        BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}
      run: |
        gem install bundler
        bundle install --jobs 4 --retry 3
        bundle exec rspec

    - name: Upload coverage
      env:
        CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
        # GIT_COMMIT_SHA は設定しなくても参照できているが、ドキュメントに GIT_BRANCH と共に指定するよう記載されているので一応セットしておく
        GIT_COMMIT_SHA: ${{ github.sha }}
      run: |
        # ブランチ名のみを保持する環境変数や context は見つからなかったので、$GIT_BRANCH から抽出する
        GIT_BRANCH=$(echo $GITHUB_REF | sed -e 's|.*/||') ./cc-test-reporter after-build

なお、テストカバレッジを取得するにはテスト実行時にSimpleCovも実行しておかなければならない。

# spec/spec_helper.rb

require 'simplecov'
SimpleCov.start

テストカバレッジの取得をCI時のみに限る場合は以下のようにする。

if ENV["COVERAGE"]
  require 'simplecov'
  SimpleCov.start
end
    - name: Build and test
      env:
        BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}
        # COVERAGE をセット
        COVERAGE: true
      run: |
        gem install bundler
        bundle install --jobs 4 --retry 3
        bundle exec rspec

これでCode Climateで結果を確認できるようになる。 同時にRepo SettingsBadgesでバッヂも取得できるので、READMEに追加する。