freeeの開発情報ポータルサイト

Claude Code SkillsでRailsアップグレードを仕組み化した話

はじめに

こんにちは。freee請求書チームでエンジニアをやっているnuresenです。 この記事では、Rails 7系から8.1へのアップデートを Claude Code の Skills を使って実施した記録を紹介します。

みなさん、Railsのアップデートはできていますか? Railsは定期的に新しいバージョンがリリースされますが、大規模なプロダクトになるとキャッチアップするのもなかなか大変ですよね。 freee請求書では、現在のバージョンがEOLを迎える前にアップデートしていく方針をとっており、だいたい年に1、2回くらいの頻度でバージョンアップを実施しています。

今回は7系から8.0へのバージョンアップを予定していましたが、8.0のEOLが2026年11月に迫っていることを踏まえ、8.1まで一気に上げてしまうことにしました。 ただし、Railsガイドにも書いてあるように、アップデートはジャンプアップせずに1つずつ段階的に上げていくのが鉄則です。そのため、今回は 7系 → 8.0 → 8.1 と2段階に分けてアップデートすることにしました。

2回同じ作業を繰り返すこと、そして今後も定期的にアップデートが必要になることを考えると、この作業自体を仕組み化しておきたいところです。そこで Claude Code の Skills を活用して、できるだけ楽にアップデートできないかを試してみました。

今回はClaude Skillsについての詳細は説明していません。詳細が知りたい方はぜひ公式Docを参照してください。

アプローチ

いきなり Skills を作るのではなく、まずは Claude Code で素朴に8.0へのアップグレードを一通りやってみることにしました。実際の作業ステップを体験しておかないと、どこを自動化すべきか判断できないためです。

手動で行った手順は以下のとおりです。

  1. アップグレードガイド・リリースノートの確認
  2. ナレッジページにある過去のアップグレードレポートを読む
  3. Gemfile の Rails バージョンを更新して bundle update rails を実行
  4. 依存関係で対応が必要な gem のリストアップと更新
  5. ローカルで起動して動作することを確認
  6. PoC の PR を作成して CI を回し、Claude Code にログを渡して失敗を解析
  7. bin/rails app:update の実行と設定ファイルの差分確認

この一連の作業を Claude Code に任せてみたところ、1つの会話で最初から最後まで完了させるのは難しいことがわかりました。理由は大きく2つあります。

  • コンテキストの肥大化による精度低下
    • 調査結果、bundle update のログ、CI の失敗ログなど大量の情報が積み重なると、LLM の注意力が分散して的確な判断ができなくなる
  • 人間のレビューポイントが必要
    • gem の更新方針や設定ファイルの取捨選択など、プロダクト固有の判断が求められる場面では、AI に丸投げするのではなく各チェックポイントで人間が確認して責任を持つ必要がある

そこで、上記の手順を5つのステップに分割し、それぞれを独立した Skill として定義することにしました。

また、各ステップでは成果物をマークダウン形式のドキュメントとして出力し、それが次のステップの入力になるパイプライン構造を採用しました。これにより、途中で作業を中断してやり直したり、セッションをリセットして別の会話から再開することも可能です。前のステップの成果物さえあればコンテキストを圧迫せずに次のステップを実行でき、LLM の精度を保ったまま作業を進めることができました。

実践編 ── 各ステップの概要

以下が今回作成した5つの Skill の概要です。各 Skill はターゲットバージョンを引数に取り、成果物としてマークダウンを出力します。

Skill実行例

/rails-upgrade-step1-research 8.0

Step 1: Research(調査)

Skill定義イメージ

---
name: rails-upgrade-step1-research
description: 'Railsアップグレード調査フェーズ: リリースノート確認、既知の問題調査、ナレッジ収集'
argument-hint: <target_version (例: 8.1)>
allowed-tools: [Read, Write, Grep, Glob, WebFetch, WebSearch, AskUserQuestion]
---

# rails-upgrade-step1-research

## ワークフロー

### Phase 1: 現在のバージョン確認
- `bundle show rails` で現在のRailsバージョンを確認
- `config/application.rb``config.load_defaults` を確認

### Phase 2: リリースノート調査
WebSearch/WebFetchで以下を調査:
- Rails公式リリースノート
- Breaking Changes / Deprecations / Removals / New Features をカテゴリ別に整理

### Phase 3: コミュニティナレッジ調査
- ruby-jpのナレッジページ
- GitHubのissue/discussion

### Phase 4: 調査結果ドキュメント作成
リリースノートサマリー、依存gem対応状況、リスク評価をまとめる

## 成果物
出力先: `./ai_tasks/rails_upgrade/v{target_version}/01_research.md`

## 禁止事項
- ファイルの編集(調査ドキュメント作成以外)
- コードの変更
- 依存関係の変更

