diff --git a/CHANGELOG.md b/CHANGELOG.md index f61ef5b90a..4b8fab34b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,22 +18,9 @@ All historical references to "CFWheels" in this changelog have been preserved fo ---- -# [Unreleased] +# [4.0.3](https://github.com/wheels-dev/wheels/releases/tag/v4.0.3) => 2026-06-09 -### Changed - -- CLI user-error paths now exit non-zero instead of silently returning success. Several commands printed a red error message and then `return ""`, which LuCLI maps to exit code 0 — so a typo'd subcommand or a failed migration looked like success to CI pipelines, deploy scripts, and pre-commit hooks. The following now throw a typed exception (`Wheels.InvalidArguments` for unknown input; the original error is re-thrown for runtime failures), which LuCLI maps to a non-zero exit while still printing the same friendly diagnostic first: `wheels generate `, `wheels create `, `wheels migrate `, `wheels db `, a failed `wheels migrate latest|up|down|info|doctor|rename-system-tables` (re-throws the underlying `MigrationError` instead of swallowing it), and `wheels routes` when the server returns an unparseable or unsuccessful response (`Wheels.RoutesFailed`). Help/no-args paths (`wheels generate`, `wheels db` with no subcommand) and non-error states (`wheels routes` with zero configured routes, `wheels db reset` without `--force`) are unchanged and still exit 0. Over MCP these surface as proper tool errors instead of empty results. **Scripts that previously relied on these error paths exiting 0 will now see a non-zero exit — that is the intended fix.** - -### Fixed - -- CLI-audit tail polish (follow-up to the #2882–#2886 audit sweep): `wheels info` now renders the framework-version line again — it read the long-gone `vendor/wheels/events/onapplicationstart/settings.cfm` path and silently printed nothing, so it now reads the authoritative `vendor/wheels/wheels.json` manifest by absolute path (no `wheels` mapping needed) and applies the same structural placeholder guard as `wheels.BuildInfo` (an unstamped dev checkout reports `0.0.0-dev` rather than leaking the raw `@build.version@` token). Two internal `$`-prefixed test helpers (`$normalizeTestFilter`, `$resolveAppTestDataSource`) were leaking into the MCP `tools/list` as callable tools — they are now listed in `mcpHiddenTools()` (kept `public` so `TestCommandSpec` can still unit-test them directly; LuCLI matches hidden names case-insensitively). `wheels --help` now lists the `create app` command (it was a working command + MCP tool but absent from the banner) and its `notes` line no longer advertises a `HACK` default the parser doesn't use (the default stays `TODO,FIXME,OPTIMIZE`; `--annotations` customizes it). `wheels reload` now honors an explicit `--password=` override (parity with `wheels console`; auto-detect from `.env`/config remains the default). The interactive console `/help` now lists the `/datasource` and `/q` aliases it already accepts. The `wheels mcp` instructions and the deprecated `/wheels/mcp` endpoint's deprecation notice now point to the live MCP integration guide instead of a doc path (`mcp-configuration-guide.md`) that never existed. Docs: the `wheels test` flag table documents the real `--directory` alias for `--filter`, and the agent `CLAUDE.md` packages block lists the real `wheels packages registry info` verb. -- `wheels generate scaffold` and `wheels generate api-resource` now honor `--hasOne`. The flag worked for `wheels generate model` and is documented for scaffold/api-resource (`code-generation.mdx`), but `Scaffold.cfc`'s `generateScaffold()` / `generateApiResource()` neither declared a `hasOne` parameter nor forwarded one to `CodeGen.generateModel()` (which already accepts it and renders the `{{hasOneRelationships}}` placeholder), so it was silently dropped. Both signatures now accept `hasOne` and pass it through, and the Module.cfc scaffold/api-resource handlers forward `arrayToList(parsed.hasOne)` (mirroring how `belongsTo`/`hasMany` are already threaded). `wheels generate scaffold Employee name:string --hasOne=Profile` now emits `hasOne('Profile');` in the model's `config()`; same for api-resource. Covered by new `ScaffoldSpec` cases for both paths. -- `wheels reload` and `wheels generate admin` now refuse to attach to a server that isn't bound to the current project, closing the same #2878 gap for two more server-dependent commands that #2879 fixed for the write-side migrators. Both reached `cli.lucli.Module::$requireRunningServer()` without the `requireProjectConfig` flag, so in a project with no `lucee.json` / `.env` port they still fell back to the hardcoded common-port probe (`[8080, 60000, 3000, 8500]`) and could silently attach to a sibling app: `reload` would reset the wrong app's state, and `generate admin` would introspect the wrong schema and scaffold its controller/views into the current project from a sibling's model — wrong-schema output written into the right project. Both now pass `requireProjectConfig = true`; with no project-bound port they throw `Wheels.ServerNotRunning` with a "set 'port' in lucee.json (or PORT in .env), then start with: wheels start" diagnostic instead of proceeding. `generate admin` is gated (rather than left on the read-side fallback alongside `info` / `routes`) precisely because it both reads a schema and writes files into cwd, so a wrong-server attach is a correctness bug, not just a wrong read. Covered by new server-free specs in `cli/lucli/tests/specs/services/ServerDetectionSpec.cfc` that drive `reload()` and `generateAdmin()` in a no-config project and assert the guard refuses to attach (#2878) -- `wheels migrate` (and its sibling write-side runners — `seed`, `migrate forget` / `pretend`, `migrate rename-system-tables`) refuse to attach to a server that isn't bound to the current project. `cli.lucli.Module::detectServerPort()` previously fell back to a hardcoded common-port probe (`[8080, 60000, 3000, 8500]`) after exhausting `lucee.json` and `.env`, so a freshly-scaffolded project with no port config could silently attach to a sibling app's open Lucee instance and run its migrations against the wrong database (the #2876 / #2878 repro: `wheels new app_a` + `wheels start` in `app_a`, then `wheels migrate latest` in `app_b` ran `app_b`'s migrations against `app_a`'s PostgreSQL). `detectServerPort()` now accepts a `requireProjectConfig` flag that skips the common-port fallback, and `$requireRunningServer()` threads it through to every write-side caller. When the flag is set and no project-bound port resolves, the CLI throws `Wheels.ServerNotRunning` with a clear "set 'port' in lucee.json (or PORT in .env), then start with: wheels start" diagnostic instead of proceeding. Read-side commands (`info`, `routes`, `console`, `dbStatus`, `dbVersion`) keep the legacy fallback — they don't mutate anything, and removing it would regress the no-config development experience. Covered by new server-free specs in `cli/lucli/tests/specs/services/ServerDetectionSpec.cfc` that simulate a sibling app on an ephemeral port and assert the write-side guard refuses to attach (#2878) -- `wheels migrate latest` no longer crashes on PostgreSQL (and CockroachDB) when a migration emits an inline foreign-key constraint — e.g. anything `wheels generate scaffold ... --belongsTo=author` produces. `wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator` was missing the public `addForeignKeyOptions(sql, options)` method that every other adapter implements (`MySQLMigrator`, `SQLiteMigrator`, `MicrosoftSQLServerMigrator`, `OracleMigrator`); `Abstract.createTable()` builds the inline FK clause via `foreignKeys[i].toForeignKeySQL()` → `ForeignKeyDefinition.cfc` → `adapter.addForeignKeyOptions(...)`, so every PostgreSQL FK column threw `Component [wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator] has no function with name [addForeignKeyOptions]` and aborted the migration. The new implementation mirrors the MySQL signature (`FOREIGN KEY (col) REFERENCES tbl (refCol)`), which PostgreSQL accepts verbatim, and `CockroachDBMigrator` (which extends `PostgreSQLMigrator`) inherits the fix automatically. The reporter's "works on Windows" observation lined up with the `wheels new` SQLite default — only PostgreSQL/CockroachDB targets ever hit the missing method (#2876) -- CLI services in `Module.cfc` now instantiate via the module-relative path (`new services.X()`) instead of the absolute FQN (`new cli.lucli.services.X()`), so `wheels new` and the other subcommands resolve their service classes when running from the installed distribution. The module tarball is built with `tar -C cli/lucli .`, which flattens the module root so services live at `/services/` with no `cli/lucli/` tree and no `cli.lucli` mapping — the absolute form only resolved against the source-tree layout. That split is why the `fast-test` job (which runs from source, where both forms resolve) stayed green while the snapshot smoke test — which installs the built tarball and runs `wheels new` — failed on every `develop` push since #2861 with `could not find component or class with name [cli.lucli.services.ArgSpec]`. All 8 absolute references (7× `ArgSpec`, 1× the latent `TestRunner` call) are converted to the relative form the 17 sibling services already use; the `ArgSpec` docblock example is updated to match so it cannot re-seed the pattern (#2873) -- Oracle `DROP TABLE` / `DROP VIEW` in the migrator now work on Oracle 19c/21c. `wheels.databaseAdapters.Oracle.OracleMigrator::dropTable()` emitted `DROP TABLE IF EXISTS CASCADE CONSTRAINTS` and `dropView()` inherited `DROP VIEW IF EXISTS` from `Abstract`, but Oracle only added the `IF EXISTS` DDL modifier in 23c — on 19c/21c both are a hard parse error (ORA-00933). Because the `remove-table` migration template re-throws on error, `migrate down`, rollbacks, `force`-create, and migrator test re-runs failed outright on pre-23c Oracle. Both helpers now emit the version-agnostic Oracle PL/SQL idiom — `BEGIN EXECUTE IMMEDIATE 'DROP TABLE CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;` — which runs the bare DROP and swallows ORA-00942 ("table or view does not exist"), preserving "drop if exists" semantics on every supported Oracle version with no version detection. `$execute` (`vendor/wheels/migrator/Base.cfc`) never splits on `;` and deliberately omits the trailing-semicolon append for Oracle, so the anonymous block reaches the driver intact. Framework-side counterpart to the demo-app test-populate fix in #2864 (#2869) -- `application.wheels.protectedControllerMethods` is now populated at application start from the public method surface of `wheels.Global` plus the `wheels.controller.*` and `wheels.view.*` mixin components, so framework helpers like `env()`, `model()`, `findAll()`, `redirectTo()`, and `linkTo()` can no longer be invoked as controller actions from a URL. The list was previously initialized to an empty string (the orphaned `local.allowedGlobalMethods = "get,set,mapper"` line in `onapplicationstart.cfc` pointed to the intent but never wired it up), so `$callAction()`'s allow-list check was a no-op. Any unauthenticated `GET //env` request reached the global `env()` helper directly and raised `"The parameter [name] to function [env] is required but was not passed in."` as a 500; other helper names dispatched into unintended code paths. Derived from `getMetaData().functions` on each source component (excluding `$`-prefixed internal methods, which are already gated separately), so the list stays in sync with the framework's mixin surface automatically. Reaching one of these names now throws `Wheels.ActionNotAllowed` and falls through to the missing-action / 404 path, matching every other non-existent action. **Migration note:** applications that defined controller actions with the same name as a public framework helper (e.g. `env`, `model`, `redirectTo`) will need to rename those actions — they now return 404 rather than dispatching, since the protection gate at `processing.cfc:132` fires before the `StructKeyExists(this, action)` lookup that would otherwise reach a same-named user action (#2844) +> **Wheels 4.0.3** — third patch on the 4.0 line. Completes the CLI argument-parsing overhaul (`ArgSpec` consumes LuCLI's structured arguments in every command — `--no-*` negations and named-only flags now reach their parsers, and user-error paths exit non-zero) and lands the fixes from a full 24-command CLI audit; write-side commands (`migrate`, `seed`, `reload`, `generate admin`) now refuse to attach to a sibling project's server instead of running against the wrong database; PostgreSQL/CockroachDB foreign-key migrations and pre-23c Oracle `DROP TABLE`/`DROP VIEW` work again; framework helpers can no longer be invoked as controller actions from a URL; auto-derived model properties preserve database column casing; and scaffolded apps keep their reload password out of source control (`WHEELS_RELOAD_PASSWORD` in `.env`). ~45 PRs since the 4.0.2 GA (2026-05-27). ### Added @@ -46,6 +33,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Changed +- CLI user-error paths now exit non-zero instead of silently returning success. Several commands printed a red error message and then `return ""`, which LuCLI maps to exit code 0 — so a typo'd subcommand or a failed migration looked like success to CI pipelines, deploy scripts, and pre-commit hooks. The following now throw a typed exception (`Wheels.InvalidArguments` for unknown input; the original error is re-thrown for runtime failures), which LuCLI maps to a non-zero exit while still printing the same friendly diagnostic first: `wheels generate `, `wheels create `, `wheels migrate `, `wheels db `, a failed `wheels migrate latest|up|down|info|doctor|rename-system-tables` (re-throws the underlying `MigrationError` instead of swallowing it), and `wheels routes` when the server returns an unparseable or unsuccessful response (`Wheels.RoutesFailed`). Help/no-args paths (`wheels generate`, `wheels db` with no subcommand) and non-error states (`wheels routes` with zero configured routes, `wheels db reset` without `--force`) are unchanged and still exit 0. Over MCP these surface as proper tool errors instead of empty results. **Scripts that previously relied on these error paths exiting 0 will now see a non-zero exit — that is the intended fix.** - The `console` and `test` CLI subcommands now consume LuCLI's structured `argCollection` directly via `cli.lucli.services.ArgSpec` (`parseConsoleArgs` / `parseTestArgs` calling `.parse(structuredArgs(arguments))`), continuing the #2861 migration (whose `ArgSpec` foundation shipped in #2862) past the eight leaf commands. `console` reads `--password=`; `test` reads `--filter` (and its documented `--directory` alias), `--reporter`, `--db` (tracked as explicit so the runner distinguishes an implicit default from a chosen one), the `--verbose` / `--ci` / `--core` flags, `--no-test-db` (test-db=false), a bare positional filter, and the `-v` shorthand (which LuCLI delivers as a positional). Both also fix the latent `arg1`-gate the round trip masked: named-only invocations like `wheels console --password=x` and `wheels test --core` (no positional) now take effect instead of silently running with defaults. **One deliberate behavioral delta per command:** the space-separated option forms (`wheels console --password secret`, `wheels test --filter models`) are dropped for the `--key=value` forms — LuCLI delivers a space-separated value as a bare flag plus a separate positional, never a named value. Everything else is preserved: `test`'s APP-vs-`--core` mode default, `$normalizeTestFilter` short-name normalization, and the full `runTests(...)` argument set; `console`'s reload-password auto-detection when none is supplied. Covered by 13 new server-free specs in `cli/lucli/tests/specs/commands/CommandArgParsingSpec.cfc` (via `ModuleArgvProbe`). This leaves the dispatchers (`generate`, `create`, `db`, `browser`), the parser-delegating `deploy` / `packages` / `migrate`, and the LuCLI-passthrough `start` on the `getArgs()` shim, which is removed once those are converted (#2861) - Eight leaf CLI subcommands — `new`, `seed`, `notes`, `analyze`, `doctor`, `stats`, `upgrade`, and `destroy` — now consume LuCLI's structured `argCollection` directly via `cli.lucli.services.ArgSpec` (`.parse(structuredArgs(arguments))`) instead of flattening it back to argv and re-parsing with a hand-rolled token loop (the round trip tracked in #2861, whose `ArgSpec` foundation shipped in #2862). Beyond removing the per-command parsing duplication, this fixes a latent bug the round trip masked: the legacy `getArgs()` only rebuilt argv when a positional `arg1` was present, so **named-only** invocations were silently dropped one layer in — `wheels seed --environment=production`, `wheels doctor --verbose`, `wheels stats --verbose`, and `wheels notes --annotations=...` all ran with defaults regardless of what the user passed. Consuming the structured map directly means the named keys (and `--no-X` negations) survive. **One deliberate behavioral delta:** `wheels new` with options but no app name (e.g. `wheels new --no-sqlite`) now errors with the #2214 `Wheels.InvalidArguments` "app name required" exception instead of falling through to the usage guide — previously the `arg1`-gate dropped the named-only args, leaving an empty arg list that took the usage branch. Everything else is preserved: each command keeps its usage branches and the #2214 throw, `destroy`'s ` ` / ` ` smart reorder (now gap-tolerant, so `--force` may appear before or after the positionals), `upgrade`'s `check`-gate and `--dry-run` / `--to` "did you mean" nudge, and `doctor` / `stats`'s `-v` shorthand (which LuCLI delivers as a positional, not a flag). A new private `structuredArgs()` / `argvToCollection()` helper pair sources the collection — preferring LuCLI's live handoff and reconstructing it from the instance-level `__arguments` fallback for internal delegation (e.g. `create` → `new`) and unit tests. The migrated parse logic is covered by server-free specs in `cli/lucli/tests/specs/commands/CommandArgParsingSpec.cfc` (via `ModuleArgvProbe`). `getArgs()` / `argsFromCollection()` remain as the deprecated shim for the not-yet-migrated commands — the dispatchers (`generate`, `create`, `db`, `browser`), the parser-delegating `deploy` / `packages`, `migrate`, and the space-separated-flag `test` / `console` — and the shim is removed once those are converted (#2861) - The final eight CLI subcommands — the dispatchers `generate`, `create`, `db`, `browser`; the parser-delegating `deploy`, `packages`, `migrate`; and the LuCLI-passthrough `start` — now source LuCLI's structured `argCollection` through `structuredArgs(arguments)` and reconstruct argv once via the new `cli.lucli.services.ArgSpec.toArgv()` passthrough, instead of the per-command `getArgs()` / `argsFromCollection()` round trip. With every call site converted, the `getArgs()` / `argsFromCollection()` shim is **removed** from `Module.cfc`, completing the #2861 migration (whose `ArgSpec` foundation shipped in #2862 and whose leaf/`console`/`test` batches landed in #2872 and #2874). These commands forward to downstream consumers that take a flat argv array (the deep `generate` sub-handlers, the unit-tested `DeployArgsParser` and packages parsers, `runForgetOrPretend`, and LuCLI's own `server start`), so the maintainer-chosen passthrough keeps those parsers — and their dedicated test suites — unchanged rather than rewriting them. `toArgv()` is non-lossy: it re-emits `--no-X` for `key=false` (the #2856 fix, now centralized in one tested place) so negations like `--no-routes` / `--no-migration` survive. The migration is behavior-preserving — `structuredArgs()` and the deleted `getArgs()` read the `__arguments` fallback identically, and `toArgv()` copies `argsFromCollection()`'s emit logic exactly — and incidentally retires the same `arg1`-gate latent bug the leaf-command migration did: `wheels start --force` with no positional previously fell through to an empty arg list. The #2855 `--no-sqlite` negation is now pinned end-to-end by a new assertion in `tools/test-onboarding.sh` (real CLI → LuCLI → ArgSpec → scaffolder: no `db/*.sqlite` files, `lucee.json` `configuration.datasources == {}`), closing the test-robustness gap the #2856 unit test left open. `argsFromCollection`'s unit coverage moved to `ArgSpec.toArgv` specs; the `getArgs` arg1-gate regression spec was retired with the function. Closes #2861 @@ -55,6 +43,15 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- CLI-audit tail polish (follow-up to the #2882–#2886 audit sweep): `wheels info` now renders the framework-version line again — it read the long-gone `vendor/wheels/events/onapplicationstart/settings.cfm` path and silently printed nothing, so it now reads the authoritative `vendor/wheels/wheels.json` manifest by absolute path (no `wheels` mapping needed) and applies the same structural placeholder guard as `wheels.BuildInfo` (an unstamped dev checkout reports `0.0.0-dev` rather than leaking the raw `@build.version@` token). Two internal `$`-prefixed test helpers (`$normalizeTestFilter`, `$resolveAppTestDataSource`) were leaking into the MCP `tools/list` as callable tools — they are now listed in `mcpHiddenTools()` (kept `public` so `TestCommandSpec` can still unit-test them directly; LuCLI matches hidden names case-insensitively). `wheels --help` now lists the `create app` command (it was a working command + MCP tool but absent from the banner) and its `notes` line no longer advertises a `HACK` default the parser doesn't use (the default stays `TODO,FIXME,OPTIMIZE`; `--annotations` customizes it). `wheels reload` now honors an explicit `--password=` override (parity with `wheels console`; auto-detect from `.env`/config remains the default). The interactive console `/help` now lists the `/datasource` and `/q` aliases it already accepts. The `wheels mcp` instructions and the deprecated `/wheels/mcp` endpoint's deprecation notice now point to the live MCP integration guide instead of a doc path (`mcp-configuration-guide.md`) that never existed. Docs: the `wheels test` flag table documents the real `--directory` alias for `--filter`, and the agent `CLAUDE.md` packages block lists the real `wheels packages registry info` verb. +- `wheels generate scaffold` and `wheels generate api-resource` now honor `--hasOne`. The flag worked for `wheels generate model` and is documented for scaffold/api-resource (`code-generation.mdx`), but `Scaffold.cfc`'s `generateScaffold()` / `generateApiResource()` neither declared a `hasOne` parameter nor forwarded one to `CodeGen.generateModel()` (which already accepts it and renders the `{{hasOneRelationships}}` placeholder), so it was silently dropped. Both signatures now accept `hasOne` and pass it through, and the Module.cfc scaffold/api-resource handlers forward `arrayToList(parsed.hasOne)` (mirroring how `belongsTo`/`hasMany` are already threaded). `wheels generate scaffold Employee name:string --hasOne=Profile` now emits `hasOne('Profile');` in the model's `config()`; same for api-resource. Covered by new `ScaffoldSpec` cases for both paths. +- A full audit of the CLI's 24 commands repaired the broken or mis-documented paths it surfaced (#2882, #2883, #2884, #2885): `wheels g` works as a true `generate` alias again; `wheels console` accepts `--password=`; `wheels generate api-resource` registers its resource route; `wheels validate` strips CFML comments before source-scanning so commented-out code can't satisfy (or trip) a check; `wheels migrate --help` documents the real subcommand surface; `wheels test` exit-code handling and `wheels deploy` flag handling were corrected alongside release-channel and guide docs; the generators emit `enum()` definitions again, warn on view-generation failures instead of continuing silently, and the duplicate-route message names the offending route; and `wheels start` warns when its pinned port is already taken instead of failing opaquely. +- `wheels reload` and `wheels generate admin` now refuse to attach to a server that isn't bound to the current project, closing the same #2878 gap for two more server-dependent commands that #2879 fixed for the write-side migrators. Both reached `cli.lucli.Module::$requireRunningServer()` without the `requireProjectConfig` flag, so in a project with no `lucee.json` / `.env` port they still fell back to the hardcoded common-port probe (`[8080, 60000, 3000, 8500]`) and could silently attach to a sibling app: `reload` would reset the wrong app's state, and `generate admin` would introspect the wrong schema and scaffold its controller/views into the current project from a sibling's model — wrong-schema output written into the right project. Both now pass `requireProjectConfig = true`; with no project-bound port they throw `Wheels.ServerNotRunning` with a "set 'port' in lucee.json (or PORT in .env), then start with: wheels start" diagnostic instead of proceeding. `generate admin` is gated (rather than left on the read-side fallback alongside `info` / `routes`) precisely because it both reads a schema and writes files into cwd, so a wrong-server attach is a correctness bug, not just a wrong read. Covered by new server-free specs in `cli/lucli/tests/specs/services/ServerDetectionSpec.cfc` that drive `reload()` and `generateAdmin()` in a no-config project and assert the guard refuses to attach (#2878) +- `wheels migrate` (and its sibling write-side runners — `seed`, `migrate forget` / `pretend`, `migrate rename-system-tables`) refuse to attach to a server that isn't bound to the current project. `cli.lucli.Module::detectServerPort()` previously fell back to a hardcoded common-port probe (`[8080, 60000, 3000, 8500]`) after exhausting `lucee.json` and `.env`, so a freshly-scaffolded project with no port config could silently attach to a sibling app's open Lucee instance and run its migrations against the wrong database (the #2876 / #2878 repro: `wheels new app_a` + `wheels start` in `app_a`, then `wheels migrate latest` in `app_b` ran `app_b`'s migrations against `app_a`'s PostgreSQL). `detectServerPort()` now accepts a `requireProjectConfig` flag that skips the common-port fallback, and `$requireRunningServer()` threads it through to every write-side caller. When the flag is set and no project-bound port resolves, the CLI throws `Wheels.ServerNotRunning` with a clear "set 'port' in lucee.json (or PORT in .env), then start with: wheels start" diagnostic instead of proceeding. Read-side commands (`info`, `routes`, `console`, `dbStatus`, `dbVersion`) keep the legacy fallback — they don't mutate anything, and removing it would regress the no-config development experience. Covered by new server-free specs in `cli/lucli/tests/specs/services/ServerDetectionSpec.cfc` that simulate a sibling app on an ephemeral port and assert the write-side guard refuses to attach (#2878) +- `wheels migrate latest` no longer crashes on PostgreSQL (and CockroachDB) when a migration emits an inline foreign-key constraint — e.g. anything `wheels generate scaffold ... --belongsTo=author` produces. `wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator` was missing the public `addForeignKeyOptions(sql, options)` method that every other adapter implements (`MySQLMigrator`, `SQLiteMigrator`, `MicrosoftSQLServerMigrator`, `OracleMigrator`); `Abstract.createTable()` builds the inline FK clause via `foreignKeys[i].toForeignKeySQL()` → `ForeignKeyDefinition.cfc` → `adapter.addForeignKeyOptions(...)`, so every PostgreSQL FK column threw `Component [wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator] has no function with name [addForeignKeyOptions]` and aborted the migration. The new implementation mirrors the MySQL signature (`FOREIGN KEY (col) REFERENCES tbl (refCol)`), which PostgreSQL accepts verbatim, and `CockroachDBMigrator` (which extends `PostgreSQLMigrator`) inherits the fix automatically. The reporter's "works on Windows" observation lined up with the `wheels new` SQLite default — only PostgreSQL/CockroachDB targets ever hit the missing method (#2876) +- CLI services in `Module.cfc` now instantiate via the module-relative path (`new services.X()`) instead of the absolute FQN (`new cli.lucli.services.X()`), so `wheels new` and the other subcommands resolve their service classes when running from the installed distribution. The module tarball is built with `tar -C cli/lucli .`, which flattens the module root so services live at `/services/` with no `cli/lucli/` tree and no `cli.lucli` mapping — the absolute form only resolved against the source-tree layout. That split is why the `fast-test` job (which runs from source, where both forms resolve) stayed green while the snapshot smoke test — which installs the built tarball and runs `wheels new` — failed on every `develop` push since #2861 with `could not find component or class with name [cli.lucli.services.ArgSpec]`. All 8 absolute references (7× `ArgSpec`, 1× the latent `TestRunner` call) are converted to the relative form the 17 sibling services already use; the `ArgSpec` docblock example is updated to match so it cannot re-seed the pattern (#2873) +- Oracle `DROP TABLE` / `DROP VIEW` in the migrator now work on Oracle 19c/21c. `wheels.databaseAdapters.Oracle.OracleMigrator::dropTable()` emitted `DROP TABLE IF EXISTS CASCADE CONSTRAINTS` and `dropView()` inherited `DROP VIEW IF EXISTS` from `Abstract`, but Oracle only added the `IF EXISTS` DDL modifier in 23c — on 19c/21c both are a hard parse error (ORA-00933). Because the `remove-table` migration template re-throws on error, `migrate down`, rollbacks, `force`-create, and migrator test re-runs failed outright on pre-23c Oracle. Both helpers now emit the version-agnostic Oracle PL/SQL idiom — `BEGIN EXECUTE IMMEDIATE 'DROP TABLE CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;` — which runs the bare DROP and swallows ORA-00942 ("table or view does not exist"), preserving "drop if exists" semantics on every supported Oracle version with no version detection. `$execute` (`vendor/wheels/migrator/Base.cfc`) never splits on `;` and deliberately omits the trailing-semicolon append for Oracle, so the anonymous block reaches the driver intact. Framework-side counterpart to the demo-app test-populate fix in #2864 (#2869) +- `application.wheels.protectedControllerMethods` is now populated at application start from the public method surface of `wheels.Global` plus the `wheels.controller.*` and `wheels.view.*` mixin components, so framework helpers like `env()`, `model()`, `findAll()`, `redirectTo()`, and `linkTo()` can no longer be invoked as controller actions from a URL. The list was previously initialized to an empty string (the orphaned `local.allowedGlobalMethods = "get,set,mapper"` line in `onapplicationstart.cfc` pointed to the intent but never wired it up), so `$callAction()`'s allow-list check was a no-op. Any unauthenticated `GET //env` request reached the global `env()` helper directly and raised `"The parameter [name] to function [env] is required but was not passed in."` as a 500; other helper names dispatched into unintended code paths. Derived from `getMetaData().functions` on each source component (excluding `$`-prefixed internal methods, which are already gated separately), so the list stays in sync with the framework's mixin surface automatically. Reaching one of these names now throws `Wheels.ActionNotAllowed` and falls through to the missing-action / 404 path, matching every other non-existent action. **Migration note:** applications that defined controller actions with the same name as a public framework helper (e.g. `env`, `model`, `redirectTo`) will need to rename those actions — they now return 404 rather than dispatching, since the protection gate at `processing.cfc:132` fires before the `StructKeyExists(this, action)` lookup that would otherwise reach a same-named user action (#2844) - wheels-bot no longer re-fires Reviewer A/B on commits it has already reviewed. The review idempotency markers (`` / `review-b`) embedded a SHA the skill prompts re-derived at review time via `gh pr view --json headRefOid`, which races with pushes that land mid-session: between the workflow's checkout and the model's `gh pr view` call a new push could move the PR head, so the emitted marker SHA lagged the commit the review actually ran against. The skip-check gate then failed to recognise an already-reviewed head and Reviewer A re-fired on superseded commits while Reviewer B emitted contradictory verdicts on different SHAs (observed across the #2847 review cycle, where Reviewer B self-diagnosed the drift twice). The workflows now capture the head SHA exactly once and thread it into the prompts as an explicit `` argument: `bot-review-a.yml` passes the already-checked-out `steps.pr.outputs.sha` into `/review-pr` and `/respond-to-critique`, and `bot-review-b.yml` keys its checkout, skip-check marker-pattern, and `/review-the-review` invocation off `github.event.review.commit_id` (the commit Reviewer A's review was attached to, immune to head drift from concurrent pushes). The prompts emit the marker from that argument instead of re-deriving it — the Reviewer A/B Bash allowlist is `gh` + read-only `git` (no `echo`/`printenv`), so a step-level env var would be unreadable by the model and the SHA must travel in the prompt text, the same channel the PR number already uses. A structural spec, `vendor/wheels/tests/specs/cli/BotReviewMarkerShaThreadingSpec.cfc`, guards the wiring across both workflow YAMLs and all three prompts (#2848) - wheels-bot's convergence/deadlock loop now emits its idempotency markers from a workflow-captured head SHA, closing the same stale-SHA race fixed for Reviewer A/B (#2848) in the two commands that were out of scope there because they fire on the convergence/deadlock trigger path rather than the `pull_request` / review-submitted paths. `/address-review` (the consensus implementer) and `/advise-on-deadlock` (the senior advisor) previously re-derived the marker SHA via `gh pr view --json headRefOid`, which floats to the PR's current head when a push lands between the workflow's checkout and the model's call — so the emitted `wheels-bot:address-review::`, `wheels-bot:advisor::`, and `converged-approve`/`converged-changes` markers could lag the commit actually being addressed, defeating the per-SHA idempotency gate. `bot-advisor.yml` now threads its already-resolved `steps.pr.outputs.sha` into `/advise-on-deadlock`; `bot-address-review.yml` gains an equivalent resolve step that captures `headRefOid` alongside the head ref it already needed for the branch checkout and threads `steps.pr.outputs.sha` into `/address-review` (its checkout stays branch-name-keyed because that stage commits and pushes back, so the captured SHA is the head at run start — the marker's ``). Both prompts take an explicit `` argument and emit every marker from it; as with #2848 the prohibition is scoped narrowly to "don't re-derive the SHA" — `gh pr view` remains the normal way to read comments, reviews, and the diff, because a blanket ban made Reviewer A flood permission denials and post nothing. A structural spec, `vendor/wheels/tests/specs/cli/BotConvergenceMarkerShaThreadingSpec.cfc`, guards the wiring across both workflow YAMLs and both prompts (#2848) - `wheels new --no-sqlite`, `wheels generate admin --no-routes`, `wheels test --no-test-db` and every other `--no-*` flag the CLI documents now reach their command-level parsers again. LuCLI normalizes `--no-key` on the command line to `key=false` in the arg collection it hands modules, and `Module.cfc::argsFromCollection()` was silently dropping `false` entries — so the literal-token matchers in `new()`, `g admin`, and `test()` never saw the user's negation and the defaults stuck (SQLite still scaffolded, routes still generated, test DB still applied). The rebuild now re-emits `--no-` for `false` values, so all four `--no-*` flags surface to the command handlers unchanged. `--nosqlite` (no hyphen) was never affected because LuCLI does not strip a leading `no` that lacks the hyphen. Spotted in #2855 after the prior `--no-sqlite` plumbing fix in #2624. @@ -66,9 +63,8 @@ All historical references to "CFWheels" in this changelog have been preserved fo - The `apt.wheels.dev` publishing template (`tools/distribution-drafts/apt-repo/`) no longer wipes the `stable` package index when a `bleeding-edge` snapshot publishes. `regenerate-apt-metadata.sh` rebuilt *both* channels on every run while the workflow synced only the dispatched channel's pool into the runner, so a frequent bleeding-edge publish scanned an empty local `pool/stable/`, produced an empty `Packages`, and the unscoped upload overwrote the good stable index on R2 — leaving `apt install wheels` with "Unable to locate package wheels" even though the `.deb` was present in the pool. The regen now honors a `CHANNELS` env (the workflow passes only the dispatched channel) and the upload is scoped to that channel's `dists/` subtree, so the two channels can no longer clobber each other (#2838) - The Wheels CLI test suite (`cli/lucli/tests/specs`, served at `/wheels/cli/tests`) is green again after the BDDRunner error-count fix unmasked 13 pre-existing failures the old `-1` bundle-error sentinel had been arithmetically cancelling (a negative error total netted real failures down to `<= 0`, so the CI gate read the suite as passing). The eight `*CommandSpec` bundles that instantiate `new cli.lucli.Module()` no longer fail to load with `can't find component [modules.BaseModule]`: a lightweight `BaseModule` test double under `cli/lucli/tests/_modules/` plus a `/modules` mapping (added alongside the existing `/modules/wheels`, which longest-prefix resolution keeps authoritative for the wheels module) lets `Module.cfc` instantiate under TestBox — resurrecting the Db/Info command specs as real behavioral coverage. The stale `AdminSpec` route assertion now expects `.namespace("admin")` (the service's current named-route-prefixed output) instead of the legacy `.scope(path="admin")`. Command specs that need the LuCLI runtime, a running Wheels server, CodeGen harness fixtures, or the CLI bash wrapper (Deploy/Destroy/Generate/Packages, plus the server-dependent Migrate/Test cases) and the unbuilt-feature specs (Doctor #2260 mixin-detail, Scaffold route-model-binding) are `xdescribe`/`xit`-skipped with documented reasons, pending a command-by-command CLI test audit. Finally, `tools/ci/run-tests.sh` now clamps a negative error count for its pass/fail decision and fails explicitly when it sees one, so this masking class of bug can never silently turn a red suite green again (#2829) - WheelsTest BDD runner now captures spec-load and bundle-execution errors against the offending bundle instead of bubbling out as an anonymous `BundleRunnerMajorException`, and reports the resulting error count as a positive number (was the `-1` sentinel) so summaries read "1 error(s)" with the bundle path and `globalException` populated — covers both `it()` called outside a `describe()` body and a `beforeAll()` that throws during spec load (#2829) -- `application.wheels.protectedControllerMethods` is now populated at application start from the public method surface of `wheels.Global` plus the `wheels.controller.*` and `wheels.view.*` mixin components, so framework helpers like `env()`, `model()`, `findAll()`, `redirectTo()`, and `linkTo()` can no longer be invoked as controller actions from a URL. The list was previously initialized to an empty string (the orphaned `local.allowedGlobalMethods = "get,set,mapper"` line in `onapplicationstart.cfc` pointed to the intent but never wired it up), so `$callAction()`'s allow-list check was a no-op. Any unauthenticated `GET //env` request reached the global `env()` helper directly and raised `"The parameter [name] to function [env] is required but was not passed in."` as a 500; other helper names dispatched into unintended code paths. Derived from `getMetaData().functions` on each source component (excluding `$`-prefixed internal methods, which are already gated separately), so the list stays in sync with the framework's mixin surface automatically. Reaching one of these names now throws `Wheels.ActionNotAllowed` and falls through to the missing-action / 404 path, matching every other non-existent action. **Migration note:** applications that defined controller actions with the same name as a public framework helper (e.g. `env`, `model`, `redirectTo`) will need to rename those actions — they now return 404 rather than dispatching, since the protection gate at `processing.cfc:132` fires before the `StructKeyExists(this, action)` lookup that would otherwise reach a same-named user action (#2844) ----- +--- # [4.0.2](https://github.com/wheels-dev/wheels/releases/tag/v4.0.2) => 2026-05-27