1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EDAFでRails 6.1→8.1へアップグレードしてみた(webpacker→esbuild込み)

1
Last updated at Posted at 2025-12-20

この記事は 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に変更もお願いします。

これだけです。

スクリーンショット 2025-12-01 19.12.05.png


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

  1. Ruby Version Upgrade: Safely upgrade from Ruby 3.0.2 to 3.3.10, taking advantage of performance improvements and security patches
  2. Rails Framework Upgrade: Incrementally upgrade Rails through all intermediate versions to reach Rails 8.1.1
  3. Asset Pipeline Modernization: Replace Webpacker with esbuild for faster build times and simpler configuration
  4. Maintain Functionality: Ensure all existing features (authentication, LINE bot integration, operator dashboard) continue to work
  5. Dependency Compatibility: Update all gems and npm packages to versions compatible with the new Rails version
  6. 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-version file
  • 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:update at 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 webpacker gem and related configurations
  • Install jsbundling-rails gem
  • 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.js to 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_tag with javascript_include_tag
  • Replace stylesheet_pack_tag with stylesheet_link_tag
  • Replace favicon_pack_tag with 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 .gitignore for 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-api gem 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 enum declarations are Rails 7+ compatible
  • Update any deprecated serialize calls
    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:

  1. Backup Database Before Upgrade

    # Development
    mysqldump -u root reline_development > backup_dev.sql
    
    # Production (if testing)
    pg_dump reline_production > backup_prod.sql
    
  2. Run Pending Migrations (if any)

    rails db:migrate
    
  3. 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_tag to javascript_include_tag
  • Add defer: true for optimal loading performance
  • Asset served from app/assets/builds/ instead of public/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_tag to stylesheet_link_tag
  • CSS compiled by Dart Sass instead of Webpack
  • Asset served from app/assets/builds/ instead of public/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 .env files
  • 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:

  1. Identify conflicting gem versions
  2. Check gem changelog for breaking changes
  3. Update gem to compatible version
  4. 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 --conservative to 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:

  1. Review diff of proposed changes
  2. If custom changes exist, select "d" to see diff
  3. Merge changes manually if needed
  4. 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:

  1. Verify npm packages are installed: yarn install
  2. Check import paths are correct for esbuild
  3. Ensure package.json includes all dependencies
  4. Clear build cache: rm -rf app/assets/builds
  5. 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:

  1. Check CSS build script in package.json
  2. Verify Sass compiler is installed: yarn add sass --dev
  3. Ensure import path is correct: @import 'bootstrap/scss/bootstrap';
  4. Check for missing node_modules: yarn install
  5. Verify cssbundling-rails gem is installed
  6. 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:

  1. Update test gems to compatible versions
  2. Review test framework changelogs (RSpec, Capybara)
  3. Fix deprecated test syntax
  4. Update factory_bot if needed
  5. 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:

  1. Verify CSRF token is skipped for webhook endpoint
  2. Check LINE signature verification logic
  3. Test webhook endpoint manually: curl -X POST ...
  4. Review webhook controller for deprecations
  5. 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:

  1. Verify assets were precompiled: rails assets:precompile
  2. Check asset manifest: cat public/assets/.sprockets-manifest-*.json
  3. Ensure web server serves /assets correctly
  4. Verify config.assets.compile = false in production.rb
  5. 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:

  1. Verify database.yml configuration
  2. Check database gem version (mysql2 or pg)
  3. Test connection manually: rails dbconsole
  4. Verify database server is running
  5. 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:

  1. Real-time Application Status

    • Current error rate
    • Active connections
    • Request throughput
  2. Rails Upgrade Timeline

    • Version deployment timeline
    • Performance before/after comparison
    • Error rate changes by version
  3. Asset Pipeline Performance

    • Build times (Webpacker vs esbuild)
    • Asset sizes
    • Client-side load times
  4. 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:

  1. Ruby Gem: rails-upgrade-toolkit

    • Contains all utility classes
    • Provides CLI interface
    • Includes default configuration templates
    • Supports plugins for custom upgrade logic
  2. GitHub Repository: Example Rails upgrade projects

    • Before/after comparisons
    • Step-by-step guides
    • Common pitfall documentation
    • Community-contributed patterns
  3. 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:

  1. Operator login/logout
  2. LINE bot webhook receives message
  3. Content creation/editing
  4. Alarm content management
  5. Feedback submission
  6. 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:

  1. JavaScript loads and executes
  2. CSS applies correctly
  3. Bootstrap modal works
  4. FontAwesome icons display
  5. Favicon appears
  6. Custom fonts load
  7. Images display
  8. 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):

  1. Page renders correctly
  2. Bootstrap styles apply
  3. FontAwesome icons display
  4. JavaScript executes
  5. Forms submit
  6. AJAX requests work
  7. 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:

  1. Deploy to staging
  2. Run full test suite in staging environment
  3. Perform manual smoke tests
  4. Monitor logs for 24 hours
  5. Check error tracking service (Sentry)
  6. Verify asset loading from CDN (if applicable)
  7. 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:

  1. Run bundle info <gem> for each gem to check current versions
  2. Check each gem's GitHub/RubyGems for Rails 8 compatibility
  3. 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:

  1. Rails 7.1 → 8.0 (major version, most breaking changes)
  2. Asset migration (requires thorough testing)
  3. 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 TODO or FIXME comments 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

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 -v shows Ruby 3.3.10, bundle exec rspec passes 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ファイルのドキュメントが自動生成されて、何が変わったのか、なぜその変更が必要だったのかが全部記録に残ります。


リンク集

フィードバック、めちゃくちゃ欲しいです!
GitHub Issues で待ってます 🐱

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?