Skip to content

Rails 8.1 + Ruby 4.0.3 Upgrade — IT/Dev Team Notes

Merged: 2026-05-02 19:23 UTC Merge commit: ae9129898764696856bebfad7819b43763a63874 PR: #673 — 42 commits

This document is the post-merge dossier. Use it to bring your local checkout up to date and to understand what changed under the hood.


AreaBeforeAfter
Ruby3.4.74.0.3 (YJIT enabled; rust = "latest" in .mise.toml so source compiles include YJIT)
Rails7.28.1.3
Cacheredis-railsSolid Cache (Postgres-backed, Brotli-wrapped)
CableRedis-cableSolid Cable
SidekiqRedisUnchanged (Sidekiq stays on Redis for queue)
Session storeRedisSolid Cache (DB-backed)
HTTP outboundHTTPClient + Net::HTTP + Faraday (mixed)Faraday + http.rb adapter only (httpclient gem dropped, 6 service classes + 7 Net::HTTP sites migrated, all with VCR coverage)
arel-helpers gemExternal dep, 264 call sitesIn-housed as lib/arel_table_shortcut.rb (10 lines, gem dropped)
Docker data volumes./docker/volumes/ (in repo tree)~/Projects/heatwave-infra/ (one location for all worktrees)
Per-PR linter checks3 GitHub Actions workflows (pronto.yml, yard-lint.yml, bundler-cache-warm.yml)bin/ci runs on PR via .github/workflows/ci.yml + CodeRabbit
Test suite on CINever (gap)Available via bin/ci workflow_dispatch (“Run full bin/ci including Minitest”)
bin/setupProcedural fresh-machine script12-mode gum-styled launcher
zsh / oh-my-zshbin/setup installed bothbin/setup installs zsh only — OMZ no longer auto-installed
Production app serverPhusion PassengerPhusion Passenger (Kamal 2 + Thruster aspirational; config/deploy/templates/puma.service.erb is migration scaffolding only)

Terminal window
cd ~/Projects/heatwave_master # main checkout
git pull --ff-only
# Pick up the new Ruby 4.0.3 + Rust pin from .mise.toml
mise install
# If you were on Ruby 3.4.7, force a clean reinstall so YJIT compiles:
mise uninstall ruby && mise install
# Pick up Rails 8.1.3, Solid Cache/Cable, drop httpclient/arel-helpers
mise exec -- bundle install
mise exec -- yarn install --immutable
# Apply the 3 new migrations (Solid Cache + Solid Cable tables + 1 data fix)
mise exec -- bin/rails db:migrate
# If your Docker data volumes are still under ./docker/volumes/,
# move them out of the repo tree to the shared infra location:
bin/migrate-docker-volumes-external

Or one-liner equivalents:

Terminal window
bin/setup --update-tools # mise + bundle + yarn (post-pull refresh)
bin/setup --doctor # verify everything is in place

The test DB schema_migrations may drift after this large a change. Reset cleanly via:

Terminal window
bin/setup --reset-test-db
Terminal window
cd ~/Projects/worktrees/<your-worktree>
git pull --ff-only
mise exec -- bundle install
mise exec -- yarn install --immutable
# direnv re-allows on cd; if not: direnv allow

Terminal window
bin/setup --doctor # health check across mise, docker, postgres, redis, .env*, master.key, etc.
bin/setup --full # fresh machine / major rebuild
bin/setup --worktree # per-worktree bootstrap (also runs from direnv on first cd)
bin/setup --update-tools # post-`git pull` refresh: mise + bundle + yarn
bin/setup --refresh-deps # bundle + yarn only
bin/setup --refresh-mcp # 1Password → .env.mcp + reload launchd MCP env bridge
bin/setup --restore-db # pull a Wasabi backup → local DB (delegates to script/db_restore.sh)
bin/setup --reset-test-db # drop/create/schema:load/migrate the test DB
bin/setup --reset-docker # compose down → image prune → rebuild → up (data preserved)
bin/setup --clean # wipe tmp/cache, tmp/pids, .corepack, bootsnap caches
bin/setup --credentials [env] # `rails credentials:edit -e <env>`
bin/setup --zombies # reap stale Claude Code monitor/wrapper processes
bin/setup --help # full reference

The old workflow (running individual commands) still works.


WorkflowStatusWhat it does
ci.ymlNEWbin/ci --quick (Reek diff + yard-lint + bundle-audit) on every PR. ~30s. Manual workflow_dispatch to also run the full Minitest suite on a runner.
pronto.ymlDELETEDReplaced by bin/ci
yard-lint.ymlDELETEDReplaced by bin/ci
bundler-cache-warm.ymlDELETEDNo longer needed
design-md-lint.ymlUnchangedTiny — only fires when DESIGN.md changes
yard-docs.ymlUnchangedBuilds + deploys YARD docs on push to master
CodeRabbitUnchangedAI review on every PR — RuboCop + Brakeman + semantic

Cost savings: ~5–10 min of GHA compute per PR.

No pre-push hook is installed by default. Run bin/ci manually before opening a PR if you want to catch issues before the PR check fires. lefthook gem is in the bundle; if you want pre-push gating for your own checkout, drop a private lefthook.yml and run mise exec -- bundle exec lefthook install -f.


Terminal window
bin/ci # full pipeline: Reek + yard-lint + bundle-audit + Minitest (~6 min)
bin/ci --quick # skip the test suite (~30s)
bin/ci --tests-only # skip the linters, just the test suite
bin/ci --since=HEAD~1 # tighten the diff window for diff-only checks
bin/ci --help # full option reference