アップグレードに必要な情報を収集し、影響範囲を把握するフェーズです。

主な処理内容

  • 現在の Rails バージョンと config.load_defaults の確認
  • Rails 公式リリースノートの調査
  • Breaking Changes・Deprecations・Removals・New Features のカテゴリ別整理
  • コミュニティの既知の問題を調査(ruby-jp、GitHub Issues など)
  • 依存 gem の Rails 対応状況の確認
  • リスク評価と推奨対応順序の策定

このステップではコードの変更は行わず、調査とドキュメント作成のみに制限しています。許可ツールを Read / WebSearch / WebFetch 等に絞り、Edit や Bash での変更操作を禁止することで、調査フェーズで誤ってコードを書き換えてしまうリスクをなくしています。

成果物: 01_research.md(リリースノートサマリー、依存 gem 対応状況、リスク評価)

Step 2: Gemfile(依存関係解決)

Skill定義イメージ

---
name: rails-upgrade-step2-gemfile
description: 'Railsアップグレード Gemfile更新フェーズ: 依存関係の解決とbundle update'
argument-hint: <target_version (例: 8.1)>
allowed-tools: [Read, Edit, Write, Grep, Glob, Bash(bundle *), Bash(git *), AskUserQuestion]
---

# rails-upgrade-step2-gemfile

## 前提条件
- step1(調査フェーズ)が完了していること

## ワークフロー

### Phase 1: 前ステップの確認
- step1の調査ドキュメントを読み込む

### Phase 2: Gemfile の Rails バージョン更新
- Gemfile を編集: `gem 'rails', '~> {target_version}.0'`
- `bundle install` を試行

### Phase 3: 依存関係エラーの解決
1. エラーメッセージを解析
2. gemのGitHub/RubyGemsで対応バージョンを確認
3. Gemfileを更新
4. 再度 `bundle install` を試行
5. エラーが解消されるまで繰り返し

### Phase 4: bundle update 実行
- `bundle update rails`

### Phase 5: 変更内容の記録
- `git diff Gemfile.lock` で更新されたgemを確認・記録

## 成果物
出力先: `./ai_tasks/rails_upgrade/v{target_version}/02_gemfile.md`

## 禁止事項
- step1の調査なしでの実行
- bundle installのエラーを無視した強制更新
- 依存関係解決の経緯を記録しないこと

Gemfile の Rails バージョンを更新し、依存関係を解決するフェーズです。

主な処理内容

  • Step 1 の調査ドキュメントを読み込み
  • Gemfile の Rails バージョン指定を更新
  • bundle install → エラー解析 → Gemfile 修正 → 再試行のループ
  • 依存関係が解決したら bundle update rails を実行
  • git diff Gemfile.lock で更新された gem を記録

依存関係解決の経緯(どのエラーに対してどう対応したか)をすべて記録するようにしています。
bundle install のエラーを無視した強制更新も禁止しており、1つずつ原因を解消していくようにしています。

成果物: 02_gemfile.md(変更した gem の一覧、依存関係解決の経緯)

Step 3: CI Analysis(CI 結果分析)

Skill定義イメージ

---
name: rails-upgrade-step3-ci
description: 'Railsアップグレード CI結果分析フェーズ: CI結果を受け取り、失敗を分類・ドキュメント化'
argument-hint: <target_version (例: 8.1)>
allowed-tools: [Read, Edit, Write, Grep, Glob, AskUserQuestion]
---

# rails-upgrade-step3-ci

## 前提条件
- step2(Gemfile更新)が完了していること
- PR が作成済みで CI が実行されていること

## ワークフロー

### Phase 1: CI 結果の受け取り
ユーザーに CI 結果の提供を依頼(PR番号、失敗ログ等)

### Phase 2: CI 結果の分析
失敗を以下のカテゴリに分類:
1. Rails変更による破壊的変更
2. 依存gemの変更
3. Deprecation警告
4. RuboCop違反

### Phase 3: 修正方法の検討
- テストコードの修正が必要か
- アプリケーションコードの修正が必要か
- 設定変更で対応可能か

### Phase 4: 分析結果のドキュメント化
失敗分類レポートと修正タスク一覧を優先度別に整理

## 成果物
出力先: `./ai_tasks/rails_upgrade/v{target_version}/03_ci_analysis.md`

## 禁止事項
- このステップで修正を実施すること(分析のみ)
- 失敗の原因を調査せずに分類すること

PR を作成して CI を回し、失敗を分類・ドキュメント化するフェーズです。

