Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Calling `wheels upgrade` with no subcommand prints concise usage listing both ve
1. **Reads the current version** from `vendor/wheels/box.json`. If the file is missing or malformed, the version is reported as `unknown` and the comparison below is skipped.
2. **Resolves the target version.** If you pass `--to=<version>`, that wins. Otherwise the command hits the GitHub releases API (`api.github.com/repos/wheels-dev/wheels/releases/latest`) and strips the leading `v` from the tag name. If the network call fails, the command prints a yellow notice and exits — it will not invent a target.
3. **Compares major versions.** If current and target share the same major, the command notes "Same major version — no known breaking changes" for the major-transition checks, then still runs its advisory scan for the same-major patterns below.
4. **Runs breaking-change checks.** Each check is either a directory existence test or a regex grep across a sub-tree of the project. Major-version transitions add their own checks on top of the advisory scan. Hits are printed as issues (yellow); absences are printed as passed checks (green).
4. **Runs breaking-change checks.** Each check is either a directory existence test or a regex grep across a sub-tree of the project. Major-version transitions add their own checks on top of the advisory scan. Every run, whatever the versions, also includes the app template drift advisory described below. Hits are printed as issues (yellow); absences are printed as passed checks (green).

**Exit status:** when breaking findings exist the command throws `Wheels.UpgradeCheckFailed` after the report flushes, exiting non-zero so it can gate CI. Without `--strict`, advisory findings never affect the exit code. With `--strict`, advisory findings escalate to the same hard-fail path — useful for CI pipelines that want to gate on opt-in convention changes, not just breaking ones. With `--format=json` the human report is replaced by a single JSON document (`currentVersion`, `targetVersion`, `success`, `strict`, `breaking`, `advisories`, `passed`, `guide`) — the non-zero exit on breaking or strict-mode advisory findings still applies. The document's `success` field tracks the exit code precisely: it is `false` whenever the process exits non-zero, including the `--strict` + advisory-only case, and the `strict` field is echoed back so consumers can tell `success: false` with empty `breaking[]` apart from a data inconsistency.

