CIでRailsのmasterブランチを使ってテストを実行する

Rails edgeでCIを動かしたい酔狂な人向けに記事を書いておく。

Rails edgeのGemfileを用意する

# gemfiles/rails_61.gemfile
eval_gemfile File.expand_path('../Gemfile', __dir__)

dependencies.delete_if { |d| d.name == 'rails' }
gem 'rails', github: 'rails/rails'

Rails edge用のGemfileを使う

BUNDLE_GEMFILE を指定することで、別ディレクトリにある Gemfile を使用できる。

$ BUNDLE_GEMFILE=gemfiles/rails_61.gemfile bundle install

CircleCIで定期的に実行する

Gemfile のときのキャッシュが効くように BUNDLE_PATH_RELATIVE_TO_CWD をつけている。

  rspec-edge:
    executor: rails
    environment:
      BUNDLE_GEMFILE: gemfiles/rails_61.gemfile
      BUNDLE_PATH_RELATIVE_TO_CWD: true
    steps:
      - checkout
      - run: cp Gemfile.lock gemfiles/rails_61.lock
      # あとは bundle-install して、テストを実行すれば良い

workflows:
  edge-rails:
    triggers:
      - schedule:
          cron: "0 0 * * 0" # At 00:00 on Sunday
          filters:
            branches:
              only:
                - master
    jobs:
      - rspec-edge:

gemでVERSION定数が定義されていない場合にgemのバージョンを取得する方法

ときどきVERSIONの定義されていないgemに遭遇するので、備忘録として書いておく。

VERSION の無いgem

いくつかのパターンがある。

バージョン情報をBundlerで取得する

Bundlerで取得すれば、確実に文字列のバージョン情報を得ることができる。

Bundler.definition.specs['database_rewinder'][0].version.to_s
#=> "0.9.4"

るりまの開発環境をDockerで作ってみた

はじめに

あとはVSCodeなどでコンテナにアクセスするなどして編集すればOKそうです(もしくはvimとか入れてコンテナ内で編集するとか)

gamelinks007.hatenablog.com

この記事を読んで「ホスト側で編集するようにできそう」と思ったので、試しに開発環境を作ってみた。

rurema/doctree のクローン

まず、Gitリポジトリをクローンします。

$ git clone https://github.com/rurema/doctree.git

Docker の設定

rurema/doctreeディレクトリ直下に Dockerfile を作ります。

FROM ruby:2.7.0

WORKDIR /home

RUN gem install --no-document bundler:1.16.1

COPY Gemfile ./
COPY Gemfile.lock ./
RUN bundle install

ENTRYPOINT ["bundle", "exec"]

次に docker-compose.yml を作ります。

version: "3.8"

services:
  rurema:
    build: .
    volumes:
      - .:/home
      - html-data:/home/_site
    environment:
      HTML_DIRECTORY_BASE: _site

  nginx:
    image: nginx:alpine
    volumes:
      - html-data:/usr/share/nginx/html:ro
    ports:
      - "80:80"

volumes:
  html-data:

htmlを誤ってコミットしないように、 .gitigreno に記載されていた _site にhtmlを出力するようにしてある。

htmlを生成する

$ docker-compose run rurema rake

複数のバージョンをビルドすると時間かかるので、普段はバージョンを指定してビルドした方が良いかも。

$ docker-compose run rurema rake statichtml:2.7.0

ブラウザでhtmlを確認する

$ docker-compose up nginx

http://localhost/2.7.0/ をブラウザで開くとhtmlを確認できる。

Railsで認証機能を自作する?それともDeviseを使う?

定期的にDevise批判の話が出てくるので、個人的な考えを書いてみます。

Railsに詳しくないなら、Deviseを使わないべきか?

認証自作、 Rails 、 Devise」の記事で以下のような記載がある。

「Rails について深い理解がないならば、 Devise は使うな」とあります。この方針は10 年近く前から書かれています。

これ元の英語とあってない気がするんですよね。

If you are building your first Rails application, we recommend you do not use Devise. Devise requires a good understanding of the Rails Framework. In such cases, we advise you to start a simple authentication system from scratch. Here's a few resources that should help you get started:

