管理スクリプトのRuby化によるNode.js依存の排除
Ruby開発環境の管理スクリプト add-ruby-version を440行のNode.jsスクリプトから246行のRubyライブラリに書き直し、Node.js依存を完全に排除しました。この変更により、Rails DevContainerプロジェクト自体が純粋なRuby環境で完結するようになっています。
背景
add-ruby-version スクリプトは、新しいRubyバージョンをdevcontainer設定に追加する自動化ツールです。従来はNode.jsで実装されており、js-yaml や semver などのnpmパッケージに依存していました。
このPRでは、440行のNode.jsスクリプトを246行のRubyライブラリに置き換える形で全面的な書き直しが行われています。Diffを見ると、bin/add-ruby-version から443行のコードが削除され、10行のシンプルなエントリポイントに置き換えられています。実装の本体は新規作成された lib/add_ruby_version.rb に移動し、標準ライブラリの json と fileutils のみで動作する設計に変わりました。
技術的な変更
スクリプトの全面書き直し
bin/add-ruby-version が10行のシンプルなエントリポイントに変わりました:
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative "../lib/add_ruby_version"
if ARGV.length != 1
warn "Usage: bin/add-ruby-version <ruby-version>"
warn "Example: bin/add-ruby-version 3.4.5"
exit 1
end
AddRubyVersion.call(ARGV[0], working_dir: Dir.pwd)
Shebangが #!/usr/bin/env node から #!/usr/bin/env ruby に変更され、ランタイムがNode.jsからRubyに切り替わっています。実装の本体は lib/add_ruby_version.rb に移動し、AddRubyVersion モジュールとして整理されました。コード量は443行から246行へと半減し、外部パッケージ依存がゼロになっています。
lib/add_ruby_version.rb の構造は以下の通りです:
module AddRubyVersion
class Error < StandardError; end
VERSIONS_JSON_FILE = ".github/ruby-versions.json"
FEATURE_JSON_FILE = "features/src/ruby/devcontainer-feature.json"
README_FILE = "features/src/ruby/README.md"
TEST_FILES = [
"features/test/ruby/test.sh",
"features/test/ruby/with_rbenv.sh"
].freeze
VERSION_PATTERN = /\d+\.\d+\.\d+/
class << self
def call(version, working_dir:, output: $stdout)
runner = Runner.new(version, working_dir: working_dir, output: output)
runner.call
end
end
class Runner
# 実装の詳細
end
end
Node.js版の js-yaml と semver への依存が、Rubyの標準ライブラリに置き換えられています。YAMLパースは不要になり(後述のJSON化による)、バージョン比較は正規表現と文字列操作で実装されています。
ワークフロー設定の分離
Rubyバージョンリストを .github/ruby-versions.json として外部化しました:
[
"4.0.0",
"3.4.8",
"3.4.7",
...
"3.2.0"
]
.github/workflows/publish-new-image-version.yaml では、このJSONファイルからmatrixを動的に生成する方式に変更されています:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: set-matrix
run: echo "matrix=$(cat .github/ruby-versions.json | jq -c '.')" >> $GITHUB_OUTPUT
build:
name: Build Images
needs: setup
strategy:
fail-fast: false
matrix:
RUBY_VERSION: ${{ fromJSON(needs.setup.outputs.matrix) }}
YAMLファイル内に32個のバージョンをハードコードしていた構造(32行の削除)が、JSONファイルの読み込みに置き換えられました。setup ジョブが追加され、jq でJSONを読み込んでmatrixとして出力する方式です。build ジョブは needs: setup で依存関係を宣言し、fromJSON 関数でmatrixを展開します。
テスト環境の整備
Ruby実装への移行に伴い、本格的なテストスイートが追加されています。test/add_ruby_version_test.rb では、Minitestを使用した391行のテストケースが実装されています:
class AddRubyVersionTest < Minitest::Test
def setup
@temp_dir = Dir.mktmpdir("add-ruby-version-test")
# Create the directory structure
FileUtils.mkdir_p(File.join(@temp_dir, ".github"))
FileUtils.mkdir_p(File.join(@temp_dir, "features/src/ruby"))
end
def test_rejects_invalid_version_format_no_dots
setup_valid_environment
result = run_script("330")
refute result[:success]
assert_match(/invalid version format/i, result[:output])
end
end
テンポラリディレクトリを使用した隔離環境でのテストにより、実ファイルへの影響なく動作検証が可能になっています。.github/workflows/test-features.yaml に ruby-tests ジョブが追加され、CIパイプラインでの自動テストが実行されます:
ruby-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Run tests
run: bundle exec rake test
新たに追加された ci ジョブは、4つのテストジョブすべての結果を統合し、いずれかが失敗した場合にCI全体を失敗させます。
開発環境の統一
プロジェクトルートに .ruby-version、Gemfile、Rakefile が追加され、標準的なRubyプロジェクト構成になりました:
4.0.1
# frozen_string_literal: true
source "https://rubygems.org"
gem "minitest"
gem "rake"
# frozen_string_literal: true
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
t.verbose = true
end
task default: :test
.devcontainer/devcontainer.json にもRubyフィーチャー(version 4.0.1)が追加され、DevContainer内で一貫したRuby環境が提供されます:
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/rails/devcontainer/features/ruby": {
"version": "4.0.1"
},
"ghcr.io/devcontainers/features/github-cli": {}
}
これにより、DevContainer起動時に自動的にRuby 4.0.1がインストールされ、bundle install も実行される環境が整います。
設計判断
Node.jsスクリプトをそのまま移植するのではなく、Rubyのイディオムに沿った設計への再構築が行われました。
モジュール設計: 元のスクリプトがグローバル関数の集合だったのに対し、Ruby版は AddRubyVersion モジュールと Runner クラスによる階層化された構造を採用しています。AddRubyVersion.call というシンプルなエントリポイントを提供しつつ、内部実装の詳細を Runner クラスにカプセル化する方針です。カスタムエラークラス AddRubyVersion::Error の定義により、エラーハンドリングもRubyの慣習に従っています。
JSONファイルの外部化: ワークフローYAML内のmatrix定義をJSONファイルに分離した判断は、データと設定の分離原則に基づいています。Rubyスクリプトは .github/ruby-versions.json を更新するだけで済み、GitHub Actionsの fromJSON 関数がmatrix展開を担当します。YAMLパーサーへの依存が不要になり、標準の json ライブラリだけで動作します。この変更により、スクリプトの責務は「バージョン配列の更新」に特化され、ワークフロー構文の理解は不要になっています。
テストファーストへの転換: Node.js版にはテストが存在しませんでしたが、Ruby版では391行のテストスイートが最初から用意されています。一時ディレクトリを使用した隔離テストにより、実ファイルを汚染せずに動作検証が可能です。この判断は、スクリプトの信頼性を高めると同時に、将来の機能追加時の安全性を確保しています。ci ジョブの追加により、すべてのテストが通過しない限りCIが成功しない仕組みも導入されました。
本PRは、ツールの実装言語とプロジェクトの主要言語を一致させることで、技術スタックを統一した変更です。Node.jsおよびnpmパッケージへの依存が完全に排除され、Rubyの標準ライブラリのみで動作する構成になっています。テストスイートの追加は、単なる移植ではなく品質向上を伴う刷新であることを示しています。