Intentionally NOT gated by bin/ci:

  • RuboCop — the codebase has accumulated Metrics/* warnings on legacy long classes (Order = 2954 lines, Opportunity = 729, etc.). Gating push on these would block every commit. CodeRabbit reviews on PR.
  • Brakeman — same story, with High-confidence findings on legacy controllers/views. CodeRabbit reviews on PR.

Run them manually as needed:

Terminal window
mise exec -- bundle exec rubocop <files>
mise exec -- bundle exec brakeman --quiet --no-pager -w 3

Seven .env* files plus .envrc, each with one consumer. Full reference: doc/development/ENVIRONMENT_FILES.md.

FileLoaded byPurpose
.envDotenv::Rails (dev/test)App-level dev secrets (Cloudflare/HF/Vultr API tokens)
.env.localRails (test only)Per-developer test overrides
.env.production.localHand-sourced into a shellProd DB creds for laptop scripts
.env.wasabiscript/db_restore.shWasabi S3 creds
.env.mcpdirenv via .envrcTeam-shared MCP secrets (Postgres prod, AppSignal, Ahrefs, Clarity, …)
.env.mcp.localdirenv via .envrcPer-developer MCP secrets (e.g. GITHUB_PAT)
.env.mcp.example, .env.mcp.local.example(templates)Documentation

direnv allow is one-time per .envrc content change, not per cd. bin/setup-worktree auto-allows on fresh worktrees.


  1. Ruby 4.0.3 must be installed on every prod server via rbenv. Already done on the staging stack; confirm production:
    Terminal window
    rbenv install 4.0.3
  2. config/deploy.rb already pins set :rbenv_ruby, '4.0.3'.
  3. DB migrations — 3 new migrations land with this PR:
    • 20260501182556_create_solid_cache_entries
    • 20260501182557_create_solid_cable_messages
    • 20260502142641_fix_subscriber_list2663_state_filter (idempotent data fix; runs as no-op if already applied)
  4. Capistrano deploy — no Capistrano config changes needed beyond the rbenv pin. Run bin/deploy as usual.
  5. No nginx / Passenger config changes required. Solid Cache/Cable share the existing primary DB; no new DB connection pool config needed.
  6. Solid Queue is NOT adopted in this PR — Sidekiq stays.
  7. MCP env bridge LaunchAgentbin/setup --full installs a per-machine LaunchAgent (~/Library/LaunchAgents/com.warmlyyours.mcp-env.plist) so GUI-launched apps (Zed, Cursor, Claude Desktop) inherit .env.mcp secrets. Only relevant for developer machines, not production.

The codebase had three HTTP libraries fighting each other (HTTPClient, Net::HTTP, Faraday). Now consolidated to Faraday + http.rb adapter only:

  • Faraday.default_adapter = :http (set in config/initializers/faraday.rb)
  • httpclient gem dropped — 6 services migrated: Api::Esignatures, AssemblyaiClient, Phone::Pbx, Seo::LinkAnalyzer, Seo::InternalLinkValidator, Shipping::Shipping
  • 7 direct Net::HTTP call sites migrated. One deliberate keep: app/services/video_poster_extraction_service.rbDown::Http (which is http.rb-based) produced mysterious 400s on the Cloudflare thumbnail endpoint in production while Net::HTTP works. Faraday’s :http adapter routes through the same http.rb library, so the repo standard doesn’t apply to that one specific call.

VCR cassettes pin the post-migration behavior end-to-end (17 cassettes across 8 services, mimicking documented response shapes — AssemblyAI v2, Esignatures.com, Cloudflare Turnstile, Google Search Console URL Inspection, Amazon SP-API, ShipEngine, etc.).

All outbound HTTP calls now flow through:

your code → Faraday → :http adapter → http.rb gem → llhttp-ffi (C parser)

IssueSeverityNotes
VoicemailsMailbox autoload race🟡Pre-existing flake — passes in isolation, fails ~5% of full-suite runs under load order. Fix: eager-load the mailbox subclass list, or make ApplicationMailbox routing lazy.
Brakeman has ~7 Medium-confidence warnings on legacy controllers🟡bin/ci doesn’t gate (CodeRabbit reviews on PR). Curate config/brakeman.ignore for waivers if you want to raise the bar to -w 2.
RuboCop Metrics/* backlog on huge legacy models🟡bin/ci doesn’t gate. Touch-and-fix opportunistically, or generate .rubocop_todo.yml to grandfather.
YARD docstrings missing on some newly added public methodsCodeRabbit nag — cosmetic. Not blocking.

Terminal window
bin/setup --doctor # diagnose what's wrong
bin/ci --quick # run the lint chain locally (~30s)
bin/ci # run lint + full test suite (~6 min)
bin/setup --reset-test-db # if test DB feels stuck
bin/setup --zombies # if stale processes pile up from agent sessions
mise exec -- bundle exec rubocop <file> # one-off RuboCop
mise exec -- bundle exec brakeman -w 3 # one-off Brakeman (High-confidence only)
  • doc/development/LINTERS.md — full linter / CI flow
  • doc/development/ENVIRONMENT_FILES.md — every .env* file mapped
  • bin/setup --help, bin/ci --help — CLI references
  • doc/tasks/202604301200_RAILS_81_UPGRADE.md — pre-merge planning notes
  • doc/tasks/202604251235_PR480_FOLLOWUPS.md — items deferred from this PR