Skip to content

fix(cli): restore read-side common-port fallback for migrate info/doctor - #3149

Merged
bpamiri merged 2 commits into
developfrom
peter/issue-3080-migrate-port-fallback
Jun 12, 2026
Merged

fix(cli): restore read-side common-port fallback for migrate info/doctor#3149
bpamiri merged 2 commits into
developfrom
peter/issue-3080-migrate-port-fallback

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

wheels migrate info and wheels migrate doctor honor 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 no lucee.json / .env port is configured — but cli.lucli.Module::runMigration() hardcoded requireProjectConfig = true for every migrate subcommand, so the two read-only inspectors refused a server on 8080 (the first entry of their own documented fallback list) with Wheels.ServerNotRunning. The issue's repro (wheels new scratch && wheels start, remove lucee.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 vs makeBridgePost) is hoisted to the top and now also decides the server-identity gate: latest/up/down keep the strict requireProjectConfig = true gate from Wheels migration script runs against wrong Lucee instance #2878; info/doctor pass false, matching the other read-side commands (wheels info, routes, console, dbStatus, dbVersion).
  • Wrong-project transparency — the issue's "point in favor of (2)" (and the bot's cross-framework research) flag the real hazard: a fallback attach can reach a sibling project's server and report the wrong app's migration state. So when the attach came from the fallback (i.e. detectServerPort(requireProjectConfig=true) resolves nothing), info/doctor print a yellow notice naming the attached port plus the lucee.json / PORT hint 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, and generate admin are untouched — they keep their strict gates.

Test evidence (TDD red-first, lucee7 Docker harness)

New specs in cli/lucli/tests/specs/services/ServerDetectionSpec.cfc:

  • Call-site wiring via MockBox call log — capturedRequireProjectConfig(action) mocks $requireRunningServer() and asserts the exact flag runMigration() passes: info → false, doctor → false, and regression pins latest/up/down → true.
  • End-to-end no-config case — migrate info in a project with no lucee.json/.env must never throw the project-bound refusal (environment-tolerant: nothing on a common port → read-side ServerNotRunning naming the probed ports; something listening → proceeds past the guard).
Run ServerDetectionSpec Full CLI suite
Red (spec only, pre-fix) 8 pass / 3 fail (the 3 new #3080 specs, exactly) 926 pass / 2 fail / 7 error
Green (post-fix) 11 pass / 0 fail / 0 error 932 pass / 1 fail / 2 error

The 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.shdocker: command not found inside the test container) and ServerCommandsSpec::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/--url selector). 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 only migrate info/doctor was the inconsistency; the yellow fallback notice mitigates the wrong-project-state hazard the research flagged. An explicit --port selector remains a compatible follow-up if wanted.

Fixes #3080

🤖 Generated with Claude Code

bpamiri and others added 2 commits June 12, 2026 11:14
#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>
@github-actions github-actions Bot added the docs label Jun 12, 2026

@wheels-bot wheels-bot Bot left a comment

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.

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 mutatingAction list (latest,up,down) is exhaustive for runMigration(): the migrate() dispatcher (cli/lucli/Module.cfc:597-637) routes only latest/up/down/info/doctor into runMigration(); forget/pretend go through runForgetOrPretend() and rename-system-tables through runRenameSystemTables(), both of which keep their own requireProjectConfig = true gates (lines 4116, 4167). db reset's runMigration("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 = true call sites are exactly reload() (818), generateAdmin() (3642), the runMigration() mutating branch (3994), runForgetOrPretend() (4116), runRenameSystemTables() (4167), and runSeed() (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 default requireProjectConfig = 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/doctorfalse, with latest/up/downtrue as 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/stubs mkdirs workaround has real prior art (vendor/wheels/tests/specs/controller/channelSpec.cfc:253), and the catch-block state is correctly carried via a struct field (state.sawProjectBoundRefusal) per Cross-Engine Invariant 11, not a bare local assignment.

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.mdx aside 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.md is present with the correct .fixed.md type; no direct CHANGELOG.md edit.
  • No stale .ai/wheels/ references: shared-dev-databases.md doesn't document the port gate, so nothing there needs updating.

Commits

  • Both commits (2c3c6b7d8 fix(cli), 32f058e8c docs(web/guides)) conform to commitlint — valid types, headers well under 100 chars, not ALL-CAPS — and both carry Signed-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.

@bpamiri
bpamiri enabled auto-merge (squash) June 12, 2026 18:54
@bpamiri
bpamiri merged commit 66a9878 into develop Jun 12, 2026
15 checks passed
@bpamiri
bpamiri deleted the peter/issue-3080-migrate-port-fallback branch June 12, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli: wheels migrate info/doctor refuse the common-port fallback that #2879's own changelog and guide promise

1 participant