Claude Media
Claude CodeでRailsをアップグレードする — app:updateの差分と非推奨警告の片付け方

Claude CodeでRailsをアップグレードする — app:updateの差分と非推奨警告の片付け方

Railsのバージョンを上げるとき、app:updateが出す設定差分の判断と非推奨警告の後始末をClaude Codeに任せる手順です。差分をgit diffで読ませ、警告をclaude -pで分類して直す流れを扱います。

Railsのアップグレードは、Gemfileの数字を変えるだけでは終わりません。bin/rails app:updateが設定ファイルの差分を出し、テストを回すと非推奨警告(deprecation warning)が並び、そのどれを直し、どれを見送るかを自分で決める作業が続きます。Claude Codeに任せやすいのは、この「差分を読んで分類する」部分です。

要点は2つあります。app:updateはターミナルで対話的に答え、その結果のgit diffをClaude Codeに読ませます。警告のほうは、テスト出力から抜き出してclaude -pで分類し、機械的に直せるものだけを実行させます。

作業の順番を決める:RubyとRailsは別々に、マイナー版は1つずつ

Railsのアップグレードガイドは、進め方を先に固定しています。Claudeに「Rails 8.1まで上げて」と一括で頼むと、この順番が崩れます。

手順

ガイドが示すアップグレードの流れ

  1. 1

    Rubyを先に上げる

    RubyとRailsは別々に上げます。Rails 8.0と8.1はRuby 3.2.0以上が必要です。

  2. 2

    現行バージョンの最新パッチへ

    テストが通る状態を作り、まず今のマイナー版の最新パッチに上げます。

  3. 3

    非推奨警告とテスト失敗を直す

    次のマイナー版へ進む前に、出ている警告を片付けます。

  4. 4

    次のマイナー版の最新パッチへ

    目的のバージョンに着くまで、これを繰り返します。

マイナー版を1つずつ上げる理由は、非推奨警告を活かすためです。警告は「次のバージョンで挙動が変わる」という予告なので、飛び級するとその予告を読まずに通り過ぎます。たとえばRails 7.2のalias_attributeの変更は、7.1の時点で警告として予告されていました。7.0から7.2へ直接上げると、その警告に一度も出会えません。

Gemfileのバージョンを変えたあとの操作は、ガイドでは次の4つです。

  • GemfileのRailsのバージョンを変え、bundle update railsを実行する
  • jsbundling-railsを使っているなら、package.jsonのRails関連パッケージも変え、bin/rails javascript:installを実行する
  • app:updateを実行する
  • テストを実行する

この記事は、このうち3つ目と4つ目をClaude Codeでどう進めるかを扱います。

着手前:テストのベースラインとClaudeへの前提を用意する

ガイドの冒頭には、テストカバレッジが十分あることがアップグレードの前提だと書かれています。カバレッジが薄いと、アップグレード後に画面を全部手で確かめることになります。

Claude Codeを使う場合は、もう1つ用意しておくものがあります。作業のルールをCLAUDE.mdに置くことです。claude -pは、--bareを付けない限りCLAUDE.mdを読み込みます。毎回プロンプトに書かなくて済みます。

例えば次のような内容です。数字や手順は自分のアプリに合わせて変えてください。

## Railsアップグレード作業のルール(作業中のみ)
 
- マイナー版は1つずつ上げる。一度に2つ以上飛ばさない
- 変更前に `bin/rails test` を実行し、失敗件数を記録する
- `config.load_defaults` の値は、明示的に指示されるまで変えない
- `config/initializers/new_framework_defaults_*.rb` の設定は、
  1回の指示につき1項目だけ有効にする
- 非推奨警告の修正では、挙動が変わる箇所は直さず、一覧に残して報告する

作業が終わったら、この節は消します。アップグレード専用のルールが日常の開発に残ると、普段の指示を邪魔するためです。

テストを回すコマンドは、権限の面でも先に決めておきます。Claude Codeにbin/rails testを実行させるなら、.claude/settings.jsonに許可ルールを置く方法があります。

{
  "permissions": {
    "allow": [
      "Bash(bin/rails test *)",
      "Bash(bundle exec rubocop *)",
      "Bash(git diff *)",
      "Bash(git status *)"
    ]
  }
}

許可ルールの書式は--allowedToolsと同じです。末尾の *が前方一致を表し、*の前の空白が必要です。この空白がないと、Bash(git diff*)はgit diff-indexにも一致します。

app:updateの差分を読む:対話は自分で、判断はClaudeに

bin/rails app:updateは、新しいファイルの作成と古いファイルの変更を対話形式で進めます。ガイドの出力例では、既存ファイルと競合すると次のように上書きするかを聞かれます。

    conflict  config/application.rb
Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]

