Repository navigation
chore: release 4.2.0 - #4552
Merged
Merged
chore: release 4.2.0#4552
Conversation
* fix(examples): tweet's H2 datasources use paths relative to the app
config/app.cfm hard-coded a developer's absolute /Users/... H2 file path, so the
datasources only worked on that one machine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(examples): tweet declares the H2 Lucee extension in server.json
Only box.json listed the H2 .lex, so a plain box server start served pages that
don't touch the database and failed on the rest ('org.h2.Driver not found').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(examples): tweet's README describes the app and how to run it
It was the generic wheels-base-template README. Now: what the app shows, and run
steps verified end to end (box install, box server start, the browser migrator,
sign up).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(examples): tweet's tests run on a tweet_test H2 database
Adds a tweet_test datasource (app-relative path, like tweet's) and points
coreTestDataSourceName at it, so tweet's tests/runner.cfm keeps app data out of
the run: on today's core, and on a core whose project-runner path requires
<datasource>_test (otherwise HTTP 409). The README walk gains the test step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…itself (#4258) * ci(cli): watchdog stops curl itself; progress= accepts only booleans Review follow-ups on the CLI suite watchdog: background curl directly so a stall kill reaches it (a killed curl writes no status, read as 000), and guard progress= with IsBoolean so progress=abc is ignored rather than throwing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * ci(cli): watchdog trips on any progress gap and names the phase #4251 timed only bundles, so a stall before the first bundle (the request queue, reload lock or application start, or suite setup) or between bundles would not trip it. clitests.cfm now also prints a "begin" line, and the watchdog stops the run after WHEELS_CLI_BUNDLE_TIMEOUT seconds without any progress line (counted from the request when there is none yet), naming where it stopped: before the runner view began, in suite setup, in a bundle and its last spec, or between bundles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): the starter app runs its tests on the test datasource examples/starter-app/tests/runner.cfm was an old copy of the framework core-test runner that set the datasource from the folder name, so /wheels/app/tests failed with Datasource [public]. It is now the app template runner, which runs the specs on starterApp_test (#4234). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(cli): the starter app migrates its test database and documents its tests Review fixes: tests/populate.cfm (empty) now carries the template's migrateToLatest() on the test datasource, with comments for the starter app's H2 starterApp_test. The README's testing section names tests/runner.cfm, populate.cfm and tests/specs/ (RocketUnit/ is legacy and not run) and points at /wheels/app/tests instead of the 3.x `wheels test run`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4255) * fix(cli): browser setup launches the browser before reporting ready `wheels browser setup` installed the Playwright binaries and printed "ready" without ever starting the browser, so a host missing Chromium's OS libraries (a bare Ubuntu) passed setup and failed the first browser test. Setup now takes a headless screenshot of about:blank through Playwright's CLI; on failure it prints Playwright's output and, for missing host libraries, the install-deps command for this install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(cli): own the browser setup processes and exit non-zero on failure Review fixes on the launch probe: - The install and the probe run through $browserRunProcess: a ProcessBuilder with an argv array (paths with spaces stay one argument), output via a temp file, and a bounded wait that kills the process and its children on timeout (cfexecute's timeout doesn't stop the process on Lucee). A timed-out probe is never ready, even if a screenshot appeared; the screenshot is removed in a finally. - A failed install or launch throws Wheels.BrowserSetupFailed after printing the output and remedy, so the command exits non-zero. - Specs drive the probe through a stubbed process result and check the runner with real commands (timeout kill incl. the child, exit code, argument with spaces). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rce (#4254) * fix(test): keep the framework test runners off an app primary datasource Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(test): pin how a request from a running app test run re-enters the project runner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(config): give the demo app a test datasource; note the RocketUnit app runner in the upgrade guide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(test): the framework test runners ignore allowTestsAgainstPrimaryDatasource That setting lets app tests from older CLIs run on the primary datasource. The framework suite and its populate.cfm (which drops and recreates tables) now never run there through it: without <datasource>_test the run is refused, a wheels.log warning says the setting was ignored, and only an explicit useTestDB=false reaches the primary. App runners keep honouring the setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#4262) An absent-pattern finding listed its scanDir but printed only "(no occurrences found)" for checks scoped with scanTargets, such as the public/Application.cfc fix checks. $upgradeAbsentHint() names every path the check scanned, e.g. "public/Application.cfc (no occurrences found)". Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#4260) * docs(testing): how to clear the per-request query cache in specs (F6) A spec runs in a single request, and finders cache their results for the rest of the request (cacheQueriesDuringRequest, on by default). So a finder re-run after a write — update()/create(), raw SQL, or a test-client request — returns the rows cached before the write, which quietly fails assertions that expect fresh data. Document the two ways to read fresh rows: pass reload=true to the finder, or clear the whole request cache with StructDelete(request.wheels, "$queryCache"). Added to .ai/testing.md (both the consumer-ai source and the wheels new template copy, kept byte-identical) and to the v4-2-0 model-tests guide's Common pitfalls. A public forgetCachedQueries() helper would be a new API; it is captured in the new-API notes for the 4.2-vs-4.3 decision rather than added here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com> * docs(testing): correct the stale-cache cases (model writes self-clear) rev1-r1 review of #4260: a save through a model (create/update/updateAll/delete/ deleteAll/insertAll) already calls $clearRequestCache() for that model, so re-reading the SAME model after its own save returns fresh rows — the earlier 'stale after update()/create()' claim and the user.update + reload example were wrong. Correct the guidance to the real stale cases: raw SQL, a write through a DIFFERENT model than the one re-queried (e.g. a Comment create vs a cached Post.findAll(include=comments)), and data changed in a separate (test-client) request. reload=true and StructDelete(request.wheels, '$queryCache') stay as the two ways to read fresh rows. Guides pnpm build green (852 pages; /v4-2-0/testing/model-tests compiled). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com> * docs(testing): use a real query property in the reload example (.recordCount) rev1-r1 nit on #4260: the example referenced .commentCount, which is not a Post property and would be copied into real specs and break. Use model("Post").findAll(reload = true).recordCount (a real query property) and frame it as data changed outside the model's own save. Both .ai/testing.md copies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com> --------- Signed-off-by: Peter Amiri <petera@pai.com> Co-authored-by: Peter Amiri <petera@pai.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ercase and/or (#4241) * fix(model): stop where strings erroring on LIKE ... ESCAPE and on lowercase and/or A quoted value in a where string ended only at the end of the clause, a closing parenthesis or an upper-case AND/OR. A following ESCAPE clause, or a lowercase or mixed-case and/or, let it run on to a later quote, and the value failed to decode. A value now ends at its own closing quote, AND/OR match in any case, ESCAPE ends the bound pattern and stays in the SQL, and the condition split is case-insensitive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(model): cover BETWEEN ... AND in any case next to and/or and quoted literals Lowercase and mixed-case between/and now go through the same case-insensitive condition split as upper-case BETWEEN ... AND. Pin that they return the right rows on their own, next to a lowercase and/or condition, and next to a quoted literal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(model): cover BETWEEN with quoted bounds, including a quote and a backslash Quoted BETWEEN bounds stay in the SQL and are written back by the adapter. Pin lowercase and upper-case forms, an empty range, a range next to a lowercase or, and bounds holding a quote and a backslash (the MySQL escaping path). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): browser setup launches the browser before reporting ready `wheels browser setup` installed the Playwright binaries and printed "ready" without ever starting the browser, so a host missing Chromium's OS libraries (a bare Ubuntu) passed setup and failed the first browser test. Setup now takes a headless screenshot of about:blank through Playwright's CLI; on failure it prints Playwright's output and, for missing host libraries, the install-deps command for this install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(cli): own the browser setup processes and exit non-zero on failure Review fixes on the launch probe: - The install and the probe run through $browserRunProcess: a ProcessBuilder with an argv array (paths with spaces stay one argument), output via a temp file, and a bounded wait that kills the process and its children on timeout (cfexecute's timeout doesn't stop the process on Lucee). A timed-out probe is never ready, even if a screenshot appeared; the screenshot is removed in a finally. - A failed install or launch throws Wheels.BrowserSetupFailed after printing the output and remedy, so the command exits non-zero. - Specs drive the probe through a stubbed process result and check the runner with real commands (timeout kill incl. the child, exit code, argument with spaces). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(cli): browser setup uses the platform classpath separator wheels browser setup joined the Playwright jars with ":", which Java on Windows reads as one path. $browserClasspath() joins them with java.io.File.pathSeparator. Also, from #4255 review: the POSIX-only runner specs skip() on Windows, and the timeout-kill spec uses a unique sleep duration so its pgrep check can only match its own process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ll can't hang the CLI suite (#4232) (#4264) docs fetch: the checksum download gets 30 s, the bundle 300 s, through $docsFetchTimeouts(). On Lucee 7, cfhttp can wait for its full timeout after a fast response, so shorter timeouts bound that delay; the specs use 5 s. Test fixtures and helpers: - StubHttpServer binds 127.0.0.1 by default (the CLI dials 127.0.0.1, never localhost), so another socket on the same port can't take the connection. It also gains a HOLD route (read the request, never answer). - StubHttpServer and ChallengeStubServer keep idle connections 10 s, longer than the CLI's 3 s peer-identity check, which used to see them closed. - DocsFetchCommandSpec: new 'gives up on a release server that answers nothing' spec (2 s timeouts; 90 s before this change, 2 s after). - ServerChallengeLiveSpec: the request back into the suite's own server reads with a 15 s timeout. - DeployShellHelper, ServerRegistrySpec and ServerStartTokenSpec bound their process waits. ServerRegistry.$runCommand does too (15 s; a timeout is reported like a command that could not run). - The sshd fixture stays up after the specs (opt-in teardown with WHEELS_DEPLOY_SSHD_TEARDOWN=1): it is one Compose project on fixed ports, so a per-spec teardown stopped it under other checkouts' suites. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ault (#4248) * fix(migrator): treat an empty default on non-string columns as no default An empty default on a column that is not string-like renders as DEFAULT NULL when the column allows null and as no DEFAULT clause on a NOT NULL column. bigInteger and uniqueidentifier used to emit a bare DEFAULT, binary emitted DEFAULT , and MySQL rejected DEFAULT NULL NOT NULL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(migrator): changeColumn adds no default for an empty default on a NOT NULL column SQL Server built the DF_ constraint without the column's allowNull, so an empty default became DEFAULT NULL even on a NOT NULL column. It now adds no constraint, as createTable and addColumn do. A spec changes a bigInteger column to NOT NULL and back on the current database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(migrator): read SQLite's changeColumn statements as one SQL string in the empty-default spec SQLiteMigrator.changeColumnInTable() returns the table rebuild as an array of statements. The spec joins them before checking, so Adobe no longer errors and SQLite's check is real; it also asserts the generated change names the column and makes it NOT NULL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4265) runUpgradeApply printed its plan and summary with out() and returned them too, so the terminal showed the report twice; it returns "" now. new declared its ArgSpec inline, so --help found no options; newArgSpec() holds it, examples come from $commandExamples, and a bare `wheels new` prints the same help. $commandHint() walks the extends chain so help works in Module subclasses. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…pp with none (F20) (#4267) An app that got Wheels via CommandBox's box install has no vendor/wheels/ the 4.x CLI uses. 'wheels framework install' drops the CLI's bundled framework into vendor/wheels/ so the app can move onto the wheels CLI (unzip -> install -> wheels start -> wheels migrate latest). Thin dispatch over the existing FrameworkUpgrader (the same path 'wheels upgrade apply' uses): validateSwap + applyUpgrade (fresh-install when vendorDir absent), creating vendor/ if needed, and $upgradeApplyBoxPin / $upgradeApplyUpdateBoxPin for the box.json wheels-core pin (box.json absent is the normal case). It is fresh-install only: when vendor/wheels/ already exists it refuses and points to 'wheels upgrade apply' (one path for replacing a framework); a non-framework dir is always refused. --to must match the bundled version (no downloads). Non-interactive; print-then-throw (Wheels.FrameworkInstallFailed) on failure for a non-zero exit; out() does the display and the command returns "" (no MCP double-print); added to mcpHiddenTools() since it mutates vendor/. Name verified on 2867: 'install' as a 2nd-position sub-verb reaches Module.cfc (bare 'wheels install' is LuCLI's own InstallCommand; the install->add alias is packages-specific). FrameworkInstallCommandSpec covers help, fresh install, refuse-on-existing (framework and non-framework), --to mismatch, and unknown subcommand. CLI suite green (2194), core-suite cli area green (235), guides pnpm build green (852 pages; upgrade + 3x-to-4x compiled). Signed-off-by: Peter Amiri <petera@pai.com> Co-authored-by: Peter Amiri <petera@pai.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…gins (#4259) * fix(cli): upgrade check finds WireBox in public/Application.cfc, not in packages "Direct WireBox references" never scanned public/Application.cfc, where 3.x apps bootstrap WireBox (new wirebox.system.ioc.Injector(...), application.wirebox), and it flagged third-party packages under app/ (e.g. app/lib/logbox), which use WireBox themselves. It now scans public/Application.cfc and skips package code: box.json install paths, and any folder below the project root with its own box.json (a package moved or committed somewhere its installPaths entry doesn't say). * fix(cli): upgrade check reports box.json plugins when plugins/ is empty The 3.x -> 4.x plugin check only looked at whether plugins/ had contents, so a 3.x app passed when the folder was empty or missing, as 3.0's default deletePluginDirectories=true leaves it at runtime, even though box.json still installs plugins there. It now also reports box.json plugin dependencies (an installPath under plugins/, or a cfwheels-* package with no installPath) and code that reads application.wheels.plugins. * refactor(cli): run the plugins upgrade check in its own helper The plugins branch pushed $upgradeExecuteCheck to complexity 31, over the CLI's gate of 30 (develop: 29). $upgradePluginResult now builds the result, so the branch adds one decision point. No behavior change.
- ServerRegistry.$runCommand: the temp-file cleanup in finally can't replace the result (on Windows a killed process may still hold the file). - StubHttpServer: only a route whose value is exactly "HOLD" holds the connection; other non-binary values are written as before. - test-cli-local.sh: note that the deploy SSH specs leave the shared sshd fixture running, and how to stop it. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…not run never reads as a pass (#4263) * fix(test): a test run that did not finish or a browser spec that did not run never reads as a pass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(test): the starter app populate.cfm no longer lowers the test run timeout Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(test): browserSpecGuard() for browser specs outside browserDescribe() A launch failure no longer sets browserTestSkipped, so a spec guarded by the old `if (this.browserTestSkipped) return;` does not pass silently; this.browser raises the launch error. browserSpecGuard() gives specs in a plain describe() the same skip / error behaviour as browserDescribe(). The guides and the tutorial describe the new output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rker commands (#4271) * docs(consumer-ai): add a mailers topic to the app AI docs Apps' AI docs had no email topic: CLAUDE.md never mentioned mail, and the only guidance was app/mailers/README.md, which nothing linked to. .ai/mailers.md covers a mailer CFC and its view, SMTP defaults, a mail layout and testing a mailer with deliver=false; CLAUDE.md's topic index and .ai/README.md link it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(consumer-ai): jobs.md's example names the mailer it calls The job example called new app.mailers.UserMailer().sendWelcome(user), but no .ai doc defined UserMailer. It now points at .ai/mailers.md, whose UserMailer is the one it calls, and says what else it needs (a users table with email and firstName). Also: var-scope user in perform(), and call processQueue()/queueStats() on a variable; a parenthesized (new X()).method() call doesn't compile on Adobe CF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(consumer-ai): jobs.md names the worker commands, not framework internals The worker section pointed app developers at cli/lucli/Module.cfc and vendor/wheels/public/views/cli.cfm, which don't exist in an app. It now gives the commands and what they need (a server started with wheels start), says what --max-jobs actually does (stops after N processed jobs, polling until then), and names the retry/purge equivalents without the Adobe-incompatible call form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
wheels new wrote shutdownPort = port + 1 into lucee.json without checking it. Sibling apps are usually created with adjacent --port values, so port + 1 was often another running app's port. The template context now comes from $newTemplateContext(), which takes the first free port above the HTTP port ($nextFreePort, the probe wheels start already uses), warns when it skips port + 1, and the summary prints the shutdown port. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… emits (#4273) The topic said the generated User model hashes with PBKDF2 through the passwordHasher service. The generator's templates use the global bcryptHash() / bcryptVerify() helpers, and the generated services.cfm needs no hashing service. The Password hashing section, which lists both schemes, is unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…guard, discoverability (#4275) Folds rev1-r3 + dev1-cli review of #4267: - $printFrameworkHelp returns "" (out() is the sole display) — bare 'wheels framework' no longer prints the help block twice (the U4/#4265 double-print); specs assert via capturedOutput() and a Usage:-appears-once check. - Refuse unless the cwd is a Wheels app root (config/settings.cfm, or config/app.cfm / public/Application.cfc) — no more creating vendor/wheels/ in an arbitrary directory; the message names the directory. - Register 'framework' in the top-level 'wheels --help' listing and in $commandExamples, so it is advertised and 'wheels framework --help' gets an Examples block. - Read the box.json pin BEFORE creating vendor/ (so a pin-read failure leaves no half-made vendor/ behind). - CopyFailed on the install path now says to remove the partial vendor/wheels/ and re-run 'wheels framework install' (not the upgrade/re-vendor recovery). - --to mismatch message names the exact bundled version to pass and notes the snapshot-version case. - New specs: install into an app with NO vendor/ at all; the box.json pin step; refusal when not a Wheels app root; help prints once. CLI suite green (2195/0/0), core-suite cli area green (235). Signed-off-by: Peter Amiri <petera@pai.com> Co-authored-by: Peter Amiri <petera@pai.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): the starter app runs on wheels start with SQLite The starter app was CommandBox-only: box install, box server start, H2, and migrations from the migrator page. The release zip now ships the framework in vendor/wheels (prepare-starterApp.sh copies the version-stamped build-wheels-core/wheels that prepare-core.sh builds) and a lucee.json, so it runs with wheels start, wheels migrate latest and wheels reload. config/app.cfm uses SQLite, which the CLI provides a driver for, unless WHEELS_STARTER_DB=h2. server.json sets that and keeps the H2 extension, so box server start still works; the README has a CommandBox section. .env.example sets WHEELS_ENV and blank reload and Lucee admin passwords. vendor/ is no longer git-ignored in the app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(cli): the starter app's populate.cfm names the SQLite test database Review fix: tests/populate.cfm still said the test datasource was H2 and told users to delete db/h2/starterApp_test.* to reset it. It now names db/test.sqlite, with the H2 files as the CommandBox case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(cli): the starter README's CommandBox steps start with the .env step Review fix: the "Using CommandBox instead" steps skipped creating .env, which the reload password needs under CommandBox too. Step 1 now copies .env.example and sets WHEELS_RELOAD_PASSWORD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#4270 made wheels new skip a shutdown port something is listening on. That misses the common case: app A pins shutdown 63122 in lucee.json, app B is created with --port=63122, and neither is running. B's port then collides with A's pin as soon as both start. $otherProjectPins() reads the HTTP and shutdown pins from the lucee.json of every project in the LuCLI server registry (.project-path) and of each folder next to the new app, skipping the new app itself. $nextFreePort() takes those as an avoid set, and wheels new warns when the requested --port is pinned by another project. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ments (#4277) * docs(web/guides): job examples declare perform() as the base class does The background-jobs and sending-email guides declared perform(required struct data); wheels.Job, .ai/jobs.md and app/jobs/README.md use perform(struct data = {}). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(web/guides): a real spec for the job-testing example The background-jobs 'Testing jobs' example ended with expect(true).toBeTrue() and pointed to an email fake no guide describes. It now builds the email with deliver=false and checks that perform() runs to the end (it throws for a missing user), points to testing the mailer for the email's contents, and says why config/testing/settings.cfm doesn't reach app specs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(consumer-ai): say which environment app specs run in wheels test runs specs in the environment .env sets (development in a new app), not testing, so config/testing/settings.cfm never reaches them. .ai/testing.md now says so and shows keeping mail undelivered for every spec with a development setting; .ai/mailers.md points there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: a job enqueued in a transaction commits or rolls back with it enqueue() writes on the app's datasource, so inside invokeWithTransaction() the job is part of the transaction: useful for an outbox, and a reason to enqueue must-run work (a failure notice) after the transaction. The v4.2 background-jobs guide gets a section with a model method + controller example, and .ai/jobs.md a paragraph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(cli): the generated model comment points at the app's own docs It named docs/consumer-ai/CLAUDE.md, a framework-repo path; in an app the model reference is .ai/models.md, indexed from the root CLAUDE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(consumer-ai): wheels test needs the project's own server running CLAUDE.md said the CLI boots the app on an isolated port for the test run. wheels test runs the suite on this project's own server and stops with 'No running Wheels server detected' until it is started with wheels start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(cli): Scaffold.cfc's auth docblock says bcrypt, as the generator emits Follows #4273: the generated User model hashes with bcryptHash()/bcryptVerify(). Comment only. * docs(web/guides): keep the v4.0 job-testing section as it was The new job spec relies on the guide's job sending mail through a mailer. The v4.0 guide's job still calls model() and sendEmail() inside perform(), which a 4.0.x wheels.Job doesn't have, so the spec would fail against that tree's own example. The v4.1 and v4.2 trees keep the new section; v4.0 keeps only the perform() signature fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4278) The guide offered `wheels jobs work --max-jobs=100` for "one-shot batches from cron or CI", but --max-jobs only stops after N jobs. With fewer pending, the worker polls forever and the cron or CI step never ends. --stop-when-empty exits on the first poll that finds no job ready to run, printing the processed and failed counts. --max-jobs keeps its meaning (a supervised worker relies on it) and still wins when the queue outlasts it. The guide and the consumer jobs.md (both copies) use the new flag. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#4266) A failed job insert was logged as a warning and returned persisted: false, which callers rarely check, so jobs were lost silently. On Adobe ColdFusion every enqueue inside a transaction on a tenant datasource failed this way (a second datasource inside a transaction is refused), even when the transaction committed. enqueue(), enqueueIn() and enqueueAt() now log at error and throw. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…uery has include (#4257) * fix(model): qualify a column compared with a function call when the query has include $whereClause qualifies a condition's column when it binds the value as a parameter. For a function-call value it read only the call's argument, so the column stayed bare and the database rejected it as ambiguous when the included tables share the name. Under include, such a condition's leading property is now qualified the same way a bound condition's is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(model): cover the useIndex alias and a calculated property for function-call conditions Under useIndex (non-soft-delete model) the qualified column uses the tbl alias, as a bound condition does; a calculated property is written as its SQL. Both checked with include. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(model): qualify a function-call condition after a lowercase or mixed-case and/or Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(model): read post ids from the fixtures in the function-call qualification specs CockroachDB assigns SERIAL ids with unique_rowid(), so hard-coded ids 1 and 2 matched no rows there. The specs now take the ids from the post fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Peter Amiri <petera@pai.com>
…ler's missing base (#4250) * fix(controller): hint app.controllers.Controller for a nested controller's missing base A nested controller (app/controllers/<pkg>/X.cfc) that declares extends="Controller" cannot find its base class: a bare extends name resolves relative to the controller's own package, so CF looks for app/controllers/<pkg>/Controller.cfc. The request previously surfaced only the opaque engine error ("invalid component definition, can't find component [Controller]") with no pointer to the fix. $createControllerClass now catches the instantiation failure and, via the pure $missingBaseControllerHint(exception, name) helper, appends a hint to use extends="app.controllers.Controller" when a NESTED controller fails to find the base Controller. Additive message text only: the error type, 500 status and control flow are unchanged, and a top-level controller or an unrelated error is left untouched. NestedControllerHintSpec unit-tests the helper against each engine's component-not-found phrasing; verified end-to-end on a nested extends="Controller" request. Full controller suite green on Lucee 7 (597). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com> * fix(controller): preserve the original error; carry the nested-controller hint via request + log Addresses rev1-r2 review of #4250: (1) Preserve the original exception. The catch no longer throws a NEW exception (which lost the tag context, cause and stack). It sets request.wheels.errorHint and WriteLog's the hint to wheels.log from framework code, then rethrows the ORIGINAL exception unchanged. Per orch1, public/Application.cfc and the CLI app template are NOT touched (that only reaches new apps and would re-drift the starter app that #4244 made a byte copy), so the hint is log-only — the error page already points to wheels.log. (2) Tie the hint to the missing-component OPERAND. $missingBaseControllerHint now parses the missing component from the engine message (Lucee/BoxLang "component [X]", Adobe "component or interface X", trailing period stripped) and hints only when that operand is the base Controller — so a different missing component whose detail merely mentions "Controller" no longer false-positives (rev1-r2 negative case added). (3) Real end-to-end catch-path coverage: a brokennest.Broken fixture declares extends="Controller" from a nested package; the spec drives controller() through the live catch path and asserts the rethrown exception is the ORIGINAL (component error intact, hint NOT merged into its message) and that request.wheels.errorHint carries the hint. Verified on the canonical image: wheels.log holds both the hint line and the original error with full stack. Full Lucee 7 + SQLite core suite green (6571). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com> * fix(controller): hint only on a bare Controller operand; correct the log-only comment rev1-r2 delta (#4250): - $missingBaseControllerHint now matches the missing operand EXACTLY against the bare "Controller" (CompareNoCase), not ListLast(...). An already-qualified operand such as "app.controllers.Controller" that still failed is a different problem, and "use app.controllers.Controller" would be wrong advice — it now gets no hint. Added a negative unit test for that operand. - Corrected the catch comment: the hint is log-only (wheels.log); the app's minimal error-page template is deliberately not touched. - End-to-end spec also asserts the rethrown exception is NOT re-typed to a Wheels.* error (further evidence the original is preserved). Full Lucee 7 + SQLite core suite green (6572). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com> * fix(controller): make the nested-controller extends hint spec engine-aware (BoxLang) The #4250 compat matrix failed all six BoxLang legs: the end-to-end test asserted Lucee/Adobe exception phrasing ("component" in the message) that BoxLang does not produce. BoxLang reports a missing base class as a generic "Could not initialize class <child>$cfc" — both e.message and e.cause.message carry that same text, with no operand and an empty detail. So $missingBaseControllerHint cannot identify "Controller" as the missing operand there and correctly returns "" (its first gate needs "could not find"/"can't find"; "could not initialize" does not match) — no hint, and crucially no FALSE hint. The framework behaviour is already correct on every engine; the bug was spec-only. The end-to-end test now drives the errorHint expectation off the pure $missingBaseControllerHint helper applied to the REAL caught exception: it asserts the original is rethrown unchanged on all engines, the hint is present where the engine names the missing base (Lucee/Adobe), and NO hint is set on BoxLang. The BoxLang limitation is now documented in the helper docblock. Verified locally: NestedControllerHintSpec 9/0/0 on both Lucee 7 and BoxLang (SQLite). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <petera@pai.com> Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Peter Amiri <petera@pai.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… undo request writes (#4283) * docs(web/guides): testing examples name the test client testClient, not client client is a CFML scope. A spec that declares var client = $testClient() and then calls client.get(...) resolves client to the disabled client scope on Lucee and errors with 'client scope is not enabled', so the controller, functional, integration and view/form testing examples failed as written. They now use testClient, which the controllers guide already recommends. v4.0, v4.1 and v4.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(web/guides): a spec's rollback can't undo writes made through test-client requests The controller and integration testing guides said to wrap a destructive spec in a transaction and roll back, with examples whose writes go through $testClient(). Each test-client request runs as its own request and commits on its own, so those writes survive the rollback. The guides now say so and show afterEach cleanup for request writes, and keep the rollback for writes the spec makes itself. The fixtures example's post-rollback count now uses reload=true: the identical count() inside the transaction is cached for the request, so it read 1 and the spec failed. v4.0, v4.1 and v4.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ers as dates on SQLite (#4281) * fix(model): compare date values in the query builder and dynamic finders as dates on SQLite Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(model): unwrap only exact CFML date-literal shapes when quoting a value $unwrapDateLiteral() now changes a value only when it is exactly {ts 'yyyy-mm-dd HH:mm:ss[.f]'}, {d 'yyyy-mm-dd'} or {t 'HH:mm:ss[.f]'}; text such as {d 'abc'} stays verbatim. It still applies to any column type, because SQLite date columns are often declared TEXT and are typed as strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): new apps declare the root route before the wildcard The wheels new routes template ended with .wildcard() and then .root(), while the app's CLAUDE.md says routes match first to last: root, then the wildcard last. The root route still matched (a placeholder-free pattern is resolved before the ordered scan), but the template taught the opposite of the docs, and generate auth's fallback anchor (insert before .root() when the CLI-Appends-Here marker is gone) landed its routes after the wildcard. The template, the demo app, the tweet example and the v4-2-0 first-15- minutes tutorial now put .root() first and .wildcard() last, with a note to add routes above it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(cli): the v4-2-0 tutorial shows the root route before the wildcard Review fix: chapter 1 shows config/routes.cfm as a new app starts, and chapters 2, 3, 5 and 6 show the whole chain; all of them still had the old wildcard-then-root order. They now match a fresh wheels new, and the prose in chapters 1, 2 and 8 says to add routes above .root() with the wildcard last. The /up route lines are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ork (#4523) * ci: run the core suite on SQLite with the engine JVM in America/New_York Every compat-matrix leg runs the engine's JVM in UTC, where converting between local time and UTC changes nothing, so a local/UTC mix-up can't show. Three lanes (lucee7, adobe2025, boxlang) now run the core suite on SQLite with the JVM in America/New_York, a zone with a UTC offset and daylight saving. They run in every full matrix run, and in a narrowed dispatch when the engines input includes `tz`. compose.yml passes ENGINE_TZ and ENGINE_JAVA_TOOL_OPTIONS (empty unless set) to the three engine services as TZ and JAVA_TOOL_OPTIONS; the workflow sets them at job level for the TZ lanes only. The lanes' job names, artifacts and JUnit labels carry the zone, and the summary grid shows them as extra rows. tools/test-matrix.sh documents the local equivalent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * ci: set the TZ lanes' zone through TZ alone, and fail a leg whose engine didn't take it compose.yml no longer sets JAVA_TOOL_OPTIONS on the three engine services (it was an empty value on every leg that isn't a TZ lane); TZ alone sets the zone, defaulting to UTC. EngineTimeZoneSpec checks it took: when the container has TZ, the JVM's default zone (user.timezone) must be that zone and the engine's offset must agree, and on the America/New_York lanes Now() must be 4 hours from UTC during daylight saving and 5 otherwise, computed in integer arithmetic (BoxLang's GetTimeZoneInfo() leaves daylight saving out of utcTotalOffset). An engine without TZ, or without a JVM zone (RustCFML), skips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…eues (#4524) * feat(job): configure the jobs tick route's batch size, timeout and queues The /wheels/jobs/tick route ran JobRunner.tick() with its defaults, so each call ran at most one job and nothing could raise that. It now passes three settings through: jobsRunnerTickMaxJobs (default 1), jobsRunnerTickTimeout (default 300) and jobsRunnerTickQueues (default every queue). The defaults keep today's behaviour. They are settings rather than request parameters, so a caller holding the token can't widen the work one request does; an invalid value falls back to its default. Documented in the multi-server jobs guide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(job): bound the tick route's batch and timeout settings to whole-number ranges jobsRunnerTickMaxJobs takes a whole number from 1 to 1000 and jobsRunnerTickTimeout one from 1 to 86400; a fractional, out-of-range or non-numeric value falls back to the default instead of being truncated (0.5 became 0) or passed through past the INTEGER columns it reaches. The guide says a batch must fit inside the engine's request timeout, which the setting doesn't extend, and that a request killed mid-job leaves the job for the reaper to recover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(job): read the tick route's numeric settings only as plain whole numbers IsNumeric() accepts exponent notation, and Val() reads only a numeric prefix, so "1e3" became 1. A setting now has to read as plain digits (an engine's ".0" tail allowed); anything else, including "1e3", "1e99" and a leading sign, falls back to the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4526) A step on a single value now follows Vixie cron and cronie: N/S runs from N to the field's maximum, every S. "5/1" in the minute field was minute 5 alone and is now 5-59 ("5/15" was already 5,20,35,50). Day-of-week runs through 7, which is Sunday again: "7/2" is accepted (it was refused as a reversed range) and means Sunday, and "1/2" now includes Sunday. Ranges with a step ("5-20/5") and "*/S" are unchanged. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ction (#4527) With jobsAutoCreateTables = false, $ensureJobTable() probed wheels_jobs and asked the catalog only after the probe failed. On PostgreSQL and CockroachDB a failed statement aborts the transaction, so inside one the catalog check then failed with "current transaction is aborted" instead of reporting Wheels.Job.SchemaMissing. There it now asks the catalog first. Also, before the 4.2.0 cut: - a spec that a job idle past the 30-second heartbeat-grace minimum is reclaimed when the setting is below it (the existing spec covers a job inside the minimum); - a spec, and a guide line, that an empty-string perform() return is stored as NULL, like a return of nothing. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…alizable (#4528) * fix(migrator): run a CockroachDB migration step's transaction at serializable CockroachDB refuses a schema change after any other statement in a transaction at a weak isolation level, and Lucee leaves a pooled connection at read committed once a transaction on it has ended. So on Lucee a migration step that read or wrote before its DDL failed with "to use multi-statement transactions involving a schema change under weak isolation levels" on every step after a connection's first transaction. On CockroachDB the step's transaction now asks for serializable, CockroachDB's own default; other databases are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(migrator): changelog for the CockroachDB step isolation fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(migrator): write through a model inside the CockroachDB step isolation fixture The step creates a record through a model and saves it before its schema changes, so the spec also shows model writes join the step's serializable transaction on CockroachDB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…CFML (#4529) A route `constraints` value is a plain CFML string that the router drops into the route regex verbatim (`$patternToRegex` wraps each in a group). CFML strings have no backslash escapes, so a digit class is written `"\d{4}"`, not `"\\d{4}"` — a doubled backslash reaches the regex as a literal backslash and the pattern never matches, so the route silently falls through. Add a short caution to the "Constrain pattern placeholders" section (the example there already uses the correct form). Verified against the router source and the framework's own passing route specs, which declare `constraints = {year = "\d{4}", ...}` and compile to a `\d{4}` route regex that matches a numeric path. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
RustCFML marks most of its releases as prereleases (v0.723.0, v0.724.0 and v0.724.1 on 10-09), so a check that only looked at GitHub's latest stable release never evaluated most builds. check-version.sh now takes the newest release, prereleases included (plain vX.Y.Z tags, drafts excluded, version-sorted), as the candidate; the compare run stays the only quality gate. If that candidate is rejected or listed in KNOWN_BAD and the latest stable is newer than the pin, the stable is evaluated and pinned if green. A rejected newest release is still reported, so the workflow can open the bump PR and the issue in the same run. A candidate that can't be evaluated still fails the run, with no fallback. sha256 pinning (bump-pin.sh, run-suite.sh) is unchanged. Tests: the fake gh now answers the release list and releases/latest; the stub suite answers per version; 10 new cases cover the selection, the fallback and its exports. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…L outside UTC (#4532) $normalizeDbTimestamp()'s JVM-free fallback (RustCFML) added the epoch seconds to CreateDate(1970, 1, 1), which is LOCAL midnight, so every epoch-to-date conversion was off by the host's UTC offset for that date (the jobs clock's toLocal(); JobClockSpec and JobHardenerSpec failed by exactly that offset on a non-UTC host). It now counts from the epoch instant: DateConvert("utc2local", CreateDateTime(1970, 1, 1, 0, 0, 0)). Checked in America/Los_Angeles (now, a winter date and the epoch itself round-trip exactly) on RustCFML v0.724.1 and on Lucee 7.0.4.34. JVM engines take the java.util.Calendar path and are unchanged. Fixes #4525 Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ils (#4535) Since #4534, check-version.sh exports a rejected newest release and then evaluates the latest stable. If that fallback can't be evaluated, the script exits 1 and the notify step (default if: success()) was skipped, so the newest's rejection was never filed. The notify step now runs whenever RUSTCFML_REJECTED is set, unless the run was cancelled. The bump PR step is unchanged (nothing is pinned when the fallback fails). Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nst (#4531) * fix(model): pass 1 = 0 through where= and name a value it can't bind where= binds every `column <operator> literal`, and the column must be the model's or an included model's. Two valid shapes failed with errors that didn't say why: - A comparison between two numbers (1 = 0, 1 = 1) threw Wheels.ColumnNotFound for a column named 1. findAll(), updateAll() and deleteAll() now mark its operator (Chr(8), outside quoted literals) so no pass binds or counts its value, and $whereClause() writes it as typed. - A literal compared with another table's column inside a subquery was masked but never got a parameter, so the SQL builder failed with "Array index [1] out of range". It now throws Wheels.UnbindableWhereValue naming the condition and the alternatives (whereIn() with a value array, or running the subquery first). A ColumnNotFound from a where with a subquery says the same. The query guide and .ai/models.md (and the app template's copy) document the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(model): keep where= to model columns compared with values, and hide values in its error where= accepts model columns compared with values; a condition with no column, such as 1 = 0, isn't supported. It throws Wheels.ColumnNotFound as before, whose detail now says so and names the alternatives (whereIn() with an empty array for no rows; leave the condition out for every row). The guide, .ai/models.md and the changelog say the same. Wheels.UnbindableWhereValue now hides every value in the condition it echoes: a quoted literal that wasn't bound is shown as '...' rather than in its masked form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * fix(model): hide numbers too in the condition an unbindable where= value echoes Wheels.UnbindableWhereValue echoed numeric literals in the condition as written. A number that isn't part of an identifier is now shown as ..., alongside bound values (?) and quoted literals ('...'), and the wording says exactly that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * Revert "fix(model): hide numbers too in the condition an unbindable where= value echoes" This reverts commit e848f53: only quoted and bound values need hiding, and unquoted numbers in the developer's own where text may show. The comment, the error, the spec name and the changelog now say "quoted values hidden" rather than "every value". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ct is set up (#4536) * fix(controller): include the global helpers when each controller object is set up wheels.Controller included <viewPath>/helpers.cfm in its pseudo-constructor, and only once the application scope was ready. That relies on the engine running the pseudo-constructor per instance. RustCFML v0.721.0+ reuses a class's pseudo-constructor include across instances, so a controller first created during application start-up left every later one without the global helpers. $includeGlobalHelpers() now includes the file from $initControllerClass() (before config(), which could call a global helper before) and from $initControllerObject() (before the controller's own helpers, so one can still override a global helper). Whether the file exists is cached in helperFileCache when cacheFileChecking is on, and a missing file is skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(controller): changelog note on when global helpers become available Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… 2.x condition strings
…4537) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ci: mirror the Docker Hub images CI pulls to GHCR Adds tools/ci/mirror-images.txt (the Docker Hub images CI and the local test setup pull, as the repo pins them) and a workflow that copies each one, unchanged, to ghcr.io/wheels-dev/mirror/<name> with crane, keeping multi-arch indexes and digests. It runs weekly, on dispatch, and when the list changes. Each image copies in its own job, and an image whose digest the mirror already holds costs no Docker Hub pull. Nothing points at the mirror yet; that follows separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * ci: pin setup-crane to a commit and drop checkout credentials in the mirror Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * ci: keep the CockroachDB and Oracle images out of the mirror Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Write back discussionUrl for newly-published posts. Opened by blog-announce-on-publish.yml after a publish PR merged to develop. Co-authored-by: wheels-bot[bot] <wheels-bot[bot]@users.noreply.github.com>
…4544) * ci: dry-check the mirror list on PRs and write tags only after merge A PR that changed the image list copied images with its own, unreviewed copy of the script, writing the same tags develop's CI pulls. PRs, forks included, now get a read-only check: each reference resolves on Docker Hub and its digest is compared with the mirror's, reporting what the copy after merge would do. Copies run only on push to develop, dispatch and the weekly schedule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * ci: run the mirror copy only from develop Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…heckout v6) (#4546) The RustCFML daily check's first green run (38060110634) failed in the bump-PR step: create-pull-request@v6's git preparation ran 'git remote prune origin' and got HTTP 400. actions/checkout@v6 keeps its credentials in an includeIf'd git-credentials config, which v6 of the action does not handle; v8.0.0 updated it for checkout v6. Pinned to the exact release in both workflows that use it (rustcfml-version-check.yml, bump-develop-version.yml). Neither uses v7's renamed input (git-token -> branch-token) or removed output (PULL_REQUEST_NUMBER), and both run on GitHub-hosted ubuntu-24.04 (v8 needs runner >= 2.327.1 only when self-hosted). Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.693.0 -> v0.730.0 (stable, 2026-10-10). The engine bugs that kept v0.721.0-v0.728.0 rejected are fixed upstream: RustCFML#481 (conditional pseudo-constructor include), #486 (struct-form cookie overwrite) and #487 (internal __dyn_include key on instances). bump-pin.sh moved ENGINE_VERSION, the per-asset sha256 pins and the CLI's copies in RustCFMLEngine.cfc; baseline.json regenerated (TZ=UTC): 7268 pass, 0 fail, 0 error, 180 skipped, 0 known-failing entries. Compare run on develop 0fc0f7f: accepted, also with the host in America/Los_Angeles. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
"skips port + 1 when another process is listening on it" asserted that the shutdown port the CLI chose was still free afterwards. That port is in the ephemeral range, so another process on a shared runner can bind it between the choice and the check. Assert only on the chosen port (above the busy one) and the notice text. Also folds in two small CLI tidy-ups: - delete cli/lucli/tests/_fixtures/deploy/sshd/authorized_keys, unused since the sshd fixture switched to an inline PUBLIC_KEY (#4504); - the --job-timeout help says plain --timeout stops the worker only on some CLI runtimes. Fixes #4547 Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs(api): add the 4.2.0 API reference JSON release.yml builds the `wheels lookup` docs index with --strict against docs/api/v<version>.json, so the 4.2.0 release needs this file committed. It is the API JSON that the develop snapshot build generated on c3665d9 (/wheels/api?format=json&type=core), unchanged: 406 functions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(api): generate the v4-2-0 API reference tree node web/scripts/generate-api-docs.mjs 4.2.0, from docs/api/v4.2.0.json: 406 function pages across 10 sections (19 more than v4-1-0, none removed). The tree isn't listed as current until the 4.2.0 versions flip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rent (#4322) * docs(web): mark the 4.2 guides current Flip GUIDES_VERSIONS so v4-2-0 is the current line and v4-1-0 is archived, and update the guides landing page to match. For the 4.2.0 release cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(web): make the 4.2 API reference and landing links current API_VERSIONS lists v4-2-0 as current and v4-1-0 as archived; the API index table and the wheels.dev home page links move to v4-2-0. Needs the v4-2-0 API tree to be on develop first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * chore(web): refresh visual baseline(s) (api) Manually triggered baseline refresh via .github/workflows/refresh-visual-baselines.yml targeting branch peter/guides-4-2-current. Run when an intentional content/layout change makes the visual-regression check fail. The new PNG(s) under web/tests/visual-baselines/ are now the expected rendering. * chore(web): refresh visual baseline(s) (guides) Manually triggered baseline refresh via .github/workflows/refresh-visual-baselines.yml targeting branch peter/guides-4-2-current. Run when an intentional content/layout change makes the visual-regression check fail. The new PNG(s) under web/tests/visual-baselines/ are now the expected rendering. * chore(web): trigger CI after the baseline refreshes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore(release): promote changelog fragments into 4.2.0 Runs tools/changelog-promote.sh 4.2.0 2026-10-11: assembles the 272 changelog.d fragments into a new 4.2.0 section and empties changelog.d. The date is the prep date and moves to the tag date at the cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * chore(release): add the Java 21+ package line and a reference to the 4.2.0 changelog #4237 and #4243 changed released Linux package behaviour without a fragment. The framework-suite datasource line (added by #4295 for #4254) gains its issue reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * chore(release): describe job heartbeats as they ship in 4.2 Folds the heartbeat Added entry (#4477) and the grace Fixed entry (#4502) into one Added entry. The Fixed entry described a 'before' that never shipped, and the Added entry's 'heartbeats more often than its timeout' contradicted the grace rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4551) * chore(release): list the 4.2.0 security fixes with their advisories Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * chore(release): date 4.2.0 for 2026-10-12 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Wheels Test Matrix
A ':x: N tests' cell means the leg reported fewer than 4000 testcases (suite never ran; counted as failed). ':x: no result' means the leg returned no parseable result. Results for commit 08ea9b1. |
Contributor
Wheels Test Results260 610 tests 254 873 ✅ 1h 59m 36s ⏱️ Results for commit 7ce2eb6. ♻️ This comment has been updated with latest results. |
…#4553) $isCsrfCookiePayload() accepted any non-empty JSON, but a wrong-cipher decrypt of a legacy-format cookie can return garbage that happens to parse as JSON (a number, say). Then the legacy decrypt was skipped and the token reader failed on the non-struct. The payload check now requires a struct with a non-empty authenticityToken, deserializing safely, and the token reader ignores JSON that isn't a struct. Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cut Wheels 4.2.0 from
developat2cbc6c417. SeeCHANGELOG.md→# [4.2.0] => 2026-10-12.What happens on merge
The push to
mainrunsrelease.yml. It validates the package and the CHANGELOG section, builds thewheels lookupdocs index fromdocs/api/v4.2.0.json, publishes to ForgeBox, creates thev4.2.0tag and GitHub Release (notes from the CHANGELOG section), and dispatches the Homebrew, Scoop, apt/yum and develop version-bump follow-ups.Gates
deferredwith a reasondevelop→mainmerges cleanly (main's history is already contained in develop's tree), so no "ours" merge is needed.🤖 Generated with Claude Code