From 6ae60e2c9c0fe76a3b8b9cd89a9afb21c1fd9601 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 20 May 2026 08:55:02 -0700 Subject: [PATCH 01/34] chore: bump develop snapshot target to 4.0.2 (#2770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manual bump after the v4.0.1 GA — `bump-develop-version.yml` fired via `repository_dispatch` (the #2609 fix worked) but failed in 12s on a second issue: `peter-evans/create-pull-request@v6` hit `remote: Duplicate header: "Authorization"` because the `actions/checkout` step left credentials persisted that conflict with the action's own token. See run 26173817714 for the failed log. Setting `wheels.json` to `4.0.2` so subsequent develop snapshots are tagged `4.0.2-snapshot.`. This is a baseline, not a commitment — the next GA's scope decision is made at tag-cut time. Follow-up issue tracks the workflow fix (add `persist-credentials: false` to the checkout step). Signed-off-by: Peter Amiri Co-authored-by: Claude Opus 4.7 (1M context) --- wheels.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wheels.json b/wheels.json index 2e49c4b73e..c85d509e01 100644 --- a/wheels.json +++ b/wheels.json @@ -1,7 +1,7 @@ { "$schema": "https://wheels.dev/schema/wheels.json/v1.json", "name": "Wheels.fw", - "version": "4.0.1", + "version": "4.0.2", "description": "Wheels — CFML MVC framework", "homepage": "https://wheels.dev", "documentation": "https://wheels.dev/guides/", From c8efebbd13be6b723a1860a7f88ba33cadb99ca5 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 20 May 2026 08:58:18 -0700 Subject: [PATCH 02/34] ci(release): fix bump-develop-version.yml duplicate Authorization header (#2771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `actions/checkout@v6` defaults to `persist-credentials: true`, which writes `http.https://github.com/.extraheader = AUTHORIZATION: basic ` to the local `.git/config`. `peter-evans/create-pull-request@v6` then sets its own `extraheader` for the dispatch token, and the next git operation sends both Authorization headers — GitHub returns HTTP 400 with `remote: Duplicate header: "Authorization"`. First observed on the v4.0.1 GA (2026-05-20, run 26173817714); manual workaround was #2770. Setting `persist-credentials: false` keeps peter-evans/create-pull-request as the sole Authorization authority. This is a documented peter-evans/create-pull-request gotcha when the caller uses a non-default token. Signed-off-by: Peter Amiri Co-authored-by: Claude Opus 4.7 (1M context) --- .github/workflows/bump-develop-version.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/bump-develop-version.yml b/.github/workflows/bump-develop-version.yml index fbf61371bf..a634c9fe28 100644 --- a/.github/workflows/bump-develop-version.yml +++ b/.github/workflows/bump-develop-version.yml @@ -61,6 +61,11 @@ jobs: with: ref: develop fetch-depth: 1 + # Don't persist GITHUB_TOKEN as a git extraheader — peter-evans/create-pull-request + # below sets its own Authorization, and two simultaneous extraheaders make GitHub + # reject git operations with "Duplicate header: Authorization" → HTTP 400. + # First hit on the v4.0.1 GA (run 26173817714); manual workaround was #2770. + persist-credentials: false - name: Compute next snapshot target run: | From d22f97c3dff941d0152cea478a02652afbaa3f67 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 20 May 2026 15:19:34 -0700 Subject: [PATCH 03/34] docs(web/blog): Wheels 4.0.1: Adobe CF hardening, Windows Scoop fixes, and the post-GA shakeout (#2772) Walks through the ~100 PRs that landed between 4.0.0 and 4.0.1: Adobe CF 2023/2025 attributeCollection + onError + env() + Vite asset-walk chain, the Windows Scoop wheels.cmd cmd.exe pre-parser fix, paginationNav() viewStyle presets, whereIn([]) short-circuit, CORS preflight/Vary/multi- origin fixes, plural mappings, Oracle bulk-insert + CockroachDB advisory locks, BoxLang adapter fixes, deploy CLI hardening, and Rocky Linux RPM fixes from the titan production cutover. Signed-off-by: Peter Amiri Co-authored-by: Claude Opus 4.7 (1M context) --- .../blog/posts/wheels-4-0-1-released.md | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 web/content/blog/posts/wheels-4-0-1-released.md diff --git a/web/content/blog/posts/wheels-4-0-1-released.md b/web/content/blog/posts/wheels-4-0-1-released.md new file mode 100644 index 0000000000..bfe7e1e17e --- /dev/null +++ b/web/content/blog/posts/wheels-4-0-1-released.md @@ -0,0 +1,162 @@ +--- +title: 'Wheels 4.0.1: Adobe CF hardening, Windows Scoop fixes, and the post-GA shakeout' +slug: wheels-4-0-1-released +publishedAt: '2026-05-20T23:00:00.000Z' +updatedAt: '2026-05-21T03:51:31.000Z' +author: Peter Amiri +tags: + - wheels-4 + - release-notes + - frameworks +categories: + - Releases +excerpt: >- + Wheels 4.0.1 is out — the first patch on the 4.0 line. It hardens Adobe + ColdFusion 2023/2025 compatibility, fixes the Windows Scoop install + regressions reported after GA, adds CSS-framework presets to + paginationNav(), short-circuits whereIn([]) so empty filters stop emitting + invalid SQL, and threads about a hundred smaller fixes through the rest of + the framework. +coverImage: null +--- + + +Wheels 4.0.1 ships today, eight days after the 4.0.0 GA. It is a patch release in the SemVer sense — no breaking changes, no new public APIs you have to learn — but it is the first patch on a brand-new major, and the post-GA week surfaced a longer list of "this only matters at scale" issues than a normal point release would carry. Roughly a hundred PRs landed between [4.0.0](https://github.com/wheels-dev/wheels/releases/tag/v4.0.0) and [4.0.1](https://github.com/wheels-dev/wheels/releases/tag/v4.0.1). + +This post walks through what changed, organized by who hit it. + +## Adobe ColdFusion 2023 and 2025: the compat-matrix gauntlet + +The headline story is Adobe CF. The 4.0 release verified clean on Lucee 5/6/7 and BoxLang, but the Adobe 2023 and 2025 legs of the compatibility matrix were reporting `0 pass / 0 fail / 0 err` — every request was failing before any test could complete. The root cause was a cascade of three Adobe-specific behaviors stacked on top of each other, each one masking the next. + +**`attributeCollection = arguments` is rejected on Adobe CF 2023/2025** ([#2741](https://github.com/wheels-dev/wheels/pull/2741), [#2750](https://github.com/wheels-dev/wheels/pull/2750)). Lucee, BoxLang, and Adobe 2018/2021 all accept the `arguments` scope passed directly to a built-in CFML tag's `attributeCollection`. Adobe 2023 tightened that — it now demands a plain struct, and throws `InvalidHeaderException: Failed to add HTML header` on `cfheader` (and equivalents on `cfcache`, `cfcontent`, `cfmail`, `cfdirectory`, `cffile`, `cflocation`, `cfhtmlhead`, `cfimage`, `cfdbinfo`, `cfinvoke`, `cfwddx`, `cfzip`). The fix is a uniform copy-arguments-to-struct shim across thirteen sites in `vendor/wheels/Global.cfc`. `$header()` is the dispatch-path blocker, but every other helper that wraps a built-in tag needed the same treatment. + +**`$header()` masks the original exception when called from `onError`** ([#2756](https://github.com/wheels-dev/wheels/pull/2756)). Even after the `attributeCollection` fix, Adobe CF still rejects `cfheader` calls when the response buffer has already been committed — and CF commits the buffer aggressively when any view content flushes mid-render. The error handler at `EventMethods.cfc:113` calls `$header("Content-Type", "application/json")`, which then threw `InvalidHeaderException` and replaced the upstream exception in the stack. `$header()` now probes `response.isCommitted()` and short-circuits with a best-effort write when the buffer has flushed. A new companion helper, `$responseCommitted()`, lets other tag wrappers adopt the same short-circuit incrementally. + +**`env("KEY", "fallback")` silently returned `""` on Adobe CF** (also in [#2756](https://github.com/wheels-dev/wheels/pull/2756)). The second parameter was named `default` — a CFML reserved word (`switch`/`case`/`default`) — and Adobe refuses to bind a parameter with that name at all. Neither the signature default nor a caller-supplied value populated `arguments.default`. Lucee and BoxLang bind it correctly. The fix renames the parameter to `defaultValue` and adds a back-compat shim for the legacy named-arg form `env(name="X", default="Y")` by checking the `arguments` scope for the literal `default` key first. Positional callers — the framework's own pattern — are unaffected. + +**Vite asset-walk lost transitive imports on Adobe CF** ([#2756](https://github.com/wheels-dev/wheels/pull/2756)). `viteScriptTag`, `viteStyleTag`, `vitePreloadTag`, and `$viteHtmlHead` were passing arrays from a struct literal to a recursive walker — and Adobe CF copies arrays by value in struct literals, while Lucee and BoxLang share the reference. Every `ArrayAppend()` inside the recursion was writing to a garbage copy. The fix is to pass the parent struct by reference and mutate `arguments.rv.preloads` / `arguments.rv.styles` instead. This is Cross-Engine Invariant #6 in [CLAUDE.md](https://github.com/wheels-dev/wheels/blob/develop/CLAUDE.md) — the kind of thing that bites once per release cycle. + +Two more in the same chain: the vendored TestBox `BaseReporter.resetHTMLResponse()` was throwing `IllegalStateException` on already-committed responses, and the test runner's bare `cfheader`/`cfcontent` calls in `runner.cfm` needed to route through the defensive `$header()`/`$content()` helpers so the runner could finish its own end-of-suite reporting. Both shipped in [#2756](https://github.com/wheels-dev/wheels/pull/2756) and [#2745](https://github.com/wheels-dev/wheels/pull/2745). + +The Adobe CF 2023/2025 legs are now green. + +## Windows: Scoop install actually works + +The second user-visible cliff was Windows install. Scoop is the canonical Windows install path for 4.0 (Chocolatey is no longer maintained), and the GA Scoop wrapper was failing on a clean Windows 11 install before LuCLI could even run. + +**The `wheels.cmd` wrapper tripped cmd.exe's pre-parser** ([#2766](https://github.com/wheels-dev/wheels/pull/2766), [#2767](https://github.com/wheels-dev/wheels/pull/2767)). The wrapper had been doing `call "%~dp0lucli-.bat" %*`, which makes cmd.exe pre-parse the entire bat-jar concatenation looking for labels. The bat preamble is ~915 KB, then a `:JAR_BOUNDARY` marker, then raw JAR ZIP bytes. The pre-parser tripped on byte sequences in the ZIP tail and printed `The filename, directory name, or volume label syntax is incorrect.` before exiting. The fix dispatches LuCLI via `"%JAVA_HOME%\bin\java.exe" -client -jar "%~dp0lucli-.bat" %*` directly — `java` reads the JAR via stream and skips the bat preamble, bypassing cmd's pre-parser entirely. A one-line fallback handles the case where Scoop's extraction lands the inlined JDK at a different path than expected. + +**Release artifacts now ship `.zip.sha512` sidecars instead of `.sha512`** ([#2761](https://github.com/wheels-dev/wheels/pull/2761)). The scoop-wheels `autoupdate` config uses `$url.sha512` substitution and expects the `.zip.sha512` shape; the GA artifacts shipped `*.sha512` and `*.md5`, so every non-module artifact 404'd the autoupdater. Four release artifacts and three workflows now produce the correct shape. + +**The Scoop install docs now mention the `java` bucket prerequisite**. Scoop's `depends:` declaration doesn't add the dependency bucket on the user's behalf, so users hit `Couldn't find manifest for 'openjdk21' from 'java' bucket` before they could even attempt install. `start-here/installing.mdx` and `command-line-tools/installation.mdx` now lead with `scoop bucket add java`. + +If you tried to install on Windows after 4.0.0 dropped and hit any of the above, this is the release that fixes it. + +## Pagination view helpers: CSS-framework presets + +This is the only meaningfully new user-facing surface in 4.0.1, and it lands because the like-for-like 3.x → 4.0 swap from `paginationLinks()` to `paginationNav()` was forcing Bootstrap apps to do a `Replace()` regex hack to move the `active` class from the anchor to the wrapping `
  • `. `paginationNav()` and `pageNumberLinks()` now accept a `viewStyle` argument with named presets ([#2718](https://github.com/wheels-dev/wheels/pull/2718)): + +```cfm +#paginationNav(viewStyle="bootstrap5")# +#paginationNav(viewStyle="bootstrap4")# +#paginationNav(viewStyle="tailwind")# +#paginationNav(viewStyle="plain")# +``` + +The Bootstrap presets emit the canonical `