Expand Down Expand Up @@ -120,6 +120,10 @@ The checks are hard-coded in the CLI and keyed by the major-version transition:
- *(advisory)* `new wheels.middleware.SecurityHeaders` in `config/` — HSTS defaults on in production in 4.0; pass `hsts=false` if your load balancer already sets it (#2081).
- *(advisory)* `protectsFromForgery` under `app/` — the CSRF cookie now sets `SameSite`; cross-site POSTs from third-party frames will break (#2035).

**Every run (any version)**

- *(advisory)* App template drift (added in 4.1.0, #3379) — `public/Application.cfc` and `public/index.cfm` are compared byte-for-byte against the CLI's bundled app template (the one `wheels new` scaffolds from). A file that differs, or is missing, is reported by name only; no diff is printed. These files are app-owned, so a `vendor/wheels/` swap never updates them, and drift can mean the app is missing framework-side hardening such as the Adobe teardown guards in `onError`/`onSessionEnd`. Customized apps legitimately drift — diff the files yourself and reconcile, keeping your local changes.

Each grep scan covers the file types relevant to that check (`.cfc` and `.cfm` for config and view checks; `Makefile`, `package.json`, `.yml`, `.yaml`, and `.sh` for the `wheels snippets` check) and reports `path:lineNumber` for every hit. Directory checks report only the top-level path when the directory exists and is non-empty.

<Aside type="note" title="Not an exhaustive migration list">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ set(reloadPassword = env("WHEELS_RELOAD_PASSWORD", ""));

One exception: don't `set(environment=...)` here. Environment selection belongs in `config/environment.cfm` only — by the time `settings.cfm` runs, the framework has already derived its environment-dependent defaults, so setting the environment this late produces a half-switched app (the environment reports `production` while verbose error details, development caching flags, and `allowMigrationDown` persist from the boot-time value).

Also note that a blank `reloadPassword` currently permits *anonymous* `?reload=true` restarts — the password check is skipped entirely when no password is configured ([#3062](https://github.com/wheels-dev/wheels/issues/3062)). Set a real reload password in every environment.
Also note that a blank `reloadPassword` disables URL-based reload entirely: since 4.0.4 the gate fails closed, so `?reload=true` requires a non-empty configured password and a matching `password` parameter ([#3062](https://github.com/wheels-dev/wheels/issues/3062)). Set a real reload password in every environment where you want reload over HTTP to work.

Settings cascade: framework defaults load first, then `config/settings.cfm`, then `config/<environment>/settings.cfm`. Later writes win. A value you set in the shared file is the default for every environment; the environment-specific file only needs to name the keys it changes.

Expand Down Expand Up @@ -91,9 +91,9 @@ The gate is `allowEnvironmentSwitchViaUrl`. Any explicit boolean you `set()` is

In development, the debug bar's Environment panel offers the same switch as quick-switch links: they render when a `reloadPassword` is configured and switching via the URL is allowed, and clicking one prompts for the reload password (it is never embedded in the page).

One caveat, tracked as an open issue:
One caveat, now fixed:

- The `wheels reload` CLI command can report success even when the reload didn't take effect ([#3059](https://github.com/wheels-dev/wheels/issues/3059)) — verify with the URL form.
- Before 4.0.4, the `wheels reload` CLI command could report success even when the reload didn't take effect ([#3059](https://github.com/wheels-dev/wheels/issues/3059)). It now checks for the framework's restart redirect (302), reports anything else in red, and exits non-zero.

## Secrets handling

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Run through this before the first production request. Each item maps to a framew
4. **`dataSourceName` points at a real database.** Not H2, not `wheelstestdb`, not a shared staging instance. Confirm with a read-only action that calls `model("User").count()` or similar.
5. **`csrfCookieEncryptionSecretKey` is set** if `csrfStore="cookie"`. `csrfStore` defaults to `"session"` unconditionally — cookie storage is always an explicit opt-in. With `csrfStore="cookie"` and no key, the first CSRF check throws `Wheels.Security.MissingCsrfKey`.
6. **Session storage matches the node topology.** `session` flash storage (the default when session management is on) requires sticky sessions or a session-replication setup behind the load balancer. With multiple nodes and no stickiness, use `flashStorage="cookie"` explicitly.
7. **Error email is wired.** `sendEmailOnError` is auto-enabled in production. If `errorEmailToAddress` is empty, Wheels still tries to send — the address is read at error time — and the mail fails silently. Set it.
7. **Error email is wired.** `sendEmailOnError` is auto-enabled in production, but `errorEmailAddress` defaults to `""` (since 4.1.2, [GHSA-8gcv-3v9h-7gj2](https://github.com/wheels-dev/wheels/security/advisories/GHSA-8gcv-3v9h-7gj2)). With no recipient configured, no error email is sent — Wheels writes a one-time warning to `wheels.log` instead. Set `errorEmailAddress` (or `errorEmailToAddress`) explicitly. The sender is the first one set of `errorEmailFromAddress`, `errorEmailAddress` and the recipient.
8. **URL rewriting matches the web server.** `set(URLRewriting="On")` only works if nginx/Apache/Tuckey rewrites `/posts/1` to `/index.cfm/posts/1`. If rewrites aren't configured, leave it at `"Partial"` or `"Off"`, or every link breaks.
9. **`allowEnvironmentSwitchViaUrl` is not re-enabled.** Confirm by grepping `config/production/settings.cfm` — don't set it to `true`. If you need to toggle environments, redeploy.
10. **Caches are warmed before traffic cutover.** The very first request to a freshly started node is dominated by one-time CFML-to-bytecode compilation — profiling a stock app puts roughly **85% of a ~1.2s cold start in the engine's compiler**, versus sub-millisecond once warm. That cost is paid once per process, so the goal is to move it off the first *real* visitor and onto the deploy sequence:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ Teams with their own deployment workflow — IIS or nginx in front of Lucee or A

## What to download

Every release publishes its artifacts on the [GitHub releases page](https://github.com/wheels-dev/wheels/releases). For a new app you need exactly two (substitute the current version number):
Every release publishes its artifacts on the [GitHub releases page](https://github.com/wheels-dev/wheels/releases). For a new app you need exactly two. The names below are from 4.1.2, the current 4.1 release at the time of writing; use the latest release and substitute its version number:

| File | What it is | Where it goes |
|---|---|---|
| `wheels-base-template-4.0.5.zip` | The application skeleton: `app/`, `config/`, `public/`, `tests/`, an empty `vendor/` | Extract as your app root |
| `wheels-core-4.0.5.zip` | The framework itself — a single `wheels/` folder | Extract into `vendor/`, so it becomes `vendor/wheels/` |
| `wheels-base-template-4.1.2.zip` | The application skeleton: `app/`, `config/`, `public/`, `tests/`, an empty `vendor/` | Extract as your app root |
| `wheels-core-4.1.2.zip` | The framework itself — a single `wheels/` folder | Extract into `vendor/`, so it becomes `vendor/wheels/` |

That's the complete list. Wheels 4 has **no external dependencies**: the DI container (`vendor/wheels/Injector.cfc`) and the test framework (`vendor/wheels/wheelstest/`) are built in. `.md5`/`.sha512` checksum files are published alongside each zip if you want to verify downloads.

Expand All @@ -53,7 +53,7 @@ The 3.x recipe — wheels-base-template + wheels-core + **WireBox**, plus **Test
server.json
```

A zero-byte marker file named `wheels-base-template-4.0.5-<timestamp>` ships in the zip; it identifies the build and is safe to delete.
A zero-byte marker file named `wheels-base-template-4.1.2-<timestamp>` ships in the zip; it identifies the build and is safe to delete.

2. **Extract the core zip into `vendor/`.** The zip contains a single `wheels/` folder, so after extraction you have `vendor/wheels/` with `Dispatch.cfc`, `Model.cfc`, `Controller.cfc`, etc. directly inside it. The folder name must be exactly `wheels` — the mappings in `public/Application.cfc` depend on it.

Expand Down Expand Up @@ -90,8 +90,8 @@ The identical artifacts are published to ForgeBox as `wheels-base-template` and
Prefer `git clone` over zips? Clone the repo and copy `vendor/wheels/` from a release tag:

```bash title="your shell"
git clone --depth=1 --branch v4.0.5 https://github.com/wheels-dev/wheels.git /tmp/wheels-src
git clone --depth=1 --branch v4.1.2 https://github.com/wheels-dev/wheels.git /tmp/wheels-src
cp -R /tmp/wheels-src/vendor/wheels myapp/vendor/wheels
```

The repo's `vendor/wheels/` at a release tag is the same code as the core zip, with one cosmetic difference: the release pipeline stamps the real version into `wheels.json`, while a git checkout carries the `@build.version@` placeholder — the app runs identically, but version reporting (the debug bar, `application.$wheels.version`) shows the placeholder-derived value instead of `4.0.5`. If exact version metadata matters to you, prefer the zips. (Either way, don't track the framework from `develop` in production — tags are the supported line.)
The repo's `vendor/wheels/` at a release tag is the same code as the core zip, with one cosmetic difference: the release pipeline stamps the real version into `wheels.json`, while a git checkout carries the `@build.version@` placeholder — the app runs identically, but version reporting (the debug bar, `application.$wheels.version`) shows the placeholder-derived value instead of `4.1.2`. If exact version metadata matters to you, prefer the zips. (Either way, don't track the framework from `develop` in production — tags are the supported line.)
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ component extends="wheels.WheelsTest" {

See the [Testing guide](/v4-1-0/testing/) for matchers and the phantom-matcher list.

There is also a `wheels.Testbox` shim — a pure alias of `wheels.WheelsTest` retained for specs written before the rename. It adds no behavior. Deprecated since 4.0 with a removal target of 5.0; both it and `wheels.Test` should migrate to `wheels.WheelsTest`. Note: the released 4.0.3 `wheels upgrade check` only flags `extends="wheels.Test"`; the combined rule that also flags `extends="wheels.Testbox"` lives on `develop` and ships in a post-4.0.3 release.
There is also a `wheels.Testbox` shim — a pure alias of `wheels.WheelsTest` retained for specs written before the rename. It adds no behavior. Deprecated since 4.0 with a removal target of 5.0; both it and `wheels.Test` should migrate to `wheels.WheelsTest`. Note: the 4.0.3 `wheels upgrade check` only flags `extends="wheels.Test"`; the combined rule that also flags `extends="wheels.Testbox"` shipped in 4.0.4, so use a 4.0.4 or newer CLI.

### 9. Tests directory renamed: `tests/specs/functions/` → `tests/specs/functional/`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ If you deploy from git and `vendor/wheels/` is committed (common for deliberate-
4.1.0 moved the model mixins to compile-time includes ([#3213](https://github.com/wheels-dev/wheels/issues/3213)), so a model that overrides a mixin and delegates through the old `variables.super<name>` alias (e.g. `variables.superdelete()`) must call the parent method instead — `super.delete()` (or `super.updateAll()`, etc.). Controller and view helper `super<name>` overrides are unaffected ([#3325](https://github.com/wheels-dev/wheels/issues/3325)).
</Aside>

<Aside type="caution" title="Upgrading to 4.1.1? Patch the `.env` boolean parser">
4.1.1 stops coercing a `.env` value of numeric 1 (such as `1.0`) to the boolean `true` ([#3641](https://github.com/wheels-dev/wheels/issues/3641)). The parser lives in the app-owned `public/Application.cfc`, so the `vendor/wheels/` swap does **not** deliver the fix. In your copy, find the `.env` type-casting block that compares with `==`:

```cfm title="before — public/Application.cfc"
if (local.value == "true" || local.value == "false") {
local.value = (local.value == "true");
```

and replace those two lines with the template's string comparison:

```cfm title="after — public/Application.cfc (4.1.1 template)"
if (Compare(lCase(local.value), "true") == 0 || Compare(lCase(local.value), "false") == 0) {
local.value = (Compare(lCase(local.value), "true") == 0);
```
</Aside>

## The 3.x → 4.0 upgrade

The mechanics are the same swap, but 4.0 changes framework behavior and some app-side files, so there's required reading and a few extra steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Measured on Lucee 7 + SQLite:
The framework suite is only partly instance-creation, so its delta understates the win for an instance-heavy app suite like the one in the original report.

<Aside type="note">
A follow-up experiment (cloning a pre-mixed prototype, which profiled ~5.8× faster on instance creation) was investigated and abandoned as a dead end — #3236 is the shipped fix.
A follow-up experiment (cloning a pre-mixed prototype, which profiled ~5.8× faster on instance creation) was investigated and abandoned as a dead end — #3236 is the fix that shipped in 4.0.6. Later per-object work landed in 4.1.0 (see below).
</Aside>

## Where 4.0 stands after the fix
Expand All @@ -58,16 +58,18 @@ The practical takeaway for benchmarking your upgraded app: warm the app first (t

## How to get the fix

The fix merged to `develop` on 2026-06-20 — one day **after** v4.0.5 was tagged, so it is not in any stable release yet. It ships with **v4.0.6**. Until then it is available on the bleeding-edge channel, which tracks `develop`:
All of this work is in stable releases — no channel switch is needed:

- **4.0.6** — the per-application mixin-plan cache described above ([#3236](https://github.com/wheels-dev/wheels/pull/3236)).
- **4.1.0** — the model mixin surface moved to compile-time includes, so instances inherit the full model API instead of copying ~270 method references each ([#3462](https://github.com/wheels-dev/wheels/pull/3462)), and model instances are created with a direct constructor call instead of the DI container's resolve/auto-wire path plus a reflective invoke ([#3464](https://github.com/wheels-dev/wheels/pull/3464)). Together with request-scope caching of the integration plan ([#3461](https://github.com/wheels-dev/wheels/pull/3461)), this took 3,000 × `model().new()` from ~1,290 ms on 4.0.3 to ~508 ms on the [#3213](https://github.com/wheels-dev/wheels/issues/3213) benchmark rig (Lucee 7, warmed), narrowing the gap to 2.5 on that workload from ~4.9× to ~1.9×. If a model overrides a mixin and delegates through `variables.super<name>`, see the 4.1.0 caution in [Upgrading Without the CLI](/v4-1-0/upgrading/manual-upgrades/).
- **4.1.1** — flash locking is scoped to where the data lives: session-stored flash takes a per-session lock instead of one application-wide lock, and cookie-stored flash is no longer locked at all ([#3656](https://github.com/wheels-dev/wheels/pull/3656), by @zspitzer). Under a 128-user load test this lifted throughput 10–14%.

Upgrade the CLI through your package manager, then swap the framework into your app:

```bash
# install the bleeding-edge CLI (brew/apt/yum/scoop all have a -be channel;
# see the Release Channels guide for your platform's exact package)
wheels upgrade check # preview what would change
wheels upgrade apply # swap your app's vendor/wheels to the BE framework
wheels upgrade apply # swap your app's vendor/wheels to the CLI's bundled framework
wheels server start
```

`wheels upgrade apply` swaps `vendor/wheels/` in place — do it on a branch or a copy of the app so it's easy to roll back. Once v4.0.6 is out, the stable channel carries the fix and no channel switch is needed.

→ [Release Channels](/v4-1-0/start-here/release-channels/) covers switching between stable and bleeding-edge (and back) per platform.
`wheels upgrade apply` swaps `vendor/wheels/` in place — do it on a branch or a copy of the app so it's easy to roll back.
Loading