fix(cli): restore read-side common-port fallback for migrate info/doctor - #3149
Conversation
#2879 documented that read-only commands keep the legacy common-port fallback (8080, 60000, 3000, 8500), but runMigration() gated every migrate subcommand behind requireProjectConfig=true — so 'wheels migrate info' and 'wheels migrate doctor' refused a server running on 8080, the first entry of their own documented fallback list, with Wheels.ServerNotRunning. The schema-mutating actions (latest/up/down) keep the strict project-bound gate from #2878. info/doctor now pass requireProjectConfig=false, and because a fallback attach can reach a sibling project's server (reporting the WRONG app's migration state), they print a yellow notice naming the attached port whenever the fallback was used, with the lucee.json / PORT pin hint. New call-site gating specs in ServerDetectionSpec capture the exact requireProjectConfig flag runMigration() hands $requireRunningServer() per action (info/doctor false; latest/up/down true) plus an end-to-end no-config case asserting the project-bound refusal is gone. Fixes #3080 Signed-off-by: Peter Amiri <peter@alurium.com>
…e info/doctor Replace the migrations-guide aside paragraph that deferred to #3080 with the resolved contract: info/doctor keep the common-port fallback like the other read-side commands, and print a yellow notice when the fallback (not lucee.json/.env) chose the port. Adds the changelog.d fragment for the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer
TL;DR: This PR restores the read-side common-port fallback for wheels migrate info / wheels migrate doctor, resolving the code-vs-docs contradiction #2879 shipped (#3080). It hoists the existing mutatingAction read/write split in runMigration() to also decide the requireProjectConfig gate, adds a yellow transparency notice when the fallback attach is used, pins the call-site wiring with MockBox call-log specs, and updates the migrations guide + changelog fragment. The change is correct, consistent with the rest of the read-side command family, and well-tested. Verdict: comment — no blocking findings, two minor non-blocking nits below.
Correctness — verified, no blocking findings
- The
mutatingActionlist (latest,up,down) is exhaustive forrunMigration(): themigrate()dispatcher (cli/lucli/Module.cfc:597-637) routes onlylatest/up/down/info/doctorintorunMigration();forget/pretendgo throughrunForgetOrPretend()andrename-system-tablesthroughrunRenameSystemTables(), both of which keep their ownrequireProjectConfig = truegates (lines 4116, 4167).db reset'srunMigration("latest")(line 4295) lands in the strict branch. Unknown actions throw before reaching the gate. - The write-side family is untouched: after this PR,
requireProjectConfig = truecall sites are exactlyreload()(818),generateAdmin()(3642), therunMigration()mutating branch (3994),runForgetOrPretend()(4116),runRenameSystemTables()(4167), andrunSeed()(4237) — matching the PR description's claim verbatim. - The relaxed branch matches the established read-side defaults:
routes(),console(),dbStatus(),dbVersion()all call$requireRunningServer()with the defaultrequireProjectConfig = false.
Nit (non-blocking) — cli/lucli/Module.cfc:4006-4011, the fallback notice wording. detectServerPort(requireProjectConfig = true) returns false both when no port is configured and when a port is configured in lucee.json/.env but nothing is listening on it (detectServerPort() only returns the configured port when isPortOpen(config.port) passes — Module.cfc:6630). In that second case — configured port dead, sibling server alive on 8080 — the notice still fires, which is correct (that is exactly the dangerous attach), but its text claims "no project-bound port in lucee.json / .env" when one exists, and the hint "set 'port' in lucee.json" tells the user to do something they have already done. Suggested wording accurate for both cases: "Attached to localhost:N via the common-port fallback (no responding project-bound port from lucee.json / .env)." Optional follow-up (not needed for this PR): having detectServerPort() report how it resolved (config vs fallback) would avoid the second probe and make the notice condition exact.
Tests
- Good coverage shape: the call-log specs (
cli/lucli/tests/specs/services/ServerDetectionSpec.cfc:154-172) pin the exact flag each migrate action passes to$requireRunningServer()—info/doctor→false, withlatest/up/down→trueas regression pins — and the end-to-end spec (lines 174-192) exercises the real guard in a no-config project. The red-first evidence in the PR body is consistent with the spec set. - The
/testbox/system/stubsmkdirs workaround has real prior art (vendor/wheels/tests/specs/controller/channelSpec.cfc:253), and thecatch-block state is correctly carried via a struct field (state.sawProjectBoundRefusal) per Cross-Engine Invariant 11, not a barelocalassignment.
Nit (non-blocking) — ServerDetectionSpec.cfc:187. Distinguishing the two Wheels.ServerNotRunning variants by !findNoCase("8080", e.message) couples the spec to a port number appearing in the read-side message text (Module.cfc:6700). Correct today, but if the fallback list or phrasing ever changes, the test can silently stop discriminating. Matching the project-bound message's distinctive phrase directly — findNoCase("for this project", e.message) — would be more robust and self-describing. The call-log specs are the primary pins, so this is purely a robustness improvement for the end-to-end check.
Docs
- The
web/sites/guides/.../basics/migrations.mdxaside now states the resolved contract, including the notice behavior — it matches the implemented code paths. - Changelog fragment
changelog.d/3080-migrate-info-doctor-read-side-fallback.fixed.mdis present with the correct.fixed.mdtype; no directCHANGELOG.mdedit. - No stale
.ai/wheels/references:shared-dev-databases.mddoesn't document the port gate, so nothing there needs updating.
Commits
- Both commits (
2c3c6b7d8fix(cli),32f058e8cdocs(web/guides)) conform to commitlint — valid types, headers well under 100 chars, not ALL-CAPS — and both carrySigned-off-by: Peter Amiri <peter@alurium.com>matching the author email (DCO satisfied). The PR title is itself a valid conventional-commit header for the squash merge.
Summary
wheels migrate infoandwheels migrate doctorhonor the read-side common-port fallback again. #2879 shipped a contradiction inside one PR: its changelog entry and migrations-guide aside promised that read-only commands keep the legacy port probe (8080, 60000, 3000, 8500) when nolucee.json/.envport is configured — butcli.lucli.Module::runMigration()hardcodedrequireProjectConfig = truefor every migrate subcommand, so the two read-only inspectors refused a server on 8080 (the first entry of their own documented fallback list) withWheels.ServerNotRunning. The issue's repro (wheels new scratch && wheels start, removelucee.json,wheels migrate info) reproduced exactly:detectServerPort()step 4 ("Try common ports (read-side only)") was dead code for every migrate subcommand.What changed
cli/lucli/Module.cfc::runMigration()— the read/write split (mutatingAction, already used to pick GET vsmakeBridgePost) is hoisted to the top and now also decides the server-identity gate:latest/up/downkeep the strictrequireProjectConfig = truegate from Wheels migration script runs against wrong Lucee instance #2878;info/doctorpassfalse, matching the other read-side commands (wheels info,routes,console,dbStatus,dbVersion).detectServerPort(requireProjectConfig=true)resolves nothing),info/doctorprint a yellow notice naming the attached port plus thelucee.json/PORThint to pin the project. Config-resolved attaches print nothing new.web/sites/guides/.../basics/migrations.mdx— the aside paragraph that deferred to cli: wheels migrate info/doctor refuse the common-port fallback that #2879's own changelog and guide promise #3080 ("there is no common-port fallback for them either… tracks whether the read-side fallback comes back") is replaced with the resolved contract, including the notice behavior.changelog.d/3080-migrate-info-doctor-read-side-fallback.fixed.md— fragment.Write-side reconciliation (
forget/pretend),seed,rename-system-tables,reload, andgenerate adminare untouched — they keep their strict gates.Test evidence (TDD red-first, lucee7 Docker harness)
New specs in
cli/lucli/tests/specs/services/ServerDetectionSpec.cfc:capturedRequireProjectConfig(action)mocks$requireRunningServer()and asserts the exact flagrunMigration()passes:info→ false,doctor→ false, and regression pinslatest/up/down→ true.migrate infoin a project with nolucee.json/.envmust never throw the project-bound refusal (environment-tolerant: nothing on a common port → read-sideServerNotRunningnaming the probed ports; something listening → proceeds past the guard).ServerDetectionSpecThe 3 remaining full-suite failures are pre-existing container-environment artifacts, identical before and after, in bundles this PR doesn't touch:
SshClientSpec+SshPoolSpec(tools/deploy-sshd-up.sh→docker: command not foundinside the test container) andServerCommandsSpec::reload endpoint responds(needs a live project server).Design note
The wheels-bot cross-framework research on #3080 recommended the opposite direction (remove the fallback promise; add an explicit
--port/--urlselector). This PR takes the issue's direction (1) — code matches #2879's stated contract — because the read-side family (wheels info,routes,console,dbStatus,dbVersion) already keeps the fallback today, so gating onlymigrate info/doctorwas the inconsistency; the yellow fallback notice mitigates the wrong-project-state hazard the research flagged. An explicit--portselector remains a compatible follow-up if wanted.Fixes #3080
🤖 Generated with Claude Code