管理スクリプトのRuby化によるNode.js依存の排除

rails/devcontainer

Ruby開発環境の管理スクリプト add-ruby-version を440行のNode.jsスクリプトから246行のRubyライブラリに書き直し、Node.js依存を完全に排除しました。この変更により、Rails DevContainerプロジェクト自体が純粋なRuby環境で完結するようになっています。

背景

add-ruby-version スクリプトは、新しいRubyバージョンをdevcontainer設定に追加する自動化ツールです。従来はNode.jsで実装されており、js-yamlsemver などのnpmパッケージに依存していました。

このPRでは、440行のNode.jsスクリプトを246行のRubyライブラリに置き換える形で全面的な書き直しが行われています。Diffを見ると、bin/add-ruby-version から443行のコードが削除され、10行のシンプルなエントリポイントに置き換えられています。実装の本体は新規作成された lib/add_ruby_version.rb に移動し、標準ライブラリの jsonfileutils のみで動作する設計に変わりました。

技術的な変更

スクリプトの全面書き直し

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-yamlsemver への依存が、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.yamlruby-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-versionGemfileRakefile が追加され、標準的な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の標準ライブラリのみで動作する構成になっています。テストスイートの追加は、単なる移植ではなく品質向上を伴う刷新であることを示しています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が明確です。特に、PRに明記されていない設計思想をコードから読み解き「設計判断」セクションとして独立させている点は、読者の深い理解を助ける素晴らしい構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトやGitHubのPRリンク記法が、ガイドライン通りに正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

DevContainerやGitHub Actionsに関する知識を前提とした、専門のエンジニア向けに適切な技術レベルと表現で記述されています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているすべてのコードブロックは、提供されたDiffの内容と正確に一致しています。変更箇所の抜粋や要約も適切です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Shebang、jq、fromJSON、Minitestなど、関連する技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

Node.jsからRubyへの書き換え、GitHub Actionsワークフローのmatrix生成方法の変更、テストスイートの追加など、すべての技術的な説明がDiffの内容に基づいており、正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

PR Descriptionが存在しないにもかかわらず、Diffの内容を徹底的に分析し、事実に基づいた記述に終始しています。コード行数の変化や依存関係の解消など、すべての主張がDiffから裏付け可能です。ハルシネーションは一切ありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#110)、コード行数(440行、246行など)、Rubyバージョン(4.0.1)など、記事中のすべての数値と固有名詞が正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「管理スクリプトのRuby化によるNode.js依存の排除」は、PRの主題「Rewrite add-ruby-version in Ruby」をより具体的に、かつ変更の意義を含めて表現しており、非常に秀逸です。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はPR情報に限定されており、バージョンのサポート状況など、PRに記載のない外部知識の不適切な追加はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内で使用されている時間表現に問題はなく、PRの内容を正確に反映しています。