この記事は Claude Code Advent Calendar 2025 の12月21日の記事です。
「Railsのアップグレード、やらなきゃな...」
わかります。私もずっとそう思ってました。
Ruby 3.0.2、Rails 6.1.4.1で動いていた個人開発のLINEボットアプリ。コロナ禍も落ち着いてサービスの役目も終わりつつあったことと、Rails 6.1.x のEOLも2024年10月1日で終了したこともあってお片付けしようと思っていました。
ただその際にふっと先日の Claude Code Advent Calendar 2025 3日目に投稿したEDAFを使ってRailsアップデートしたらネタになるのでは?と思ってアップグレードを試してみることにしました。
結論から言うと、約1時間半で完了しました。
TL;DR
| 項目 | Before | After |
|---|---|---|
| Ruby | 3.0.2 | 3.4.6 |
| Rails | 6.1.4.1 | 8.1.1 |
| JSビルド | webpacker | esbuild |
| JSバンドルサイズ | 1.5MB | 356KB(76%削減) |
| 変更ファイル数 | - | 196ファイル |
| コミット数 | - | 9コミット |
| 自動生成されたドキュメント | - | 38ファイル |
| 生成されたタスク数 | - | 49タスク(12フェーズ) |
| 所要時間 | - | 約1時間半 |
PRはこちら:
https://github.com/Tsuchiya2/edaf-rails-upgrade-demo/pull/1
EDAFとは?
EDAFについては、12/3の記事で詳しく解説しています。
👉 Claude Codeで実現する評価駆動開発 - 32個のエージェントが品質を守る仕組み
簡単に言うと:
- 設計 → 計画 → 実装 → コードレビュー → デプロイ準備の5フェーズ
- 各フェーズで専門のEvaluatorが品質をチェック
- 7.0/10.0以上で合格、不合格なら修正して再評価
今回はこのEDAFを「Railsアップグレード」で使ってみた、という話です。
やったこと
指示した内容
エージェントフローに沿って、Rubyを3.3.10、Railsを8.1.1にアップグレードしてください。その際にwebpackerをesbuildに変更もお願いします。
これだけです。
Designerが生成した設計書
正直、一番驚いたのはこれです。
Designerエージェントが3500行以上の設計書を自動生成しました。
docs/designs/rails-upgrade.md (3537行)
中身を見てみると:
11フェーズの段階的アップグレード計画
Phase 1: Ruby バージョンアップ(準備)
Phase 2: Rails 6.1 → 7.0
Phase 3: Rails 7.0 → 7.1
Phase 4: Rails 7.1 → 8.0
Phase 5: Rails 8.0 → 8.1
Phase 6: Webpacker 削除
Phase 7: esbuild インストール
Phase 8: アセット移行
Phase 9: View更新
Phase 10: テスト & 修正
Phase 11: ドキュメント更新
Rails公式が推奨している「メジャーバージョンを1つずつ上げる」アプローチを、ちゃんと踏襲してくれています。
docs/designs/rails-upgrade.mdに記載された内容(3537行あるので注意)
Design Document - Ruby/Rails Upgrade with Webpacker to esbuild Migration
Feature ID: FEAT-UPGRADE-001
Created: 2025-12-01
Last Updated: 2025-12-01 (Iteration 2)
Designer: designer agent
Metadata
design_metadata:
feature_id: "FEAT-UPGRADE-001"
feature_name: "Ruby/Rails Upgrade with Webpacker to esbuild Migration"
created: "2025-12-01"
updated: "2025-12-01"
iteration: 2
current_versions:
ruby: "3.0.2"
rails: "6.1.4.1"
webpacker: "5.4.3"
webpack: "4.46.0"
target_versions:
ruby: "3.3.10"
rails: "8.1.1"
bundler: "esbuild (jsbundling-rails)"
1. Overview
This design document outlines the comprehensive upgrade path for the ReLINE application from Ruby 3.0.2/Rails 6.1.4.1 to Ruby 3.3.10/Rails 8.1.1, along with a critical migration from Webpacker 5 to esbuild via the jsbundling-rails gem. This upgrade represents a major version jump spanning multiple Rails versions (6.1 → 7.0 → 7.1 → 8.0 → 8.1) and a fundamental shift in asset pipeline architecture.
Goals and Objectives
- Ruby Version Upgrade: Safely upgrade from Ruby 3.0.2 to 3.3.10, taking advantage of performance improvements and security patches
- Rails Framework Upgrade: Incrementally upgrade Rails through all intermediate versions to reach Rails 8.1.1
- Asset Pipeline Modernization: Replace Webpacker with esbuild for faster build times and simpler configuration
- Maintain Functionality: Ensure all existing features (authentication, LINE bot integration, operator dashboard) continue to work
- Dependency Compatibility: Update all gems and npm packages to versions compatible with the new Rails version
- Zero Downtime: Design the upgrade to allow for rollback at any point if issues are discovered
Success Criteria
- Application starts successfully with Ruby 3.3.10 and Rails 8.1.1
- All existing tests pass without modification
- Asset compilation works correctly with esbuild
- Bootstrap 5.1.3 and FontAwesome 5.15.4 styles render properly
- LINE bot webhook functionality remains intact
- Sorcery authentication and Pundit authorization work correctly
- No console errors in browser developer tools
- Asset build times are equal to or faster than Webpacker
- All existing routes and controllers function as expected
2. Requirements Analysis
2.1 Functional Requirements
FR-1: Ruby Runtime Upgrade
- Install and configure Ruby 3.3.10 via rbenv/rvm
- Update
.ruby-versionfile - Verify all Ruby-level code is compatible with 3.3.10
FR-2: Incremental Rails Upgrade
- Upgrade from Rails 6.1.4.1 to 7.0.x first
- Then upgrade to Rails 7.1.x
- Then upgrade to Rails 8.0.x
- Finally upgrade to Rails 8.1.1
- Run
rails app:updateat each major version step - Address deprecation warnings at each step
FR-3: Gem Dependency Updates
- Update all gems in Gemfile to versions compatible with Rails 8.1.1:
-
puma→ 6.x -
sorcery→ latest compatible version -
pundit→ 2.x -
line-bot-api→ latest version -
slim-rails→ latest version -
enum_help→ latest version - Test gems (rspec-rails, factory_bot_rails, capybara, etc.)
- Development gems (rubocop suite, bullet, better_errors, etc.)
-
FR-4: Webpacker to esbuild Migration
- Remove
webpackergem and related configurations - Install
jsbundling-railsgem - Install esbuild npm package
- Configure esbuild build script in
package.json - Create new esbuild configuration file
- Migrate JavaScript entry points from
app/javascript/packs/to new structure - Update asset helper calls in views
FR-5: JavaScript/CSS Asset Migration
- Migrate
app/javascript/packs/application.jsto esbuild entry point - Convert Webpack-specific imports to esbuild-compatible imports
- Handle Bootstrap SCSS imports with cssbundling-rails
- Migrate FontAwesome integration
- Handle image assets (favicon, other images)
- Preserve custom JavaScript (scroll.js)
- Maintain Rails UJS and ActiveStorage functionality
FR-6: View Helper Updates
- Replace
javascript_pack_tagwithjavascript_include_tag - Replace
stylesheet_pack_tagwithstylesheet_link_tag - Replace
favicon_pack_tagwith standard favicon helpers - Update any other Webpacker-specific helpers
FR-7: Configuration Updates
- Remove
config/webpacker.yml - Remove
config/webpack/directory - Add esbuild build configuration
- Update
.gitignorefor new build artifacts - Configure Procfile.dev for concurrent processes
- Update deployment scripts for new asset compilation
2.2 Non-Functional Requirements
NFR-1: Performance
- Asset build times should be ≤50% of current Webpacker build times
- Development server hot reload time should be <2 seconds
- Production asset bundle size should not increase by >10%
NFR-2: Compatibility
- Support for latest LTS browsers (Chrome, Firefox, Safari, Edge)
- Maintain IE11 compatibility if currently supported (unlikely for Rails 8)
- MySQL 5.7+ and PostgreSQL 12+ database compatibility
NFR-3: Developer Experience
- Clear documentation for new asset pipeline
- Simple commands for development (
bin/dev) - Fast feedback loop during development
NFR-4: Maintainability
- Simpler configuration compared to Webpacker
- Fewer dependencies in package.json
- Clear separation between JavaScript and CSS builds
NFR-5: Security
- All gems updated to versions without known CVEs
- Rails security patches applied through version upgrades
- Asset compilation process doesn't introduce XSS vulnerabilities
2.3 Constraints
CONST-1: Incremental Upgrade Required
- Rails must be upgraded incrementally (6.1 → 7.0 → 7.1 → 8.0 → 8.1)
- Cannot skip major versions due to breaking changes
CONST-2: Backward Compatibility
- Must maintain API compatibility with LINE bot webhook
- Cannot change database schema during upgrade
- Must preserve existing operator authentication flow
CONST-3: Third-Party Dependencies
-
line-bot-apigem must remain functional - Bootstrap 5.1.3 must continue to work
- FontAwesome 5.15.4 must render correctly
CONST-4: Testing Requirements
- All existing tests must pass or be updated appropriately
- No functionality can be removed without explicit approval
3. Architecture Design
3.1 System Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ ReLINE Application │
├─────────────────────────────────────────────────────────────┤
│ Ruby 3.3.10 Runtime │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Rails 8.1.1 Framework │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Controllers (Operator, Customers, etc.) │ │ │
│ │ ├──────────────────────────────────────────────┤ │ │
│ │ │ Models (Operator, LineGroup, Content, etc.) │ │ │
│ │ ├──────────────────────────────────────────────┤ │ │
│ │ │ Views (Slim templates) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Authentication: Sorcery │ │
│ │ Authorization: Pundit │ │
│ │ External API: LINE Bot API │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Asset Pipeline Architecture │
├─────────────────────────────────────────────────────────────┤
│ BEFORE (Webpacker 5) AFTER (esbuild) │
│ │
│ app/javascript/packs/ → app/javascript/ │
│ application.js application.js │
│ application.scss application.css │
│ │
│ Webpack 4 → esbuild + cssbundling │
│ - Complex config - Simple config │
│ - Slow builds (30-60s) - Fast builds (2-5s) │
│ - Many dependencies - Minimal dependencies │
│ │
│ public/packs/ → app/assets/builds/ │
│ application-[hash].js application.js │
│ application-[hash].css application.css │
│ │
│ <%= javascript_pack_tag %> → <%= javascript_include_tag %>│
│ <%= stylesheet_pack_tag %> → <%= stylesheet_link_tag %> │
└─────────────────────────────────────────────────────────────┘
3.2 Component Breakdown
3.2.1 Rails Application Core
- Controllers: No changes required, but verify deprecations
- Models: Update any Rails 6.1-specific syntax
- Views: Update asset helper methods
- Routes: Verify compatibility with Rails 8.1 routing
3.2.2 Authentication & Authorization
- Sorcery: Update to latest version, test login/logout flows
- Pundit: Update to 2.x, verify policy classes work correctly
3.2.3 External Integrations
- LINE Bot API: Ensure webhook handling remains functional
- Database: MySQL (dev/test) and PostgreSQL (production) connections
3.2.4 Asset Pipeline
- JavaScript Bundler: esbuild via jsbundling-rails
- CSS Bundler: Dart Sass via cssbundling-rails
- Build Process: Concurrent processes via Procfile.dev
3.3 Data Flow
3.3.1 Asset Build Flow (Development)
1. Developer edits app/javascript/application.js
↓
2. esbuild watch process detects change
↓
3. esbuild bundles JavaScript → app/assets/builds/application.js
↓
4. Dart Sass compiles SCSS → app/assets/builds/application.css
↓
5. Rails asset pipeline serves from app/assets/builds/
↓
6. Browser receives updated assets (with live reload)
3.3.2 Asset Build Flow (Production)
1. Run: rails assets:precompile
↓
2. Triggers: yarn build (runs esbuild)
↓
3. Triggers: yarn build:css (runs Dart Sass)
↓
4. Assets compiled to app/assets/builds/
↓
5. Rails asset pipeline processes and fingerprints
↓
6. Final assets in public/assets/ with digest hashes
↓
7. Served via CDN or web server
3.3.3 Upgrade Process Flow
Phase 1: Preparation
├── Install Ruby 3.3.10
├── Update bundler
├── Create git branch: feature/rails-8-upgrade
└── Backup database
Phase 2: Rails 6.1 → 7.0
├── Update Gemfile: rails ~> 7.0.0
├── bundle update rails
├── rails app:update
├── Fix deprecations
├── Run tests
└── Commit
Phase 3: Rails 7.0 → 7.1
├── Update Gemfile: rails ~> 7.1.0
├── bundle update rails
├── rails app:update
├── Fix deprecations
├── Run tests
└── Commit
Phase 4: Rails 7.1 → 8.0
├── Update Gemfile: rails ~> 8.0.0
├── bundle update rails
├── rails app:update
├── Fix deprecations
├── Run tests
└── Commit
Phase 5: Rails 8.0 → 8.1
├── Update Gemfile: rails ~> 8.1.1
├── bundle update rails
├── rails app:update
├── Fix deprecations
├── Run tests
└── Commit
Phase 6: Webpacker Removal
├── Remove webpacker gem
├── Remove webpack dependencies
├── Delete config/webpacker.yml
├── Delete config/webpack/
└── Commit
Phase 7: esbuild Installation
├── Add jsbundling-rails gem
├── Run: rails javascript:install:esbuild
├── Add cssbundling-rails gem
├── Run: rails css:install:sass
└── Configure build scripts
Phase 8: Asset Migration
├── Move app/javascript/packs/ → app/javascript/
├── Update import statements
├── Configure Bootstrap imports
├── Configure FontAwesome imports
├── Update image handling
└── Test in browser
Phase 9: View Updates
├── Update application.html.slim
├── Replace pack tags with include tags
├── Update favicon handling
└── Test all pages
Phase 10: Testing & Validation
├── Run full test suite
├── Manual testing of all features
├── Check browser console for errors
├── Verify asset loading
├── Test LINE bot webhook
└── Performance benchmarking
Phase 11: Documentation & Deployment
├── Update README.md
├── Document new asset pipeline
├── Update deployment scripts
├── Create rollback plan
└── Deploy to staging
4. Data Model
4.1 Database Schema Changes
Good News: This upgrade does NOT require database schema changes. All models and tables remain the same:
-
operators- User authentication via Sorcery -
line_groups- LINE group chat information -
contents- Content management -
alarm_contents- Alarm/notification content -
feedbacks- User feedback storage -
schedulers- Scheduled task management
4.2 Model Changes Required
4.2.1 Rails 7.0+ Changes
ActiveRecord::Base Inheritance
- No changes required - models already inherit from ApplicationRecord
Attribute API Updates - Verify
enumdeclarations are Rails 7+ compatible - Update any deprecated
serializecalls
Validations - Ensure all validations use modern syntax
- Update any custom validators if needed
4.2.2 Rails 8.x Changes
Potential Deprecations to Address:
# Before (Rails 6.1)
belongs_to :line_group, optional: true
# After (Rails 8.1) - syntax remains the same but verify behavior
belongs_to :line_group, optional: true
4.3 Migration Strategy
Since no schema changes are required, the migration strategy focuses on:
-
Backup Database Before Upgrade
# Development mysqldump -u root reline_development > backup_dev.sql # Production (if testing) pg_dump reline_production > backup_prod.sql -
Run Pending Migrations (if any)
rails db:migrate -
Verify Data Integrity
# In rails console after upgrade Operator.count LineGroup.count Content.count # Verify counts match pre-upgrade
5. API Design
5.1 Asset Helper Method Changes
This section focuses on the "API" of asset handling in views and how it changes.
5.1.1 JavaScript Inclusion
Before (Webpacker):
/ app/views/layouts/application.html.slim
= javascript_pack_tag 'application'
After (esbuild + jsbundling-rails):
/ app/views/layouts/application.html.slim
= javascript_include_tag 'application', defer: true
Changes:
- Helper method name changes from
javascript_pack_tagtojavascript_include_tag - Add
defer: truefor optimal loading performance - Asset served from
app/assets/builds/instead ofpublic/packs/
5.1.2 Stylesheet Inclusion
Before (Webpacker):
/ app/views/layouts/application.html.slim
= stylesheet_pack_tag 'application', media: 'all'
After (esbuild + cssbundling-rails):
/ app/views/layouts/application.html.slim
= stylesheet_link_tag 'application', media: 'all'
Changes:
- Helper method name changes from
stylesheet_pack_tagtostylesheet_link_tag - CSS compiled by Dart Sass instead of Webpack
- Asset served from
app/assets/builds/instead ofpublic/packs/
5.1.3 Image/Favicon Handling
Before (Webpacker):
/ app/views/layouts/application.html.slim
= favicon_pack_tag 'media/images/favicon.ico'
After (Rails asset pipeline):
/ app/views/layouts/application.html.slim
= favicon_link_tag 'favicon.ico'
Changes:
- Use standard Rails asset pipeline helpers
- Move favicon to
app/assets/images/ - Remove
media/images/path prefix
5.2 JavaScript Import API Changes
5.2.1 Bootstrap Import
Before (Webpacker):
// app/javascript/packs/application.js
import 'bootstrap'
After (esbuild):
// app/javascript/application.js
import * as bootstrap from 'bootstrap'
Alternative (CSS-only):
// app/assets/stylesheets/application.scss
@import 'bootstrap/scss/bootstrap';
5.2.2 FontAwesome Import
Before (Webpacker):
// app/javascript/packs/application.js
import '@fortawesome/fontawesome-free/js/all'
After (esbuild):
// app/javascript/application.js
import '@fortawesome/fontawesome-free/js/all'
// OR import specific icons
import '@fortawesome/fontawesome-free/js/solid'
import '@fortawesome/fontawesome-free/js/regular'
5.2.3 Rails UJS and ActiveStorage
Before (Webpacker):
// app/javascript/packs/application.js
import Rails from "@rails/ujs"
import * as ActiveStorage from "@rails/activestorage"
Rails.start()
ActiveStorage.start()
After (Rails 7+):
// app/javascript/application.js
// Rails 7+ includes Turbo by default, but we can keep UJS
import Rails from "@rails/ujs"
Rails.start()
// ActiveStorage (if used)
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
Note: Rails 7+ uses Turbo/Hotwire by default. Decide whether to keep UJS or migrate to Turbo.
5.2.4 Image Context (Webpack-specific)
Before (Webpacker):
// app/javascript/packs/application.js
const images = require.context('../images', true)
After (esbuild):
// Remove this line - not needed with Rails asset pipeline
// Images go in app/assets/images/ and use standard helpers
5.3 Build Script API
5.3.1 Package.json Scripts
Before (Webpacker):
{
"scripts": {
"webpack": "webpack",
"webpack-dev-server": "webpack-dev-server"
}
}
After (esbuild):
{
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets",
"build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
}
}
5.3.2 Development Process
Before (Webpacker):
# Terminal 1
rails server
# Terminal 2
./bin/webpack-dev-server
After (esbuild with Procfile.dev):
# Single command runs all processes
bin/dev
Procfile.dev:
web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch
6. Security Considerations
6.1 Threat Model
THREAT-1: Gem Vulnerabilities
- Description: Outdated gems may contain known security vulnerabilities
- Impact: HIGH - Could lead to RCE, SQL injection, XSS, etc.
- Affected Components: All gem dependencies
THREAT-2: Asset Pipeline XSS
- Description: Migration to esbuild could introduce XSS if user content is not properly escaped
- Impact: MEDIUM - Could allow attackers to execute JavaScript in user browsers
- Affected Components: View rendering, JavaScript compilation
THREAT-3: Rails Session Security
- Description: Rails 7+ has updated session security defaults
- Impact: MEDIUM - Improper migration could break session handling
- Affected Components: Sorcery authentication, session management
THREAT-4: CSRF Token Handling
- Description: Rails 7+ updates CSRF protection mechanisms
- Impact: MEDIUM - Could break form submissions or API calls
- Affected Components: Forms, AJAX requests, LINE webhook
THREAT-5: Dependency Confusion
- Description: New npm packages could be compromised or malicious
- Impact: MEDIUM - Could introduce malicious code into build process
- Affected Components: esbuild, node dependencies
6.2 Security Controls
SEC-1: Gem Audit Process
# Before upgrade
bundle audit check --update
# After each upgrade phase
bundle audit check
# Verify no high/critical CVEs
bundle exec rails security:check
Automated: Add to CI/CD pipeline
SEC-2: npm Audit Process
# Before migration
npm audit
# After migration
npm audit
npm audit fix
# Verify no critical vulnerabilities
Automated: Add to CI/CD pipeline
SEC-3: Content Security Policy
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https
# Development: Allow live reload
if Rails.env.development?
policy.connect_src :self, :https, "ws://localhost:3035"
end
end
SEC-4: Session Security
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_reline_session',
secure: Rails.env.production?,
httponly: true,
same_site: :lax
SEC-5: Authentication Security
- Verify Sorcery uses secure password hashing (bcrypt)
- Ensure session timeouts are appropriate
- Test password reset flow after upgrade
- Verify "remember me" functionality
SEC-6: CSRF Protection
# Verify in ApplicationController
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# For LINE webhook (if needed)
skip_before_action :verify_authenticity_token,
only: [:webhook],
if: -> { line_signature_valid? }
end
SEC-7: Asset Integrity
- Use Subresource Integrity (SRI) for CDN assets
- Verify asset fingerprinting works correctly
- Ensure no sensitive data in JavaScript bundles
6.3 Data Protection Measures
DP-1: Environment Variables
- Ensure all secrets use Rails credentials or ENV vars
- Never commit
.envfiles - Rotate secrets after upgrade (if concerned about leakage)
DP-2: Database Encryption
- Verify encrypted attributes still work after upgrade
- Test database connections in all environments
DP-3: LINE Bot Signature Verification
# Maintain signature verification for webhook
def line_signature_valid?
body = request.body.read
signature = request.env['HTTP_X_LINE_SIGNATURE']
return false if signature.blank?
hash = OpenSSL::HMAC.digest(
OpenSSL::Digest.new('SHA256'),
ENV['LINE_CHANNEL_SECRET'],
body
)
Base64.strict_encode64(hash) == signature
end
7. Error Handling
7.1 Upgrade Process Error Scenarios
ERROR-1: Gem Dependency Conflicts
Scenario: bundle install fails due to incompatible gem versions
Error Message:
Bundler could not find compatible versions for gem "sorcery":
In Gemfile:
sorcery
Rails (~> 8.1.1) requires sorcery (>= 0.17.0)
Recovery Strategy:
- Identify conflicting gem versions
- Check gem changelog for breaking changes
- Update gem to compatible version
- If no compatible version exists:
- Search for alternative gems
- Consider forking and patching
- Temporarily pin to earlier Rails version
Prevention:
- Check gem compatibility before starting upgrade
- Use
bundle update --conservativeto minimize changes
ERROR-2: Rails Generator Conflicts
Scenario: rails app:update wants to overwrite customized files
Error Message:
conflict config/boot.rb
Overwrite /path/to/config/boot.rb? (enter "h" for help) [Ynaqdhm]
Recovery Strategy:
- Review diff of proposed changes
- If custom changes exist, select "d" to see diff
- Merge changes manually if needed
- Use version control to track changes
Prevention:
- Commit all changes before running
rails app:update - Review each conflict carefully
- Keep custom code in separate files when possible
ERROR-3: Webpacker Asset Build Failure
Scenario: Assets fail to compile during migration
Error Message:
Error: Cannot find module 'bootstrap'
Referenced from: app/javascript/application.js
Recovery Strategy:
- Verify npm packages are installed:
yarn install - Check import paths are correct for esbuild
- Ensure package.json includes all dependencies
- Clear build cache:
rm -rf app/assets/builds - Rebuild:
yarn build
Prevention:
- Test asset compilation after each change
- Keep Webpacker working until esbuild is fully configured
- Commit working state before migration
ERROR-4: Bootstrap Styles Not Loading
Scenario: Bootstrap CSS doesn't load or looks broken
Error Message: (Browser console)
Failed to load resource: app/assets/builds/application.css
Recovery Strategy:
- Check CSS build script in package.json
- Verify Sass compiler is installed:
yarn add sass --dev - Ensure import path is correct:
@import 'bootstrap/scss/bootstrap'; - Check for missing node_modules:
yarn install - Verify
cssbundling-railsgem is installed - Run CSS build:
yarn build:css
Prevention:
- Test each asset type separately (JS, CSS, images)
- Use browser dev tools to inspect loaded assets
- Check Network tab for 404s
ERROR-5: Test Suite Failures
Scenario: Tests fail after Rails upgrade
Error Message:
NoMethodError: undefined method `use_transactional_fixtures'
for RSpec
Recovery Strategy:
- Update test gems to compatible versions
- Review test framework changelogs (RSpec, Capybara)
- Fix deprecated test syntax
- Update factory_bot if needed
- Run tests incrementally:
rspec spec/models
Prevention:
- Run tests after each upgrade phase
- Keep test gems up to date
- Review test framework upgrade guides
ERROR-6: LINE Webhook Failure
Scenario: LINE bot stops responding after upgrade
Error Message: (Rails logs)
ActionController::InvalidAuthenticityToken
Recovery Strategy:
- Verify CSRF token is skipped for webhook endpoint
- Check LINE signature verification logic
- Test webhook endpoint manually:
curl -X POST ... - Review webhook controller for deprecations
- Check LINE Bot API gem compatibility
Prevention:
- Test webhook immediately after upgrade
- Keep LINE bot gem at latest version
- Use staging environment with test LINE bot
7.2 Runtime Error Handling
ERROR-7: Asset Loading Failures (Production)
Scenario: Assets don't load in production after deployment
Detection:
- Monitor application logs for 404s
- Check browser console for errors
- Use application monitoring (e.g., Sentry, Honeybadger)
Recovery:
- Verify assets were precompiled:
rails assets:precompile - Check asset manifest:
cat public/assets/.sprockets-manifest-*.json - Ensure web server serves
/assetscorrectly - Verify
config.assets.compile = falsein production.rb - Check CDN configuration if used
Rollback:
git revert <commit-hash>
rails assets:precompile
rails db:migrate:status # Verify no new migrations
restart application server
ERROR-8: Database Connection Issues
Scenario: Application can't connect to database after upgrade
Detection:
- Health check endpoint fails
- Application won't start
- Error logs show connection errors
Recovery:
- Verify database.yml configuration
- Check database gem version (mysql2 or pg)
- Test connection manually:
rails dbconsole - Verify database server is running
- Check connection pool settings
Rollback Plan:
# If migrations were run
rails db:rollback STEP=<number_of_migrations>
# If application won't start
git checkout <previous-release-tag>
bundle install
rails assets:precompile
restart application server
7.3 Error Monitoring
Monitor-1: Application Performance Monitoring
# Add to Gemfile
gem 'sentry-ruby'
gem 'sentry-rails'
# Configure
Sentry.init do |config|
config.dsn = ENV['SENTRY_DSN']
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
config.traces_sample_rate = 0.1
config.profiles_sample_rate = 0.1
end
Monitor-2: Log Aggregation
- Ensure all errors are logged properly
- Use structured logging for easier parsing
- Monitor logs for deprecation warnings
Monitor-3: Health Checks
# config/routes.rb
get '/health', to: 'health#index'
# app/controllers/health_controller.rb
class HealthController < ApplicationController
skip_before_action :require_login
def index
render json: {
status: 'ok',
rails_version: Rails.version,
ruby_version: RUBY_VERSION
}
end
end
8. Observability Strategy
8.1 Overview
Comprehensive observability is critical for monitoring the upgrade process and ensuring production stability. This strategy covers structured logging, metrics collection, distributed tracing, and health monitoring.
8.2 Structured Logging
8.2.1 Logging Framework
Lograge + Semantic Logger Configuration:
# config/initializers/lograge.rb
Rails.application.configure do
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
# Add custom fields to every log entry
config.lograge.custom_options = lambda do |event|
{
request_id: event.payload[:request_id],
rails_version: Rails.version,
ruby_version: RUBY_VERSION,
upgrade_phase: ENV['UPGRADE_PHASE'] || 'completed',
user_id: event.payload[:user_id],
operator_id: event.payload[:operator_id],
host: Socket.gethostname,
timestamp: Time.now.utc.iso8601
}
end
# Log additional request details
config.lograge.custom_payload do |controller|
{
user_id: controller.current_operator&.id,
params: controller.params.except(:controller, :action, :format, :password).to_h
}
end
end
# config/initializers/semantic_logger.rb
Rails.application.configure do
# Set log level
config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym
# Use semantic logger
config.rails_semantic_logger.format = :json
config.rails_semantic_logger.add_file_appender = true
# Add application context
config.semantic_logger.application = 'ReLINE'
config.semantic_logger.environment = Rails.env
end
8.2.2 Log Context Fields
Every log entry includes:
- request_id: Unique identifier for request tracing
- rails_version: Current Rails version (tracks upgrade phases)
- ruby_version: Ruby runtime version
- upgrade_phase: Which phase of upgrade (e.g., "6.1", "7.0", "7.1", "8.0", "8.1", "completed")
- user_id/operator_id: User context for debugging
- host: Server hostname for distributed systems
- timestamp: UTC timestamp in ISO 8601 format
8.2.3 Centralized Logging
Option A: ELK Stack (Elasticsearch, Logstash, Kibana)
# config/initializers/logstash.rb
if Rails.env.production?
logstash_output = LogStashLogger.new(
type: :tcp,
host: ENV['LOGSTASH_HOST'],
port: ENV['LOGSTASH_PORT']
)
Rails.logger.extend(ActiveSupport::Logger.broadcast(logstash_output))
end
Option B: AWS CloudWatch
# config/initializers/cloudwatch.rb
if Rails.env.production?
require 'aws-sdk-cloudwatchlogs'
cloudwatch_logger = ActiveSupport::Logger.new(
CloudWatchLogger.new(
ENV['AWS_CLOUDWATCH_LOG_GROUP'],
ENV['AWS_CLOUDWATCH_LOG_STREAM']
)
)
Rails.logger.extend(ActiveSupport::Logger.broadcast(cloudwatch_logger))
end
8.3 Metrics and Monitoring
8.3.1 Key Metrics
Track the following metrics to measure upgrade impact:
Application Metrics:
- error_rate: Percentage of requests resulting in 5xx errors
- response_time_p50/p95/p99: Response time percentiles
- throughput: Requests per second
- memory_usage: Heap size and RSS memory
- cpu_usage: CPU utilization percentage
- active_connections: Database connection pool usage
- queue_depth: Background job queue size
Business Metrics:
- line_webhook_success_rate: Percentage of successful LINE webhook processing
- authentication_failures: Failed login attempts
- content_operations: CRUD operations per minute
- operator_sessions: Active operator sessions
Asset Pipeline Metrics (during migration):
- asset_compile_time: Time to compile assets
- asset_bundle_size: JavaScript/CSS bundle sizes
- asset_load_time: Client-side asset loading time
8.3.2 Prometheus Integration
# config/initializers/prometheus.rb
require 'prometheus_exporter/middleware'
require 'prometheus_exporter/instrumentation'
# Middleware to track requests
Rails.application.middleware.unshift PrometheusExporter::Middleware
# Start prometheus exporter process
unless Rails.env.test?
PrometheusExporter::Instrumentation::Process.start(type: 'web')
PrometheusExporter::Instrumentation::ActiveRecord.start(
custom_labels: { rails_version: Rails.version },
config_labels: [:database, :host]
)
end
# Custom metrics
module PrometheusMetrics
class << self
def webhook_processed(success:)
PrometheusExporter::Client.default.send_json(
type: 'line_webhook',
success: success,
rails_version: Rails.version
)
end
def asset_compile_time(duration:, type:)
PrometheusExporter::Client.default.send_json(
type: 'asset_compilation',
duration: duration,
asset_type: type,
rails_version: Rails.version
)
end
end
end
8.3.3 Grafana Dashboards
Create Grafana dashboards to visualize:
Dashboard 1: Application Health
- Error rate (target: <1%)
- Response time percentiles (target: p95 <500ms)
- Throughput (requests/sec)
- Memory/CPU usage
Dashboard 2: Rails Upgrade Impact
- Before/after comparison of key metrics
- Version-specific performance
- Deprecation warning counts
- Asset compilation times
Dashboard 3: LINE Bot Integration
- Webhook success rate (target: >99%)
- Webhook processing time
- LINE API error responses
- Message throughput
8.3.4 Alert Rules
Configure alerts with appropriate thresholds:
# prometheus_alerts.yml
groups:
- name: rails_application
interval: 30s
rules:
# Critical: Error rate spike
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
for: 2m
severity: critical
annotations:
summary: "High error rate detected (>5%)"
# Critical: Response time degradation
- alert: SlowResponseTime
expr: histogram_quantile(0.95, http_request_duration_seconds) > 1.0
for: 5m
severity: critical
annotations:
summary: "P95 response time >1s"
# Warning: Memory usage
- alert: HighMemoryUsage
expr: process_resident_memory_bytes > 1e9 # 1GB
for: 5m
severity: warning
annotations:
summary: "Memory usage above 1GB"
# Critical: LINE webhook failures
- alert: LineWebhookFailures
expr: rate(line_webhook_failures_total[5m]) > 0.1
for: 2m
severity: critical
annotations:
summary: "LINE webhook failure rate >10%"
# Warning: Database connection pool exhaustion
- alert: DatabaseConnectionPoolExhausted
expr: active_record_connection_pool_busy / active_record_connection_pool_size > 0.9
for: 3m
severity: warning
annotations:
summary: "Database connection pool >90% utilized"
8.4 Distributed Tracing
8.4.1 OpenTelemetry Integration
# config/initializers/opentelemetry.rb
require 'opentelemetry/sdk'
require 'opentelemetry/instrumentation/rails'
require 'opentelemetry/instrumentation/active_record'
require 'opentelemetry/instrumentation/net_http'
OpenTelemetry::SDK.configure do |c|
c.service_name = 'reline-application'
c.service_version = Rails.version
# Configure exporters (Jaeger, Zipkin, etc.)
c.use_all({
'OpenTelemetry::Instrumentation::Rails' => {
enable_recognize_route: true
},
'OpenTelemetry::Instrumentation::ActiveRecord' => {
enable_sql_obfuscation: true
}
})
end
8.4.2 Custom Spans for Critical Operations
# app/services/line_webhook_service.rb
class LineWebhookService
def process_event(event)
tracer = OpenTelemetry.tracer_provider.tracer('reline-line-bot')
tracer.in_span('line_webhook.process_event',
attributes: {
'event.type' => event['type'],
'rails.version' => Rails.version
}) do |span|
# Process LINE webhook
result = handle_message(event)
span.set_attribute('event.success', result.success?)
span.set_attribute('event.message_id', event['message']['id'])
result
rescue StandardError => e
span.record_exception(e)
span.status = OpenTelemetry::Trace::Status.error("Failed: #{e.message}")
raise
end
end
end
8.5 Health Checks
8.5.1 Health Check Endpoints
# config/routes.rb
namespace :health do
get 'readiness', to: 'health#readiness'
get 'liveness', to: 'health#liveness'
end
# app/controllers/health_controller.rb
class HealthController < ApplicationController
skip_before_action :require_login
skip_before_action :verify_authenticity_token
# Readiness: Can the application serve traffic?
def readiness
checks = {
database: check_database,
line_api: check_line_api,
disk_space: check_disk_space,
memory: check_memory
}
all_passed = checks.values.all? { |check| check[:status] == 'ok' }
status_code = all_passed ? :ok : :service_unavailable
render json: {
status: all_passed ? 'ready' : 'not_ready',
checks: checks,
rails_version: Rails.version,
ruby_version: RUBY_VERSION,
timestamp: Time.now.utc.iso8601
}, status: status_code
end
# Liveness: Is the application alive?
def liveness
render json: {
status: 'alive',
rails_version: Rails.version,
ruby_version: RUBY_VERSION,
uptime: Time.now.to_i - $process_start_time
}
end
private
def check_database
ActiveRecord::Base.connection.execute('SELECT 1')
{ status: 'ok', response_time_ms: measure_response_time { ActiveRecord::Base.connection.execute('SELECT 1') } }
rescue StandardError => e
{ status: 'error', message: e.message }
end
def check_line_api
# Simple connectivity check to LINE API
response = Net::HTTP.get_response(URI('https://api.line.me/v2/bot/info'))
response.is_a?(Net::HTTPSuccess) || response.code == '401' ? # 401 is ok (auth issue, but API is reachable)
{ status: 'ok' } :
{ status: 'error', message: "HTTP #{response.code}" }
rescue StandardError => e
{ status: 'error', message: e.message }
end
def check_disk_space
stat = Sys::Filesystem.stat('/')
available_gb = stat.bytes_available / (1024.0 ** 3)
if available_gb > 5.0
{ status: 'ok', available_gb: available_gb.round(2) }
else
{ status: 'warning', available_gb: available_gb.round(2), message: 'Low disk space' }
end
rescue StandardError => e
{ status: 'error', message: e.message }
end
def check_memory
require 'get_process_mem'
mem = GetProcessMem.new
memory_mb = mem.mb
if memory_mb < 1024
{ status: 'ok', memory_mb: memory_mb.round(2) }
else
{ status: 'warning', memory_mb: memory_mb.round(2), message: 'High memory usage' }
end
rescue StandardError => e
{ status: 'error', message: e.message }
end
def measure_response_time
start = Time.now
yield
((Time.now - start) * 1000).round(2)
end
end
# config/initializers/process_start_time.rb
$process_start_time = Time.now.to_i
8.5.2 Health Check Dependencies
# Gemfile
gem 'sys-filesystem' # For disk space checks
gem 'get_process_mem' # For memory usage checks
8.6 Error Tracking
Already covered in Section 7.3, but enhanced with observability context:
# config/initializers/sentry.rb
Sentry.init do |config|
config.dsn = ENV['SENTRY_DSN']
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
# Performance monitoring
config.traces_sample_rate = ENV.fetch('SENTRY_TRACES_SAMPLE_RATE', 0.1).to_f
config.profiles_sample_rate = ENV.fetch('SENTRY_PROFILES_SAMPLE_RATE', 0.1).to_f
# Set context for all events
config.before_send = lambda do |event, hint|
event.contexts[:rails] = {
version: Rails.version,
environment: Rails.env
}
event.contexts[:ruby] = {
version: RUBY_VERSION,
platform: RUBY_PLATFORM
}
event
end
# Filter sensitive data
config.excluded_exceptions += ['ActionController::RoutingError']
config.sanitize_fields = ['password', 'password_confirmation', 'secret', 'token']
end
8.7 Dashboard Visualization
Create a centralized observability dashboard combining:
Metrics Sources:
- Prometheus metrics → Grafana dashboards
- Application logs → ELK/CloudWatch dashboards
- Distributed traces → Jaeger UI
- Error tracking → Sentry dashboard
- Health checks → Monitoring system (Datadog, New Relic, etc.)
Key Dashboard Panels:
-
Real-time Application Status
- Current error rate
- Active connections
- Request throughput
-
Rails Upgrade Timeline
- Version deployment timeline
- Performance before/after comparison
- Error rate changes by version
-
Asset Pipeline Performance
- Build times (Webpacker vs esbuild)
- Asset sizes
- Client-side load times
-
LINE Bot Integration Status
- Webhook success rate
- Message processing latency
- API error distribution
8.8 Observability Testing
During Upgrade:
# Verify structured logging
tail -f log/production.log | jq .
# Check Prometheus metrics endpoint
curl http://localhost:9394/metrics
# Test health checks
curl http://localhost:3000/health/readiness | jq .
curl http://localhost:3000/health/liveness | jq .
# Verify tracing
# Access Jaeger UI and search for recent traces
Post-Upgrade Validation:
- All metrics are being collected
- Logs are flowing to centralized system
- Traces are visible in tracing UI
- Alerts are configured and tested
- Dashboards display accurate data
9. Runtime Error Handling Strategy
This section complements Section 7 (Error Handling) by focusing on production runtime error scenarios.
9.1 Controller-Level Error Handling
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Global error handlers
rescue_from StandardError, with: :handle_standard_error
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized
rescue_from ActionController::ParameterMissing, with: :handle_bad_request
private
def handle_standard_error(exception)
Rails.logger.error({
error: exception.class.name,
message: exception.message,
backtrace: exception.backtrace.first(10),
request_id: request.request_id,
operator_id: current_operator&.id,
params: params.to_unsafe_h
}.to_json)
# Send to error tracking
Sentry.capture_exception(exception)
respond_to do |format|
format.html { render 'errors/500', status: :internal_server_error, layout: 'error' }
format.json { render json: { error: 'Internal server error' }, status: :internal_server_error }
end
end
def handle_not_found(exception)
Rails.logger.warn("Record not found: #{exception.message}")
respond_to do |format|
format.html { render 'errors/404', status: :not_found, layout: 'error' }
format.json { render json: { error: 'Not found' }, status: :not_found }
end
end
def handle_unauthorized(exception)
Rails.logger.warn("Unauthorized access: #{exception.message} - Operator: #{current_operator&.id}")
respond_to do |format|
format.html {
flash[:alert] = 'この操作を実行する権限がありません。'
redirect_to root_path
}
format.json { render json: { error: 'Forbidden' }, status: :forbidden }
end
end
def handle_bad_request(exception)
Rails.logger.warn("Bad request: #{exception.message}")
respond_to do |format|
format.html {
flash[:alert] = '無効なリクエストです。'
redirect_back(fallback_location: root_path)
}
format.json { render json: { error: 'Bad request', message: exception.message }, status: :bad_request }
end
end
end
9.2 User-Facing Error Messages
Create user-friendly error pages with assets properly loaded:
/ app/views/errors/500.html.slim
doctype html
html
head
title システムエラー - ReLINE
= stylesheet_link_tag 'application', media: 'all'
body
.error-container
h1 申し訳ございません
p システムエラーが発生しました。
p しばらく時間をおいてから再度お試しください。
- if Rails.env.development?
.debug-info
p Request ID: #{request.request_id}
p Timestamp: #{Time.now}
= link_to 'トップページに戻る', root_path, class: 'btn btn-primary'
9.3 LINE Bot API Fault Tolerance
9.3.1 Circuit Breaker Pattern
# config/initializers/circuitbox.rb
require 'circuitbox'
Circuitbox.configure do |config|
config.default_circuit_store = Circuitbox::MemoryStore.new
end
# app/services/line_client_wrapper.rb
class LineClientWrapper
def self.circuit
@circuit ||= Circuitbox.circuit(:line_api, {
exceptions: [Net::ReadTimeout, Net::OpenTimeout, Errno::ECONNREFUSED],
sleep_window: 30, # 30 seconds before trying again
volume_threshold: 10, # Minimum requests before opening circuit
error_threshold: 50, # Open circuit if 50% fail
timeout_seconds: 5 # Request timeout
})
end
def self.send_message(user_id, message)
circuit.run do
client = Line::Bot::Client.new do |config|
config.channel_id = ENV['LINE_CHANNEL_ID']
config.channel_secret = ENV['LINE_CHANNEL_SECRET']
config.channel_token = ENV['LINE_CHANNEL_TOKEN']
end
client.push_message(user_id, message)
end
rescue Circuitbox::OpenCircuitError => e
Rails.logger.error("Circuit breaker open for LINE API: #{e.message}")
# Fallback: enqueue for later or notify operations team
LineMessageJob.perform_later(user_id, message)
false
end
end
9.3.2 Retry Logic with Exponential Backoff
# app/services/line_message_service.rb
class LineMessageService
MAX_RETRIES = 3
BASE_DELAY = 1 # second
def send_with_retry(user_id, message)
retries = 0
begin
LineClientWrapper.send_message(user_id, message)
rescue Net::ReadTimeout, Net::OpenTimeout => e
retries += 1
if retries <= MAX_RETRIES
delay = BASE_DELAY * (2 ** (retries - 1)) # Exponential backoff: 1s, 2s, 4s
Rails.logger.warn({
message: "LINE API retry attempt #{retries}/#{MAX_RETRIES}",
delay_seconds: delay,
error: e.message,
user_id: user_id
}.to_json)
sleep(delay)
retry
else
Rails.logger.error("LINE API max retries exceeded for user #{user_id}")
Sentry.capture_exception(e, extra: { user_id: user_id, retries: retries })
# Fallback: queue for background processing
LineMessageJob.perform_later(user_id, message)
false
end
end
end
end
9.3.3 Queue-Based Webhook Processing
# app/controllers/operator/webhooks_controller.rb
class Operator::WebhooksController < ApplicationController
skip_before_action :require_login
skip_before_action :verify_authenticity_token
def callback
unless line_signature_valid?
Rails.logger.warn("Invalid LINE signature")
return head :bad_request
end
body = request.body.read
events = JSON.parse(body)['events']
# Queue webhook processing instead of synchronous handling
events.each do |event|
LineWebhookJob.perform_later(event)
end
# Return immediately to LINE
head :ok
rescue JSON::ParserError => e
Rails.logger.error("Invalid JSON in webhook: #{e.message}")
Sentry.capture_exception(e)
head :bad_request
end
private
def line_signature_valid?
body = request.body.read
request.body.rewind
signature = request.env['HTTP_X_LINE_SIGNATURE']
return false if signature.blank?
hash = OpenSSL::HMAC.digest(
OpenSSL::Digest.new('SHA256'),
ENV['LINE_CHANNEL_SECRET'],
body
)
Base64.strict_encode64(hash) == signature
end
end
# app/jobs/line_webhook_job.rb
class LineWebhookJob < ApplicationJob
queue_as :critical
retry_on StandardError, wait: :exponentially_longer, attempts: 5
def perform(event)
LineWebhookService.new.process_event(event)
end
end
9.4 Graceful Degradation Strategy
When external dependencies fail, degrade functionality gracefully:
# app/services/content_delivery_service.rb
class ContentDeliveryService
def deliver_to_line_group(content, line_group)
# Primary: Send via LINE Bot API
begin
result = LineMessageService.new.send_with_retry(
line_group.line_user_id,
build_message(content)
)
if result
content.update(delivered_at: Time.current, delivery_status: 'delivered')
return true
end
rescue StandardError => e
Rails.logger.error("LINE delivery failed: #{e.message}")
Sentry.capture_exception(e)
end
# Fallback: Mark for manual retry
content.update(
delivery_status: 'pending',
delivery_error: 'LINE API unavailable',
retry_after: 30.minutes.from_now
)
# Notify operations team
notify_delivery_failure(content, line_group)
false
end
private
def notify_delivery_failure(content, line_group)
# Send email, Slack notification, etc.
OperationsMailer.delivery_failure(content, line_group).deliver_later
end
end
9.5 Transaction Patterns
9.5.1 Service Layer Transactions
# app/services/content_create_service.rb
class ContentCreateService
def create(params, operator)
ActiveRecord::Base.transaction do
content = Content.create!(params)
# Audit log
AuditLog.create!(
resource: content,
action: 'create',
operator: operator,
changes: content.attributes
)
# Schedule delivery if needed
if content.scheduled_at.present?
ContentDeliveryJob.set(wait_until: content.scheduled_at)
.perform_later(content.id)
end
content
rescue ActiveRecord::RecordInvalid => e
Rails.logger.warn("Content creation failed: #{e.message}")
raise # Re-raise to rollback transaction
end
end
end
9.5.2 Idempotency for Webhook Processing
# app/services/line_webhook_service.rb
class LineWebhookService
def process_event(event)
event_id = event.dig('webhookEventId')
# Check if already processed (idempotency)
if ProcessedEvent.exists?(event_id: event_id)
Rails.logger.info("Skipping duplicate webhook event: #{event_id}")
return true
end
result = ActiveRecord::Base.transaction do
case event['type']
when 'message'
handle_message_event(event)
when 'follow'
handle_follow_event(event)
when 'unfollow'
handle_unfollow_event(event)
end
# Mark as processed
ProcessedEvent.create!(
event_id: event_id,
event_type: event['type'],
processed_at: Time.current,
data: event
)
true
end
result
rescue StandardError => e
Rails.logger.error({
message: "Webhook processing error",
event_id: event_id,
error: e.message,
backtrace: e.backtrace.first(5)
}.to_json)
Sentry.capture_exception(e, extra: { event: event })
false
end
end
# Migration for processed_events table
class CreateProcessedEvents < ActiveRecord::Migration[8.1]
def change
create_table :processed_events do |t|
t.string :event_id, null: false, index: { unique: true }
t.string :event_type
t.datetime :processed_at
t.jsonb :data
t.timestamps
end
end
end
9.5.3 Atomic Asset Deployment
# lib/tasks/deploy.rake
namespace :deploy do
desc 'Deploy assets atomically'
task :assets => :environment do
timestamp = Time.now.to_i
# Build assets to temporary directory
build_dir = "tmp/asset_builds/#{timestamp}"
FileUtils.mkdir_p(build_dir)
# Compile assets
system("yarn build --outdir=#{build_dir}/js") || raise("JS build failed")
system("yarn build:css --output=#{build_dir}/css/application.css") || raise("CSS build failed")
# Precompile Rails assets
ENV['RAILS_ASSET_BUILD_DIR'] = build_dir
Rake::Task['assets:precompile'].invoke
# Atomic switch: symlink new build
public_assets = 'public/assets'
FileUtils.rm_f(public_assets)
FileUtils.ln_s(File.expand_path(build_dir), public_assets)
puts "✅ Assets deployed successfully to #{build_dir}"
rescue StandardError => e
puts "❌ Asset deployment failed: #{e.message}"
FileUtils.rm_rf(build_dir)
raise
end
end
10. Reusability and Extraction Patterns
This section addresses how upgrade procedures can be abstracted for reuse in other Rails projects.
10.1 Overview
While this design document is tailored to the ReLINE application, many upgrade patterns can be extracted into reusable utilities and shared with the Rails community. This section outlines abstraction strategies for maximum reusability.
10.2 RailsUpgradeManager Class Concept
A reusable class to orchestrate Rails upgrades across versions:
# lib/rails_upgrade_manager.rb
class RailsUpgradeManager
attr_reader :config, :current_version, :target_version
def initialize(config_file = 'config/upgrade.yml')
@config = YAML.load_file(config_file)
@current_version = Gem::Version.new(Rails.version)
@target_version = Gem::Version.new(@config['target_rails_version'])
end
def upgrade_path
# Calculate incremental upgrade steps
versions = MAJOR_VERSIONS.select do |v|
v > current_version && v <= target_version
end
versions.map { |v| UpgradeStep.new(v, config) }
end
def execute_upgrade
upgrade_path.each do |step|
puts "📦 Upgrading to Rails #{step.version}..."
step.pre_upgrade_checks
step.update_gemfile
step.bundle_update
step.run_app_update
step.fix_deprecations
step.run_tests
step.post_upgrade_validations
puts "✅ Rails #{step.version} upgrade complete"
end
end
def rollback_to(version)
RollbackManager.new(version, config).execute
end
private
MAJOR_VERSIONS = [
Gem::Version.new('6.1.0'),
Gem::Version.new('7.0.0'),
Gem::Version.new('7.1.0'),
Gem::Version.new('8.0.0'),
Gem::Version.new('8.1.0')
].freeze
end
# Usage in any Rails project:
# manager = RailsUpgradeManager.new
# manager.upgrade_path.each { |step| puts "Step: #{step.version}" }
# manager.execute_upgrade
Configuration File (config/upgrade.yml):
# Parameterized configuration for any Rails project
target_rails_version: '8.1.1'
target_ruby_version: '3.3.10'
database:
adapter: <%= ENV.fetch('DB_ADAPTER', 'mysql2') %>
backup_command: 'mysqldump -u root <%= db_name %> > <%= backup_file %>'
tests:
command: 'bundle exec rspec'
coverage_threshold: 0.90
assets:
current_bundler: 'webpacker' # or 'sprockets', 'webpack'
target_bundler: 'esbuild' # or 'webpack', 'vite'
notifications:
slack_webhook: <%= ENV['SLACK_WEBHOOK_URL'] %>
email: <%= ENV['UPGRADE_NOTIFICATION_EMAIL'] %>
rollback:
strategy: 'git' # or 'capistrano', 'heroku'
backup_retention_days: 7
10.3 AssetPipelineMigrator Class Concept
A reusable class to migrate between asset bundlers:
# lib/asset_pipeline_migrator.rb
class AssetPipelineMigrator
def initialize(from:, to:, config: {})
@from = from # :webpacker, :sprockets, :webpack
@to = to # :esbuild, :vite, :webpack
@config = config
end
def migrate
puts "🔄 Migrating from #{@from} to #{@to}..."
validate_compatibility
backup_current_setup
remove_old_bundler
install_new_bundler
migrate_entry_points
update_view_helpers
configure_build_scripts
test_compilation
puts "✅ Asset pipeline migration complete"
end
def validate_compatibility
# Check if migration path is supported
unless supported_migrations.include?([@from, @to])
raise "Migration from #{@from} to #{@to} is not yet supported"
end
end
def migrate_entry_points
case [@from, @to]
when [:webpacker, :esbuild]
migrate_webpacker_to_esbuild
when [:sprockets, :esbuild]
migrate_sprockets_to_esbuild
when [:webpack, :vite]
migrate_webpack_to_vite
else
raise "Unknown migration path"
end
end
private
def migrate_webpacker_to_esbuild
# Move files
FileUtils.mv('app/javascript/packs', 'app/javascript')
# Update imports
Dir.glob('app/javascript/**/*.js').each do |file|
content = File.read(file)
# Update require.context (Webpack-specific)
content.gsub!(/require\.context\(['"](.+?)['"],\s*true\)/, '/* Removed Webpack require.context */')
# Update dynamic imports if needed
content.gsub!(/import\(['"](.+?)['"]\)/, 'import("\1")')
File.write(file, content)
end
puts "✅ Entry points migrated"
end
def update_view_helpers
Dir.glob('app/views/**/*.{erb,slim,haml}').each do |file|
content = File.read(file)
case @to
when :esbuild
content.gsub!(/javascript_pack_tag/, 'javascript_include_tag')
content.gsub!(/stylesheet_pack_tag/, 'stylesheet_link_tag')
content.gsub!(/favicon_pack_tag ['"]media\/images\/(.+?)['"]/, 'favicon_link_tag \'\1\'')
end
File.write(file, content)
end
puts "✅ View helpers updated"
end
def supported_migrations
[
[:webpacker, :esbuild],
[:sprockets, :esbuild],
[:webpack, :vite],
[:webpacker, :vite]
]
end
end
# Usage in any Rails project:
# migrator = AssetPipelineMigrator.new(
# from: :webpacker,
# to: :esbuild,
# config: { preserve_source_maps: true }
# )
# migrator.migrate
10.4 Shared Utility Designs
10.4.1 BackupManager
# lib/utilities/backup_manager.rb
module Utilities
class BackupManager
def initialize(config = {})
@database = config[:database] || Rails.configuration.database_configuration[Rails.env]
@backup_dir = config[:backup_dir] || 'tmp/backups'
@retention_days = config[:retention_days] || 7
end
def create_backup
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
backup_file = "#{@backup_dir}/#{database_name}_#{timestamp}.sql"
FileUtils.mkdir_p(@backup_dir)
case @database['adapter']
when 'mysql2'
system("mysqldump -u #{@database['username']} -p#{@database['password']} #{database_name} > #{backup_file}")
when 'postgresql'
system("pg_dump #{database_name} > #{backup_file}")
else
raise "Unsupported database adapter: #{@database['adapter']}"
end
compress_backup(backup_file)
cleanup_old_backups
"#{backup_file}.gz"
end
def restore_backup(backup_file)
# Decompress and restore
system("gunzip -c #{backup_file} | mysql -u #{@database['username']} -p#{@database['password']} #{database_name}")
end
private
def database_name
@database['database']
end
def compress_backup(file)
system("gzip #{file}")
end
def cleanup_old_backups
cutoff_date = @retention_days.days.ago
Dir.glob("#{@backup_dir}/*.sql.gz").each do |file|
File.delete(file) if File.mtime(file) < cutoff_date
end
end
end
end
# Usage:
# backup = Utilities::BackupManager.new(retention_days: 7)
# backup.create_backup
10.4.2 DeprecationChecker
# lib/utilities/deprecation_checker.rb
module Utilities
class DeprecationChecker
attr_reader :deprecations
def initialize
@deprecations = []
subscribe_to_deprecations
end
def check_codebase
check_active_record_syntax
check_controller_syntax
check_view_helpers
check_gem_compatibility
generate_report
end
def subscribe_to_deprecations
ActiveSupport::Deprecation.behavior = lambda do |message, callstack, deprecation_horizon, gem_name|
@deprecations << {
message: message,
callstack: callstack.first(3),
deprecation_horizon: deprecation_horizon,
gem_name: gem_name,
detected_at: Time.now
}
end
end
def check_active_record_syntax
# Check for deprecated ActiveRecord syntax
Dir.glob('app/models/**/*.rb').each do |file|
content = File.read(file)
# Example: Check for deprecated `update_attributes`
if content.match?(/\.update_attributes\(/)
@deprecations << {
type: 'active_record',
file: file,
message: 'update_attributes is deprecated, use update instead',
severity: 'high'
}
end
# Check for deprecated `find_by_*` dynamic finders
if content.match?(/\.find_by_(\w+)\(/)
@deprecations << {
type: 'active_record',
file: file,
message: 'Dynamic find_by_* methods are deprecated, use find_by instead',
severity: 'medium'
}
end
end
end
def check_controller_syntax
Dir.glob('app/controllers/**/*.rb').each do |file|
content = File.read(file)
# Check for deprecated `before_filter`
if content.match?(/before_filter/)
@deprecations << {
type: 'controller',
file: file,
message: 'before_filter is deprecated, use before_action instead',
severity: 'medium'
}
end
end
end
def check_view_helpers
Dir.glob('app/views/**/*.{erb,slim,haml}').each do |file|
content = File.read(file)
# Check for deprecated asset helpers (if migrating from Webpacker)
if content.match?(/javascript_pack_tag|stylesheet_pack_tag/)
@deprecations << {
type: 'view_helper',
file: file,
message: 'Webpacker helpers detected, need migration to esbuild helpers',
severity: 'high'
}
end
end
end
def check_gem_compatibility
gemfile = File.read('Gemfile')
lockfile = File.read('Gemfile.lock')
# Parse and check each gem's Rails compatibility
# (Implementation would use Bundler API)
end
def generate_report
report = {
total: @deprecations.count,
by_severity: @deprecations.group_by { |d| d[:severity] }.transform_values(&:count),
by_type: @deprecations.group_by { |d| d[:type] }.transform_values(&:count),
details: @deprecations
}
File.write('tmp/deprecation_report.json', JSON.pretty_generate(report))
puts "\n📋 Deprecation Report"
puts "Total deprecations found: #{report[:total]}"
puts "\nBy severity:"
report[:by_severity].each { |severity, count| puts " #{severity}: #{count}" }
puts "\nFull report: tmp/deprecation_report.json"
report
end
end
end
# Usage:
# checker = Utilities::DeprecationChecker.new
# checker.check_codebase
10.4.3 AssetCompiler
# lib/utilities/asset_compiler.rb
module Utilities
class AssetCompiler
def initialize(bundler: :esbuild)
@bundler = bundler
@start_time = nil
@metrics = {}
end
def compile(environment: :production)
@start_time = Time.now
case @bundler
when :esbuild
compile_esbuild(environment)
when :webpack
compile_webpack(environment)
when :vite
compile_vite(environment)
else
raise "Unsupported bundler: #{@bundler}"
end
collect_metrics
report_metrics
end
def watch
case @bundler
when :esbuild
system('yarn build --watch')
when :webpack
system('yarn webpack --watch')
when :vite
system('yarn vite build --watch')
end
end
private
def compile_esbuild(environment)
# Set NODE_ENV
ENV['NODE_ENV'] = environment.to_s
# Compile JavaScript
js_result = system('yarn build')
raise 'JavaScript compilation failed' unless js_result
# Compile CSS
css_result = system('yarn build:css')
raise 'CSS compilation failed' unless css_result
# Rails asset precompilation
if environment == :production
rails_result = system('bundle exec rails assets:precompile')
raise 'Rails asset precompilation failed' unless rails_result
end
end
def collect_metrics
@metrics = {
duration_seconds: (Time.now - @start_time).round(2),
js_bundle_size_kb: file_size('app/assets/builds/application.js'),
css_bundle_size_kb: file_size('app/assets/builds/application.css'),
total_bundle_size_kb: file_size('app/assets/builds/application.js') +
file_size('app/assets/builds/application.css')
}
end
def report_metrics
puts "\n⚡ Asset Compilation Metrics"
puts "Duration: #{@metrics[:duration_seconds]}s"
puts "JS bundle: #{@metrics[:js_bundle_size_kb]} KB"
puts "CSS bundle: #{@metrics[:css_bundle_size_kb]} KB"
puts "Total size: #{@metrics[:total_bundle_size_kb]} KB"
# Send metrics to Prometheus
PrometheusMetrics.asset_compile_time(
duration: @metrics[:duration_seconds],
type: 'full'
)
end
def file_size(path)
File.exist?(path) ? (File.size(path) / 1024.0).round(2) : 0
end
end
end
# Usage:
# compiler = Utilities::AssetCompiler.new(bundler: :esbuild)
# compiler.compile(environment: :production)
10.4.4 TestRunner
# lib/utilities/test_runner.rb
module Utilities
class TestRunner
def initialize(config = {})
@test_framework = config[:framework] || detect_test_framework
@coverage_threshold = config[:coverage_threshold] || 0.90
@parallel = config[:parallel] || false
end
def run_all
puts "🧪 Running full test suite (#{@test_framework})..."
result = case @test_framework
when :rspec
run_rspec
when :minitest
run_minitest
else
raise "Unsupported test framework: #{@test_framework}"
end
validate_coverage if result[:success]
result
end
def run_subset(path)
case @test_framework
when :rspec
system("bundle exec rspec #{path}")
when :minitest
system("bundle exec rails test #{path}")
end
end
private
def detect_test_framework
return :rspec if File.exist?('spec/spec_helper.rb')
return :minitest if File.exist?('test/test_helper.rb')
raise "Cannot detect test framework"
end
def run_rspec
command = 'bundle exec rspec'
command += ' --format documentation' unless @parallel
command += ' --parallel' if @parallel
start_time = Time.now
success = system("COVERAGE=true #{command}")
duration = (Time.now - start_time).round(2)
{
success: success,
duration: duration,
framework: :rspec
}
end
def validate_coverage
return unless File.exist?('coverage/.resultset.json')
require 'json'
resultset = JSON.parse(File.read('coverage/.resultset.json'))
coverage = resultset.dig('RSpec', 'coverage')
total_lines = coverage.values.sum { |lines| lines.count { |l| l.is_a?(Integer) } }
covered_lines = coverage.values.sum { |lines| lines.count { |l| l.is_a?(Integer) && l > 0 } }
coverage_percentage = (covered_lines.to_f / total_lines * 100).round(2)
puts "\n📊 Code Coverage: #{coverage_percentage}%"
if coverage_percentage < (@coverage_threshold * 100)
puts "❌ Coverage below threshold (#{@coverage_threshold * 100}%)"
return false
end
puts "✅ Coverage meets threshold"
true
end
end
end
# Usage:
# runner = Utilities::TestRunner.new(framework: :rspec, coverage_threshold: 0.90)
# runner.run_all
10.4.5 UpgradeValidator
# lib/utilities/upgrade_validator.rb
module Utilities
class UpgradeValidator
def initialize(target_rails_version:, target_ruby_version:)
@target_rails = Gem::Version.new(target_rails_version)
@target_ruby = Gem::Version.new(target_ruby_version)
@validation_results = []
end
def validate
puts "🔍 Validating Rails upgrade..."
check_ruby_version
check_rails_version
check_gem_dependencies
check_database_connection
check_asset_compilation
check_routes
check_tests
check_deprecations
generate_validation_report
end
private
def check_ruby_version
current_ruby = Gem::Version.new(RUBY_VERSION)
if current_ruby >= @target_ruby
pass("Ruby version: #{RUBY_VERSION}")
else
fail("Ruby version #{RUBY_VERSION} is below target #{@target_ruby}")
end
end
def check_rails_version
current_rails = Gem::Version.new(Rails.version)
if current_rails >= @target_rails
pass("Rails version: #{Rails.version}")
else
fail("Rails version #{Rails.version} is below target #{@target_rails}")
end
end
def check_gem_dependencies
result = system('bundle check')
if result
pass("All gem dependencies satisfied")
else
fail("Gem dependencies not satisfied (run 'bundle install')")
end
end
def check_database_connection
ActiveRecord::Base.connection.execute('SELECT 1')
pass("Database connection successful")
rescue StandardError => e
fail("Database connection failed: #{e.message}")
end
def check_asset_compilation
compiler = AssetCompiler.new
compiler.compile(environment: :development)
pass("Asset compilation successful")
rescue StandardError => e
fail("Asset compilation failed: #{e.message}")
end
def check_routes
Rails.application.routes.routes.each do |route|
# Basic route validation
end
pass("Routes validated (#{Rails.application.routes.routes.count} routes)")
rescue StandardError => e
fail("Route validation failed: #{e.message}")
end
def check_tests
runner = TestRunner.new
result = runner.run_all
if result[:success]
pass("Test suite passed")
else
fail("Test suite failed")
end
end
def check_deprecations
checker = DeprecationChecker.new
report = checker.check_codebase
high_severity = report[:by_severity]&.fetch('high', 0) || 0
if high_severity == 0
pass("No high-severity deprecations found")
else
warn("#{high_severity} high-severity deprecations found")
end
end
def pass(message)
@validation_results << { status: :pass, message: message }
puts "✅ #{message}"
end
def fail(message)
@validation_results << { status: :fail, message: message }
puts "❌ #{message}"
end
def warn(message)
@validation_results << { status: :warning, message: message }
puts "⚠️ #{message}"
end
def generate_validation_report
passed = @validation_results.count { |r| r[:status] == :pass }
failed = @validation_results.count { |r| r[:status] == :fail }
warnings = @validation_results.count { |r| r[:status] == :warning }
puts "\n📋 Validation Summary"
puts "Passed: #{passed}"
puts "Failed: #{failed}"
puts "Warnings: #{warnings}"
if failed > 0
puts "\n❌ Upgrade validation FAILED"
exit 1
elsif warnings > 0
puts "\n⚠️ Upgrade validation passed with warnings"
else
puts "\n✅ Upgrade validation PASSED"
end
@validation_results
end
end
end
# Usage:
# validator = Utilities::UpgradeValidator.new(
# target_rails_version: '8.1.1',
# target_ruby_version: '3.3.10'
# )
# validator.validate
10.5 Parameterization Strategy
Replace hardcoded values with configuration parameters:
Current (Hardcoded):
# Bad: Hardcoded values
backup_file = "reline_development_backup.sql"
system("mysqldump -u root reline_development > #{backup_file}")
Improved (Parameterized):
# Good: Configuration-driven
config = {
database_name: ENV.fetch('DB_NAME', Rails.configuration.database_configuration[Rails.env]['database']),
database_user: ENV.fetch('DB_USER', 'root'),
backup_directory: ENV.fetch('BACKUP_DIR', 'tmp/backups')
}
backup_manager = Utilities::BackupManager.new(config)
backup_manager.create_backup
Configuration File Structure (config/upgrade.yml):
# Example configuration that can be copied to any Rails project
project:
name: <%= ENV.fetch('PROJECT_NAME', 'MyApp') %>
current_versions:
ruby: <%= RUBY_VERSION %>
rails: <%= Rails.version %>
bundler: <%= ENV.fetch('CURRENT_ASSET_BUNDLER', 'webpacker') %>
target_versions:
ruby: '3.3.10'
rails: '8.1.1'
bundler: 'esbuild'
database:
adapter: <%= ENV.fetch('DB_ADAPTER', 'postgresql') %>
name: <%= ENV.fetch('DB_NAME') %>
user: <%= ENV.fetch('DB_USER') %>
backup_command: |
<%= case ENV.fetch('DB_ADAPTER', 'postgresql')
when 'postgresql' then "pg_dump #{ENV['DB_NAME']} > %{backup_file}"
when 'mysql2' then "mysqldump -u #{ENV['DB_USER']} #{ENV['DB_NAME']} > %{backup_file}"
end %>
testing:
framework: <%= File.exist?('spec/spec_helper.rb') ? 'rspec' : 'minitest' %>
coverage_threshold: 0.90
parallel: <%= ENV.fetch('PARALLEL_TESTS', 'false') == 'true' %>
notifications:
slack_webhook: <%= ENV['SLACK_WEBHOOK_URL'] %>
email: <%= ENV['NOTIFICATION_EMAIL'] %>
upgrade_phases:
- from: '6.1'
to: '7.0'
breaking_changes:
- 'ActionMailer::Preview.preview_path renamed'
- 'Zeitwerk enabled by default'
- from: '7.0'
to: '7.1'
breaking_changes:
- 'ActiveRecord::Base.connection_db_config changes'
- from: '7.1'
to: '8.0'
breaking_changes:
- 'Propshaft replaces Sprockets by default'
- 'Solid Queue, Solid Cache, Solid Cable introduced'
- from: '8.0'
to: '8.1'
breaking_changes:
- 'Check release notes for 8.1-specific changes'
10.6 Extraction for Open Source
These patterns can be extracted into:
-
Ruby Gem:
rails-upgrade-toolkit- Contains all utility classes
- Provides CLI interface
- Includes default configuration templates
- Supports plugins for custom upgrade logic
-
GitHub Repository: Example Rails upgrade projects
- Before/after comparisons
- Step-by-step guides
- Common pitfall documentation
- Community-contributed patterns
-
Blog Posts/Guides: Sharing knowledge
- "Upgrading Rails 6 to 8: A Complete Guide"
- "Migrating from Webpacker to esbuild"
- "Asset Pipeline Migration Patterns"
Example Gem Structure:
rails-upgrade-toolkit/
├── lib/
│ ├── rails_upgrade_manager.rb
│ ├── asset_pipeline_migrator.rb
│ └── utilities/
│ ├── backup_manager.rb
│ ├── deprecation_checker.rb
│ ├── asset_compiler.rb
│ ├── test_runner.rb
│ └── upgrade_validator.rb
├── templates/
│ ├── upgrade.yml.template
│ └── Procfile.dev.template
├── bin/
│ └── rails-upgrade (CLI tool)
└── README.md
11. Testing Strategy
11.1 Pre-Upgrade Testing
TEST-1: Baseline Test Suite
Objective: Ensure all tests pass before starting upgrade
# Run full test suite
bundle exec rspec
# Record results
echo "Tests passed: $(rspec --format json | jq '.summary.example_count')" > pre_upgrade_results.txt
Success Criteria: 100% of tests pass
TEST-2: Manual Smoke Testing
Objective: Verify critical user paths work
Test Cases:
- Operator login/logout
- LINE bot webhook receives message
- Content creation/editing
- Alarm content management
- Feedback submission
- Scheduler functionality
Document: Screenshots and results
TEST-3: Performance Baseline
Objective: Measure current performance for comparison
# Asset compilation time
time bundle exec rails assets:precompile
# Server boot time
time rails runner "puts 'Ready'"
# Test endpoint response times
ab -n 100 -c 10 http://localhost:3000/
Record: All metrics for comparison
8.2 Incremental Upgrade Testing
TEST-4: After Each Rails Version Upgrade
Test Matrix:
After Rails 6.1 → 7.0:
✓ bundle exec rspec (all tests)
✓ Manual smoke test (critical paths)
✓ Check deprecation warnings
✓ Verify assets compile
After Rails 7.0 → 7.1:
✓ bundle exec rspec (all tests)
✓ Manual smoke test (critical paths)
✓ Check deprecation warnings
✓ Verify assets compile
After Rails 7.1 → 8.0:
✓ bundle exec rspec (all tests)
✓ Manual smoke test (critical paths)
✓ Check deprecation warnings
✓ Verify assets compile
After Rails 8.0 → 8.1:
✓ bundle exec rspec (all tests)
✓ Manual smoke test (critical paths)
✓ Check deprecation warnings
✓ Verify assets compile
Stop Condition: If any test fails, fix before proceeding
8.3 Asset Pipeline Migration Testing
TEST-5: Webpacker Functional Test
Objective: Verify Webpacker still works before removal
# Clear cache
rm -rf public/packs tmp/cache
# Recompile
bundle exec rails webpacker:compile
# Verify in browser
# - Bootstrap styles load
# - FontAwesome icons render
# - Custom JS (scroll.js) works
# - No console errors
TEST-6: esbuild Migration Test
Objective: Verify esbuild produces working assets
# Install dependencies
yarn install
# Build with esbuild
yarn build
yarn build:css
# Check output
ls -lh app/assets/builds/
cat app/assets/builds/application.js | wc -l
cat app/assets/builds/application.css | wc -l
# Start server
rails server
# Verify in browser
# - Bootstrap styles load
# - FontAwesome icons render
# - Custom JS works
# - No console errors
# - Assets load from correct path
TEST-7: Asset Loading Test
Test Cases:
- JavaScript loads and executes
- CSS applies correctly
- Bootstrap modal works
- FontAwesome icons display
- Favicon appears
- Custom fonts load
- Images display
- Source maps work (development)
Tools:
- Chrome DevTools Network tab
- Console for errors
- Elements tab to inspect styles
8.4 Integration Testing
TEST-8: Authentication Flow Test
# spec/features/authentication_spec.rb
RSpec.describe 'Operator Authentication', type: :feature do
scenario 'Operator logs in successfully' do
operator = create(:operator, email: 'test@example.com', password: 'password123')
visit login_path
fill_in 'Email', with: 'test@example.com'
fill_in 'Password', with: 'password123'
click_button 'Login'
expect(page).to have_content 'ログインしました'
expect(current_path).to eq operator_root_path
end
scenario 'Operator logs out' do
operator = create(:operator)
login_as(operator)
click_link 'Logout'
expect(page).to have_content 'ログアウトしました'
expect(current_path).to eq root_path
end
end
TEST-9: LINE Webhook Test
# spec/requests/webhooks_spec.rb
RSpec.describe 'LINE Webhook', type: :request do
let(:valid_signature) { generate_line_signature(request_body) }
let(:request_body) { line_message_event.to_json }
it 'processes message events successfully' do
post operator_webhooks_path,
params: request_body,
headers: { 'X-Line-Signature' => valid_signature }
expect(response).to have_http_status(:ok)
expect(Feedback.count).to eq(1)
end
it 'rejects invalid signatures' do
post operator_webhooks_path,
params: request_body,
headers: { 'X-Line-Signature' => 'invalid' }
expect(response).to have_http_status(:bad_request)
end
end
TEST-10: Authorization Test
# spec/policies/content_policy_spec.rb
RSpec.describe ContentPolicy do
subject { described_class.new(operator, content) }
let(:operator) { create(:operator) }
let(:content) { create(:content) }
context 'when operator is admin' do
let(:operator) { create(:operator, role: :admin) }
it { is_expected.to permit_action(:destroy) }
end
context 'when operator is not admin' do
it { is_expected.not_to permit_action(:destroy) }
end
end
8.5 Performance Testing
TEST-11: Asset Compilation Speed
# Measure Webpacker (before)
time bundle exec rails webpacker:compile
# Record: X seconds
# Measure esbuild (after)
time yarn build && yarn build:css
# Record: Y seconds
# Target: Y ≤ 0.5 * X (50% improvement)
TEST-12: Page Load Performance
# Use Rails performance tests
require 'test_helper'
require 'rails/performance_test_help'
class HomePagePerfTest < ActionDispatch::PerformanceTest
test "homepage loads quickly" do
get root_path
assert_response :success
assert_performance_less_than(100) do
get root_path
end
end
end
TEST-13: Asset Size Comparison
# Before (Webpacker)
ls -lh public/packs/application-*.js
ls -lh public/packs/application-*.css
# After (esbuild)
ls -lh app/assets/builds/application.js
ls -lh app/assets/builds/application.css
# Target: Size increase < 10%
8.6 Browser Compatibility Testing
TEST-14: Cross-Browser Testing
Browsers to Test:
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Test Cases (per browser):
- Page renders correctly
- Bootstrap styles apply
- FontAwesome icons display
- JavaScript executes
- Forms submit
- AJAX requests work
- No console errors
8.7 Edge Case Testing
TEST-15: Error Scenarios
# Test error pages still work
RSpec.describe 'Error Handling', type: :feature do
scenario 'displays 404 page' do
visit '/nonexistent'
expect(page).to have_http_status(404)
# Verify assets still load on error pages
end
scenario 'displays 500 page' do
allow_any_instance_of(ApplicationController)
.to receive(:index).and_raise(StandardError)
visit root_path
expect(page).to have_http_status(500)
# Verify assets still load on error pages
end
end
TEST-16: Load Testing
# Simulate concurrent requests
ab -n 1000 -c 50 http://localhost:3000/
# Monitor:
# - Response time
# - Error rate
# - Memory usage
# - CPU usage
8.8 Post-Upgrade Validation
TEST-17: Production Staging Test
Environment: Staging (production-like)
Test Plan:
- Deploy to staging
- Run full test suite in staging environment
- Perform manual smoke tests
- Monitor logs for 24 hours
- Check error tracking service (Sentry)
- Verify asset loading from CDN (if applicable)
- Test rollback procedure
Success Criteria:
- Zero critical errors
- All smoke tests pass
- Performance metrics within acceptable range
- No user-reported issues
8.9 Test Coverage Requirements
Coverage Targets:
- Model Tests: 95%+ coverage
- Controller Tests: 90%+ coverage
- Integration Tests: All critical user paths covered
- Feature Tests: All user-facing features covered
Verify Coverage:
COVERAGE=true bundle exec rspec
open coverage/index.html
9. Rollback Plan
9.1 Git-Based Rollback
# If upgrade is on a branch
git checkout main
bundle install
rails db:migrate:status # Verify no new migrations
rails assets:precompile
# Restart application
# If committed to main
git revert <commit-hash-range>
bundle install
rails assets:precompile
# Restart application
9.2 Database Rollback
# If migrations were run (unlikely for this upgrade)
rails db:rollback STEP=<number>
# Verify data integrity
rails console
# Check record counts
9.3 Deployment Rollback
# Using Capistrano (example)
cap production deploy:rollback
# Using Heroku (example)
heroku rollback
# Manual
git checkout <previous-release-tag>
bundle install
rails assets:precompile
# Deploy
10. Documentation Requirements
10.1 README Updates
Document the new setup process:
## Setup
### Prerequisites
- Ruby 3.3.10
- Node.js 18+ (for esbuild)
- MySQL 5.7+ (development)
- PostgreSQL 12+ (production)
### Installation
1. Install Ruby dependencies: `bundle install`
2. Install JavaScript dependencies: `yarn install`
3. Setup database: `rails db:setup`
4. Build assets: `yarn build && yarn build:css`
### Development
Run all processes concurrently:
```bash
bin/dev
This starts:
- Rails server (port 3000)
- esbuild watch (JavaScript)
- Sass watch (CSS)
### 10.2 Asset Pipeline Documentation
Create `docs/ASSET_PIPELINE.md`:
```markdown
# Asset Pipeline - esbuild & cssbundling-rails
## Overview
This application uses esbuild for JavaScript bundling and Dart Sass for CSS compilation.
## Directory Structure
- `app/javascript/` - JavaScript source files
- `app/assets/stylesheets/` - SCSS source files
- `app/assets/builds/` - Compiled assets (gitignored)
- `public/assets/` - Precompiled assets (production)
## Development Workflow
1. Edit files in `app/javascript/` or `app/assets/stylesheets/`
2. esbuild/Sass watches for changes and recompiles
3. Refresh browser to see changes
## Production Deployment
```bash
rails assets:precompile
Troubleshooting
[Common issues and solutions]
### 10.3 Upgrade Notes
Create `docs/UPGRADE_NOTES.md`:
```markdown
# Rails 8.1.1 Upgrade Notes
## Completed: 2025-12-01
### Changes Made
1. Ruby upgraded from 3.0.2 → 3.3.10
2. Rails upgraded from 6.1.4.1 → 8.1.1
3. Webpacker removed, replaced with esbuild
4. All gems updated to compatible versions
### Breaking Changes
- Asset helpers changed (see ASSET_PIPELINE.md)
- [List any other breaking changes]
### Rollback Procedure
[Document how to roll back if needed]
11. Dependencies
11.1 Current Dependencies
Gems (Gemfile):
ruby '3.0.2'
gem 'rails', '~> 6.1.4', '>= 6.1.4.1'
gem 'puma', '~> 5.0'
gem 'sass-rails', '>= 6'
gem 'webpacker', '~> 5.0'
gem 'sorcery'
gem 'pundit'
gem 'line-bot-api'
gem 'slim-rails'
gem 'enum_help'
# ... (see Gemfile for complete list)
npm (package.json):
{
"@rails/webpacker": "5.4.3",
"webpack": "^4.46.0",
"bootstrap": "^5.1.3",
"@fortawesome/fontawesome-free": "^5.15.4",
"@rails/ujs": "^6.0.0",
"@rails/activestorage": "^6.0.0"
}
11.2 Target Dependencies
Gems (Updated Gemfile):
ruby '3.3.10'
gem 'rails', '~> 8.1.1'
gem 'puma', '~> 6.0'
gem 'jsbundling-rails', '~> 1.3'
gem 'cssbundling-rails', '~> 1.4'
gem 'sorcery', '~> 0.17' # Verify latest compatible
gem 'pundit', '~> 2.3'
gem 'line-bot-api', '~> 1.28' # Verify latest
gem 'slim-rails', '~> 3.6'
gem 'enum_help', '~> 0.0.19' # Verify compatibility
# Observability and monitoring
gem 'lograge', '~> 0.14'
gem 'semantic_logger', '~> 4.15'
gem 'prometheus_exporter', '~> 2.1'
gem 'sentry-ruby', '~> 5.18'
gem 'sentry-rails', '~> 5.18'
gem 'opentelemetry-sdk', '~> 1.4'
gem 'opentelemetry-instrumentation-rails', '~> 0.31'
# Fault tolerance
gem 'circuitbox', '~> 2.0'
# Remove: webpacker, sass-rails
# ... (update test/development gems)
npm (Updated package.json):
{
"esbuild": "^0.19.0",
"sass": "^1.69.0",
"bootstrap": "^5.3.0", // Update if needed
"@fortawesome/fontawesome-free": "^6.5.0", // Update if needed
"@rails/ujs": "^7.0.0",
"@rails/activestorage": "^7.0.0",
"@popperjs/core": "^2.11.8"
// Remove: @rails/webpacker, webpack, webpack-cli
}
11.3 Compatibility Matrix
| Gem | Current | Target | Rails 8.1 Compatible | Notes |
|---|---|---|---|---|
| puma | 5.x | 6.x | ✅ | Upgrade recommended |
| sorcery | (unknown) | 0.17+ | ✅ | Verify with bundle info sorcery
|
| pundit | (unknown) | 2.3+ | ✅ | Latest version compatible |
| line-bot-api | (unknown) | 1.28+ | ✅ | Check LINE API changes |
| slim-rails | (unknown) | 3.6+ | ✅ | Stable with Rails 8 |
| enum_help | (unknown) | 0.0.19 | ⚠️ | May need alternative if unmaintained |
Action Items:
- Run
bundle info <gem>for each gem to check current versions - Check each gem's GitHub/RubyGems for Rails 8 compatibility
- Test thoroughly after each gem update
12. Timeline Estimate
12.1 Phase Breakdown
| Phase | Task | Estimated Time | Risk Level |
|---|---|---|---|
| Prep | Environment setup, Ruby install | 2 hours | Low |
| Phase 1 | Rails 6.1 → 7.0 upgrade | 4 hours | Medium |
| Phase 2 | Rails 7.0 → 7.1 upgrade | 3 hours | Medium |
| Phase 3 | Rails 7.1 → 8.0 upgrade | 4 hours | High |
| Phase 4 | Rails 8.0 → 8.1 upgrade | 2 hours | Medium |
| Phase 5 | Webpacker removal | 2 hours | Low |
| Phase 6 | esbuild installation | 3 hours | Medium |
| Phase 7 | Asset migration | 6 hours | High |
| Phase 8 | View updates | 2 hours | Low |
| Phase 9 | Testing & fixes | 8 hours | High |
| Phase 10 | Documentation | 3 hours | Low |
| Phase 11 | Staging deployment | 2 hours | Medium |
| Total | 41 hours |
Recommendation: Plan for 5-7 business days with a skilled Rails developer
12.2 Critical Path
Ruby Install → Rails 6.1→7.0 → Rails 7.0→7.1 → Rails 7.1→8.0 → Rails 8.0→8.1
↓
Remove Webpacker ← Testing ←┘
↓
Install esbuild
↓
Migrate Assets
↓
Update Views
↓
Testing & Fixes
↓
Documentation
↓
Deploy to Staging
Bottlenecks:
- Rails 7.1 → 8.0 (major version, most breaking changes)
- Asset migration (requires thorough testing)
- Testing & fixes (unpredictable issues may arise)
13. Success Metrics
13.1 Technical Metrics
- Ruby version: 3.3.10
- Rails version: 8.1.1
- All tests pass: 100% green
- No Bundler warnings
- No npm audit vulnerabilities (high/critical)
- Asset build time: ≤ 5 seconds
- Zero console errors in browser
- Response time: Within 10% of baseline
13.2 Functional Metrics
- Operator authentication works
- LINE webhook receives and processes messages
- Content CRUD operations work
- Alarm content management functional
- Feedback submission successful
- Scheduler runs as expected
- All pages render correctly
- Bootstrap styles apply
- FontAwesome icons display
13.3 Quality Metrics
- Code coverage: ≥ 90%
- RuboCop offenses: 0 (or only minor)
-
No
TODOorFIXMEcomments related to upgrade - Documentation updated
- README instructions tested by a new developer
- Rollback plan tested
14. Risk Assessment
14.1 High Risk Items
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Gem incompatibility | Medium | High | Check compatibility matrix, have alternatives ready |
| Asset pipeline broken | Medium | High | Test thoroughly at each step, keep Webpacker working until esbuild ready |
| LINE webhook fails | Low | High | Test webhook immediately, have LINE test bot |
| Data loss | Low | Critical | Backup database before starting, test rollback |
| Extended downtime | Low | High | Use staging environment, plan deployment window |
14.2 Medium Risk Items
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Test failures | High | Medium | Fix incrementally, don't skip testing phases |
| Performance regression | Medium | Medium | Benchmark before/after, optimize if needed |
| Third-party API changes | Low | Medium | Review LINE API changelog, test thoroughly |
| Deprecation warnings | High | Low | Address warnings immediately, don't accumulate |
14.3 Low Risk Items
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| UI styling issues | Medium | Low | Visual regression testing, manual checks |
| Development workflow changes | High | Low | Document new processes, train team |
| Increased bundle size | Low | Low | Monitor asset sizes, optimize if needed |
15. Appendix
15.1 Useful Commands
# Check current versions
ruby -v
rails -v
bundle info rails
# Update bundler
gem update bundler
# Install Ruby (via rbenv)
rbenv install 3.3.10
rbenv local 3.3.10
# Upgrade Rails incrementally
bundle update rails --conservative
# Run tests
bundle exec rspec
bundle exec rspec spec/models # specific directory
# Asset compilation
bundle exec rails webpacker:compile # Before
yarn build && yarn build:css # After
# Check for deprecations
DEPRECATION_TRACKER=stderr rails server
# Database operations
rails db:migrate:status
rails db:rollback STEP=1
# Code quality
bundle exec rubocop
bundle exec rubocop -a # Auto-correct
# Security audits
bundle audit
npm audit
15.2 Reference Documentation
- Rails 7.0 Upgrade Guide
- Rails 7.1 Upgrade Guide
- Rails 8.0 Release Notes
- jsbundling-rails Documentation
- cssbundling-rails Documentation
- esbuild Documentation
- Webpacker Migration Guide
15.3 Community Resources
END OF DESIGN DOCUMENT
互換性マトリクス
| Gem | Current | Target | Rails 8.1 Compatible | Notes |
|---|---|---|---|---|
| puma | 5.x | 6.x | ✅ | Upgrade recommended |
| sorcery | - | 0.17+ | ✅ | Verify with bundle info sorcery
|
| pundit | - | 2.3+ | ✅ | Latest version compatible |
| line-bot-api | - | 1.28+ | ✅ | Check LINE API changes |
| slim-rails | - | 3.6+ | ✅ | Stable with Rails 8 |
各Gemの互換性まで調査してくれてます。
リスク評価
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Gem incompatibility | Medium | High | Check compatibility matrix, have alternatives ready |
| Asset pipeline broken | Medium | High | Test thoroughly at each step |
| LINE webhook fails | Low | High | Test webhook immediately |
| Data loss | Low | Critical | Backup database before starting |
ロールバック戦略まで含めた設計書になっていて、「これ、人間がやったら相当面倒なのでは...」と思いました。
Design Gateの評価結果
Design Gateでは7つのEvaluatorが設計書を多角的にレビューします。
今回の評価結果:
| Evaluator | Score | Status |
|---|---|---|
| Goal Alignment(目標整合性) | 4.7 / 5.0 | ✅ Approved |
| Maintainability(保守性) | 4.5 / 5.0 | ✅ Approved |
| Extensibility(拡張性) | 評価済み | ✅ Approved |
| Reliability(信頼性) | 評価済み | ✅ Approved |
| Observability(可観測性) | 評価済み | ✅ Approved |
| Consistency(一貫性) | 評価済み | ✅ Approved |
| Reusability(再利用性) | 評価済み | ✅ Approved |
Requirements Coverage: 5.0 / 5.0(100%)
19個の要件(機能要件・非機能要件・制約)すべてに対応した設計になっていました。
Evaluatorからの指摘例
Goal Alignment Evaluatorからは、こんな指摘がありました:
Minor Gap: Design doesn't explicitly quantify business impact metrics (e.g., "reduced developer onboarding time by X hours")
Recommendation: Consider adding a "Business Impact" subsection that translates technical improvements to business metrics.
「技術的なメリットはわかるけど、ビジネスインパクトの数値化が弱いよ」という指摘。確かに。
こういうフィードバックがあると、設計の質が上がりますね。
Plannerが生成したタスク計画
Design Gateを通過すると、次はPlanning Gate。Plannerエージェントが設計書をもとにタスクを分解します。
今回は49個のタスク、12フェーズの実行計画が生成されました。
docs/plans/rails-upgrade-tasks.md (1927行)
12フェーズの構成
| Phase | 内容 | タスク数 |
|---|---|---|
| 1 | Preparation(準備) | 2 |
| 2 | Reusability Infrastructure(再利用基盤) | 6 |
| 3 | Rails 6.1 → 7.0 | 6 |
| 4 | Rails 7.0 → 7.1 | 6 |
| 5 | Rails 7.1 → 8.0 | 6 |
| 6 | Rails 8.0 → 8.1 | 6 |
| 7 | Webpacker Removal | 2 |
| 8 | esbuild Installation | 2 |
| 9 | Asset Migration | 4 |
| 10 | View Updates | 2 |
| 11 | Testing | 5 |
| 12 | Documentation | 2 |
面白いのは、Phase 2の「Reusability Infrastructure」。
Plannerが「このアップグレード作業、24個以上の重複タスクがあるな」と判断して、自動化ユーティリティを先に作るフェーズを追加してくれました。
# 生成されたRakeタスクの例(TASK-004より)
namespace :upgrade do
desc "Prepare for Rails upgrade"
task :prepare, [:version] => :environment do |t, args|
# Update Gemfile with target version from config
# Run bundle install
# Create backup of Gemfile.lock
end
end
Planning Gateの評価結果
| Evaluator | Score | Status |
|---|---|---|
| Clarity(明確性) | 4.7 / 5.0 | ✅ Approved |
| Granularity(粒度) | 評価済み | ✅ Approved |
| Dependency(依存関係) | 評価済み | ✅ Approved |
| Goal Alignment(目標整合性) | 評価済み | ✅ Approved |
| Deliverable Structure(成果物構造) | 評価済み | ✅ Approved |
| Responsibility Alignment(責任整合性) | 評価済み | ✅ Approved |
| Reusability(再利用性) | 評価済み | ✅ Approved |
Clarity Evaluatorからは:
Definition of Done: 4.9/5.0
Every task has clear, testable completion conditions.
Example:ruby -vshows Ruby 3.3.10,bundle exec rspecpasses 100%
各タスクに「何をもって完了とするか」が明確に定義されていて、実行者が迷わないようになっています。
自動生成されたドキュメント
最終的に、docs/ 配下に38ファイルのドキュメントが生成されました。
docs/
├── designs/ # 設計書(3ファイル)
│ ├── rails-upgrade.md
│ ├── fix-javascript-migration.md
│ └── css-build-fix.md
├── evaluations/ # Evaluator評価結果(19ファイル)
│ ├── design-goal-alignment-rails-upgrade.md
│ ├── design-maintainability-rails-upgrade.md
│ ├── planner-clarity-rails-upgrade.md
│ └── ... (他16ファイル)
├── plans/ # タスク計画(3ファイル)
├── issues/ # 問題分析(2ファイル)
├── reviews/ # コードレビュー結果(3ファイル)
├── security-audits/ # セキュリティ監査(1ファイル)
└── ... (その他)
これ、全部EDAFが自動で生成したものです。
ハマったところ
完全にスムーズだったわけではありません。
1. JavaScriptが動かない問題
webpackerからesbuildへの移行後、JavaScriptが効かなくなりました。
原因:app/javascript/application.jsが空のままになっていた
webpackerではapp/javascript/packs/application.jsがエントリーポイントでしたが、esbuildではapp/javascript/application.jsに変わります。移行時にこのファイルの中身がコピーされていませんでした。
対応:エラー内容をClaude Codeに伝えたら、EDAFが新たにfix-javascript-migration.mdという設計書を生成して対応してくれました。
// 修正後の app/javascript/application.js
import Rails from "@rails/ujs"
Rails.start()
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
import * as bootstrap from "bootstrap"
import "@fortawesome/fontawesome-free/js/all"
import "./javascript/scroll.js"
window.bootstrap = bootstrap
2. 必要なJSファイルが1つ不足
scroll.jsというカスタムJSファイルのimportパスが間違っていました。
これもエラー文を渡して調査させたら、すぐに修正されました。
ポイント: EDAFは万能ではないです。ただ、エラーが出たときに「エラー文を渡して調査させる」だけで解決できるのは、かなり楽でした。問題が発生すると、EDAFは新たな設計書を生成して対応してくれます。
Code Review Gateで一度FAILした話
ここが一番面白かったところです。
Implementation完了後、Code Review Gateで7つのEvaluatorがコードをレビューしました。
初回の評価結果
| Evaluator | Score | Status |
|---|---|---|
| Code Quality | 8.5 / 10.0 | ✅ PASS |
| Code Testing | 5.5 / 10.0 | ❌ FAIL |
| Code Security | 6.5 / 10.0 | ❌ FAIL |
| Code Documentation | 8.5 / 10.0 | ✅ PASS |
| Code Maintainability | 8.5 / 10.0 | ✅ PASS |
| Code Performance | 6.8 / 10.0 | ❌ FAIL |
| Implementation Alignment | 9.5 / 10.0 | ✅ PASS |
3つのEvaluatorがFAILしました。
指摘された問題
Testing(5.5): JavaScriptのテストインフラがない
Security(6.5):
- esbuild 0.20.0にCVE脆弱性
- postcss-cliにReDoS脆弱性
- Railsパッケージが古い
Performance(6.8): FontAwesomeのフルインポートでバンドルサイズが1.5MB(84%を占める)
修正後の評価結果
EDAFがこれらの問題を自動修正して、再評価。
| Evaluator | Before | After | Status |
|---|---|---|---|
| Code Testing | 5.5 | 8.0 | ✅ PASS |
| Code Security | 6.5 | 9.5 | ✅ PASS |
| Code Performance | 6.8 | 9.3 | ✅ PASS |
具体的な改善
バンドルサイズ: 1.5MB → 356KB(76%削減)
FontAwesomeのフルインポートをやめて、実際に使っている11個のアイコンだけを選択的にインポートするように変更。
// Before: 1.5MB
import "@fortawesome/fontawesome-free/js/all"
// After: 356KB(実際に使う11アイコンのみ)
import { library, dom } from "@fortawesome/fontawesome-svg-core"
import { faTwitter, faLine } from "@fortawesome/free-brands-svg-icons"
// ... 必要なアイコンのみimport
セキュリティ脆弱性の修正
esbuild: 0.20.0 → 0.25.12 (CVE fixed)
postcss-cli: 10.1.0 → 11.0.1 (braces fixed)
@rails/actioncable: 7.0.0 → 8.1.100
@rails/activestorage: 7.0.0 → 8.1.100
npm audit: 0 vulnerabilities
テストインフラの追加
Vitestを導入して、JavaScriptのテストを3件追加。
これがEDAFの真骨頂だと思いました。
「コードレビューでFAIL → 問題を特定 → 修正 → 再評価でPASS」
このサイクルが自動で回るので、人間が見落としがちな問題(セキュリティ脆弱性、パフォーマンス問題)も検出してくれます。
所感
良かった点
認知負荷が激減
Railsアップグレードで一番つらいのは「何を変えればいいかわからない」こと。
EDAFのDesignerが3500行の設計書で影響範囲を洗い出してくれるので、人間は確認するだけで済みます。しかも互換性マトリクスやリスク評価まで含まれているので、「この変更大丈夫かな...」という不安が減りました。
段階的に進む安心感
一気にドカンと変更されるのではなく、設計→計画→実装と段階を踏むので、途中で軌道修正しやすいです。
今回も、実装後にJSが動かない問題が発覚しましたが、EDAFが新たな設計書を生成して対応してくれました。
レビューの自動化
196ファイルの変更を人間が全部見るのは現実的じゃないです。Evaluatorが多角的にチェックしてくれるのは助かりました。
注意点
最終確認は人間
EDAFはあくまで補助です。実際にブラウザで動かして確認するのは必須。今回のJSが動かない問題も、実際に画面を見て気づきました。
エラー対応は対話で
完璧に動くわけではないので、エラーが出たら対話的に解決していく姿勢が必要です。
まとめ
「Railsアップグレード、めんどくさい...」と思っている方、EDAFを試してみてください。
約1時間半で、Ruby 3.0→3.4、Rails 6.1→8.1、webpacker→esbuildの移行が完了しました。
38ファイルのドキュメントが自動生成されて、何が変わったのか、なぜその変更が必要だったのかが全部記録に残ります。
リンク集
- EDAF: https://github.com/Tsuchiya2/evaluator-driven-agent-flow
- 今回のPR: https://github.com/Tsuchiya2/edaf-rails-upgrade-demo/pull/1
- EDAFの詳細解説(12/3記事): https://qiita.com/Tsuchiya2/items/013a467c07286c6732f5
フィードバック、めちゃくちゃ欲しいです!
GitHub Issues で待ってます 🐱