主な処理内容

  • ユーザーから CI 結果(失敗ログ)を受け取る
  • 失敗を4つのカテゴリに分類
    • Rails 変更による破壊的変更(API変更、デフォルト値の変更など)
    • 依存 gem の変更による挙動変更
    • Deprecation 警告(Rails / RSpec / 外部 gem 由来)
    • RuboCop 違反(新しい cop、設定変更)
  • 各失敗に対して関連コードを調査し、修正方法を検討
  • 修正タスクを優先度別に整理

このステップではステップ1と同様に分析のみを行い、コードの修正は行いません。許可ツールから Bash や Edit を除外し、分析に専念させる設計です。「分析」と「修正」をあえて分離することで、修正方針について人間がレビューするポイントを自然に挟むことができます。

成果物: 03_ci_analysis.md(失敗分類レポート、修正タスク一覧)

Step 4: Fix(修正)

Skill定義イメージ

---
name: rails-upgrade-step4-fix
description: 'Railsアップグレード 修正フェーズ: CI失敗の修正、ローカルテスト確認'
argument-hint: <target_version (例: 8.1)>
allowed-tools: [Read, Edit, Write, Grep, Glob, Bash(bundle exec rspec *), Bash(bundle exec rubocop *), Bash(bin/rails *), Bash(git *), AskUserQuestion]
---

# rails-upgrade-step4-fix

## 前提条件
- step3(CI結果分析)が完了していること

## ワークフロー

### Phase 1: 修正タスクの確認
- step3のCI分析レポートを読み込み、修正タスク一覧を確認

### Phase 2: テスト失敗の修正
優先度順に修正:
1. Rails変更による破壊的変更
2. 依存gem変更による失敗
3. Deprecation 警告への対応

### Phase 3: RuboCop違反の修正
- `bundle exec rubocop -a` で自動修正
- 自動修正できないものは手動で修正

### Phase 4: ローカルテスト実行
- 失敗していたテストを個別実行して確認
- RuboCop確認

## 成果物
出力先: `./ai_tasks/rails_upgrade/v{target_version}/04_fix_record.md`

## 禁止事項
- 分析レポートを確認せずに修正を開始すること
- 修正内容をドキュメント化しないこと

Step 3 の分析結果に基づき、CI 失敗を修正するフェーズです。

主な処理内容

  • CI 分析レポートの修正タスク一覧を読み込み
  • 優先度順に修正
    1. Rails 変更による破壊的変更への対応
    2. 依存 gem 変更による失敗への対応
    3. Deprecation 警告の解消
    4. RuboCop 違反の修正(rubocop -a で自動修正 + 手動修正)
  • ローカルでテストを実行して修正を確認

許可ツールに bundle exec rspecbundle exec rubocop を含め、テスト実行と修正を繰り返せるようにしています。修正内容(どのファイルをなぜ変更したか)もすべて記録し、後からレビューできるようにしています。

成果物: 04_fix_record.md(修正ファイル一覧、修正理由、ローカルテスト結果)

Step 5: App Update(設定ファイル更新)

Skill定義イメージ

---
name: rails-upgrade-step5-app-update
description: 'Railsアップグレード app:update実行フェーズ: 設定ファイルの差分生成と分析'
argument-hint: <target_version (例: 8.1)>
allowed-tools: [Read, Edit, Write, Grep, Glob, Bash(bin/rails app:update), Bash(git *), AskUserQuestion]
---

# rails-upgrade-step5-app-update

## 前提条件
- step4(修正フェーズ)が完了していること
- CI が通過していること

## ワークフロー

### Phase 1: app:update の実行
- `bin/rails app:update` を実行(全上書き)
- `git diff --stat` で差分を確認

### Phase 2: 差分の分析
生成された差分をカテゴリに分類:
- 採用: `new_framework_defaults_{version}.rb`
- 削除: `Dockerfile`, `docker-compose.yml` など既存設定と競合するもの
- 復元必須: カスタム設定、i18n設定、タイムゾーン設定など

### Phase 3: 復元と適用
1. `git checkout -- .` で全ファイルを復元
2. 不要な新規ファイルを削除
3. 必要な設定のみ手動で適用

## 成果物
出力先: `./ai_tasks/rails_upgrade/v{target_version}/05_app_update.md`

## 禁止事項
- 差分を分析せずにそのまま採用すること
- 既存のカスタム設定を削除したままにすること

bin/rails app:update を実行し、設定ファイルの差分を分析・適用するフェーズです。

主な処理内容

  • bin/rails app:update を実行(全ファイルを上書き)
  • 差分を分析し、以下を判断
    • 採用すべき新設定(new_framework_defaults など)
    • 復元すべき既存設定(カスタム設定、i18n、タイムゾーンなど)
    • 不要な新規ファイル(Dockerfile など既存設定と競合するもの)
  • git checkout で全ファイルを復元し、必要な変更のみ手動で適用

app:update の差分をそのまま採用するのではなく、一旦全復元してから必要なものだけ適用する方針を取っています。また、config.load_defaults の更新は見送り、new_framework_defaults をコメントアウト状態にして段階的に有効化していきました。 この時点では設定変更の影響が読みきれないこともあり、安全側に倒す判断をしました。

成果物: 05_app_update.md(差分分析結果、採用した変更、復元した設定の一覧)

実践編 ── 実際の移行作業の進め方

ここまで5つの Skill の概要を紹介しましたが、実際の移行作業はこれらを単純に1回実行して終わりではありませんでした。PoC による検証と、タスクを分割した本番移行の2段階で進めています。

PoC フェーズ

まず Step 1〜5 を一通り実行し、1つの PoC PR を作成しました。この段階では動くことの確認が目的で、各ステップの成果ドキュメントをまとめてチーム内レビューに回しています。ここで「この設定変更は本当に問題ないか」「この gem の更新方針は正しいか」といったフィードバックを受けました。

本番移行フェーズ

PoC のレビュー結果を踏まえて、実際の移行作業ではタスクを細かく分割し、それぞれ個別の PR として作成していきました。CI 分析の結果から修正が必要な項目をチケット化し、デフォルト設定の有効化も1つずつ検証して PR を分けています。

全体の流れを図にすると以下のようになります。

移行の流れ

タスク分割の実例

今回の Rails 8.0 / 8.1 アップグレードでは、以下のようにタスクを分割しました。

  • Gemfile 更新
  • CI 分析
  • 破壊的変更への個別対応
  • RuboCop 対応
  • app:update 適用
  • デフォルト設定の個別検証

特にデフォルト設定の有効化は、設定ごとに影響範囲が異なるため1つずつ PR を分けて検証しました。各 PR では Skill を再実行して調査や修正を行い、成果ドキュメントも都度更新しています。

やってみてどうだったか

実際にこの手法でやってみて、良かった点と現段階での課題感をまとめます。

良かった点

1つ目は作業時間の短縮です。調査フェーズでのリリースノートやコミュニティナレッジの確認、CI 分析フェーズでの大量のログ解析は、人の目ですべて確認すると時間がかかるうえに見落としも出がちです。LLM はこのあたりを高精度でこなしてくれるため、かなり信頼してよいと感じました。

2つ目は再現性です。Skill にしたことで誰でも何度でも同じ手順で実行できるようになりました。今回は 7系 → 8.0、8.0 → 8.1 と2回 Skill を実行して段階的なアップデートを実施しました。また、各段階でも同じSkillを複数回実行して結果を比較することで、LLM の出力のばらつきを確認し、より安定した結果を採用することもできました。

3つ目はドキュメントが自動で残る点です。各ステップの成果物がそのまま作業記録になるため、個人でのセルフレビューだけでなくチーム内レビューでも活用することができました。 また、本移行フェーズでは私だけでなく他のチームメンバーも参加していましたが、生成されたドキュメントからタスクチケットを作成したことで、チケットをみるだけで詳細を把握できるのも利点に感じました。

課題

今回の手法は順番に Skill を実行していくだけでアップグレード作業が完了するよう設計しましたが、実際には生成されたドキュメントを人間が確認して判断するフェーズが欠かせませんでした。

一通り Skill を実行して PoC を作成したあと、チーム内レビューで各ステップの判断が正しかったかを再検討しています。特に新規デフォルト設定を導入して問題ないかの検証は、LLM の力を借りつつも最終的には人間が行う必要がありました。たとえば 8.0 で Regexp.timeout = 1 が導入されたことによるパフォーマンスへの影響や、8.1 で escape_json_responses が有効化されたことによるセキュリティ面での挙動変更など、プロダクト固有の文脈を踏まえた慎重な判断が求められる場面がありました。

今後のモデルやコーディングエージェントの進化でこのあたりは改善されていくかもしれませんが、現段階では完全な自動化は難しく、人間によるレビューとの併用が前提になると感じています。

さいごに

今回の取り組みを通じて感じたのは、Skills の本質は「手順書の構造化」だということです。普段なんとなく頭の中で組み立てている作業手順を、LLM が実行可能な形に明文化したものにすぎません。ただし、最初からうまく分割できたわけではなく、手動で一度やってみて初めて適切なステップの切り方が見えてきました。この「まず自分でやってから仕組みにする」という順序が重要だったと思います。

最後に、今回の経験から得た LLM に複雑なタスクを任せるための設計指針を3つ挙げておきます。

  1. ステップ分割でコンテキストを制御する
  2. ツール権限で LLM の行動範囲を絞る
  3. 成果物のドキュメントで人間のレビューポイントを作る

この3つは汎用的な設計パターンとして、他のタスクにも活用できるのではないかと考えています。