- Michael Hartl's online book: https://www.railstutorial.org/book/modeling_users
- Ryan Bates' Railscasts: http://railscasts.com/episodes/250-authentication-from-scratch and http://railscasts.com/episodes/250-authentication-from-scratch-revised
- Codecademy's Ruby on Rails: Authentication and Authorization: https://www.codecademy.com/learn/rails-auth

Once you have solidified your understanding of Rails and authentication mechanisms, we assure you Devise will be very pleasant to work with. 😃

私には「最初のRailsアプリではDeviseを使わず、シンプルな認証機能を自作して理解を深めるのを勧める」「Railsと認証メカニズムの理解を深めたら、Deviseが快適に動くのが分かるよ」くらいの内容に読めました。1

IPアドレスが漏れる危険性がある?

試してみた。

require "devise/version"
Devise::VERSION
#=> "4.7.2"
user = User.create(email: 'a@example.com', password: 'password')
user.to_json
#=> => "{\"id\":1,\"email\":\"a@example.com\",\"created_at\":\"2020-08-16T08:31:36.558Z\",\"updated_at\":\"2020-08-16T08:31:36.558Z\"}"
user.attributes.to_json
#=> "{\"id\":1,\"email\":\"a@example.com\",\"encrypted_password\":\"$2a$12$ECNX7kXJQKMSBp8xDcy5neVs/2NeuTdcqVnPyUbcy8fp.p4EWfBa6\",\"reset_password_token\":null,\"reset_password_sent_at\":null,\"remember_created_at\":null,\"sign_in_count\":0,\"current_sign_in_at\":null,\"last_sign_in_at\":null,\"current_sign_in_ip\":null,\"last_sign_in_ip\":null,\"confirmation_token\":null,\"confirmed_at\":null,\"confirmation_sent_at\":null,\"unconfirmed_email\":null,\"failed_attempts\":0,\"unlock_token\":null,\"locked_at\":null,\"created_at\":\"2020-08-16T08:31:36.558Z\",\"updated_at\":\"2020-08-16T08:31:36.558Z\"}"

attributesメソッドには全属性が含まれちゃうので、漏れる可能性はありそう。

ただ、これDeviseの問題なのかな...。
IPアドレスがDBに保存されている場合2、認証を自作してたとしても実装ミスると漏れるときは漏れると思うんですよね。

テストコードで防ぐ

Twitterはてブで「レビューで防ぐべき」とかあったけど、レビューのような人依存な対策だと漏れがあるので、テストコード書くのが良いかなと思いました。

# spec/rails_helper.rb
RSpec.configure do |config|
  # html/jsonに含まれてたらヤバそうな文字列を列挙しておく
  secrets = %w[password lastSignInIp]

  config.after(type: :request) do |ex|
    secrets.each { |secret| expect(response.body).not_to include(secret) }
  end
  config.after(type: :system) do |ex|
    secrets.each { |secret| expect(page.body).not_to include(secret) }
  end
end

カバレッジがある程度高ければ、上のコードでだいたい防げるんじゃないかなと。

機能が少ないので自作の方がメンテしやすい?

個人的な経験ですが、認証機能が少ないままで済んだ事がないです。

  • パスワードリセットが必要
  • パスワード失敗回数でロックしたい
  • KPIのためにログイン回数が知りたい
  • Facebook, Twitterログインが使いたい

のような要望が後から出ることが多い。

特に認証機能はプロジェクト初期に作られるためテストコードが不足していたり、誰も壊したくないので条件分岐が雑に増えていってAbcSizeが高くなる状況が生まれやすいです。

個人的な見解

認証機能を自作する自信がないなら、最初からDeviseを使っておくのが無難じゃないかなーと思ってます。

お金あるなら Deviseを使い慣れているフリーランス技術顧問 してる人と短期の契約をして、相談するのも良いと思います。

認証機能を自作するなら

