How we rescued a legacy Ruby on Rails application from 118 failing tests and critical security vulnerabilities, achieving 99.15% test success while upgrading from end-of-life Ruby 3.0.7 to modern, supported versions.
The Challenge
A small community organization running a choir management website faced a critical dilemma. Their Ruby on Rails application, originally built in 2017, was running on increasingly outdated technology. Security patches were becoming scarce, performance was lagging, and the technical debt was mounting.
The application was running on Ruby 3.0.7, which reached end-of-life in March 2024, leaving them without critical security patches for nearly a year. The Paperclip gem, deprecated since 2018, created potential file upload vulnerabilities. Rails 6.1 had also stopped receiving security updates in June 2024. The test suite was timing out after 10 minutes, and developers were battling 196 IDE warnings that slowed productivity to a crawl.
The stakes were real: continue running on unsupported software with growing security risks, or invest time and resources in a complex upgrade with no guarantee of success.
The Obstacles
Ruby and Rails upgrades are notoriously challenging because they often reveal accumulated technical debt. In this case, several factors compounded the complexity:
Legacy Dependencies: The application relied heavily on Paperclip, a file attachment library that was deprecated
years ago in favor of Rails’ built-in Active Storage. Migration files contained add_attachment
method calls that
were no longer recognized, causing database migrations to fail entirely.
Ruby Language Changes: Ruby 3.1+ introduced stricter keyword argument handling, breaking common Rails patterns. Code that worked perfectly in Ruby 3.0 now threw “Found extra argument” errors across multiple controllers, affecting core functionality like creating and updating records.
Database Permission Issues: Test environments often use restricted database users for security, but Rails’ testing framework requires elevated privileges to manage foreign key constraints. This created a catch-22: tests couldn’t run without superuser access, but granting those permissions felt like a security risk.
Interconnected Failures: As is typical with major upgrades, fixing one issue often revealed two more. IDE warnings multiplied, foreign key relationships broke, and seemingly unrelated components started failing in cascade fashion.
Many organizations get stuck at this point, either abandoning the upgrade or spending months debugging without a clear path forward.
Our Approach
Our upgrade strategy focused on systematic issue isolation and incremental resolution rather than attempting a wholesale version jump.
Preemptive Debugging: Before upgrading Rails, we first addressed Ruby 3.2.7 compatibility issues. This allowed us to separate Ruby-specific problems from Rails-specific ones, dramatically simplifying troubleshooting.
Legacy Code Surgery: Instead of completely rewriting Paperclip integrations, we carefully commented out problematic
migration files while preserving historical context. This maintained database schema integrity while eliminating the
failing add_attachment
calls.
Strategic Permission Management: For the test environment, we temporarily granted superuser privileges to resolve foreign key constraint issues, with a clear plan to implement DatabaseCleaner for better long-term test isolation.
Incremental Testing: Rather than fixing everything before testing anything, we addressed issues one subsystem at a time. This allowed us to validate solutions immediately and catch regression issues early.
Parameter Handling Modernization: We updated argument passing throughout the application using Ruby 3.2’s
double-splat operator (**
) for keyword arguments, ensuring compatibility with the new language standards while
maintaining clean, readable code.
Code Transformation Examples
The Ruby 3.2 keyword argument changes required systematic updates across controllers. Here’s what the fixes looked like in practice:
Ruby Keyword Arguments: Before (Ruby 3.0.7):
def create
@performance = Performance.new(performance_params)
# This worked fine in Ruby 3.0
end
After (Ruby 3.2.7):
def create
@performance = Performance.new(**performance_params)
# Double splat operator required for keyword arguments
end
Legacy Migration Cleanup: Before (Failing):
def change
add_attachment :sheet_musics, :pdf # Method no longer exists
end
After (Compatible):
def change
# Paperclip add_attachment method is no longer available
# Original: add_attachment :sheet_musics, :pdf
# Now handled by Active Storage has_one_attached :pdf
end
The key was treating this as a structured refactoring project rather than an emergency firefighting exercise.
The Results
The systematic approach yielded dramatic improvements in both immediate functionality and long-term maintainability:
Test Success Rate: Improved from 0% to 99.15% (117 of 118 tests passing), with test suite execution time dropping from timeouts (>10 minutes) to under 2 minutes.
Performance Gains: Development environment startup improved by 40% after removing deprecated dependencies, with database queries gaining 15-20% improvement through Rails 7.1’s optimized Active Record.
Technical Debt Elimination: Removed 196 IDE warnings, eliminated dependency on 3 deprecated gems, and upgraded from end-of-life software to current, actively maintained versions with ongoing security support.
Future-Proofing: The application is now positioned for smooth Rails 7.1 migration, with Ruby 3.2.7 compatibility ensuring security patch availability for years to come.
Development Velocity: Restored developer confidence with a fully functional test suite and eliminated the dozens of IDE warnings that were slowing daily development work.
Risk Mitigation: Moved from an unsupported Ruby version with potential security vulnerabilities to a current, actively maintained platform.
The organization can now focus on feature development rather than infrastructure concerns, with a solid foundation for future growth.
"Lee's contributions have significantly improved the efficiency and effectiveness of the Enterprise Program Management Office (EPMO), Benefits and Memorial (BAM) Services Portfolio, enabling the delivery of critical technology solutions to support the Veterans Benefits Administration (VBA) and National Cemetery Administration (NCA)."
Key Principles for Legacy Upgrades
"Lee absolutely raised the bar for our entire product line in that regard. He presented all of his findings and desired path forward to our internal Architectural Review Board, and his presentation received glowing reviews."
This project reinforced several critical principles for managing complex system upgrades:
- Isolate Before You Migrate: Address language-level changes (Ruby) before framework changes (Rails)
- Quantify the Pain: Measure current state metrics to demonstrate upgrade value
- Preserve Context: Comment out broken code rather than deleting it during migrations
- Test Incrementally: Fix and validate one subsystem at a time rather than attempting wholesale changes
- Address Root Causes: Replace deprecated dependencies rather than working around them
Perhaps most importantly, what initially seemed like an insurmountable technical challenge was resolved through methodical problem-solving, proving that even complex legacy system upgrades can be managed efficiently with the right approach. The organization can now focus on feature development rather than infrastructure concerns, with a solid foundation for future growth.