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/db migrate-volumesOr 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.
Current state (2026-08-05): the table above is rollout history; see
doc/development/LINTERS.mdfor the source of truth. Deterministic quality, Brakeman, assets, and the test suite now run in first-party PR/master lanes; CodeRabbit owns semantic review.lefthook.ymlis committed and installed bybin/setup: pre-commit runs the added-line quality/quarantine ratchets and pre-push runs the lightweight, non-booting subset ofbin/ci --quick.
bin/ci — local CI runner
Section titled “bin/ci — local CI runner”bin/ci # all deterministic gates + JavaScript tests + Minitestbin/ci --quick # all deterministic gates + JavaScript tests; skip Minitestbin/ci --tests-only # JavaScript tests + Minitest; skip deterministic gatesbin/ci --since=HEAD~1 # tighten the diff window for diff-only checksbin/ci --help # full option referenceFirst-party quality and security gates:
- RuboCop, ESLint, and Stylelint report only findings on added lines, so legacy file-wide debt cannot block unrelated work or grow unnoticed.
- Brakeman compares high-confidence findings against
config/brakeman.baseline.json; new warnings fail and fixed warnings require shrinking the baseline.
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 gates high-confidence deltas against config/brakeman.baseline.json. Curate config/brakeman.ignore for reviewed waivers before raising the bar to -w 2. |
RuboCop Metrics/* backlog on huge legacy models |
🟡 | bin/ci gates added-line findings without blocking on the backlog. Touch-and-fix opportunistically. |
| 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