少なくとも以下の調査はしておいた方が良いです。

  • Deviseのコード内にあるセキュリティ対策のコメントを調査する
    • timing attacks, session fixation attacks, ...etc
  • Deviseのリダイレクト関連の挙動を調査する
    • (認証が必要な)ページA => ログイン => ページA の画面遷移の処理
    • パスワードリセットのURLを踏んだときのリダイレクト先3
  • Deviseのテストヘルパーの挙動を調査する
    • HTTPリクエストを投げずにログイン状態を再現するので速い4
  • Deviseのモジュールと同じ要件がきた時のことを考慮する
    • アカウントロックとか後から出てきがち
  • 他gemが提供するDevise連携を使えない事を理解する

Deviseにあるような機能は将来に必要になる可能性が高いので、ある程度は考慮しておいた方が良いです。

あと、Deviseを避けて認証機能を自作した結果、Deviseが何年も前に対応済みの脆弱性を再実装するのは勿体ないです。

まとめ

Deviseが最高の認証ライブラリとは思ってないけど、十分に便利なライブラリだとは思ってます。

認証機能のセキュリティをちゃんと考えて実装できる人はさておき、Rails初心者〜中級者ならDeviseを使うでも良いのかなと。


  1. 英語力は自信ないので、各自でちゃんと英語を読んでほしい。

  2. アクセス解析、不正なログインの検知などでIPアドレスをDBに保存することはある。

  3. パスワードリセットURLを踏んで、自動ログイン…とか書かないように。メアド間違えたら、他人がログインできてしまう。

  4. Sorceryのテストヘルパーはリクエスト投げてて遅い🐢

仕事でGitのコミットメッセージをちゃんと書けているか?ゲーム

仕事のコミットメッセージは雑になっていることが多い。

それを解決する方法を考えていたらふと思いついたので、ブログに書いておく。

基本ルール

  1. 後述するコマンドでGitのコミットを 5つ 取得する
  2. git show コマンドでコミットのログとdiffを表示する
  3. そのまま変更内容を説明できれば 5点
    • 少なくとも「なぜ必要だったのか?」と「その変更は現在も必要か?」くらいは説明して欲しい
  4. コミットメッセージから外部URLを開くと -1点
    • リンク先で外部URLを開くと 更に-1点
  5. コミットに関連するプルリクの概要、コミット一覧を閲覧したら -2点
  6. コミットの author に質問する場合 -3点
  7. コミットの author が退職者でもう聞けない場合 -5点
  8. 5つのコミットで点数を計算し、足した値が合計点

バグ対応で関係あるコミットを見つけたとき、そのコミットの意図を調べる時間が短いほど高得点...みたいな意図で点数を配分してます。

普通の遊び方

以下のように自分のユーザー名を author で指定しつつ、3ヶ月前のコミットをランダムで5つ選ぶ。

$ git log --author=sinsoku \
  --since="4 months ago" --until="3 months ago" \
  --no-merges --oneline | sort --random-sort | head -n 5

「3ヶ月前の自分は他人」という言葉もあるけど、自分のコミットは説明できるかどうか?

定期的に合計点を計測しておけば、自分のコミットメッセージの改善を定量的に測れるかも。

チームで遊ぶ

authorを指定しないことでリポジトリにコミットしていた人の全員を対象にできます。
(コミット数は5よりもメンバー数にして、1人1コミットを説明する方が良さそう)

$ git log \
  --since="4 months ago" --until="3 months ago" \
  --no-merges --oneline | sort --random-sort | head -n 5

文字通り「他人の書いたコミット」を git show で表示し、その人の前で説明するのは面白いと思う。

ただ、"レビュー指摘修正" みたいな雑コミットが当たると気まずくなる可能性はある。

チーム対抗戦

2つのリポジトリ(2つのチーム)で合計点を競う。

  • コミットの説明を他チームに対して行う
  • 他チームの人が別チームのコミットの説明をする

などチームのメンバーを混ぜると面白いと思う。

これ読んで「他チームの仕様を説明できる必要があるのか?」と思う人がいると思うけど、いきなり別チームに異動することはある。
人事異動はいつだって突然降ってくる。

まとめ

深夜に思いついたネタを適当に書いた。

仕事のリポジトリで試してみたい。

Gitの書籍に出てこないようなGitの使い方を2つ紹介

