Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions changelog.d/3110-reload-refire-contract.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `wheels reload` and `wheels packages add` no longer claim that `?reload=true` skips `onApplicationStart`. An authorized reload calls `applicationStop()`, so the next request re-fires `onApplicationStart` in full (re-running `config/services.cfm` and the package loader) — the CLI now says a reload activates an installed package and notes that only a missing/wrong reload password silently skips the restart (#3110)
15 changes: 9 additions & 6 deletions cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -838,12 +838,15 @@ component extends="modules.BaseModule" {
var reloadUrl = "http://localhost:#serverPort#/?reload=true&password=#password#";
var httpResult = makeHttpRequest(reloadUrl);
out("Application reloaded successfully.", "green");
// Surface the hot-vs-cold reload contract — Wheels does NOT
// re-fire onApplicationStart on `?reload=true`. Users editing
// app/events/onapplicationstart.cfm or config/services.cfm need
// a full restart. See finding #8 in the 2026-04-29 fresh-VM
// triage.
out("Note: onApplicationStart does NOT re-fire. For init-code edits, run `wheels stop && wheels start`.", "cyan");
// Surface the actual reload contract (verified live on Lucee 7,
// see #3110): an authorized `?reload=true&password=...` calls
// applicationStop(), so the next request re-fires onApplicationStart
// in full — app/events/onapplicationstart.cfm, config/services.cfm,
// and the PackageLoader all re-run. Caveat: the restart only
// happens when the reload password resolves; a missing or wrong
// password silently serves the request without restarting
// (#3059 / #3062).
out("Note: an authorized reload re-fires onApplicationStart (re-runs config/services.cfm and the package loader). A missing or wrong reload password silently skips the restart.", "cyan");
verbose("URL: http://localhost:#serverPort#/?reload=true&password=***");
} catch (any e) {
out("Failed to reload: #e.message#", "red");
Expand Down
14 changes: 8 additions & 6 deletions cli/lucli/services/packages/PackagesMainCli.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,15 @@ component {
local.manifest = variables.registry.fetchManifest(arguments.name);
local.picked = variables.resolver.pick(local.manifest, variables.runtime, arguments.pin);
local.vendor = variables.installer.install(arguments.name, local.picked, arguments.force);
// Activation requires a cold restart, not a soft reload: PackageLoader
// runs in onApplicationStart and `wheels reload` doesn't re-fire that
// hook. Telling users to `wheels reload` here directly contradicts
// the chapter-8 caveat and produces "uiCard not defined"-style errors
// when the helper resolution fails. Onboarding F14.
// Activation only needs a reload, not a cold restart: an authorized
// `wheels reload` calls applicationStop(), so the next request re-fires
// onApplicationStart — which runs the PackageLoader ($loadPackages).
// Verified live on Lucee 7 (see #3110). A full `wheels stop && wheels
// start` also works but is no longer required. Caveat: a reload only
// restarts when its password resolves; a missing/wrong password
// silently skips the restart (#3059 / #3062).
return "Installed " & arguments.name & "@" & local.picked.version & " → " & local.vendor & Chr(10)
& "Run `wheels stop && wheels start` to activate it." & Chr(10);
& "Run `wheels reload` (or restart) to activate it." & Chr(10);
}

private string function $updateAll(struct opts) {
Expand Down
15 changes: 10 additions & 5 deletions cli/lucli/tests/specs/commands/ReloadCommandSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@
* the relevant strings appear in Module.cfc::reload(). A heavier integration
* test for reload behavior is out of scope here.
*
* See finding #8 in
* docs/superpowers/plans/2026-04-29-fresh-vm-onboarding-findings.md
* Contract (verified live on Lucee 7, see #3110): an authorized
* `?reload=true&password=...` calls applicationStop(), so the next request
* re-fires onApplicationStart in full — config/services.cfm and the
* PackageLoader re-run. The earlier "does NOT re-fire" note was wrong; it
* stemmed from reloads whose password never resolved (a missing/wrong
* password silently skips the restart, see #3059 / #3062).
*/
component extends="wheels.wheelstest.system.BaseSpec" {

function run() {

describe("wheels reload — output hints", () => {

it("emits a note that onApplicationStart does NOT re-fire on a hot reload", () => {
it("emits a note that an authorized reload re-fires onApplicationStart", () => {
var moduleSource = fileRead(expandPath("/cli/lucli/Module.cfc"));
expect(moduleSource).toInclude("onApplicationStart does NOT re-fire");
expect(moduleSource).toInclude("wheels stop && wheels start");
expect(moduleSource).toInclude("re-fires onApplicationStart");
// The old, false claim must be gone.
expect(moduleSource).notToInclude("onApplicationStart does NOT re-fire");
});

it("honors an explicit --password override before falling back to auto-detect", () => {
Expand Down
14 changes: 14 additions & 0 deletions cli/lucli/tests/specs/packages/PackagesMainCliSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ component extends="wheels.wheelstest.system.BaseSpec" {
DirectoryDelete(proj, true);
});

it("install tells the user a reload activates the package (reload re-fires onApplicationStart)", () => {
// An authorized `wheels reload` calls applicationStop(), so the
// next request re-fires onApplicationStart — which runs the
// PackageLoader. A cold restart is no longer required to
// activate an installed package (see #3110).
var proj = $scratch();
var stack = $buildStack(proj);
var out = stack.cli.install({target: "wheels-fake"});
expect(out).toInclude("wheels reload");
expect(out).notToInclude("wheels stop && wheels start");
stack.cache.refresh();
DirectoryDelete(proj, true);
});

it("install honours @version pin", () => {
var proj = $scratch();
var stack = $buildStack(proj);
Expand Down
Loading