2.x to 4.x: why it's two passes, and the map
The question we hear most from teams on Wheels 2.x is some version of “can I jump straight to 4.x, and what am I in for?” This series answers it by doing it. We take one real CFWheels 2.2 app all the way to Wheels 4.1.2, fix what breaks in the order it breaks, and publish every commit.
This first post is the map: why the upgrade takes two passes, what each pass touches, which files the upgrade never touches, and the app we’re upgrading.
Two passes, not one
The upgrade guides are direct about this. Rule 2 of the upgrade philosophy is “One major at a time”: do the 2.x→3.x upgrade, verify your app boots and its tests pass, then do 3.x→4.x. The 2.x to 3.x page repeats it for anyone hopping 2.x to 4.x in one sitting:
Skipping the 3.x landing creates a combinatorial upgrade. Many 2→3 breaking changes assume 3.x semantics once applied, and many 3→4 breaking changes assume the same.
So the plan is:
- Pass 1: 2.x to 3.0.0+33. The layout move. Your code goes under
app/, static files underpublic/, the framework moves fromwheels/tovendor/wheels/, andpublic/Application.cfcbecomes the bootstrap. The authority is the frozen Upgrading to Wheels 3.0.0 guide. - Pass 2: 3.0 to 4.1. The framework swap and the 4.x breaking changes, found with
wheels upgrade check. The authority is Upgrading from 3.x to 4.x.
Land pass 1 on exactly 3.0.0+33, the 3.0 GA build. The next post explains why a caret range isn’t safe there.
What each pass touches
| Area | Pass 1: 2.x → 3.0 | Pass 2: 3.0 → 4.1 |
|---|---|---|
| Layout | controllers/, models/, views/, global/, events/ and migrator/ move into app/; files/, images/, javascripts/, stylesheets/ and miscellaneous/ move into public/ |
No change |
| Bootstrap | The root Application.cfc, index.cfm and rewrite.cfm shims give way to public/Application.cfc, public/index.cfm and public/urlrewrite.xml |
public/Application.cfc is replaced by the 4.1 template (wheels.Injector, application.wheelsdi) |
| Framework | wheels/ is deleted; vendor/wheels/, WireBox and TestBox are declared in box.json |
vendor/wheels/ is swapped for 4.1.2; WireBox and TestBox go away |
| Tests | RocketUnit tests move to tests/RocketUnit/ |
New tests extend wheels.WheelsTest; wheels.Test still runs but is deprecated, with removal in 5.0 |
| Plugins | Stay in plugins/ at the project root |
Deprecated in 4.0, removed in 5.0: replace them with packages or app code |
| Config and code | Root-relative paths such as /global/... need fixing |
reloadPassword, CSRF settings, paginationNav(), migrations with default='', validation conditions |
| Engines | Adobe ColdFusion 2016 and earlier are no longer supported | Lucee 5/6/7, Adobe ColdFusion 2018/2021/2023/2025 and BoxLang |
The engine floor is the one item to check before you start. If you’re still on ACF 2016, upgrading the engine is its own project. The guides’ rule 3 says to change the framework and the engine one at a time, so you know which change broke what.
The files upgrades never touch
Swapping vendor/wheels/, by hand or with wheels upgrade apply, replaces one folder. Everything else in the project is yours to maintain:
app/: controllers, models, views, global functions, events and migrationsconfig/public/Application.cfc,public/index.cfmand the rewrite filestests/box.jsonand.env
public/Application.cfc needs the most care. Since 3.0 it’s the bootstrap: it sets the mappings, loads .env, starts dependency injection, and handles errors and reloads. Several 4.x fixes live in that file, and the manual upgrade guide says plainly that the vendor/wheels/ swap does not update it. wheels upgrade check reports drift from the bundled template as an advisory. A later post in this series goes through those edits one by one.
The app: cfwheels-example-app
wheels-dev/cfwheels-example-app is a real CFWheels 2.2 app, Apache-2.0, with its last original commit in June 2022. It has user management, role-based permissions, password resets and an audit log. It’s a better test than a toy app because it uses most of what 2.x apps use:
- three plugins, as zips in
plugins/: FlashMessagesBootstrap, authenticateThis and jsconfirm; - RocketUnit tests in
tests/Test.cfc,tests/functions/andtests/requests/; - 13 migrations from 2018 in
migrator/migrations/; - MySQL, with CommandBox server configs for Lucee 5 and Adobe ColdFusion 2016.
The upgrade lives in two branches next to the untouched master:
upgrade/pass-1-wheels-3.0: 2.2 to 3.0.0+33, in 9 commits.upgrade/pass-2-wheels-4.1: 3.0 to 4.1.2, in 14 commits.
Each commit does one thing, and its message says why: which guide section it follows, or what broke and how we found it. To step through them:
git clone https://github.com/wheels-dev/cfwheels-example-app.git
cd cfwheels-example-app
git log --reverse --oneline master..upgrade/pass-1-wheels-3.0
git log --reverse --oneline upgrade/pass-1-wheels-3.0..upgrade/pass-2-wheels-4.1
The finished branch boots on Lucee 7 with MySQL 8. The old RocketUnit suite still runs (46 tests, 0 failures), and 10 new WheelsTest specs pass.
The to-do list
Here’s the example app checked against the table. This is the running list for the series:
- Layout: all six app folders and all five asset folders move.
- Bootstrap:
Application.cfc,index.cfm,rewrite.cfmandroot.cfmsit in the project root. - Framework:
wheels/(2.2.0) is committed to the repo. - Engine:
server-exampleappACF.jsontargetsadobe@2016. - Tests: ten RocketUnit test cases extend
tests.Testorapp.tests.Test. - Plugins: three, all replaced in pass 2.
- Migrations: nine string and text columns declare
default=''. - Pagination: two views call
paginationLinks(). - Reload:
config/settings.cfmhard-codesreloadPassword="changeme".
Some of these only showed up once the app booted on the new version. That’s normal, and it’s why the guides insist on a working checkpoint between the passes.
If you’re upgrading
- Back up the database, commit everything, and work in a branch.
- Finish pass 1 before you start pass 2. The app should boot and its tests should run on 3.0.
- Keep the CFML engine fixed while you change the framework.
- Treat
public/Application.cfcas your file. No upgrade command replaces it for you.
Thanks
This series exists because people told us where upgrading hurt. Thanks to @mikegrogan (#3379, D#2887, D#3323), @chapmandu (#3213) and @andoradmiraal (D#1838), who reported the problems this series works through.
Comments