Twitterの140文字だと説明しづらいので、ブログに書く。

ケース1: 過去のコミットを部分的に戻す

概要

a001から分岐してb001-003まで3つコミットをした後、b001のコミットを修正したいケース。

f:id:sinsoku:20200211234320p:plain

よくある方法

git rebase -i を使って、b001のコミットを edit にする。

詳細はググれば出てくるので省略。

便利: 相殺コミットをぶつける方法

b001の一部を修正したコミットb004を用意します。

f:id:sinsoku:20200211235128p:plain

git rebase -i a001 すると以下のような状態でエディタが開く。

pick a001 hello
pick b001 foo
pick b002 bar
pick b003 buz
pick b004 fix_foo

これの順番を入れ替えて、b004をfixupに変える。

pick a001 hello
pick b001 foo
fixup b004 fix_foo
pick b002 bar
pick b003 buz

保存してエディタを閉じると、b001のコミットにb004のコミットを混ぜる(一部を戻す)ことができる。

メリット

  • 途中でコンフリクトしたときにgit rebase --abort で戻れる
  • commit --fixup と組み合わせるとより便利
  • 修正コミットは git checkout <sha1> -- <path> などで作っても良い

過去のツイート

ケース2: 作業中のファイルを一時的に退避する

a001から分岐したfeatureブランチでb001のコミットをした後、別の作業を依頼されたケース。

f:id:sinsoku:20200212002030p:plain

よくある方法

別ブランチをa001から切って、作業を進める。

便利: 一時的にリバートする

とりあえずb001をリバート(b001')して、別の作業を済ませて c001 のコミットを作ります。

f:id:sinsoku:20200212002638p:plain

次にc001だけを持つブランチを作ります。

$ git switch -c fix_bug
$ git rebase -i a001
# エディタが開くので、c001 のコミットだけ残す

fix_bugのブランチでプルリクを作ったら、元のブランチに戻ってリバートコミットを取り除きます。

$ git switch feature_1
$ git rebaes -i a001
# エディタが開くので、b001'とc001のコミットを取り除く

最終的に以下のようなグラフになって、b001から作業を再開できる。

f:id:sinsoku:20200212002647p:plain

メリット

  • ブランチの切り替えが減る
  • DBのスキーマを変えずに済む
    • テストデータなども消えない
  • ライブラリのバージョンを変えずに済む
    • masterでgemが更新されていると bundle-install に時間がかかるけど、これを回避できる
  • コンフリクトするコミットだけをリバートするだけなので簡単
    • ブランチの全コミットをリバートする必要はない

過去のツイート

まとめ

ブログに書いてみたけど、やっぱり分かりづらい。

別のディレクトリにある Gemfile をsystemメソッドで "bundle check" する

タイトルの通りなのですが、普通にやると意図した動きしなかったのでメモ。

具体的な例

factory_bot が分かりやすいです。

具体的なコマンドは以下の通り。

$ git clone --depth 1 https://github.com/thoughtbot/factory_bot.git
$ cd factory_bot
$ bundle install
$ bundle exec ruby -e 'system({ "BUNDLE_GEMFILE" => "6.0.gemfile" }, "bundle check", chdir: "gemfiles/")'
Could not find zeitwerk-2.1.9 in any of the sources
Run `bundle install` to install missing gems.

動くコマンド

bundler経由で実行すると RUBYOPT="-r/usr/local/lib/ruby/2.7.0/bundler/setup" がセットされるので、これを上書きすると動く。

$ bundle exec ruby -e 'system({ "BUNDLE_GEMFILE" => "6.0.gemfile", "RUBYOPT" => ""}, "bundle check", chdir: "gemfiles/")'
The following gems are missing
 * zeitwerk (2.1.9)
 * childprocess (2.0.0)
 * aruba (0.14.11)
 * simplecov (0.17.0)
Install missing gems with `bundle install`

bundle install する前なので、正しく 6.0.gemfile を参照してる挙動になった。

追記

Bundler.with_clean_system を使う方法を Twitter で教えてもらった。

こっちの方がシンプルで、良いですね!