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.
What changed at a glance
Section titled “What changed at a glance”| Area | Before | After |
|---|---|---|
| Ruby | 3.4.7 | 4.0.3 (YJIT enabled; rust = "latest" in .mise.toml so source compiles include YJIT) |
| Rails | 7.2 | 8.1.3 |
| Cache | redis-rails | Solid Cache (Postgres-backed, Brotli-wrapped) |
| Cable | Redis-cable | Solid Cable |
| Sidekiq | Redis | Unchanged (Sidekiq stays on Redis for queue) |
| Session store | Redis | Solid Cache (DB-backed) |
| HTTP outbound | HTTPClient + 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 gem | External dep, 264 call sites | In-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 checks | 3 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 CI | Never (gap) | Available via bin/ci workflow_dispatch (“Run full bin/ci including Minitest”) |
bin/setup | Procedural fresh-machine script | 12-mode gum-styled launcher |
| zsh / oh-my-zsh | bin/setup installed both | bin/setup installs zsh only — OMZ no longer auto-installed |
| Production app server | Phusion Passenger | Phusion Passenger (Kamal 2 + Thruster aspirational; config/deploy/templates/puma.service.erb is migration scaffolding only) |
What every developer needs to do
Section titled “What every developer needs to do”One-time per machine
Section titled “One-time per machine”cd ~/Projects/heatwave_master # main checkoutgit pull --ff-only
# Pick up the new Ruby 4.0.3 + Rust pin from .mise.tomlmise 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-helpersmise exec -- bundle installmise 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-externalOr one-liner equivalents:
bin/setup --update-tools # mise + bundle + yarn (post-pull refresh)bin/setup --doctor # verify everything is in placeTest DB reset (recommended, one-time)
Section titled “Test DB reset (recommended, one-time)”The test DB schema_migrations may drift after this large a change. Reset cleanly via:
bin/setup --reset-test-dbFor each existing worktree
Section titled “For each existing worktree”cd ~/Projects/worktrees/<your-worktree>git pull --ff-onlymise exec -- bundle installmise exec -- yarn install --immutable# direnv re-allows on cd; if not: direnv allowNew bin/setup modes
Section titled “New bin/setup modes”bin/setup --doctor # health check across mise, docker, postgres, redis, .env*, master.key, etc.bin/setup --full # fresh machine / major rebuildbin/setup --worktree # per-worktree bootstrap (also runs from direnv on first cd)bin/setup --update-tools # post-`git pull` refresh: mise + bundle + yarnbin/setup --refresh-deps # bundle + yarn onlybin/setup --refresh-mcp # 1Password → .env.mcp + reload launchd MCP env bridgebin/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 DBbin/setup --reset-docker # compose down → image prune → rebuild → up (data preserved)bin/setup --clean # wipe tmp/cache, tmp/pids, .corepack, bootsnap cachesbin/setup --credentials [env] # `rails credentials:edit -e <env>`bin/setup --zombies # reap stale Claude Code monitor/wrapper processesbin/setup --help # full referenceThe old workflow (running individual commands) still works.
CI / PR flow changes
Section titled “CI / PR flow changes”| Workflow | Status | What it does |
|---|---|---|
ci.yml | NEW | bin/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.yml | DELETED | Replaced by bin/ci |
yard-lint.yml | DELETED | Replaced by bin/ci |
bundler-cache-warm.yml | DELETED | No longer needed |
design-md-lint.yml | Unchanged | Tiny — only fires when DESIGN.md changes |
yard-docs.yml | Unchanged | Builds + deploys YARD docs on push to master |
| CodeRabbit | Unchanged | AI 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.
bin/ci — local CI runner
Section titled “bin/ci — local CI runner”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 suitebin/ci --since=HEAD~1 # tighten the diff window for diff-only checksbin/ci --help # full option referenceIntentionally 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:
mise exec -- bundle exec rubocop <files>mise exec -- bundle exec brakeman --quiet --no-pager -w 3.env* files and direnv
Section titled “.env* files and direnv”Seven .env* files plus .envrc, each with one consumer. Full reference:
doc/development/ENVIRONMENT_FILES.md.
| File | Loaded by | Purpose |
|---|---|---|
.env | Dotenv::Rails (dev/test) | App-level dev secrets (Cloudflare/HF/Vultr API tokens) |
.env.local | Rails (test only) | Per-developer test overrides |
.env.production.local | Hand-sourced into a shell | Prod DB creds for laptop scripts |
.env.wasabi | script/db_restore.sh | Wasabi S3 creds |
.env.mcp | direnv via .envrc | Team-shared MCP secrets (Postgres prod, AppSignal, Ahrefs, Clarity, …) |
.env.mcp.local | direnv via .envrc | Per-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.
Production deployment notes
Section titled “Production deployment notes”- 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 config/deploy.rbalready pinsset :rbenv_ruby, '4.0.3'.- DB migrations — 3 new migrations land with this PR:
20260501182556_create_solid_cache_entries20260501182557_create_solid_cable_messages20260502142641_fix_subscriber_list2663_state_filter(idempotent data fix; runs as no-op if already applied)
- Capistrano deploy — no Capistrano config changes needed beyond the
rbenv pin. Run
bin/deployas usual. - No nginx / Passenger config changes required. Solid Cache/Cable share the existing primary DB; no new DB connection pool config needed.
- Solid Queue is NOT adopted in this PR — Sidekiq stays.
- MCP env bridge LaunchAgent —
bin/setup --fullinstalls a per-machine LaunchAgent (~/Library/LaunchAgents/com.warmlyyours.mcp-env.plist) so GUI-launched apps (Zed, Cursor, Claude Desktop) inherit.env.mcpsecrets. Only relevant for developer machines, not production.
What changed in the HTTP stack
Section titled “What changed in the HTTP stack”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 inconfig/initializers/faraday.rb)httpclientgem dropped — 6 services migrated:Api::Esignatures,AssemblyaiClient,Phone::Pbx,Seo::LinkAnalyzer,Seo::InternalLinkValidator,Shipping::Shipping- 7 direct
Net::HTTPcall sites migrated. One deliberate keep:app/services/video_poster_extraction_service.rb—Down::Http(which is http.rb-based) produced mysterious 400s on the Cloudflare thumbnail endpoint in production whileNet::HTTPworks. Faraday’s:httpadapter 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)Known issues / follow-ups
Section titled “Known issues / follow-ups”| Issue | Severity | Notes |
|---|---|---|
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 methods | ⚪ | CodeRabbit nag — cosmetic. Not blocking. |
Quick reference for anyone hitting issues
Section titled “Quick reference for anyone hitting issues”bin/setup --doctor # diagnose what's wrongbin/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 stuckbin/setup --zombies # if stale processes pile up from agent sessionsmise exec -- bundle exec rubocop <file> # one-off RuboCopmise exec -- bundle exec brakeman -w 3 # one-off Brakeman (High-confidence only)Documentation pointers
Section titled “Documentation pointers”doc/development/LINTERS.md— full linter / CI flowdoc/development/ENVIRONMENT_FILES.md— every.env*file mappedbin/setup --help,bin/ci --help— CLI referencesdoc/tasks/202604301200_RAILS_81_UPGRADE.md— pre-merge planning notesdoc/tasks/202604251235_PR480_FOLLOWUPS.md— items deferred from this PR