この質問にclaude -pで答えさせる方法は、ガイドにもheadlessの説明にも書かれていません。対話は自分のターミナルで進め、終わったあとの差分をClaudeに読ませる分担が堅実です。上書きの可否で迷ったらhで選択肢の一覧が出ます。差分を見る道具はTHOR_DIFFとTHOR_MERGEの環境変数で変えられます。

実行後に、差分をそのままClaudeへ渡します。git diffの出力をパイプで渡すと、Claudeは差分を読むためのBash権限を必要としません。

git diff | claude -p "Railsのapp:update実行後の差分です。\
ファイルごとに、(1)新バージョンの既定値を取り込んだだけ、\
(2)アプリ独自の設定を上書きしている、(3)判断が必要、の3つに\
分けて、(2)と(3)は理由を1行で添えてください。"

(2)の「アプリ独自の設定を上書きしている」が、この作業で最も見落としやすい箇所です。config/application.rbやconfig/environments/*.rbには、アプリごとのタイムゾーン、キャッシュ設定、ホスト許可リストなどが入っています。app:updateでYを選んだファイルは、これらが生成された既定の内容に置き換わっています。Claudeに差分を分類させると、削除された行が自分の追記だったかどうかを拾いやすくなります。

新しい既定値は、一度に有効にしない

app:updateはconfig/initializers/new_framework_defaults_X_Y.rb(版の数字がファイル名に入ります)も作ります。ガイドによると、この時点ではアプリは前のバージョンの既定値で動き続けます。config/application.rbのconfig.load_defaultsをまだ変えていないためです。

新しい既定値は、このファイルのコメントを外して1つずつ有効にします。何回かのデプロイに分けて進めてかまいません。すべて有効にできたら、このファイルを削除してconfig.load_defaultsの値を新しい版に変えます。

Claudeに任せる場合は、1項目ずつ指示します。

claude -p "new_framework_defaults_8_1.rbの最初の未有効の項目を1つだけ\
有効にして、bin/rails testを実行し、結果を報告して。\
失敗したら元に戻して、失敗したテスト名を挙げて。" \
  --allowedTools "Edit,Bash(bin/rails test *),Bash(git checkout *)"

ファイル名の8_1は、上げ先のバージョンに合わせて読み替えてください。「全部有効にして」と頼むと、失敗したときにどの項目が原因か分からなくなります。

非推奨警告を分類する:claude -pで一覧を仕分ける

テストを回すと、非推奨警告が大量に出ます。同じ警告が数百回繰り返されることもあるため、まず重複を畳みます。ガイドには警告の例としてDEPRECATION WARNING: Initialization autoloaded the constant ....が載っており、この接頭辞で絞れます。

bin/rails test 2>&1 | grep "DEPRECATION WARNING" \
  | sort | uniq -c | sort -rn > tmp/deprecations.txt

警告が画面に出ない設定のアプリでは、ログに出ている警告を同じ要領で絞り込めます。

次に、一覧をClaudeに渡して3種類に分けます。--json-schemaを付けると、結果がstructured_outputフィールドに入ります。あとでjqで取り出せるため、分類結果をスクリプトで扱えます。

スキーマは長くなるので、別ファイルに置きます。

{
  "type": "object",
  "properties": {
    "warnings": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "message": { "type": "string" },
          "kind": {
            "type": "string",
            "enum": ["mechanical", "judgment", "ignore"]
          },
          "reason": { "type": "string" }
        },
        "required": ["message", "kind", "reason"]
      }
    }
  },
  "required": ["warnings"]
}
cat tmp/deprecations.txt | claude -p "Railsの非推奨警告の一覧です。\
mechanical=置換するだけで直る、judgment=挙動が変わるので人が判断、\
ignore=依存gemが出していてアプリ側では直せない、に分類してください。" \
  --output-format json \
  --json-schema "$(cat tmp/deprecation-schema.json)" \
  | jq '.structured_output.warnings'

結果は、kindごとに次のように扱いが分かれます。

分類例次の動き
mechanical例設定名の変更、メソッド名の置換次の動きclaude -pで一括して直させる
judgment例属性の別名の挙動変更次の動きClaudeに修正案を出させ、人が選ぶ
ignore例依存gemが出す警告次の動きgemの更新で消えるかを確認する

構造化出力には注意点があります。実行が有効なスキーマ出力で終わらなかった場合、たとえばClaudeが依頼を断った場合は、structured_outputフィールドが付きません。スクリプトで使う前に、フィールドがあるかを確かめてください。

判断が要る警告の例:alias_attributeの変更

分類のうちjudgmentが実際にどんなものか、ガイドの記載で具体的に見ておきます。Rails 7.2では、alias_attributeが元の属性に定義したカスタムメソッドを経由せず、データベースの生の値を直接返すようになりました。変更はRails 7.1で警告として予告されています。

警告の文言はガイドによると、Since Rails 7.2 #{method_name} will not be calling #{target_name} anymoreです。この警告が出たときの対処は、ガイドが2通りを示しています。別名のメソッドを自分で定義する方法と、alias_methodを使う方法です。

class User < ActiveRecord::Base
  def email
    "custom_#{super}"
  end
 
  def username
    email  # カスタムの email を呼ぶ
  end
end

どちらを選ぶかで、既存のコードが期待している値が変わります。ここは機械置換にせず、Claudeに「該当箇所を列挙し、それぞれ現在の挙動を保つ修正案を出して」と頼み、差分を人が確認する流れが向いています。

claude -p "tmp/deprecations.txtのalias_attribute関連の警告について、\
該当するモデルを列挙し、現在の返り値を保つ修正案を示して。\
ファイルは編集せず、提案だけを出して。" \
  --allowedTools "Read,Bash(grep *)"

--allowedToolsにEditを入れていないので、この実行ではファイルが変わりません。

版を上げるときに先に確認したい変更点

ガイドの各バージョンの節には、非推奨警告が出ないまま挙動が変わる項目もあります。これらは警告の分類では拾えないので、上げ先の節を先にClaudeに読ませて、自分のアプリに該当するかを調べさせます。

  • Rails 7.0から7.1: 開発・テスト環境のsecret_key_baseのファイルがtmp/development_secret.txtからtmp/local_secret.txtに変わります。旧ファイルをリネームしないと、新しい鍵が生成され、開発・テスト環境のセッションが無効になります。本番などは影響を受けません。
  • Rails 7.1から7.2: テストがconfig.active_job.queue_adapterの設定を守るようになります。:test以外のアダプターを設定していて、テストがTestAdapterに依存していると失敗します。
  • Rails 8.0から8.1: schema.rbのテーブルの列がアルファベット順に並びます。実際の列の順番を残したい場合はstructure.sqlを使います。マイグレーションの順序に左右されなくなるため、差分のノイズは減ります。

読ませる指示は、たとえば次のとおりです。

claude -p "Rails 7.0から7.1へ上げます。アップグレードガイドの7.1の節の\
各項目について、このリポジトリで該当するコードや設定があるかを\
grepで調べ、該当箇所をファイルと行で挙げてください。" \
  --allowedTools "Read,Bash(grep *)"

ガイド本文はWebから取得する代わりに、プロンプトに貼り付けるか、ファイルに保存してReadさせてください。

落とし穴

警告を全部直してから上げると、手戻りが増える

非推奨警告の一部は、依存gemが出しています。アプリ側のコードは直せず、gemを新しい版に上げて初めて消えます。警告が残るたびに直そうとすると、Claudeが無関係なコードを書き換えることがあります。一覧のignoreは、gemの更新で消えるかを先に確認します。

作業を区切って記録する

アップグレードの途中で止まったときに戻れるよう、段階ごとにコミットを分けます。

  1. Gemfileの更新とロックファイルの更新
  2. app:updateの差分(取り込む部分だけ)
  3. 新しい既定値を有効にした項目(1項目につき1コミット)
  4. 非推奨警告の修正(種類別)

コミットの作成は、claude -pでステージ済みの差分から行えます。headlessの説明には、git diffとgit status、git log、git commitだけを許可して実行する例があります。git pushは許可しないので、公開前に自分で差分を確かめられます。

claude -p "ステージ済みの変更を読み、コミットメッセージを付けて。" \
  --allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"

アップグレード作業全体を、CIなどの無人の実行に移す場合は、--bareの指定を検討します。ホームの~/.claudeにある設定やCLAUDE.mdを読まないため、どのマシンでも同じ結果になります。ただし--bareではサブスクリプションのログインを使えず、ANTHROPIC_API_KEYが必要です。

同じ形の作業は、ほかのライブラリでも組めます。テストの失敗を分類して潰す流れはClaude CodeでJest 30へアップグレード、ツールの自動変換を実行したあとの直し方はClaude CodeでTailwind v4へ移行するに書いています。Rails側の準備には、bin/railsコマンドの権限設計を扱うClaude CodeでRailsアプリを開発する手順が前提知識になります。

まとめ

Railsのアップグレードで、Claude Codeに任せる範囲は3つに切れます。app:updateの対話は自分、差分の分類と警告の仕分けはClaude、挙動が変わる警告の最終判断は人です。

config.load_defaultsを最後まで動かさず、新しい既定値を1項目ずつ有効にすることが、失敗の原因を特定しやすくする最大の工夫です。マイナー版を飛ばさないことと合わせて、1回の変更を小さく保てば、Claudeの修正が間違っていてもコミット単位で戻せます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. guides.rubyonrails.org/upgrading_ruby_on_rails.html
  2. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn