Skip to content

feat: add REACT_ON_RAILS_BASE_PORT for concurrent worktree port management#3142

Open
justin808 wants to merge 23 commits intomainfrom
jg/concurrent-worktree-ports
Open

feat: add REACT_ON_RAILS_BASE_PORT for concurrent worktree port management#3142
justin808 wants to merge 23 commits intomainfrom
jg/concurrent-worktree-ports

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Apr 15, 2026

Summary

  • Adds a tool-agnostic REACT_ON_RAILS_BASE_PORT env var that derives all service ports from a single base value (Rails = base+0, webpack = base+1, renderer = base+2)
  • Also recognizes CONDUCTOR_PORT as a fallback, so Conductor.build works out of the box
  • Makes all Procfiles use ${PORT:-3000} and ${RENDERER_PORT:-3800} patterns instead of hardcoded values
  • Propagates renderer port via RENDERER_PORT and REACT_RENDERER_URL env vars for automatic Pro Node Renderer configuration

This enables coding agent tools (Conductor.build, Codex, Quad Code, etc.) to run multiple worktrees concurrently without port conflicts — just set one env var and everything works.

Changes

Core logic:

  • PortSelector: new base port priority tier (checked before explicit env vars and auto-detect)
  • ServerManager.configure_ports: sets RENDERER_PORT and REACT_RENDERER_URL when base port is active

Procfile templates (all now use env var fallbacks):

  • OSS: Procfile.dev, Procfile.dev-prod-assets, Procfile.dev-static-assets
  • Pro: Procfile.dev, Procfile.prod, Procfile.static
  • Generator (pro_setup.rb): uses ${RENDERER_PORT:-3800} in generated Procfile lines

Documentation:

  • .env.example: documents base port, RENDERER_PORT, and REACT_RENDERER_URL
  • process-managers.md: new "Coding Agent / CI Integration" and "Manual Worktree Port Setup (Pro)" sections

Test plan

  • PortSelector unit tests: base port derivation, CONDUCTOR_PORT fallback, priority ordering, out-of-range fallback, nil renderer when no base port
  • RuboCop passes on all changed files
  • All pre-commit and pre-push hooks pass
  • CI passes
  • Manual test: REACT_ON_RAILS_BASE_PORT=4000 bin/dev prints correct port assignments
  • Generator tests pass (updated assertions for new Procfile pattern)

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk because it changes bin/dev port selection/env mutation behavior (including overrides and kill targeting), which could affect local/dev workflows and Pro renderer connectivity if env vars are mis-set.

Overview
Adds a new base-port mode for bin/dev via REACT_ON_RAILS_BASE_PORT (with CONDUCTOR_PORT fallback) that deterministically derives Rails/webpack/Pro renderer ports (+0/+1/+2) and propagates them through PORT, SHAKAPACKER_DEV_SERVER_PORT, RENDERER_PORT, and REACT_RENDERER_URL (overriding conflicting pre-sets with warnings).

Hardens dev startup/kill behavior by cleaning stale copied Overmind sockets and tmp/pids/server.pid from other app directories, expanding bin/dev kill to target base-port-derived ports, and validating/scrubbing invalid port env vars to avoid silent failures.

Updates Procfile templates/generator output to use shell defaults (${PORT:-…}, ${RENDERER_PORT:-…}), standardizes docs/examples on REACT_RENDERER_URL, and adds/updates specs and documentation for the new port/env behavior.

Reviewed by Cursor Bugbot for commit f949a69. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Dev ports (Rails, webpack, renderer) can be driven from a single base-port env var, deterministically deriving offsets.
  • Behavior Changes

    • Procfiles and examples now honor PORT/RENDERER_PORT via shell-defaults so runtime ports can be overridden.
    • Dev tooling forwards renderer env vars to spawned processes and synchronizes renderer URL/port when derived.
  • Documentation

    • Added guidance on base-port usage, precedence, bounds, and manual Pro renderer setup.
  • Tests

    • Expanded coverage for base-port derivation, precedence, bounds, invalid values, and fallbacks.
  • Chores

    • Minor .gitignore addition.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds deterministic base-port support via REACT_ON_RAILS_BASE_PORT (fallback CONDUCTOR_PORT), deriving Rails/webpack/renderer at offsets +0/+1/+2; surfaces renderer in port selection; updates Procfiles, templates, docs, process/env preservation, and specs to prefer environment-driven port values with shell-default expansion.

Changes

Cohort / File(s) Summary
Core Port Selection & Server Manager
react_on_rails/lib/react_on_rails/dev/port_selector.rb, react_on_rails/lib/react_on_rails/dev/server_manager.rb
Adds deterministic base-port mode using REACT_ON_RAILS_BASE_PORT (then CONDUCTOR_PORT) to derive { rails, webpack, renderer } = base+0/+1/+2; adds parsing, validation, offset constants, and branches ServerManager to apply base-derived envs or fall back to explicit/env-default logic with renderer synchronization.
Generators & Templates
react_on_rails/lib/generators/react_on_rails/pro_setup.rb, react_on_rails/lib/generators/react_on_rails/templates/base/base/.env.example
Generator output and .env.example document REACT_ON_RAILS_BASE_PORT and use shell-defaults for renderer (RENDERER_PORT=${RENDERER_PORT:-3800}); adds renderer example vars and mapping notes.
Process Env Preservation
react_on_rails/lib/react_on_rails/dev/process_manager.rb
Expanded ENV_KEYS_TO_PRESERVE to include RENDERER_PORT and REACT_RENDERER_URL so those vars are forwarded when running processes outside Bundler.
Procfile Fixtures (dummy / spec)
react_on_rails/spec/dummy/Procfile.dev, react_on_rails/spec/dummy/Procfile.*, react_on_rails_pro/spec/.../Procfile.*
Procfile entries now honor env-driven ports with shell-default fallbacks (e.g., -p ${PORT:-3000}, RENDERER_PORT=${RENDERER_PORT:-3800}) instead of hardcoded ports.
Tests / Specs
react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb, react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb, react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb, react_on_rails/spec/react_on_rails/support/...
Adds/expands specs covering base-port behavior, precedence (REACT_ON_RAILS_BASE_PORT over CONDUCTOR_PORT), out-of-range and invalid-value fallbacks, asserts no port probing when base is used, and updates generator expectations for shell-default renderer port.
Docs & Misc
docs/oss/building-features/process-managers.md, .gitignore
Documentation added/expanded describing base-port usage, precedence, and manual Pro worktree setup; .gitignore updated to ignore .gstack/.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as bin/dev
    participant PS as ReactOnRails::Dev::PortSelector
    participant SM as ReactOnRails::Dev::ServerManager
    participant Svc as Services (Rails, Webpack, Node-Renderer)

    Dev->>PS: select_ports()
    alt REACT_ON_RAILS_BASE_PORT detected
        PS->>PS: validate base_port (1..MAX_BASE_PORT)
        PS->>PS: derive rails=base+0, webpack=base+1, renderer=base+2
        PS-->>Dev: {rails, webpack, renderer}
    else CONDUCTOR_PORT detected
        PS->>PS: parse conductor as base
        PS->>PS: derive rails=base+0, webpack=base+1, renderer=base+2
        PS-->>Dev: {rails, webpack, renderer}
    else
        PS->>PS: use explicit envs or probe free ports
        PS-->>Dev: {rails, webpack, nil}
    end

    Dev->>SM: configure_ports(selected)
    alt selected.renderer present
        SM->>SM: set ENV["PORT"], ENV["SHAKAPACKER_DEV_SERVER_PORT"] (overwrite)
        SM->>SM: set ENV["RENDERER_PORT"], ENV["REACT_RENDERER_URL"]
        SM-->>Svc: processes inherit envs
    else
        SM->>SM: apply explicit/env-default PORT and dev-server using ||= 
        SM->>SM: set REACT_RENDERER_URL only if RENDERER_PORT present and URL unset
        SM-->>Svc: processes inherit envs (no renderer)
    end

    Svc->>Svc: bind services to assigned ports
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • AbanoubGhadban
  • alexeyr-ci2

Poem

🐰 I found a base port in the glen,
Rails, Webpack, Renderer — lined by ten,
Offsets hopped and ports took their place,
ENV whispers tied each little space,
I thump a drum — dev servers race 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: introducing REACT_ON_RAILS_BASE_PORT for managing concurrent worktree ports, which is the primary feature across all modified files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/concurrent-worktree-ports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 15, 2026

Code Review

Overview

This PR adds a REACT_ON_RAILS_BASE_PORT env var (with a CONDUCTOR_PORT fallback) that lets coding-agent tools assign all service ports from a single value — Rails = base+0, webpack = base+1, Pro renderer = base+2. All Procfiles are standardized to use ${PORT:-N} patterns and generator output is updated to match. The design is clean and the scope is well-contained.

What Works Well

  • Priority chain is sensible — base port > explicit per-service vars > auto-detect. The BASE_PORT_ENV_VARS list makes it easy to add new tool-specific vars later.
  • No probing when base port is set — deterministic and fast, which is exactly what CI/agent environments need.
  • Test coverage is solid — all priority tiers, edge cases (out-of-range base, both vars set), and the renderer-nil path are covered. The around blocks cleanly save/restore env state.
  • Procfile standardization is a good cleanup${PORT:-3000} should have been the pattern all along; this PR makes it consistent across OSS and Pro fixtures.
  • configure_ports uses ||= correctly — won't stomp on env vars a user already set intentionally.

Issues

Bug (medium) — derived ports can overflow 65535.
base_port validates the raw env var is in 1..65535 but doesn't check that base + 2 (the renderer offset) also stays in range. REACT_ON_RAILS_BASE_PORT=65534 would produce a renderer port of 65536, which is invalid. The fix is to cap the valid base range at 65535 - BASE_PORT_RENDERER_OFFSET. See inline comment.

Misleading log when PORT is already in the environment.
select_ports prints "Base port 4000 detected. Using Rails :4000, webpack :4001, renderer :4002" unconditionally when a base port is found, but configure_ports uses ||= — so a pre-existing PORT=3000 would still be honoured and Rails would actually run on 3000. The log then contradicts reality. Harmless in the primary coding-agent use case (where PORT wouldn't be pre-set), but worth tightening. See inline comment.

Minor — DEFAULT_RENDERER_PORT constant is unused.
It's defined at the top of PortSelector but never referenced in code. The Procfiles and generator both hardcode 3800 inline. Either use the constant or remove it. See inline comment.

Nice-to-have — REACT_RENDERER_URL not auto-derived from explicit RENDERER_PORT.
Manual-worktree users who set RENDERER_PORT=3801 also have to remember to set REACT_RENDERER_URL=http://localhost:3801. A one-liner in configure_ports could derive it automatically. The docs cover it, but easy to miss. See inline comment (can defer to follow-up).

Test Coverage Gap

There's no test for the scenario where REACT_ON_RAILS_BASE_PORT is set and PORT is already in the environment. Given the misleading-log issue above, a test that asserts configure_ports honours the pre-existing PORT (or alternatively overrides it — whichever the intended behavior is) would prevent regressions once the behavior is clarified.

Docs

The new process-managers.md section and .env.example comments are clear and accurate. The offset table is a nice touch. One small nit: the "Priority chain" callout in the docs says "base port > explicit per-service env vars" — worth noting that this is the select_ports priority, not the final configure_ports priority (which still lets pre-existing env vars win via ||=).

Summary

The approach is sound and the feature is useful. The port-overflow bug is the one concrete correctness issue to fix before merging. The log/priority conflict is worth at least a decision: either document that base port always wins (and change ||= to = in configure_ports) or document that pre-existing env vars take final precedence (and fix the log). Everything else can be a follow-up.

🤖 Generated with Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37a9ae17d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 15, 2026

Greptile Summary

Adds REACT_ON_RAILS_BASE_PORT (with CONDUCTOR_PORT as a fallback) to derive all dev service ports from a single base value, enabling concurrent worktree usage without manual per-service config. Procfiles are updated to shell-default patterns (${PORT:-3000}), renderer env vars are propagated through the process manager, and docs/.env.example are expanded with coding-agent guidance.

Prior review concerns — derived renderer port exceeding 65535 and test-environment isolation — have been addressed. Two minor points remain:

  • base_port warns on non-integer values but silently ignores out-of-range integers; a consistent warning would help diagnose near-max typos.
  • sync_renderer_port_and_url uses String#include? for the port-in-URL check, which can miss a mismatch when the renderer port number is a numeric prefix of the URL's actual port (e.g. 3800 vs. :38000).

Confidence Score: 5/5

Safe to merge — no blocking issues; remaining findings are minor UX/edge-case P2s.

Previous P0/P1 concerns (renderer port > 65535, test env isolation) have been fully addressed. The two remaining issues are P2: a missing warning for out-of-range base ports (silent fallback is safe) and a false-negative in the port-prefix substring check (only affects the warning, not actual behavior). Test coverage is thorough and the core port-selection logic is correct.

react_on_rails/lib/react_on_rails/dev/port_selector.rb and react_on_rails/lib/react_on_rails/dev/server_manager.rb have the minor findings noted above.

Important Files Changed

Filename Overview
react_on_rails/lib/react_on_rails/dev/port_selector.rb New base-port logic: validates integer format (warns on bad value) but silently ignores out-of-range integers without a warning; otherwise well-structured with correct MAX_BASE_PORT guard.
react_on_rails/lib/react_on_rails/dev/server_manager.rb Port configuration split into base-port (unconditional override) and explicit paths; sync_renderer_port_and_url mismatch check uses substring matching which can produce false negatives for port-prefix collisions.
react_on_rails/lib/react_on_rails/dev/process_manager.rb Correctly extends ENV_KEYS_TO_PRESERVE with RENDERER_PORT and REACT_RENDERER_URL so the vars survive with_unbundled_env when spawning the process manager.
react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb Top-level around block properly isolates all relevant env vars; comprehensive new coverage for base-port derivation, precedence, out-of-range, and overflow boundary.
react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb New contexts cover base-port override, explicit RENDERER_PORT derivation, URL-port consistency warning, and internal-host URL (renderer.internal) — good coverage of the new sync logic.
react_on_rails/lib/generators/react_on_rails/pro_setup.rb Generator line updated to RENDERER_PORT=${RENDERER_PORT:-3800} — allows runtime override while preserving the previous default, consistent with Procfile changes.
docs/oss/building-features/process-managers.md Adds clear "Coding Agent / CI Integration" and "Manual Worktree Port Setup (Pro)" sections with port-offset table, priority chain, and configuration examples.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([bin/dev start]) --> B{REACT_ON_RAILS_BASE_PORT or CONDUCTOR_PORT set?}
    B -->|Yes - first valid var wins| C[base_port returns Integer]
    C --> D[select_ports: derive rails=base+0, webpack=base+1, renderer=base+2]
    D --> E[apply_base_port_env: unconditionally set PORT, SHAKAPACKER_DEV_SERVER_PORT, RENDERER_PORT, REACT_RENDERER_URL]
    B -->|No| F{PORT or SHAKAPACKER_DEV_SERVER_PORT set?}
    F -->|Yes| G[Use explicit per-service vars]
    F -->|No| H[Auto-detect free ports starting from defaults]
    G --> I[apply_explicit_port_env: PORT and WEBPACK assigned conditionally]
    H --> I
    I --> J{RENDERER_PORT set?}
    J -->|No| K([Done - renderer not configured])
    J -->|Yes| L{REACT_RENDERER_URL set?}
    L -->|No| M[Derive URL from RENDERER_PORT]
    L -->|Yes - port matches| N([Done - consistent])
    L -->|Yes - port mismatch| O[Warn: vars disagree]
    E --> P([ENV_KEYS_TO_PRESERVE propagated to spawned process manager])
    M --> P
    N --> P
    O --> P
Loading

Reviews (3): Last reviewed commit: "fix: second round of port selector revie..." | Re-trigger Greptile

Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@react_on_rails/lib/react_on_rails/dev/port_selector.rb`:
- Around line 98-102: The base_port method currently allows ENV values up to
65_535 but then derives additional ports as base+1 and base+2; update the
validation in base_port (and any use of BASE_PORT_ENV_VARS) to ensure the chosen
base plus the highest offset (2) stays ≤ 65_535 (i.e., accept bases in the range
1..65_533), so change the conditional that returns val to check val between 1
and (65_535 - 2) and reject/skip values outside that safe range.

In `@react_on_rails/lib/react_on_rails/dev/server_manager.rb`:
- Around line 842-845: The current code may set ENV["REACT_RENDERER_URL"] using
selected[:renderer] even when an existing ENV["RENDERER_PORT"] was preserved;
update the logic so REACT_RENDERER_URL is constructed from the effective port
value (the ENV["RENDERER_PORT"] after the ||= assignment) rather than from
selected[:renderer]; locate the block that references selected[:renderer] in
server_manager.rb (the if selected[:renderer] branch) and change the URL
construction to use ENV["RENDERER_PORT"] (or a local port variable assigned from
ENV["RENDERER_PORT"]) so the URL always matches the actual RENDERER_PORT in use.

In `@react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb`:
- Around line 10-15: Wrap each around hook's example.run in a begin...ensure so
ENV cleanup always runs (e.g., change the around do |example| block that sets
ENV["REACT_ON_RAILS_BASE_PORT"] to use begin; example.run; ensure ... end), and
in the "CONDUCTOR_PORT only" and "out-of-range base port" contexts explicitly
unset or restore both ENV["REACT_ON_RAILS_BASE_PORT"] and ENV["CONDUCTOR_PORT"]
(capture old values, set the test values, run example, then in ensure restore
originals or delete the keys when nil) so base_port's lookup order cannot be
influenced by ambient env vars.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e7ac5b86-7952-413d-89b3-278d22119277

📥 Commits

Reviewing files that changed from the base of the PR and between de5b53a and 37a9ae1.

📒 Files selected for processing (17)
  • docs/oss/building-features/process-managers.md
  • react_on_rails/lib/generators/react_on_rails/pro_setup.rb
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/.env.example
  • react_on_rails/lib/react_on_rails/dev/port_selector.rb
  • react_on_rails/lib/react_on_rails/dev/server_manager.rb
  • react_on_rails/spec/dummy/Procfile.dev
  • react_on_rails/spec/dummy/Procfile.dev-prod-assets
  • react_on_rails/spec/dummy/Procfile.dev-static-assets
  • react_on_rails/spec/dummy/Procfile.dev.no.turbolinks
  • react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb
  • react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
  • react_on_rails/spec/react_on_rails/support/shared_examples/pro_generator_examples.rb
  • react_on_rails_pro/spec/dummy/Procfile.dev
  • react_on_rails_pro/spec/dummy/Procfile.prod
  • react_on_rails_pro/spec/dummy/Procfile.static
  • react_on_rails_pro/spec/execjs-compatible-dummy/Procfile.dev
  • react_on_rails_pro/spec/execjs-compatible-dummy/Procfile.dev-static

Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb Outdated
Comment thread react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb
ihabadham added a commit that referenced this pull request Apr 15, 2026
## Summary

- Applies `prettier --write` to five tracked docs files that are
currently out of compliance on `main`.
- No content changes — only whitespace/table-alignment fixes produced by
Prettier.

## Why this is needed

The formatting drift landed via a docs-only merge to `main`. The
`lint-js-and-ruby.yml` workflow is conditionally skipped for docs-only
commits on `main` (see the job condition at
[`.github/workflows/lint-js-and-ruby.yml:79-87`](https://github.com/shakacode/react_on_rails/blob/main/.github/workflows/lint-js-and-ruby.yml#L79-L87)),
so the check never ran on merge. It surfaces on every subsequent
code-touching PR because the lint job runs for those (e.g.
[#3151](#3151),
[#3148](#3148),
[#3142](#3142),
[#3097](#3097)).

Merging this unblocks the `build` → `Check formatting` step on those
PRs.

## Test plan

- [x] `pnpm start format.listDifferent` passes for the five files
- [x] Diff is purely whitespace — no link targets, code blocks, or prose
changed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Improved formatting across API reference, core concepts, migration,
and RSC docs: refined table layouts, adjusted column alignment, and
standardized emphasis for clearer warnings.
* Updated link text and phrasing for several guides for clearer
navigation.
* Reformatted code examples and JSX/Route snippets for more compact,
consistent presentation and improved readability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
@justin808 justin808 force-pushed the jg/concurrent-worktree-ports branch from 4216467 to 8da5a8a Compare April 18, 2026 01:59
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 18, 2026

Code Review

Overall this is a clean, well-motivated feature. The implementation is straightforward and the test coverage is solid. I found a few issues worth addressing before merge.

Bug: base-port priority not enforced in configure_ports

The documented priority chain states "base port > explicit per-service env vars", but the implementation doesn't honour this. In server_manager.rb:

ENV["PORT"] ||= selected[:rails].to_s

If PORT is already set in the environment (e.g. by a CI system or a shell export), ||= is a no-op. select_ports still returns the base-derived port in selected[:rails], but the Procfile reads ${PORT:-3000} — so Rails actually starts on the pre-existing PORT value, silently disagreeing with the log line that says "Using Rails :<base+0>". Either:

  • Override explicitly when a base port is detected: ENV["PORT"] = selected[:rails].to_s (only in the base-port branch), or
  • Downgrade the documented priority to say base port only wins when PORT is not already set.

Bug: derived ports are not range-validated

base_port validates that the base itself is in 1..65_535, but the derived ports are not checked. A base port of 65_534 would produce renderer port 65_536, which is invalid and would cause a confusing runtime error rather than a clear message.

Dead code: DEFAULT_RENDERER_PORT = 3800

This constant is defined but never referenced anywhere in the Ruby source — the renderer default of 3800 lives only in the Procfile shell fallback (${RENDERER_PORT:-3800}). Either use the constant in code or remove it to avoid misleading future readers.

Missing test: base port takes priority over pre-set PORT

There is no test covering the interaction where REACT_ON_RAILS_BASE_PORT is set AND PORT is already set. This gap would have caught the ||= priority issue above.

Minor: CONDUCTOR_PORT assumption needs verification

The PR assumes CONDUCTOR_PORT is the workspace's allocated base port. If Conductor.build uses that variable for its own internal API rather than the app's port, silently treating it as a Rails base port could cause surprising conflicts. Worth a note in the docs or a code comment citing the Conductor.build docs page that confirms this.


None of these are blockers for the feature concept, but the ||= priority bug and the overflow bug could cause hard-to-diagnose issues in the exact concurrent-workspace scenario this PR is designed to solve.

Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb Outdated
Comment thread react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8da5a8a004

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
react_on_rails/lib/react_on_rails/dev/server_manager.rb (1)

836-843: ⚠️ Potential issue | 🟠 Major

Apply selected ports as the effective env values.

PortSelector now gives the base port higher priority, but ||= lets pre-existing PORT / SHAKAPACKER_DEV_SERVER_PORT / RENDERER_PORT override that selection before child processes start. This can make REACT_ON_RAILS_BASE_PORT=4000 still boot on stale per-service ports, and it also preserves the previously flagged renderer URL drift.

🐛 Proposed fix
 def configure_ports
   selected = PortSelector.select_ports
-  ENV["PORT"] ||= selected[:rails].to_s
-  ENV["SHAKAPACKER_DEV_SERVER_PORT"] ||= selected[:webpack].to_s
+  ENV["PORT"] = selected[:rails].to_s
+  ENV["SHAKAPACKER_DEV_SERVER_PORT"] = selected[:webpack].to_s
   if selected[:renderer]
-    ENV["RENDERER_PORT"] ||= selected[:renderer].to_s
-    ENV["REACT_RENDERER_URL"] ||= "http://localhost:#{selected[:renderer]}"
+    ENV["RENDERER_PORT"] = selected[:renderer].to_s
+    ENV["REACT_RENDERER_URL"] ||= "http://localhost:#{ENV['RENDERER_PORT']}"
   end
 rescue PortSelector::NoPortAvailable => e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/dev/server_manager.rb` around lines 836 -
843, The configure_ports method currently uses ||= which lets pre-existing ENV
values override PortSelector.select_ports; change the assignments in
configure_ports (the selected = PortSelector.select_ports block) to always set
ENV["PORT"], ENV["SHAKAPACKER_DEV_SERVER_PORT"], and when selected[:renderer]
set ENV["RENDERER_PORT"] and ENV["REACT_RENDERER_URL"] using direct assignment
(=) so the selected ports become the effective env values for child processes
and the renderer URL is updated consistently.
react_on_rails/lib/react_on_rails/dev/port_selector.rb (1)

98-102: ⚠️ Potential issue | 🟠 Major

Validate the base port against the highest derived offset.

Line 101 accepts 65_534 and 65_535, but select_ports derives webpack = base + 1 and renderer = base + 2, producing invalid ports above 65_535.

🐛 Proposed fix
 def base_port
+  max_base_port = 65_535 - BASE_PORT_RENDERER_OFFSET
   BASE_PORT_ENV_VARS.each do |var|
     val = ENV[var]&.to_i
-    return val if val&.between?(1, 65_535)
+    return val if val&.between?(1, max_base_port)
   end
   nil
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails/lib/react_on_rails/dev/port_selector.rb` around lines 98 -
102, The base_port method currently accepts ENV values up to 65_535 but
select_ports computes webpack = base + 1 and renderer = base + 2, which can
yield ports > 65_535; update the validation in base_port (method name base_port,
referenced by select_ports) to enforce an upper bound of 65_533 (i.e., ensure
val.between?(1, 65_533) or subtract the maximum derived offset before
validation) so that adding +1 and +2 cannot produce invalid ports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@react_on_rails/lib/react_on_rails/dev/port_selector.rb`:
- Around line 98-102: The base_port method currently accepts ENV values up to
65_535 but select_ports computes webpack = base + 1 and renderer = base + 2,
which can yield ports > 65_535; update the validation in base_port (method name
base_port, referenced by select_ports) to enforce an upper bound of 65_533
(i.e., ensure val.between?(1, 65_533) or subtract the maximum derived offset
before validation) so that adding +1 and +2 cannot produce invalid ports.

In `@react_on_rails/lib/react_on_rails/dev/server_manager.rb`:
- Around line 836-843: The configure_ports method currently uses ||= which lets
pre-existing ENV values override PortSelector.select_ports; change the
assignments in configure_ports (the selected = PortSelector.select_ports block)
to always set ENV["PORT"], ENV["SHAKAPACKER_DEV_SERVER_PORT"], and when
selected[:renderer] set ENV["RENDERER_PORT"] and ENV["REACT_RENDERER_URL"] using
direct assignment (=) so the selected ports become the effective env values for
child processes and the renderer URL is updated consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89dd322b-8ac2-4e3f-9f75-b9ef2476038a

📥 Commits

Reviewing files that changed from the base of the PR and between 4216467 and 8da5a8a.

📒 Files selected for processing (18)
  • .gitignore
  • docs/oss/building-features/process-managers.md
  • react_on_rails/lib/generators/react_on_rails/pro_setup.rb
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/.env.example
  • react_on_rails/lib/react_on_rails/dev/port_selector.rb
  • react_on_rails/lib/react_on_rails/dev/server_manager.rb
  • react_on_rails/spec/dummy/Procfile.dev
  • react_on_rails/spec/dummy/Procfile.dev-prod-assets
  • react_on_rails/spec/dummy/Procfile.dev-static-assets
  • react_on_rails/spec/dummy/Procfile.dev.no.turbolinks
  • react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb
  • react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
  • react_on_rails/spec/react_on_rails/support/shared_examples/pro_generator_examples.rb
  • react_on_rails_pro/spec/dummy/Procfile.dev
  • react_on_rails_pro/spec/dummy/Procfile.prod
  • react_on_rails_pro/spec/dummy/Procfile.static
  • react_on_rails_pro/spec/execjs-compatible-dummy/Procfile.dev
  • react_on_rails_pro/spec/execjs-compatible-dummy/Procfile.dev-static
✅ Files skipped from review due to trivial changes (13)
  • .gitignore
  • react_on_rails_pro/spec/execjs-compatible-dummy/Procfile.dev-static
  • react_on_rails/spec/dummy/Procfile.dev-static-assets
  • react_on_rails/spec/dummy/Procfile.dev.no.turbolinks
  • react_on_rails/spec/dummy/Procfile.dev-prod-assets
  • react_on_rails_pro/spec/execjs-compatible-dummy/Procfile.dev
  • react_on_rails/lib/generators/react_on_rails/pro_setup.rb
  • react_on_rails_pro/spec/dummy/Procfile.prod
  • react_on_rails/lib/generators/react_on_rails/templates/base/base/.env.example
  • react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
  • react_on_rails/spec/react_on_rails/support/shared_examples/pro_generator_examples.rb
  • react_on_rails/spec/dummy/Procfile.dev
  • docs/oss/building-features/process-managers.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • react_on_rails_pro/spec/dummy/Procfile.static
  • react_on_rails_pro/spec/dummy/Procfile.dev
  • react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb

justin808 added a commit that referenced this pull request Apr 18, 2026
Addresses review comments on PR #3142 (REACT_ON_RAILS_BASE_PORT):

- Validate base port against the largest derived offset so base+N stays
  within the valid TCP port range (1..65_535). A base of 65_534 previously
  passed validation but produced an invalid renderer port of 65_536.
- Enforce the documented priority chain ("base port > explicit per-service
  env vars") in ServerManager.configure_ports. The former `||=` assignments
  silently preserved a pre-existing PORT/SHAKAPACKER_DEV_SERVER_PORT, making
  the effective Rails port disagree with the "Base port detected..." log line.
  When a base port is active, per-service env vars are now assigned
  unconditionally.
- Preserve RENDERER_PORT and REACT_RENDERER_URL across Bundler.with_unbundled_env
  by adding them to ProcessManager::ENV_KEYS_TO_PRESERVE. Previously they
  were dropped for process managers launched outside the bundler context.
- Derive REACT_RENDERER_URL from an explicit RENDERER_PORT in non-base-port
  worktrees when the user set the port but not the URL.
- Remove the unused DEFAULT_RENDERER_PORT constant (the 3800 fallback only
  lives in Procfile shell expressions).
- Add port_selector_spec cases for the max-safe and overflow base ports,
  and server_manager_spec cases for base-port priority override and
  explicit-RENDERER_PORT URL sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

Review Feedback Addressed (commit b6e16df)

I (Claude Code) went through all inline review comments from @claude, @cursor, @chatgpt-codex-connector, @coderabbitai, and @greptile and addressed every real issue raised. After triage (deduplicating overlapping comments across reviewers), six distinct concerns were identified and all six are now fixed in a single commit.

Fixes

  1. Base port overflow (P1/Major from multiple reviewers)PortSelector.base_port now validates against MAX_BASE_PORT (65_535 - BASE_PORT_RENDERER_OFFSET). A base of 65_534 previously passed the 1..65_535 check but produced an invalid renderer port of 65_536.
  2. Base port priority violated by ||= (cursor High, claude)ServerManager.configure_ports now splits the base-port and explicit-port paths. When a base port is active, PORT / SHAKAPACKER_DEV_SERVER_PORT / RENDERER_PORT / REACT_RENDERER_URL are assigned unconditionally so the effective ports match the Base port … detected log line even when those env vars were pre-set (e.g., from a .env file).
  3. RENDERER_PORT / REACT_RENDERER_URL lost across Bundler.with_unbundled_env (codex P1) — Added both keys to ProcessManager::ENV_KEYS_TO_PRESERVE so they survive run_process_outside_bundle.
  4. Missing test for configure_ports when PORT is pre-set with base port active (claude) — Added a when configuring ports with a base port active context to server_manager_spec that covers the override.
  5. Unused DEFAULT_RENDERER_PORT constant (claude ×2, cursor Low) — Removed. The 3800 fallback only lives in the Procfile shell expression ${RENDERER_PORT:-3800}.
  6. REACT_RENDERER_URL can drift from RENDERER_PORT (coderabbit Major, claude) — In the non-base-port path, configure_ports now derives REACT_RENDERER_URL from an explicit RENDERER_PORT when only the port is set. Added a regression spec.

Tests

Added 5 new specs to port_selector_spec.rb and server_manager_spec.rb covering the max-safe base port, overflow fallback, base-port override of pre-existing env vars, and explicit-RENDERER_PORT URL derivation. Full suite (176 examples) passes locally.

On the bot summary comments (@claude, @greptile, @coderabbitai)

Their summary bodies restate the inline findings that are already addressed above — no separate action needed. The earlier CodeRabbit "processing" notice is a stale status post.

All 16 unresolved review threads are now resolved.

Comment thread react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb Fixed
Comment thread react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb Fixed
@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@coderabbitai coderabbitai Bot added the full-ci label Apr 18, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me quickly verify the key changes in the updated code.


🏁 Script executed:

#!/bin/bash
echo "=== PortSelector: base_port method + MAX_BASE_PORT ==="
rg -n "MAX_BASE_PORT|base_port|BASE_PORT_RENDERER_OFFSET" react_on_rails/lib/react_on_rails/dev/port_selector.rb -A2 -B2

echo ""
echo "=== ServerManager: configure_ports ==="
cat react_on_rails/lib/react_on_rails/dev/server_manager.rb

echo ""
echo "=== ProcessManager: ENV_KEYS_TO_PRESERVE ==="
rg -n "ENV_KEYS_TO_PRESERVE" -A5 -B2

echo ""
echo "=== DEFAULT_RENDERER_PORT removed? ==="
rg -n "DEFAULT_RENDERER_PORT" react_on_rails/lib/react_on_rails/dev/port_selector.rb

Length of output: 58432


@justin808 — Thank you for the thorough write-up! I've verified all six fixes in the updated code:

  1. MAX_BASE_PORT overflow guard ✅ — base_port now validates against MAX_BASE_PORT = 65_535 - BASE_PORT_RENDERER_OFFSET, correctly rejecting values like 65,534 that would produce an out-of-range renderer port.

  2. Unconditional base-port assignment ✅ — configure_ports is cleanly split into apply_base_port_env (assigns all four env vars unconditionally) and apply_explicit_port_env (uses ||=), so the "Base port detected" log line always matches the effective ports.

  3. ENV_KEYS_TO_PRESERVE ✅ — Both RENDERER_PORT and REACT_RENDERER_URL are present in ProcessManager::ENV_KEYS_TO_PRESERVE, ensuring they survive Bundler.with_unbundled_env.

  4. DEFAULT_RENDERER_PORT removed ✅ — No matches found; the 3800 fallback now lives exclusively in the Procfile shell expression.

  5. REACT_RENDERER_URL/RENDERER_PORT drift (my Major comment) ✅ — apply_explicit_port_env now derives REACT_RENDERER_URL from an explicit RENDERER_PORT when the URL is unset.

Everything looks solid. The apply_base_port_env / apply_explicit_port_env split is a nice clean separation of concerns too. Great work resolving all of the feedback!


🧠 Learnings used
Learnt from: justin808
Repo: shakacode/react_on_rails PR: 0
File: :0-0
Timestamp: 2026-04-13T02:29:20.920Z
Learning: In `react_on_rails_pro/lib/react_on_rails_pro/engine.rb` and `react_on_rails/lib/react_on_rails/configuration.rb` (PR `#3094`): Do NOT reset `rendering_strategy` or `js_code_builder` inside `ReactOnRails.configure`. The Pro engine sets these exactly once during `after_initialize`; resetting them on reconfigure would silently drop Pro's NodeStrategy/ProJsCodeBuilder assignments. A proper reconfigure mechanism for these accessors is deferred to Step 3 of the strategy-pattern refactor (`#2905`).

Learnt from: ihabadham
Repo: shakacode/react_on_rails PR: 2284
File: react_on_rails/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt:10-10
Timestamp: 2026-02-12T20:43:26.843Z
Learning: In React on Rails Pro initializer templates (e.g., `react_on_rails_pro.rb.tt`), the `config.renderer_password` uses `ENV.fetch("RENDERER_PASSWORD", "devPassword")` as a development starter configuration. This pattern is intentional: the node renderer requires explicit production deployment setup, and adding a production-only raise would break generator execution since initializers are loaded during `rails generate`. The pattern is consistent with existing manual setup documentation.

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/dummy/commonWebpackConfig.js : Changes to `commonWebpackConfig.js` affect all three webpack bundles (client, server, and RSC) in the Pro dummy app

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T23:15:11.806Z
Learning: Applies to CHANGELOG.md : Changelog format: `[PR 1818](https://github.com/shakacode/react_on_rails/pull/1818) by [username](https://github.com/username)` (no hash before PR number)

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/**/*.{js,ts,tsx,jsx,json,css,md} : Pro Prettier check should be run with: `cd react_on_rails_pro && pnpm run prettier --check .`

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/react_on_rails_pro/**/*.rb : Pro gem unit tests should be run with: `cd react_on_rails_pro && bundle exec rspec spec/react_on_rails_pro/`

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/spec/dummy/spec/**/*.rb : Pro integration tests should be run with: `cd react_on_rails_pro/spec/dummy && bundle exec rspec spec/`

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T22:10:33.821Z
Learning: Applies to react_on_rails_pro/** : Ask before modifying the Pro package (`react_on_rails_pro/`)

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: react_on_rails_pro/CLAUDE.md:0-0
Timestamp: 2026-02-24T16:52:47.694Z
Learning: Applies to react_on_rails_pro/packages/react-on-rails-pro/**/*.test.rsc.{ts,tsx} : Pro RSC tests should be run with: `pnpm --filter react-on-rails-pro run test:rsc`

Learnt from: CR
Repo: shakacode/react_on_rails PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-12T23:15:11.806Z
Learning: Applies to react_on_rails/spec/dummy/e2e/playwright/e2e/**/*.{js,ts} : Use Playwright for E2E tests in `react_on_rails/spec/dummy/e2e/playwright/e2e/`

Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb
Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb
Comment thread react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb Outdated
Addresses the review comments posted after round 14:

1. `base_port_with_source` now strips whitespace before the integer regex
   so `REACT_ON_RAILS_BASE_PORT="  5000  "` is accepted the same way
   `PORT="  5000  "` already was via `consume_explicit_port_env`.

2. `run_production_like` now runs `sync_renderer_port_and_url` in the
   non-base-port branch so `bin/dev prod` auto-derives `REACT_RENDERER_URL`
   from a bare `RENDERER_PORT` and surfaces mismatch warnings — matching
   `bin/dev` and `bin/dev static`.

3. `PortSelector` gains a pure `#base_port_hash` accessor that returns the
   derived port tuple without the "Base port X detected" banner or
   derived-port-collision warnings. `#base_port_ports` still logs; callers
   that want the quiet path (e.g. `kill_processes`) can use the new helper.

4. `kill_processes` now reads `PortSelector.base_port_hash` so
   `bin/dev kill` targets the worktree's actual ports in base-port mode
   instead of the stale `[3000, 3001]` fallback.

5. `warn_if_port_will_be_overridden` and `warn_if_renderer_url_will_be_overridden`
   now `.inspect` the existing value so whitespace in env var values is
   visible — matching every other env-mutation warning in this file.

6. `run_production_like` gains a comment pointing at `configure_ports` as
   the canonical per-mode env-setup reference and explaining the two
   intentional differences (PORT scan starts at 3001, omits
   `SHAKAPACKER_DEV_SERVER_PORT` validation).

Spec coverage: whitespace-trim test for base port, new IPv6-style spec
context for production-like renderer sync, kill-processes base-port spec,
and a `#base_port_hash` describe block that locks in the absence of
stdout/stderr side effects.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

Review round 15 — 7 fixes + 1 rationale reply

I (Claude Code) addressed the new review comments posted after round 14. All changes are in d8dc1cce8.

Fixed (code + specs)

  1. cursor[bot] — whitespace in base port env varbase_port_with_source now strips raw before the integer regex and to_i, matching consume_explicit_port_env / valid_port_string?. REACT_ON_RAILS_BASE_PORT=" 5000 " is now accepted as 5000.
  2. claude[bot] — sync_renderer_port_and_url in production-likerun_production_like now calls sync_renderer_port_and_url in the non-base-port branch so bin/dev prod auto-derives REACT_RENDERER_URL from RENDERER_PORT and surfaces mismatch warnings.
  3. claude[bot] — base_port_ports public side effects — split into pure base_port_hash (no logging) + logging base_port_ports wrapper; they share a private derive_ports_from_base helper.
  4. claude[bot] — spec for sync in production-like — closed the gap rather than locking it in. Added specs asserting auto-derivation + mismatch warning in production-like mode without base port.
  5. claude[bot] — .inspect in override warnings (code review 1)warn_if_port_will_be_overridden and warn_if_renderer_url_will_be_overridden now .inspect the existing value, matching every other env-mutation warning in the file. Updated corresponding specs.
  6. claude[bot] — run_production_like divergence comment (code review 1) — added a comment block explaining why this path is structurally similar to configure_ports but intentionally different (scans from 3001, omits SHAKAPACKER_DEV_SERVER_PORT).
  7. claude[bot] — kill_processes hardcodes [3000, 3001] (code review 2) — new killable_ports helper reads PortSelector.base_port_hash so bin/dev kill targets the worktree's actual ports in base-port mode.

Rationale reply (no code change)

Overall suite: 316 dev/ specs passing, RuboCop clean.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 23, 2026

Code Review: PR #3142 — REACT_ON_RAILS_BASE_PORT deterministic port allocation

Overview

This PR adds a single-env-var port-allocation mechanism for concurrent worktree development (coding agents, CI). The design is sound: one base port → three derived ports via fixed offsets, with a CONDUCTOR_PORT fallback and an escape hatch where REACT_ON_RAILS_BASE_PORT always wins. Procfiles, generators, docs, and specs are all updated consistently. Test coverage is excellent throughout.


Bugs / Correctness Concerns

1. base_port_ports is called twice in the non-base-port path

configure_ports calls apply_base_port_if_activePortSelector.base_port_ports. When that returns false it then calls PortSelector.select_ports, which internally also calls base_port_ports. So when no base port is set, base_port_with_source runs twice. It's cheap (just ENV reads) and harmless, but it's unnecessary. Consider having select_ports accept the result of base_port_ports as an argument, or restructure configure_ports to call select_ports directly and let select_ports own the branching.

2. ${RENDERER_PORT:-3800} in package.json won't expand on Windows

package.json scripts run via cmd.exe on Windows, which doesn't support POSIX shell parameter expansion. The change in react_on_rails_pro/spec/dummy/package.json will silently pass the literal string ${RENDERER_PORT:-3800} to node on Windows. If Windows support matters (even for CI), consider a cross-platform wrapper script or note the limitation explicitly.


Design / Code Quality Observations

3. ENV mutation as a side effect of consume_explicit_port_env is surprising

Deleting from ENV inside what appears to be a parse/validate helper creates hidden coupling to ServerManager. The coupling is documented (the naming, the comment), but anyone reading explicit_rails_port in isolation won't expect it to delete env vars. Consider renaming the private method to parse_and_clear_invalid_port_env! to make the mutation obvious from the call site, or extracting the deletion into a dedicated cleanup step before either selector is called.

4. SHAKAPACKER_DEV_SERVER_PORT is set in production-like mode under base port

When base-port mode is active and start(:production_like) runs, apply_base_port_env unconditionally sets SHAKAPACKER_DEV_SERVER_PORT even though prod-like mode doesn't start webpack-dev-server. The test comment explains the rationale (tooling consistency), but this could confuse users inspecting their env after bin/dev prod. At minimum, a code comment at the call site in run_production_like would help future readers.

5. CONDUCTOR_PORT contract is an assumption, not a public API

This is well-documented in both code and docs, and the escape hatch via REACT_ON_RAILS_BASE_PORT is the right mitigation. Worth adding a note that if Conductor changes the semantics of CONDUCTOR_PORT in the future, users should pin REACT_ON_RAILS_BASE_PORT explicitly. The existing NOTE in the docs is close but could be more direct: "treat CONDUCTOR_PORT support as best-effort until Conductor documents this contract."


Positive Highlights

  • Comprehensive test coverage: edge cases for privilege ranges, non-integer values, whitespace, out-of-range, IPv6 URLs, basic-auth URLs, and the full precedence chain. The around block for env isolation is cleaner than the old manual save/restore pattern.
  • base_port_hash vs base_port_ports: splitting the silent-derivation path (for kill) from the logged path (for startup) is a good design.
  • url_port_mismatch? regex: handles IPv6 and user:password@ correctly; the URI.parse fallback prevents crashes on malformed URLs.
  • warn_if_legacy_renderer_url_env_used: a genuinely helpful migration aid for the RENDERER_URLREACT_RENDERER_URL rename.
  • preserve_runtime_env_vars now passes nil for unset keys: prevents scrubbed-but-invalid values from being resurrected by Bundler's env snapshot in child processes — a subtle correctness fix.
  • Procfile env-var fallback pattern (${PORT:-3000}): strictly more flexible than hardcoded ports and backwards-compatible.

Minor Nits

  • warn_url_without_port only warns for localhost URLs. A remote URL without a port (e.g. http://renderer.internal) could also be a misconfiguration — the renderer binds to 3800 via the Procfile default, but Rails would target port 80 (http default). Worth a broader warning or at least a doc note.
  • The base_port_mode: false key added to the non-base-port select_ports return value is useful, but the existing "default ports are free" spec does not assert it — only the new contexts check it.
  • killable_ports in the non-base-port path returns only [3000, 3001] with no renderer port. This is a pre-existing gap rather than a regression, but since this PR adds renderer awareness to the kill path in base-port mode, a follow-up to add a configurable renderer port to the fallback list would round things out.

Summary

The feature is well-implemented and meaningfully improves the multi-worktree development experience. The two correctness concerns (redundant base_port_ports call in the non-base-port path; Windows shell expansion in package.json) are low severity. The ENV-mutation coupling in consume_explicit_port_env is the most significant design smell and worth at minimum a naming change to make the side-effect self-documenting at the call site.

Comment thread react_on_rails/lib/react_on_rails/dev/port_selector.rb Outdated
Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb Outdated
Comment thread react_on_rails_pro/spec/dummy/package.json
Comment thread react_on_rails/lib/react_on_rails/dev/server_manager.rb
1. Rename PortSelector.consume_explicit_port_env → consume_explicit_port_env!
   so the ENV-mutation side effect is self-documenting at call sites
   (explicit_rails_port, explicit_webpack_port).

2. configure_ports now calls PortSelector.select_ports once and branches on
   :base_port_mode, eliminating the redundant base_port_ports invocation
   that ran on every non-base-port call. apply_base_port_if_active is
   retained for run_production_like, which doesn't go through select_ports.

3. Add POSIX-shell caveat "//" comment to react_on_rails_pro/spec/dummy
   package.json noting ${RENDERER_PORT:-3800} won't expand on Windows
   cmd.exe; this dummy app is Linux/macOS only.

4. Document why warn_url_without_port deliberately excludes remote URLs
   (scheme-default ports may be correct behind a reverse proxy; bin/dev
   doesn't launch remote renderers so it can't diagnose safely).

5. Two doc clarifications from the summary review:
   - apply_base_port_env comment explains why SHAKAPACKER_DEV_SERVER_PORT
     is set even in production-like mode (tooling consistency).
   - CONDUCTOR_PORT note tightened to say "best-effort until Conductor
     documents the contract" in both docs and port_selector.rb comment.

Specs: 316 dev/ examples passing. RuboCop clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@justin808
Copy link
Copy Markdown
Member Author

Review round 16 — 5 design/doc polish fixes

I (Claude Code) addressed the 4 new inline review comments posted after round 15 plus the two summary-only items from the latest code review. All changes are in 61f0ad286.

Fixed (code + specs + docs)

  1. claude[bot] — consume_explicit_port_env naming — Renamed consume_explicit_port_envconsume_explicit_port_env!. The ! suffix signals the ENV-mutation side effect at both call sites (explicit_rails_port, explicit_webpack_port) per Ruby convention. Updated the method doc and the spec comment that referenced the old name.
  2. claude[bot] — redundant base_port_ports callconfigure_ports now calls PortSelector.select_ports once and branches on :base_port_mode, so base_port_ports runs exactly once per invocation. apply_base_port_if_active is retained for run_production_like (which doesn't go through select_ports). Updated the "with clean port env" base-port spec context to stub both base_port_ports and select_ports consistently.
  3. claude[bot] — POSIX shell expansion in package.json — Added a "//" comment key at the top of the Pro dummy scripts object noting that ${RENDERER_PORT:-3800} requires POSIX shell and won't expand on Windows cmd.exe, and that this dummy app is Linux/macOS only. No JS wrapper since the dummy spec app only runs on Linux/macOS CI — a contributor-facing note is enough.
  4. claude[bot] — localhost-only warn_url_without_port — Added a doc comment above warn_url_without_port explaining why remote URLs are intentionally excluded: this process doesn't launch remote renderers, so scheme-default ports (80/443) may be correct behind a reverse proxy, and remote-side port mismatches are a deployment concern. Kept the warning localhost-only rather than broadening it to avoid false positives on legitimate remote-renderer setups.
  5. claude[bot] — code review summary, two doc touches:
    • apply_base_port_env doc now explains why SHAKAPACKER_DEV_SERVER_PORT is set even in production-like mode (tooling consistency: a subsequent bin/dev in the same shell sees the base-port-derived value rather than a stale explicit one).
    • CONDUCTOR_PORT note in both docs/oss/building-features/process-managers.md and port_selector.rb tightened to say "best-effort until Conductor documents the contract," matching the reviewer's wording.

Test / lint status

  • react_on_rails/spec/react_on_rails/dev/: 316 examples, 0 failures
  • RuboCop on lib/react_on_rails/dev/ + spec/react_on_rails/dev/: clean

All 4 inline threads resolved. No deferred items from this round.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61f0ad2865

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +969 to +972
ENV["PORT"] = selected[:rails].to_s
ENV["SHAKAPACKER_DEV_SERVER_PORT"] = selected[:webpack].to_s
ENV["RENDERER_PORT"] = selected[:renderer].to_s
ENV["REACT_RENDERER_URL"] = derived_url
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve legacy renderer URL env var in base-port mode

In base-port mode we derive and export REACT_RENDERER_URL but never set RENDERER_URL. That breaks Pro apps that still configure config.renderer_url from the legacy env name (a migration path this same commit acknowledges via warn_if_legacy_renderer_url_env_used): the node renderer is moved to RENDERER_PORT=base+2, while Rails can still target the old/default URL and fail SSR requests. Mirroring the derived URL to RENDERER_URL (or equivalent compatibility handling) would avoid this regression for older initializers.

Useful? React with 👍 / 👎.

base = PortSelector.base_port_hash
return [3000, 3001] unless base

[base[:rails], base[:webpack], base[:renderer]]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSS users: renderer port is always included in the kill list

When base-port mode is active, killable_ports always returns [rails, webpack, renderer], even for apps that don't use the Pro Node Renderer. Calling kill_port_processes on base + 2 is a no-op if nothing is listening there, but it silently kills any unrelated process that happens to occupy that port — which is more likely here than on the default port list because the port was never claimed by a renderer.

Consider guarding with a Pro check, or at least document this expansion:

Suggested change
[base[:rails], base[:webpack], base[:renderer]]
[base[:rails], base[:webpack], base[:renderer]].compact

This at least handles the hypothetical future case where renderer could be nil, but doesn't fully address the OSS concern. A more explicit fix would check whether the Pro renderer is actually configured before including base[:renderer].

# (explicit_rails_port / explicit_webpack_port); the "warn once + fall
# back" flow is shared with ServerManager via the cleared env, not via
# the return value. Kept in one place so the coupling is obvious.
def consume_explicit_port_env!(var_name)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ENV mutation inside a "selector" class

PortSelector is conceptually a pure-computation layer (select ports from inputs, return a hash), but consume_explicit_port_env! writes a side-effect into ENV by deleting invalid values. The ! suffix and the block comment above document this, but it still means PortSelector and ServerManager are coupled through shared global state (ENV) rather than through return values.

This is a design tradeoff that's acceptable here for the "warn once" guarantee, but worth flagging if this class gets reused in other contexts (e.g., tests that isolate ENV carefully, or future background threads). The comment should be preserved exactly as written so future readers can trace why the coupling exists.

No immediate action needed — just documenting the tradeoff for reviewers.


val = stripped.to_i
unless val.between?(1, MAX_BASE_PORT)
warn invalid_base_port_warning(var, raw, "out of range (1..#{MAX_BASE_PORT})", idx)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-facing warning message uses MAX_BASE_PORT (65533) not 65535

The warning prints "out of range (1..65533)". A developer who sets REACT_ON_RAILS_BASE_PORT=65534 will see that message and likely be confused — 65534 is a perfectly valid TCP port, but it's rejected here because 65534 + 2 = 65536 would overflow the renderer offset.

Consider making the reason explicit in the warning:

Suggested change
warn invalid_base_port_warning(var, raw, "out of range (1..#{MAX_BASE_PORT})", idx)
warn invalid_base_port_warning(var, raw, "out of range (1..#{MAX_BASE_PORT}; must leave room for +#{BASE_PORT_RENDERER_OFFSET} renderer offset)", idx)

Or alternatively, just call out the constraint in the warning body inside invalid_base_port_warning.

warn_if_port_will_be_overridden("SHAKAPACKER_DEV_SERVER_PORT", selected[:webpack])
warn_if_port_will_be_overridden("RENDERER_PORT", selected[:renderer])
ENV["PORT"] = selected[:rails].to_s
ENV["SHAKAPACKER_DEV_SERVER_PORT"] = selected[:webpack].to_s
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHAKAPACKER_DEV_SERVER_PORT set in production-like mode

apply_base_port_env is called from both the development/static and production-like code paths. In production-like mode (static assets, no webpack-dev-server), setting SHAKAPACKER_DEV_SERVER_PORT is harmless but surprising to users who inspect their environment after bin/dev prod.

The block comment at line 957–962 explains this as "tooling consistency," which is a reasonable justification. However, this means the test at server_manager_spec.rb:1627 is asserting a somewhat non-obvious property:

"applies SHAKAPACKER_DEV_SERVER_PORT from base+1 even though prod mode doesn't use webpack-dev-server"

It might be worth adding a comment inside the test itself (not just the it description) explaining that this is intentional rather than a test of a misguided implementation, so future readers don't remove the behaviour thinking it's a bug.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 23, 2026

Review: PR #3142REACT_ON_RAILS_BASE_PORT for concurrent worktree port management

Overview

This is a well-scoped, well-tested feature. The priority chain (base port → explicit per-service env vars → auto-detect) is clear, the edge-case coverage is excellent, and the documentation is honest about the CONDUCTOR_PORT contract being unofficial/best-effort. The base_port_hash vs base_port_ports distinction (silent vs. logged) shows careful attention to callers that should not produce output (e.g. kill flows). Procfile templates now properly honor runtime env vars instead of hardcoding ports.


Issues

1. killable_ports includes renderer port unconditionally — potential unintended kill for OSS users

(inline comment on server_manager.rb:73)

The old fallback was [3000, 3001]; the new base-port path returns [rails, webpack, renderer] unconditionally. For OSS users without the Pro Node Renderer, nothing owns base + 2, so kill_port_processes is a no-op in the normal case. But if anything unrelated happens to be on that port (more likely given nothing claimed it), it gets silently killed. Worth a .compact at minimum, and ideally a guard that only includes the renderer port when the Pro renderer is actually in use.

2. User-visible warning uses MAX_BASE_PORT (65533) instead of 65535 — confusing near the upper boundary

(inline comment on port_selector.rb:201)

A developer setting REACT_ON_RAILS_BASE_PORT=65534 sees "out of range (1..65533)". That number is surprising because 65534 is a valid TCP port. The underlying constraint (must leave room for the +2 renderer offset) is not surfaced. Suggest adding the reason, e.g. "out of range (1..65533; must leave room for +2 renderer offset)".

3. consume_explicit_port_env! mutates ENV inside PortSelector — layer boundary crossed

(inline comment on port_selector.rb:244)

The existing comment documents this intentionally, so no action is strictly required. Flagging for future readers: if PortSelector is extracted or reused, the hidden ENV side-effect is a trap. The coupling is kept in one tight place which is the right tradeoff.

4. apply_base_port_env sets SHAKAPACKER_DEV_SERVER_PORT in production-like mode

(inline comment on server_manager.rb:970)

Setting a webpack-dev-server port env var when running static assets is harmless but surprising. The decision is documented and tested. Suggestion: add a brief inline comment to the test that asserts this so future maintainers do not accidentally remove the behavior thinking it was a test of a bug.


What is done well

  • Test isolation: the around blocks in port_selector_spec.rb and the shared "with clean port env" context in server_manager_spec.rb robustly handle developer environments where REACT_ON_RAILS_BASE_PORT or CONDUCTOR_PORT may already be set. This is exactly the right approach.
  • IPv6 + basic-auth URL handling: the url_port_mismatch? regex correctly prevents userinfo-colon and IPv6-bracket backtracking bugs, and dedicated tests verify both. Thorough.
  • Bundler env resurrection fix: changing preserve_runtime_env_vars to pass nil for unset keys (rather than omitting them) is a subtle but correct fix — it prevents with_unbundled_env from restoring a pre-Bundler invalid value that PortSelector deliberately cleared. The test name explains the invariant clearly.
  • base_port_hash / base_port_ports split: the silent vs. logging distinction is clean and well-documented.
  • Documentation: the CONDUCTOR_PORT disclaimers ("treat as best-effort", "escape hatch: REACT_ON_RAILS_BASE_PORT takes precedence") are appropriately hedged for an unofficial contract.

Minor nits

  • procfile_port in run_production_like calls ENV.fetch("PORT", 3001).to_i after the invalid-PORT normalization block has already cleared bad values. A brief inline comment that ENV["PORT"] is guaranteed clean at that point would aid readability.
  • The warning range string "1..65535" in consume_explicit_port_env! (line 259) is inconsistent with "1..#{MAX_BASE_PORT}" used for base port validation. Both are correct for their context (per-service ports are validated against the full TCP range), but a shared constant for the display string would make the inconsistency explicit.

Overall: approve with the renderer-port kill issue flagged as the one item worth addressing before merge. The rest are documentation or cosmetic suggestions.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 25, 2026

Code Review: PR 3142 — REACT_ON_RAILS_BASE_PORT for concurrent worktree port management

Overview

This PR introduces a clean, tool-agnostic mechanism for running multiple dev stacks concurrently by deriving all service ports from a single base env var. The scope is well-contained, the documentation is clear, and the test coverage is excellent. A few items worth discussing before merging are called out below.

What works well

  • Extensive test coverage: The new specs cover an impressive range of edge cases (privileged ports, non-numeric values, whitespace padding, IPv6, basic-auth URLs, fallthrough behavior). The around block pattern for env var isolation is clean and correct.
  • Graceful degradation: Invalid or out-of-range base port values fall through to the existing auto-detection path with clear warnings rather than failing hard.
  • ENV mutation is intentional and documented: The consume_explicit_port_env! side-effect and the preserve_runtime_env_vars nil-inclusion pattern are explained with enough context to understand the coupling to Bundler's env snapshot.
  • working_directory_for_pid replaces pgrep -f overmind: Much safer — PID-file ownership is now verified by checking the actual working directory of the running process rather than searching by name, which was prone to false positives.
  • Procfile parametrization: Replacing hardcoded port literals with shell-fallback syntax is the right long-term approach regardless of base-port mode.

Concern 1: lsof dependency — silent degradation on minimal Linux hosts

working_directory_for_pid shells out to lsof. On minimal CI containers or Debian/Alpine installs, lsof is not installed by default. When it is absent, the backtick returns an empty string, path_line is nil, and the method returns nil — so cleanup_rails_pid_file returns false and a stale cross-app PID file is silently kept. That is a safe fallback, but a user debugging a "server already running" error in a container will not benefit from the new cleanup logic and will not know why.

A /proc/PID/cwd readlink check (Linux-only but zero external dependencies) used as a primary path before falling back to lsof would cover the common case on minimal hosts.

Concern 2: UNIXSocket.open has no connect timeout

socket_active? probes a UNIX socket with UNIXSocket.open. If the socket file exists and is bound but the server accept queue is saturated, connect(2) can block indefinitely. In practice this is very unlikely for local overmind sockets, but a non-blocking probe using Socket#connect_nonblock and rescuing IO::WaitWritable would be more robust.

Concern 3: Breaking change — RENDERER_URL to REACT_RENDERER_URL

The rename warning (warn_if_legacy_renderer_url_env_used) and the CHANGELOG are clear. However, the warning only fires when RENDERER_URL is set in the environment visible to bin/dev. Users who inject the env var from a secrets manager or k8s secrets at a different layer — and whose initializer reads ENV["RENDERER_URL"] — will silently fall back to the default renderer URL without any warning. A note in the CHANGELOG that the gem never reads either env var directly (the user's initializer does, and that is the code to update) would help this cohort.

Concern 4: consume_explicit_port_env! mutates global ENV from within PortSelector

Having PortSelector delete PORT or SHAKAPACKER_DEV_SERVER_PORT as a side-effect of a query method is non-obvious. The ! suffix and the surrounding comment explain the rationale (suppress a duplicate warning from ServerManager), but it means PortSelector.select_ports has observable global side effects. If select_ports is ever called more than once in a process, the second call sees different ENV state than the first. The specs guard against this with around blocks. Flagging as a future maintainability concern.

Concern 5: CONDUCTOR_PORT relies on undocumented third-party behavior

The code and docs are admirably transparent: the constant comment, process-managers.md, and the CHANGELOG all acknowledge that treating CONDUCTOR_PORT as a block base is empirical. The escape hatch (REACT_ON_RAILS_BASE_PORT takes precedence) is the right mitigation. No action required — flagging for awareness.

Minor nits

  • The regex in url_port_mismatch? is long and subtle. The surrounding comment is thorough. Consider extracting it to a named constant so the guard line is easier to parse at a glance.
  • killable_ports returns all three derived ports when base-port mode is active. A short comment noting that nil renderer is not expected in this branch would clarify intent for future readers.
  • Setting SHAKAPACKER_DEV_SERVER_PORT in production-like mode for "tooling consistency" is reasonable but may surprise Pro users who inspect their environment after bin/dev prod. The existing "Base port X detected" stdout line already names all three derived ports, so this is already surfaced — the intent just deserves a note in the code comment.

Verdict

This is well-engineered and ready to merge with the items above considered. Concerns 1 and 2 are the most actionable. Inline comments below point at specific line locations.

end

def working_directory_for_pid(pid)
path_line = `lsof -a -p #{pid} -d cwd -Fn 2>/dev/null`.lines.find { |line| line.start_with?("n") }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lsof is not installed by default on minimal Linux hosts (Alpine, Debian slim, many CI images). When it is absent this returns nil, which is a safe fallback — the PID file is kept rather than deleted. Consider a /proc/PID/cwd readlink path as a zero-dependency primary attempt on Linux before falling back to lsof. This would help users in container environments who copy an app directory and get a stale server.pid they cannot automatically clear.

def socket_active?(socket_path)
return false unless File.exist?(socket_path)

UNIXSocket.open(socket_path, &:close)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNIXSocket.open has no connect timeout. If the socket file exists and the server's accept queue is full (extremely rare for a local overmind socket but theoretically possible), connect(2) will block indefinitely here and stall bin/dev startup. A non-blocking probe using Socket#connect_nonblock + IO::WaitWritable rescue would be more robust.

next if stripped.empty?

unless stripped.match?(/\A\d+\z/)
warn invalid_base_port_warning(var, raw, "not a valid integer", idx)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting a global ENV var from within PortSelector as a side-effect of querying port values is a surprising mutation. The ! suffix and surrounding comment explain the coupling to ServerManager's duplicate-warning suppression. One consequence: if select_ports is ever called a second time in the same process (tests guard against this with around blocks), the second call sees a different environment. Consider documenting this at the class level so future contributors know select_ports is not idempotent when invalid env vars are present.

warn "WARNING: Overriding #{var_name}=#{existing.inspect} with #{derived_port} " \
"because base port mode is active."
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex here is complex and well-commented. One small improvement for future readability: extract it to a private constant (e.g. URL_WITH_EXPLICIT_PORT_RE) so the return true unless url.match?(...) guard line reads clearly at a glance without needing to parse the regex inline.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 25, 2026

Code Review

Overview

This PR adds REACT_ON_RAILS_BASE_PORT (with CONDUCTOR_PORT fallback) to derive all service ports deterministically from a single env var, enables concurrent worktrees in coding-agent sandboxes without per-service .env files, and adds stale-file cleanup for copied app directories. The scope is well-defined and the implementation is thorough.


What Works Well

  • Port validation is consistentvalid_port_string? is shared between PortSelector and ServerManager, preventing the two layers from drifting apart on what counts as a valid port.
  • ENV mutation is clearly signalledconsume_explicit_port_env! uses the ! convention and has a detailed comment explaining the coupling with ServerManager's overwrite_invalid_port_env. This is one of the harder patterns to make readable and the code does it well.
  • Graceful fallbacks throughout — invalid base port values warn and fall through to auto-detect rather than crashing; privileged ports warn but still proceed; invalid per-service env vars are scrubbed before they reach the Procfile.
  • Test coverage is comprehensive — edge cases for whitespace, non-numeric values, privileged ports, max boundary, IPv6 URLs, basic-auth passwords containing port digits, and the lsof-absent path are all covered.
  • Socket cleanup is improved — replacing the pgrep -f overmind shotgun (which would skip cleanup for any app if any overmind was running anywhere) with a per-socket UNIXSocket.open liveness check is strictly better.

Issues / Suggestions

1. lsof error handling is inconsistent with the existing pattern in this codebase

file_manager.rb uses a bare backtick call — server_manager.rb:140-143 wraps its own lsof call in Open3.capture2 with a rescue Errno::ENOENT, StandardError to handle lsof not being installed (minimal Docker containers, exotic Linux distros). The fallback when lsof is absent is safe here (nil → don't remove the file) but worth aligning. See inline comment.

2. .lychee.toml wildcard PR exclusion suppresses changelog link validation

Changing the specific PR exclusion to pull/[0-9]+$ means lychee will never check any PR link in CHANGELOG.md or docs. The motivation (intermittent 5xx from GitHub) is real, but this also silently suppresses legitimately rotted links. Consider whether a tighter pattern (e.g. anchored to recent PRs) would be sufficient. See inline comment.

3. SHAKAPACKER_DEV_SERVER_PORT is set in production-like mode

The comment in apply_base_port_env (lines 958-963) explains this is intentional for tooling consistency. Flagging it explicitly: any tooling or post-start hooks that read SHAKAPACKER_DEV_SERVER_PORT to infer whether webpack-dev-server is active will be misled after bin/dev prod in a base-port workspace. The existing comment is good — just confirming this trade-off is understood. See inline comment.

4. killable_ports silently changes return arity between modes

Minor: in base-port mode, killable_ports returns three ports (rails, webpack, renderer); without a base port it returns two. This is correct and intentional but a comment explaining why the renderer is included would help future maintainers. See inline comment.


Missing test coverage

  • file_manager_spec.rb stubs working_directory_for_pid directly so the actual lsof invocation is never exercised. A test verifying the method returns nil when lsof output is empty would document the graceful-degradation contract.
  • warn_if_legacy_renderer_url_env_used is tested in development mode but the production-like mode code path (which also calls it) is only indirectly covered.

end

def working_directory_for_pid(pid)
path_line = `lsof -a -p #{pid} -d cwd -Fn 2>/dev/null`.lines.find { |line| line.start_with?("n") }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lsof call here uses a backtick with 2>/dev/null to swallow errors, but server_manager.rb:140-143 wraps its own lsof call in Open3.capture2 with a rescue Errno::ENOENT, StandardError specifically to handle lsof not being installed.

The fallback is safe (empty string → path_line is nil → method returns nil → PID file is preserved), but the inconsistency is worth aligning. Either approach is fine, but if you want parity:

Suggested change
path_line = `lsof -a -p #{pid} -d cwd -Fn 2>/dev/null`.lines.find { |line| line.start_with?("n") }
def working_directory_for_pid(pid)
stdout, _status = Open3.capture2("lsof", "-a", "-p", pid.to_s, "-d", "cwd", "-Fn",
err: File::NULL)
path_line = stdout.lines.find { |line| line.start_with?("n") }
path = path_line&.delete_prefix("n")&.strip
return nil if path.nil? || path.empty?
path
rescue Errno::ENOENT, StandardError
nil
end

Comment thread .lychee.toml
# return 5xx responses to lychee in CI, even when the pages are live.
'^https://github\.com/shakacode/react_on_rails/compare/',
'^https://github\.com/shakacode/react_on_rails/pull/2280$', # Intermittent 502 from GitHub PR page in CI
'^https://github\.com/shakacode/react_on_rails/pull/[0-9]+$', # Intermittent 5xx from GitHub PR pages in CI
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wildcard means no PR link anywhere in the repo (CHANGELOG entries, docs, etc.) will ever be checked for validity — a rotted pull/1234 reference in CHANGELOG.md or docs would go undetected forever. The previous line only excluded a single known-flaky PR.

The intermittent 5xx issue is real, but consider whether --max-retries / --retry-wait-time in lychee config would reduce false positives without permanently blinding the checker to dead links. If the blanket exclusion is the right call, at minimum add a comment noting the trade-off so future maintainers don't narrow it back without understanding why it was widened.

# a subsequent `bin/dev` in the same shell sees the base-port-derived
# value rather than a stale explicit one, and developers inspecting
# their env after `bin/dev prod` see the full derived block.
def apply_base_port_env(selected)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The justification ("tooling consistency" for a subsequent bin/dev in the same shell) is reasonable. Worth making the trade-off explicit one more sentence: any script or hook that reads SHAKAPACKER_DEV_SERVER_PORT to decide whether a webpack-dev-server is running will be misled after bin/dev prod in a base-port workspace (the var is set but no server is listening). If that becomes a problem, the mitigation would be clearing SHAKAPACKER_DEV_SERVER_PORT at the end of production-like mode rather than leaving it set — but that's a future concern, not a blocker.

base = PortSelector.base_port_hash
return [3000, 3001] unless base

[base[:rails], base[:webpack], base[:renderer]]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return arity changes silently between modes (3 ports vs 2), and the renderer port being in the kill list is easy to miss. A brief comment would help:

Suggested change
[base[:rails], base[:webpack], base[:renderer]]
# Include all three derived ports so `bin/dev kill` in a Pro worktree
# also stops the node renderer process, not just Rails and webpack.
[base[:rails], base[:webpack], base[:renderer]]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex PRs created from codex-named branches full-ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant