rbs_rails や RBS::Inline といった型抽出ツールを使う場合、ソースコードを更新するごとにツールを実行して RBS ファイルを更新する必要があります。
各種ツールのうち、RBS::Inline はファイルの保存時に自動実行するように設定しておくと、透過的に型定義ファイルを更新できます。かなり高速に動作するため、開発体験は特に悪化しません。
RBS::Inline の README では fswatch を使って監視する方法が紹介されていますが、VSCode の場合は拙作の VSCode 拡張 RBS Helper を利用すると、コマンドを起動することなく自動的に実行されます。
一方、rbs_rails やその他の型抽出ツールはもう少しやっかいです。
これらの型抽出ツールは 1回の実行に数十秒〜数分かかるものが多く、コードを編集するたびに実行するには高コストです。実装が一段落し、PR を作成する直前に都度実行すればよいのですが、実行しわすれてコードと型が一致しない状態になりがちです。
また、チームの都合もあります。我々のチームでは、現状では型に興味があるメンバーを中心に型を整えている状態で、プロジェクト全体で型を整えるというルールにはしていません。そのため、開発時のルールを増やしてメンバーの負担を増やすのは避け、GitHub Actions を使って型の抽出を自動化することにしています。
型の便利さを理解してもらい、型に興味を持ってもらおうという太陽と北風作戦を採っているというわけです。
GitHub Actions を使った型定義の自動更新は、以下のようなフローで実現します。
この構成では 2種類のジョブを利用します。
なお、この構成では CI による型チェックは行いません。
型チェックをパスすることをマージの前提条件としてしまうと、開発者が作った PR が型不足でエラーになってしまうことがあるため、この構成は選択できません。ここでは、ソースコードから型が自動的に抽出されること、そして、抽出された型を使って開発支援 (サジェスト等) を受けられることに主眼をおいています。
今回の構成を実現するには GitHub App を作成します。
通常の GitHub Actions では GitHub Actions が用意したトークンを使いますが、このトークンで作成した PR は GitHub Actions が起動されません。どうやら、 再帰的な GitHub Actions の実行を防ぐための措置 のようです。
この制限があるため、型定義の抽出ジョブが作成した PR をトリガーにして、PR を自動マージするジョブを起動できません。
そこで、GitHub App を使ってこの制限を回避します。GitHub App を使うことで、PR の作成をトリガーにして別のジョブを起動させることができます。ここでは「型定義の抽出」と「PR の自動マージ」というふたつの App を用意します。前者を RBS Updater App、後者を PR Auto Merger App と呼ぶことにします。
なお、PAT (Personal Access Token) を使う方法もありますが、個人のアカウントに依存してしまうため、ここでは GitHub App を使います。
GitHub App の作成の詳細は ドキュメントを参考 にしてください。
今回は以下の設定を施しました。
ここで作成された GitHub App の ID と、途中でダウンロードできる秘密鍵ファイル (.pem ファイル) は後ほど利用します。手元に保管しておいてください。
続けて、リポジトリの設定も済ませてしまいましょう。
ここでは 2つの設定をします。
リポジトリの自動マージを有効にします。
General > Pull Requests > Allow auto-merge
にチェックを入れます。
この設定により、自動マージフラグを立てた PR がマージされるようになります。
なお、ブランチルールを設定しておくと自動マージの条件を細かく設定できます。今回の RBS ファイルの自動更新では必須ではないため、説明は省きますが、CI が通っている場合にのみ自動マージするといった条件が設定できます。
GitHub Actions に GitHub App の設定値を渡すために、環境変数および secrets を設定します。
Secrets and variables > Actions
を開き、secrets タブと variables タブからそれぞれ以下の項目を追加します。
*_APP_ID にはそれぞれの GitHub App の App ID を、*_PRIVATE_KEY には対応する秘密鍵ファイル (.pem ファイル) の中身を設定してください。
次に、型定義を更新する Rake タスクを作成します。ここで用意した Rake タスクが GitHub Actions から呼び出されます。
我々のチームでは以下のような Rake タスクを用意しています。
# lib/tasks/rbs.rake
return unless Rails.env.development?
require 'rbs_rails/rake_task'
namespace :rbs do
desc "Install RBS collection"
task install: :environment do
sh 'rbs', 'collection', 'install', '--frozen'
end
desc "Update RBS files"
task update: %i[clean update:all validate]
desc "Clean RBS files"
task :clean do
exceptions = [Rails.root.join('sig/gems'),
Rails.root.join('sig/handwritten')]
sig_dirs = Rails.root.glob('sig/*').reject { |path| path.in? exceptions }.map(&:to_s)
sh 'rm', '-rf', '.gem_rbs_collection/', *sig_dirs
end
desc "Validate RBS files"
task :validate do
sh 'rbs', '-Isig', 'validate'
end
namespace :update do
task all: %i[
collection
prototype
rbs_rails:generate_rbs_for_path_helpers
rbs:actionmailer:setup
rbs:activemodel:setup
rbs:activerecord:setup
rbs:config:setup
rbs:discard:setup
rbs:shrine:setup
rbs:activesupport:setup
subtract
]
task :collection do
sh 'rbs', 'collection', 'update'
end
task :prototype do
sh 'rbs-inline', '--opt-out', '--output=sig/prototype', '--base=.', 'app', 'config', 'lib'
end
task :subtract do
prototype_path = Rails.root.join('sig/prototype')
exceptions = [prototype_path,
Rails.root.join('sig/gems'),
Rails.root.join('sig/handwritten')]
Rails.root.glob('sig/*').reject { |path| path.in? exceptions }.each do |path|
sh 'rbs', 'subtract', '--write', path.to_s, "--subtrahend=#{prototype_path}"
end
end
end
end
この Rake タスクは、 RubyKaigi 2023 の pocke さんのトークで紹介されている Rake タスク をベースにしたもので、
さまざまな形抽出ツールを使うよう中身を置き換えたものです。
ここでは以下のような Rake タスクを用意しています。
rake rbs:install
: ローカル環境に rbs collection をインストールするrake rbs:update
: rbs collection を更新し、ソースコードから型定義を抽出し、型定義を検証するrake rbs:clean
: 生成した型定義を削除するrake rbs:validate
: 型定義の検証するなお、rbs:update
タスクでは以下のようなツールを使って型定義を抽出しています。
これらのツールの詳細についてはここでは触れません。gem の名前が体を表していると思います。使い方については各ツールのドキュメントを参照してください。
最後に GitHub Actions を設定します。
先ほど述べたように、ここでは 2種類のジョブを定義します。
ひとつめのジョブは次のように定義します。
# .github/workflows/rbs.yml
name: RBS
on:
push:
branches:
- develop
permissions:
contents: write
pull-requests: write
jobs:
rbs:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: y
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.RBS_UPDATER_APP_ID }}
private-key: ${{ secrets.RBS_UPDATER_PRIVATE_KEY }}
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4
bundler-cache: true
- name: App setup
run: cp config/env.yml.example config/env.yml
- name: DB setup
run: bundle exec rails db:create db:migrate db:seed
- name: Update types
run: bundle exec rails rbs:update
- name: Create a pull request
uses: peter-evans/create-pull-request@v7
with:
add-paths: |
rbs_collection.lock.yaml
sig/
commit-message: "rbs: Update type signatures"
branch: bot/rbs
title: "rbs: Update type signatures"
token: ${{ steps.app-token.outputs.token }}
このジョブでは、先頭で actions/create-github-app-token を使って GitHub App のトークンを取得しています。
そして、先ほど用意した rbs:update
タスクを実行して型定義を更新し、 peter-evans/create-pull-request を使って PR を作成しています。
ふたつめのジョブは次のように定義します。
# .github/workflows/auto-merge.yml
name: Auto merge Pull Requests
on: pull_request
jobs:
rbs:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'rbs-updater[bot]' && github.repository == 'org_name/repo_name'
steps:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.PR_AUTO_MERGER_APP_ID }}
private-key: ${{ secrets.PR_AUTO_MERGER_PRIVATE_KEY }}
- name: Approve a PR
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
ここでは、rbs-updater という GitHub App が作成した PR をトリガーにして、PR の自動マージを行っています。
gh pr merge コマンドを使って PR にマージフラグを立てています。
GitHub Actions を使って型定義の更新を自動化する方法を紹介しました。
実は PR を作成するところまでは以前から自動化されていましたが、最近までマージは手動で行っていました。自動マージはやればできることはわかっていたのですが、なかなか手を付けられず、つい後回しになっていました。
この設定により、ソースコードを更新するたびに、自動的に型定義が更新されるようになりました。開発者はいつもどおりコードを書き、リポジトリからコードを pull するだけで、最新の型定義が取得できます。
その結果、Steep LSP のサジェストやコードヒント、irb + repl_type_completor によるコード補完など、型による開発支援機能の恩恵をかんたんに受けられます。
ライトに型を導入する人向けの一歩目の構成として、参考にしてみてください。
自動マージまわりについては、スマートバンクさんの GitHub Appを使ってDependabotが作るpull requestを自動マージさせる が参考になりました。ありがとうございます。